1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app; 18 19 import static android.annotation.Dimension.DP; 20 import static android.app.Flags.evenlyDividedCallStyleActionLayout; 21 import static android.app.Flags.notificationsRedesignTemplates; 22 import static android.app.admin.DevicePolicyResources.Drawables.Source.NOTIFICATION; 23 import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED; 24 import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; 25 import static android.app.admin.DevicePolicyResources.UNDEFINED; 26 import static android.graphics.drawable.Icon.TYPE_URI; 27 import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP; 28 import static android.util.TypedValue.COMPLEX_UNIT_PX; 29 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 30 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 31 32 import static java.util.Objects.requireNonNull; 33 34 import android.annotation.ColorInt; 35 import android.annotation.ColorRes; 36 import android.annotation.DimenRes; 37 import android.annotation.Dimension; 38 import android.annotation.DrawableRes; 39 import android.annotation.FlaggedApi; 40 import android.annotation.IdRes; 41 import android.annotation.IntDef; 42 import android.annotation.NonNull; 43 import android.annotation.Nullable; 44 import android.annotation.RequiresPermission; 45 import android.annotation.SdkConstant; 46 import android.annotation.SdkConstant.SdkConstantType; 47 import android.annotation.StringRes; 48 import android.annotation.StyleableRes; 49 import android.annotation.SuppressLint; 50 import android.annotation.SystemApi; 51 import android.annotation.TestApi; 52 import android.app.admin.DevicePolicyManager; 53 import android.app.compat.CompatChanges; 54 import android.compat.annotation.ChangeId; 55 import android.compat.annotation.EnabledSince; 56 import android.compat.annotation.UnsupportedAppUsage; 57 import android.content.Context; 58 import android.content.Intent; 59 import android.content.LocusId; 60 import android.content.pm.ApplicationInfo; 61 import android.content.pm.PackageManager; 62 import android.content.pm.PackageManager.NameNotFoundException; 63 import android.content.pm.ShortcutInfo; 64 import android.content.res.ColorStateList; 65 import android.content.res.Configuration; 66 import android.content.res.Resources; 67 import android.content.res.TypedArray; 68 import android.graphics.Bitmap; 69 import android.graphics.Canvas; 70 import android.graphics.Color; 71 import android.graphics.PorterDuff; 72 import android.graphics.drawable.Drawable; 73 import android.graphics.drawable.Icon; 74 import android.media.AudioAttributes; 75 import android.media.AudioManager; 76 import android.media.PlayerBase; 77 import android.media.session.MediaSession; 78 import android.net.Uri; 79 import android.os.BadParcelableException; 80 import android.os.Build; 81 import android.os.Bundle; 82 import android.os.IBinder; 83 import android.os.Parcel; 84 import android.os.Parcelable; 85 import android.os.SystemClock; 86 import android.os.SystemProperties; 87 import android.os.Trace; 88 import android.os.UserHandle; 89 import android.os.UserManager; 90 import android.provider.Settings; 91 import android.text.BidiFormatter; 92 import android.text.SpannableStringBuilder; 93 import android.text.Spanned; 94 import android.text.TextUtils; 95 import android.text.style.AbsoluteSizeSpan; 96 import android.text.style.CharacterStyle; 97 import android.text.style.ForegroundColorSpan; 98 import android.text.style.RelativeSizeSpan; 99 import android.text.style.StrikethroughSpan; 100 import android.text.style.StyleSpan; 101 import android.text.style.TextAppearanceSpan; 102 import android.text.style.UnderlineSpan; 103 import android.util.ArraySet; 104 import android.util.Log; 105 import android.util.Pair; 106 import android.util.SparseArray; 107 import android.util.TypedValue; 108 import android.util.proto.ProtoOutputStream; 109 import android.view.ContextThemeWrapper; 110 import android.view.Gravity; 111 import android.view.View; 112 import android.view.contentcapture.ContentCaptureContext; 113 import android.widget.ProgressBar; 114 import android.widget.RemoteViews; 115 116 import com.android.internal.R; 117 import com.android.internal.annotations.VisibleForTesting; 118 import com.android.internal.graphics.ColorUtils; 119 import com.android.internal.util.ArrayUtils; 120 import com.android.internal.util.ContrastColorUtil; 121 import com.android.internal.util.NotificationBigTextNormalizer; 122 import com.android.internal.widget.NotificationProgressModel; 123 124 import java.lang.annotation.Retention; 125 import java.lang.annotation.RetentionPolicy; 126 import java.lang.reflect.Array; 127 import java.lang.reflect.Constructor; 128 import java.util.ArrayList; 129 import java.util.Collections; 130 import java.util.List; 131 import java.util.Objects; 132 import java.util.Set; 133 import java.util.function.Consumer; 134 135 /** 136 * A class that represents how a persistent notification is to be presented to 137 * the user using the {@link android.app.NotificationManager}. 138 * 139 * <p>The {@link Notification.Builder Notification.Builder} has been added to make it 140 * easier to construct Notifications.</p> 141 * 142 * <div class="special reference"> 143 * <h3>Developer Guides</h3> 144 * <p>For a guide to creating notifications, read the 145 * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html">Status Bar Notifications</a> 146 * developer guide.</p> 147 * </div> 148 */ 149 public class Notification implements Parcelable 150 { 151 private static final String TAG = "Notification"; 152 153 /** 154 * @hide 155 */ 156 @Retention(RetentionPolicy.SOURCE) 157 @IntDef({ 158 FOREGROUND_SERVICE_DEFAULT, 159 FOREGROUND_SERVICE_IMMEDIATE, 160 FOREGROUND_SERVICE_DEFERRED 161 }) 162 public @interface ServiceNotificationPolicy {} 163 164 /** 165 * If the Notification associated with starting a foreground service has been 166 * built using setForegroundServiceBehavior() with this behavior, display of 167 * the notification will usually be suppressed for a short time to avoid visual 168 * disturbances to the user. 169 * @see Notification.Builder#setForegroundServiceBehavior(int) 170 * @see #FOREGROUND_SERVICE_IMMEDIATE 171 * @see #FOREGROUND_SERVICE_DEFERRED 172 */ 173 public static final @ServiceNotificationPolicy int FOREGROUND_SERVICE_DEFAULT = 0; 174 175 /** 176 * If the Notification associated with starting a foreground service has been 177 * built using setForegroundServiceBehavior() with this behavior, display of 178 * the notification will be immediate even if the default behavior would be 179 * to defer visibility for a short time. 180 * @see Notification.Builder#setForegroundServiceBehavior(int) 181 * @see #FOREGROUND_SERVICE_DEFAULT 182 * @see #FOREGROUND_SERVICE_DEFERRED 183 */ 184 public static final @ServiceNotificationPolicy int FOREGROUND_SERVICE_IMMEDIATE = 1; 185 186 /** 187 * If the Notification associated with starting a foreground service has been 188 * built using setForegroundServiceBehavior() with this behavior, display of 189 * the notification will usually be suppressed for a short time to avoid visual 190 * disturbances to the user. 191 * @see Notification.Builder#setForegroundServiceBehavior(int) 192 * @see #FOREGROUND_SERVICE_DEFAULT 193 * @see #FOREGROUND_SERVICE_IMMEDIATE 194 */ 195 public static final @ServiceNotificationPolicy int FOREGROUND_SERVICE_DEFERRED = 2; 196 197 @ServiceNotificationPolicy 198 private int mFgsDeferBehavior; 199 200 /** 201 * An activity that provides a user interface for adjusting notification preferences for its 202 * containing application. 203 */ 204 @SdkConstant(SdkConstantType.INTENT_CATEGORY) 205 public static final String INTENT_CATEGORY_NOTIFICATION_PREFERENCES 206 = "android.intent.category.NOTIFICATION_PREFERENCES"; 207 208 /** 209 * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will 210 * contain a {@link NotificationChannel#getId() channel id} that can be used to narrow down 211 * what settings should be shown in the target app. 212 */ 213 public static final String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID"; 214 215 /** 216 * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will 217 * contain a {@link NotificationChannelGroup#getId() group id} that can be used to narrow down 218 * what settings should be shown in the target app. 219 */ 220 public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID"; 221 222 /** 223 * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will 224 * contain the tag provided to {@link NotificationManager#notify(String, int, Notification)} 225 * that can be used to narrow down what settings should be shown in the target app. 226 */ 227 public static final String EXTRA_NOTIFICATION_TAG = "android.intent.extra.NOTIFICATION_TAG"; 228 229 /** 230 * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will 231 * contain the id provided to {@link NotificationManager#notify(String, int, Notification)} 232 * that can be used to narrow down what settings should be shown in the target app. 233 */ 234 public static final String EXTRA_NOTIFICATION_ID = "android.intent.extra.NOTIFICATION_ID"; 235 236 /** 237 * Use all default values (where applicable). 238 */ 239 public static final int DEFAULT_ALL = ~0; 240 241 /** 242 * Use the default notification sound. This will ignore any given 243 * {@link #sound}. 244 * 245 * <p> 246 * A notification that is noisy is more likely to be presented as a heads-up notification. 247 * </p> 248 * 249 * @see #defaults 250 */ 251 252 public static final int DEFAULT_SOUND = 1; 253 254 /** 255 * Use the default notification vibrate. This will ignore any given 256 * {@link #vibrate}. Using phone vibration requires the 257 * {@link android.Manifest.permission#VIBRATE VIBRATE} permission. 258 * 259 * <p> 260 * A notification that vibrates is more likely to be presented as a heads-up notification. 261 * </p> 262 * 263 * @see #defaults 264 */ 265 266 public static final int DEFAULT_VIBRATE = 2; 267 268 /** 269 * Use the default notification lights. This will ignore the 270 * {@link #FLAG_SHOW_LIGHTS} bit, and {@link #ledARGB}, {@link #ledOffMS}, or 271 * {@link #ledOnMS}. 272 * 273 * @see #defaults 274 */ 275 276 public static final int DEFAULT_LIGHTS = 4; 277 278 /** 279 * Maximum length of CharSequences accepted by Builder and friends. 280 * 281 * <p> 282 * Avoids spamming the system with overly large strings such as full e-mails. 283 */ 284 private static final int MAX_CHARSEQUENCE_LENGTH = 1024; 285 286 /** 287 * Maximum entries of reply text that are accepted by Builder and friends. 288 */ 289 private static final int MAX_REPLY_HISTORY = 5; 290 291 /** 292 * Maximum aspect ratio of the large icon. 16:9 293 */ 294 private static final float MAX_LARGE_ICON_ASPECT_RATIO = 16f / 9f; 295 296 /** 297 * Maximum number of (generic) action buttons in a notification (contextual action buttons are 298 * handled separately). 299 * @hide 300 */ 301 public static final int MAX_ACTION_BUTTONS = 3; 302 303 /** 304 * If the notification contained an unsent draft for a RemoteInput when the user clicked on it, 305 * we're adding the draft as a String extra to the {@link #contentIntent} using this key. 306 * 307 * <p>Apps may use this extra to prepopulate text fields in the app, where the user usually 308 * sends messages.</p> 309 */ 310 public static final String EXTRA_REMOTE_INPUT_DRAFT = "android.remoteInputDraft"; 311 312 /** 313 * The call to WearableExtender#setBackground(Bitmap) will have no effect and the passed 314 * Bitmap will not be retained in memory. 315 */ 316 @ChangeId 317 @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) 318 @VisibleForTesting 319 static final long WEARABLE_EXTENDER_BACKGROUND_BLOCKED = 270551184L; 320 321 /** 322 * A timestamp related to this notification, in milliseconds since the epoch. 323 * 324 * Default value: {@link System#currentTimeMillis() Now}. 325 * 326 * Choose a timestamp that will be most relevant to the user. For most finite events, this 327 * corresponds to the time the event happened (or will happen, in the case of events that have 328 * yet to occur but about which the user is being informed). Indefinite events should be 329 * timestamped according to when the activity began. 330 * 331 * Some examples: 332 * 333 * <ul> 334 * <li>Notification of a new chat message should be stamped when the message was received.</li> 335 * <li>Notification of an ongoing file download (with a progress bar, for example) should be stamped when the download started.</li> 336 * <li>Notification of a completed file download should be stamped when the download finished.</li> 337 * <li>Notification of an upcoming meeting should be stamped with the time the meeting will begin (that is, in the future).</li> 338 * <li>Notification of an ongoing stopwatch (increasing timer) should be stamped with the watch's start time. 339 * <li>Notification of an ongoing countdown timer should be stamped with the timer's end time. 340 * </ul> 341 * 342 * For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this time is not shown 343 * anymore by default and must be opted into by using 344 * {@link android.app.Notification.Builder#setShowWhen(boolean)} 345 */ 346 public long when; 347 348 /** 349 * The creation time of the notification 350 * @hide 351 */ 352 public long creationTime; 353 354 /** 355 * The resource id of a drawable to use as the icon in the status bar. 356 * 357 * @deprecated Use {@link Builder#setSmallIcon(Icon)} instead. 358 */ 359 @Deprecated 360 @DrawableRes 361 public int icon; 362 363 /** 364 * If the icon in the status bar is to have more than one level, you can set this. Otherwise, 365 * leave it at its default value of 0. 366 * 367 * @see android.widget.ImageView#setImageLevel 368 * @see android.graphics.drawable.Drawable#setLevel 369 */ 370 public int iconLevel; 371 372 /** 373 * The number of events that this notification represents. For example, in a new mail 374 * notification, this could be the number of unread messages. 375 * 376 * The system may or may not use this field to modify the appearance of the notification. 377 * Starting with {@link android.os.Build.VERSION_CODES#O}, the number may be displayed as a 378 * badge icon in Launchers that support badging. 379 */ 380 public int number = 0; 381 382 /** 383 * The intent to execute when the expanded status entry is clicked. If 384 * this is an activity, it must include the 385 * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} flag, which requires 386 * that you take care of task management as described in the 387 * <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and Back 388 * Stack</a> document. In particular, make sure to read the 389 * <a href="{@docRoot}training/notify-user/navigation">Start 390 * an Activity from a Notification</a> page for the correct ways to launch an application from a 391 * notification. 392 */ 393 public PendingIntent contentIntent; 394 395 /** 396 * The intent to execute when the notification is explicitly dismissed by the user, either with 397 * the "Clear All" button or by swiping it away individually. 398 * 399 * This probably shouldn't be launching an activity since several of those will be sent 400 * at the same time. 401 */ 402 public PendingIntent deleteIntent; 403 404 /** 405 * An intent to launch instead of posting the notification to the status bar. 406 * 407 * <p> 408 * The system UI may choose to display a heads-up notification, instead of 409 * launching this intent, while the user is using the device. 410 * </p> 411 * 412 * @see Notification.Builder#setFullScreenIntent 413 */ 414 public PendingIntent fullScreenIntent; 415 416 /** 417 * Text that summarizes this notification for accessibility services. 418 * 419 * As of the L release, this text is no longer shown on screen, but it is still useful to 420 * accessibility services (where it serves as an audible announcement of the notification's 421 * appearance). 422 * 423 * @see #tickerView 424 */ 425 public CharSequence tickerText; 426 427 /** 428 * Formerly, a view showing the {@link #tickerText}. 429 * 430 * No longer displayed in the status bar as of API 21. 431 */ 432 @Deprecated 433 public RemoteViews tickerView; 434 435 /** 436 * The view that will represent this notification in the notification list (which is pulled 437 * down from the status bar). 438 * 439 * As of N, this field may be null. The notification view is determined by the inputs 440 * to {@link Notification.Builder}; a custom RemoteViews can optionally be 441 * supplied with {@link Notification.Builder#setCustomContentView(RemoteViews)}. 442 */ 443 @Deprecated 444 public RemoteViews contentView; 445 446 /** 447 * A large-format version of {@link #contentView}, giving the Notification an 448 * opportunity to show more detail when expanded. The system UI may choose 449 * to show this instead of the normal content view at its discretion. 450 * 451 * As of N, this field may be null. The expanded notification view is determined by the 452 * inputs to {@link Notification.Builder}; a custom RemoteViews can optionally be 453 * supplied with {@link Notification.Builder#setCustomBigContentView(RemoteViews)}. 454 */ 455 @Deprecated 456 public RemoteViews bigContentView; 457 458 459 /** 460 * A medium-format version of {@link #contentView}, providing the Notification an 461 * opportunity to add action buttons to contentView. At its discretion, the system UI may 462 * choose to show this as a heads-up notification, which will pop up so the user can see 463 * it without leaving their current activity. 464 * 465 * As of N, this field may be null. The heads-up notification view is determined by the 466 * inputs to {@link Notification.Builder}; a custom RemoteViews can optionally be 467 * supplied with {@link Notification.Builder#setCustomHeadsUpContentView(RemoteViews)}. 468 */ 469 @Deprecated 470 public RemoteViews headsUpContentView; 471 472 private boolean mUsesStandardHeader; 473 474 private static final ArraySet<Integer> STANDARD_LAYOUTS = new ArraySet<>(); 475 static { 476 STANDARD_LAYOUTS.add(R.layout.notification_template_material_base); 477 STANDARD_LAYOUTS.add(R.layout.notification_template_material_heads_up_base); 478 STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_base); 479 STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_picture); 480 STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_text); 481 STANDARD_LAYOUTS.add(R.layout.notification_template_material_inbox); 482 STANDARD_LAYOUTS.add(R.layout.notification_template_material_messaging); 483 STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_messaging); 484 STANDARD_LAYOUTS.add(R.layout.notification_template_material_conversation); 485 STANDARD_LAYOUTS.add(R.layout.notification_template_material_media); 486 STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_media); 487 STANDARD_LAYOUTS.add(R.layout.notification_template_material_call); 488 STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_call); 489 STANDARD_LAYOUTS.add(R.layout.notification_template_header); 490 } 491 492 /** 493 * A large bitmap to be shown in the notification content area. 494 * 495 * @deprecated Use {@link Builder#setLargeIcon(Icon)} instead. 496 */ 497 @Deprecated 498 public Bitmap largeIcon; 499 500 /** 501 * The sound to play. 502 * 503 * <p> 504 * A notification that is noisy is more likely to be presented as a heads-up notification. 505 * </p> 506 * 507 * <p> 508 * To play the default notification sound, see {@link #defaults}. 509 * </p> 510 * @deprecated use {@link NotificationChannel#getSound()}. 511 */ 512 @Deprecated 513 public Uri sound; 514 515 /** 516 * Use this constant as the value for audioStreamType to request that 517 * the default stream type for notifications be used. Currently the 518 * default stream type is {@link AudioManager#STREAM_NOTIFICATION}. 519 * 520 * @deprecated Use {@link NotificationChannel#getAudioAttributes()} instead. 521 */ 522 @Deprecated 523 public static final int STREAM_DEFAULT = -1; 524 525 /** 526 * The audio stream type to use when playing the sound. 527 * Should be one of the STREAM_ constants from 528 * {@link android.media.AudioManager}. 529 * 530 * @deprecated Use {@link #audioAttributes} instead. 531 */ 532 @Deprecated 533 public int audioStreamType = STREAM_DEFAULT; 534 535 /** 536 * The default value of {@link #audioAttributes}. 537 */ 538 public static final AudioAttributes AUDIO_ATTRIBUTES_DEFAULT = new AudioAttributes.Builder() 539 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 540 .setUsage(AudioAttributes.USAGE_NOTIFICATION) 541 .build(); 542 543 /** 544 * The {@link AudioAttributes audio attributes} to use when playing the sound. 545 * 546 * @deprecated use {@link NotificationChannel#getAudioAttributes()} instead. 547 */ 548 @Deprecated 549 public AudioAttributes audioAttributes = AUDIO_ATTRIBUTES_DEFAULT; 550 551 /** 552 * The pattern with which to vibrate. 553 * 554 * <p> 555 * To vibrate the default pattern, see {@link #defaults}. 556 * </p> 557 * 558 * @see android.os.Vibrator#vibrate(long[],int) 559 * @deprecated use {@link NotificationChannel#getVibrationPattern()}. 560 */ 561 @Deprecated 562 public long[] vibrate; 563 564 /** 565 * The color of the led. The hardware will do its best approximation. 566 * 567 * @see #FLAG_SHOW_LIGHTS 568 * @see #flags 569 * @deprecated use {@link NotificationChannel#shouldShowLights()}. 570 */ 571 @ColorInt 572 @Deprecated 573 public int ledARGB; 574 575 /** 576 * The number of milliseconds for the LED to be on while it's flashing. 577 * The hardware will do its best approximation. 578 * 579 * @see #FLAG_SHOW_LIGHTS 580 * @see #flags 581 * @deprecated use {@link NotificationChannel#shouldShowLights()}. 582 */ 583 @Deprecated 584 public int ledOnMS; 585 586 /** 587 * The number of milliseconds for the LED to be off while it's flashing. 588 * The hardware will do its best approximation. 589 * 590 * @see #FLAG_SHOW_LIGHTS 591 * @see #flags 592 * 593 * @deprecated use {@link NotificationChannel#shouldShowLights()}. 594 */ 595 @Deprecated 596 public int ledOffMS; 597 598 /** 599 * Specifies which values should be taken from the defaults. 600 * <p> 601 * To set, OR the desired from {@link #DEFAULT_SOUND}, 602 * {@link #DEFAULT_VIBRATE}, {@link #DEFAULT_LIGHTS}. For all default 603 * values, use {@link #DEFAULT_ALL}. 604 * </p> 605 * 606 * @deprecated use {@link NotificationChannel#getSound()} and 607 * {@link NotificationChannel#shouldShowLights()} and 608 * {@link NotificationChannel#shouldVibrate()}. 609 */ 610 @Deprecated 611 public int defaults; 612 613 /** 614 * Bit to be bitwise-ored into the {@link #flags} field that should be 615 * set if you want the LED on for this notification. 616 * <ul> 617 * <li>To turn the LED off, pass 0 in the alpha channel for colorARGB 618 * or 0 for both ledOnMS and ledOffMS.</li> 619 * <li>To turn the LED on, pass 1 for ledOnMS and 0 for ledOffMS.</li> 620 * <li>To flash the LED, pass the number of milliseconds that it should 621 * be on and off to ledOnMS and ledOffMS.</li> 622 * </ul> 623 * <p> 624 * Since hardware varies, you are not guaranteed that any of the values 625 * you pass are honored exactly. Use the system defaults if possible 626 * because they will be set to values that work on any given hardware. 627 * <p> 628 * The alpha channel must be set for forward compatibility. 629 * 630 * @deprecated use {@link NotificationChannel#shouldShowLights()}. 631 */ 632 @Deprecated 633 public static final int FLAG_SHOW_LIGHTS = 0x00000001; 634 635 /** 636 * Bit to be bitwise-ored into the {@link #flags} field that should be 637 * set if this notification is in reference to something that is ongoing, 638 * like a phone call. It should not be set if this notification is in 639 * reference to something that happened at a particular point in time, 640 * like a missed phone call. 641 */ 642 public static final int FLAG_ONGOING_EVENT = 0x00000002; 643 644 /** 645 * Bit to be bitwise-ored into the {@link #flags} field that if set, 646 * the audio will be repeated until the notification is 647 * cancelled or the notification window is opened. 648 */ 649 public static final int FLAG_INSISTENT = 0x00000004; 650 651 /** 652 * Bit to be bitwise-ored into the {@link #flags} field that should be 653 * set if you would only like the sound, vibrate and ticker to be played 654 * if the notification was not already showing. 655 * 656 * Note that using this flag will stop any ongoing alerting behaviour such 657 * as sound, vibration or blinking notification LED. 658 */ 659 public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008; 660 661 /** 662 * Bit to be bitwise-ored into the {@link #flags} field that should be 663 * set if the notification should be canceled when it is clicked by the 664 * user. 665 */ 666 public static final int FLAG_AUTO_CANCEL = 0x00000010; 667 668 /** 669 * Bit to be bitwise-ored into the {@link #flags} field that should be 670 * set if the notification should not be canceled when the user clicks 671 * the Clear all button. 672 */ 673 public static final int FLAG_NO_CLEAR = 0x00000020; 674 675 /** 676 * Bit to be bitwise-ored into the {@link #flags} field that should be 677 * set if this notification represents a currently running service. This 678 * will normally be set for you by {@link Service#startForeground}. 679 */ 680 public static final int FLAG_FOREGROUND_SERVICE = 0x00000040; 681 682 /** 683 * Obsolete flag indicating high-priority notifications; use the priority field instead. 684 * 685 * @deprecated Use {@link #priority} with a positive value. 686 */ 687 @Deprecated 688 public static final int FLAG_HIGH_PRIORITY = 0x00000080; 689 690 /** 691 * Bit to be bitswise-ored into the {@link #flags} field that should be 692 * set if this notification is relevant to the current device only 693 * and it is not recommended that it bridge to other devices. 694 */ 695 public static final int FLAG_LOCAL_ONLY = 0x00000100; 696 697 /** 698 * Bit to be bitswise-ored into the {@link #flags} field that should be 699 * set if this notification is the group summary for a group of notifications. 700 * Grouped notifications may display in a cluster or stack on devices which 701 * support such rendering. Requires a group key also be set using {@link Builder#setGroup}. 702 */ 703 public static final int FLAG_GROUP_SUMMARY = 0x00000200; 704 705 /** 706 * Bit to be bitswise-ored into the {@link #flags} field that should be 707 * set if this notification is the group summary for an auto-group of notifications. 708 * 709 * @hide 710 */ 711 @SystemApi 712 public static final int FLAG_AUTOGROUP_SUMMARY = 0x00000400; 713 714 /** 715 * @hide 716 */ 717 public static final int FLAG_CAN_COLORIZE = 0x00000800; 718 719 /** 720 * Bit to be bitswised-ored into the {@link #flags} field that should be 721 * set by the system if this notification is showing as a bubble. 722 * 723 * Applications cannot set this flag directly; they should instead call 724 * {@link Notification.Builder#setBubbleMetadata(BubbleMetadata)} to 725 * request that a notification be displayed as a bubble, and then check 726 * this flag to see whether that request was honored by the system. 727 */ 728 public static final int FLAG_BUBBLE = 0x00001000; 729 730 /** 731 * Bit to be bitswised-ored into the {@link #flags} field that should be 732 * set by the system if this notification is not dismissible. 733 * 734 * This flag is for internal use only; applications cannot set this flag directly. 735 * @hide 736 */ 737 public static final int FLAG_NO_DISMISS = 0x00002000; 738 739 /** 740 * Bit to be bitwise-ORed into the {@link #flags} field that should be 741 * set by the system if the app that sent this notification does not have the permission to send 742 * full screen intents. 743 * 744 * This flag is for internal use only; applications cannot set this flag directly. 745 * @hide 746 */ 747 public static final int FLAG_FSI_REQUESTED_BUT_DENIED = 0x00004000; 748 749 /** 750 * Bit to be bitwise-ored into the {@link #flags} field that should be 751 * set if this notification represents a currently running user-initiated job. 752 * 753 * This flag is for internal use only; applications cannot set this flag directly. 754 * @hide 755 */ 756 @TestApi 757 public static final int FLAG_USER_INITIATED_JOB = 0x00008000; 758 759 /** 760 * Bit to be bitwise-ored into the {@link #flags} field that should be 761 * set if this notification has been lifetime extended due to a direct reply. 762 * 763 * This flag is for internal use only; applications cannot set this flag directly. 764 * @hide 765 */ 766 @FlaggedApi(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) 767 public static final int FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY = 0x00010000; 768 769 /** 770 * Bit to be bitwise-ored into the {@link #flags} field that should be 771 * set by the system if this notification is silent. 772 * 773 * This flag is for internal use only; applications cannot set this flag directly. 774 * @hide 775 */ 776 @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) 777 public static final int FLAG_SILENT = 1 << 17; //0x00020000 778 779 /** 780 * Bit to be bitwise-ored into the {@link #flags} field that should be 781 * set by the system if this notification is a promoted ongoing notification, both because it 782 * {@link #hasPromotableCharacteristics()} and the user has not disabled the feature for this 783 * app. 784 * 785 * Applications cannot set this flag directly, but the posting app and 786 * {@link android.service.notification.NotificationListenerService} can read it. 787 */ 788 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 789 public static final int FLAG_PROMOTED_ONGOING = 0x00040000; 790 791 private static final Set<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Set.of( 792 BigTextStyle.class, 793 BigPictureStyle.class, 794 InboxStyle.class, 795 MediaStyle.class, 796 DecoratedCustomViewStyle.class, 797 DecoratedMediaCustomViewStyle.class, 798 MessagingStyle.class, 799 CallStyle.class 800 ); 801 isPlatformStyle(Style style)802 private static boolean isPlatformStyle(Style style) { 803 if (style == null) { 804 return false; 805 } 806 807 if (PLATFORM_STYLE_CLASSES.contains(style.getClass())) { 808 return true; 809 } 810 811 if (Flags.apiRichOngoing()) { 812 return style.getClass() == ProgressStyle.class; 813 } 814 815 return false; 816 } 817 isStandardLayout(int layoutId)818 private static boolean isStandardLayout(int layoutId) { 819 if (Flags.notificationsRedesignTemplates()) { 820 return switch (layoutId) { 821 case R.layout.notification_2025_template_collapsed_base, 822 R.layout.notification_2025_template_expanded_base, 823 R.layout.notification_2025_template_heads_up_base, 824 R.layout.notification_2025_template_header, 825 R.layout.notification_2025_template_collapsed_conversation, 826 R.layout.notification_2025_template_expanded_conversation, 827 R.layout.notification_2025_template_collapsed_call, 828 R.layout.notification_2025_template_expanded_call, 829 R.layout.notification_2025_template_collapsed_messaging, 830 R.layout.notification_2025_template_expanded_messaging, 831 R.layout.notification_2025_template_collapsed_media, 832 R.layout.notification_2025_template_expanded_media, 833 R.layout.notification_2025_template_expanded_big_picture, 834 R.layout.notification_2025_template_expanded_big_text, 835 R.layout.notification_2025_template_expanded_inbox -> true; 836 case R.layout.notification_2025_template_expanded_progress 837 -> Flags.apiRichOngoing(); 838 default -> false; 839 }; 840 } 841 if (Flags.apiRichOngoing()) { 842 if (layoutId == R.layout.notification_template_material_progress) { 843 return true; 844 } 845 } 846 return STANDARD_LAYOUTS.contains(layoutId); 847 } 848 849 /** @hide */ 850 @IntDef(flag = true, prefix = {"FLAG_"}, value = { 851 FLAG_SHOW_LIGHTS, 852 FLAG_ONGOING_EVENT, 853 FLAG_INSISTENT, 854 FLAG_ONLY_ALERT_ONCE, 855 FLAG_AUTO_CANCEL, 856 FLAG_NO_CLEAR, 857 FLAG_FOREGROUND_SERVICE, 858 FLAG_HIGH_PRIORITY, 859 FLAG_LOCAL_ONLY, 860 FLAG_GROUP_SUMMARY, 861 FLAG_AUTOGROUP_SUMMARY, 862 FLAG_CAN_COLORIZE, 863 FLAG_BUBBLE, 864 FLAG_NO_DISMISS, 865 FLAG_FSI_REQUESTED_BUT_DENIED, 866 FLAG_USER_INITIATED_JOB, 867 FLAG_SILENT 868 }) 869 @Retention(RetentionPolicy.SOURCE) 870 public @interface NotificationFlags{}; 871 872 public int flags; 873 874 /** @hide */ 875 @IntDef(prefix = { "PRIORITY_" }, value = { 876 PRIORITY_DEFAULT, 877 PRIORITY_LOW, 878 PRIORITY_MIN, 879 PRIORITY_HIGH, 880 PRIORITY_MAX 881 }) 882 @Retention(RetentionPolicy.SOURCE) 883 public @interface Priority {} 884 885 /** 886 * Default notification {@link #priority}. If your application does not prioritize its own 887 * notifications, use this value for all notifications. 888 * 889 * @deprecated use {@link NotificationManager#IMPORTANCE_DEFAULT} instead. 890 */ 891 @Deprecated 892 public static final int PRIORITY_DEFAULT = 0; 893 894 /** 895 * Lower {@link #priority}, for items that are less important. The UI may choose to show these 896 * items smaller, or at a different position in the list, compared with your app's 897 * {@link #PRIORITY_DEFAULT} items. 898 * 899 * @deprecated use {@link NotificationManager#IMPORTANCE_LOW} instead. 900 */ 901 @Deprecated 902 public static final int PRIORITY_LOW = -1; 903 904 /** 905 * Lowest {@link #priority}; these items might not be shown to the user except under special 906 * circumstances, such as detailed notification logs. 907 * 908 * @deprecated use {@link NotificationManager#IMPORTANCE_MIN} instead. 909 */ 910 @Deprecated 911 public static final int PRIORITY_MIN = -2; 912 913 /** 914 * Higher {@link #priority}, for more important notifications or alerts. The UI may choose to 915 * show these items larger, or at a different position in notification lists, compared with 916 * your app's {@link #PRIORITY_DEFAULT} items. 917 * 918 * @deprecated use {@link NotificationManager#IMPORTANCE_HIGH} instead. 919 */ 920 @Deprecated 921 public static final int PRIORITY_HIGH = 1; 922 923 /** 924 * Highest {@link #priority}, for your application's most important items that require the 925 * user's prompt attention or input. 926 * 927 * @deprecated use {@link NotificationManager#IMPORTANCE_HIGH} instead. 928 */ 929 @Deprecated 930 public static final int PRIORITY_MAX = 2; 931 932 /** 933 * Relative priority for this notification. 934 * 935 * Priority is an indication of how much of the user's valuable attention should be consumed by 936 * this notification. Low-priority notifications may be hidden from the user in certain 937 * situations, while the user might be interrupted for a higher-priority notification. The 938 * system will make a determination about how to interpret this priority when presenting 939 * the notification. 940 * 941 * <p> 942 * A notification that is at least {@link #PRIORITY_HIGH} is more likely to be presented 943 * as a heads-up notification. 944 * </p> 945 * 946 * @deprecated use {@link NotificationChannel#getImportance()} instead. 947 */ 948 @Priority 949 @Deprecated 950 public int priority; 951 952 /** 953 * Accent color (an ARGB integer like the constants in {@link android.graphics.Color}) 954 * to be applied by the standard Style templates when presenting this notification. 955 * 956 * The current template design constructs a colorful header image by overlaying the 957 * {@link #icon} image (stenciled in white) atop a field of this color. Alpha components are 958 * ignored. 959 */ 960 @ColorInt 961 public int color = COLOR_DEFAULT; 962 963 /** 964 * Special value of {@link #color} telling the system not to decorate this notification with 965 * any special color but instead use default colors when presenting this notification. 966 */ 967 @ColorInt 968 public static final int COLOR_DEFAULT = 0; // AKA Color.TRANSPARENT 969 970 /** 971 * Special value of {@link #color} used as a place holder for an invalid color. 972 * @hide 973 */ 974 @ColorInt 975 public static final int COLOR_INVALID = 1; 976 977 /** 978 * Sphere of visibility of this notification, which affects how and when the SystemUI reveals 979 * the notification's presence and contents in untrusted situations (namely, on the secure 980 * lockscreen and during screen sharing). 981 * 982 * The default level, {@link #VISIBILITY_PRIVATE}, behaves exactly as notifications have always 983 * done on Android: The notification's {@link #icon} and {@link #tickerText} (if available) are 984 * shown in all situations, but the contents are only available if the device is unlocked for 985 * the appropriate user and there is no active screen sharing session. 986 * 987 * A more permissive policy can be expressed by {@link #VISIBILITY_PUBLIC}; such a notification 988 * can be read even in an "insecure" context (that is, above a secure lockscreen or while 989 * screen sharing with a remote viewer). 990 * To modify the public version of this notification—for example, to redact some portions—see 991 * {@link Builder#setPublicVersion(Notification)}. 992 * 993 * Finally, a notification can be made {@link #VISIBILITY_SECRET}, which will suppress its icon 994 * and ticker until the user has bypassed the lockscreen. 995 */ 996 public @Visibility int visibility; 997 998 /** @hide */ 999 @IntDef(prefix = { "VISIBILITY_" }, value = { 1000 VISIBILITY_PUBLIC, 1001 VISIBILITY_PRIVATE, 1002 VISIBILITY_SECRET, 1003 }) 1004 @Retention(RetentionPolicy.SOURCE) 1005 public @interface Visibility {} 1006 1007 /** 1008 * Notification visibility: Show this notification in its entirety on all lockscreens and while 1009 * screen sharing. 1010 * 1011 * {@see #visibility} 1012 */ 1013 public static final int VISIBILITY_PUBLIC = 1; 1014 1015 /** 1016 * Notification visibility: Show this notification on all lockscreens, but conceal sensitive or 1017 * private information on secure lockscreens. Conceal sensitive or private information while 1018 * screen sharing. 1019 * 1020 * {@see #visibility} 1021 */ 1022 public static final int VISIBILITY_PRIVATE = 0; 1023 1024 /** 1025 * Notification visibility: Do not reveal any part of this notification on a secure lockscreen 1026 * or while screen sharing. 1027 * 1028 * {@see #visibility} 1029 */ 1030 public static final int VISIBILITY_SECRET = -1; 1031 1032 /** 1033 * @hide 1034 */ 1035 @IntDef(prefix = "VISIBILITY_", value = { 1036 VISIBILITY_PUBLIC, 1037 VISIBILITY_PRIVATE, 1038 VISIBILITY_SECRET, 1039 NotificationManager.VISIBILITY_NO_OVERRIDE 1040 }) 1041 @Retention(RetentionPolicy.SOURCE) 1042 public @interface NotificationVisibilityOverride{}; 1043 1044 /** 1045 * Notification category: incoming call (voice or video) or similar synchronous communication request. 1046 */ 1047 public static final String CATEGORY_CALL = "call"; 1048 1049 /** 1050 * Notification category: map turn-by-turn navigation. 1051 */ 1052 public static final String CATEGORY_NAVIGATION = "navigation"; 1053 1054 /** 1055 * Notification category: incoming direct message (SMS, instant message, etc.). 1056 */ 1057 public static final String CATEGORY_MESSAGE = "msg"; 1058 1059 /** 1060 * Notification category: asynchronous bulk message (email). 1061 */ 1062 public static final String CATEGORY_EMAIL = "email"; 1063 1064 /** 1065 * Notification category: calendar event. 1066 */ 1067 public static final String CATEGORY_EVENT = "event"; 1068 1069 /** 1070 * Notification category: promotion or advertisement. 1071 */ 1072 public static final String CATEGORY_PROMO = "promo"; 1073 1074 /** 1075 * Notification category: alarm or timer. 1076 */ 1077 public static final String CATEGORY_ALARM = "alarm"; 1078 1079 /** 1080 * Notification category: progress of a long-running background operation. 1081 */ 1082 public static final String CATEGORY_PROGRESS = "progress"; 1083 1084 /** 1085 * Notification category: social network or sharing update. 1086 */ 1087 public static final String CATEGORY_SOCIAL = "social"; 1088 1089 /** 1090 * Notification category: error in background operation or authentication status. 1091 */ 1092 public static final String CATEGORY_ERROR = "err"; 1093 1094 /** 1095 * Notification category: media transport control for playback. 1096 */ 1097 public static final String CATEGORY_TRANSPORT = "transport"; 1098 1099 /** 1100 * Notification category: system or device status update. Reserved for system use. 1101 */ 1102 public static final String CATEGORY_SYSTEM = "sys"; 1103 1104 /** 1105 * Notification category: indication of running background service. 1106 */ 1107 public static final String CATEGORY_SERVICE = "service"; 1108 1109 /** 1110 * Notification category: a specific, timely recommendation for a single thing. 1111 * For example, a news app might want to recommend a news story it believes the user will 1112 * want to read next. 1113 */ 1114 public static final String CATEGORY_RECOMMENDATION = "recommendation"; 1115 1116 /** 1117 * Notification category: ongoing information about device or contextual status. 1118 */ 1119 public static final String CATEGORY_STATUS = "status"; 1120 1121 /** 1122 * Notification category: user-scheduled reminder. 1123 */ 1124 public static final String CATEGORY_REMINDER = "reminder"; 1125 1126 /** 1127 * Notification category: extreme car emergencies. 1128 * @hide 1129 */ 1130 @SystemApi 1131 public static final String CATEGORY_CAR_EMERGENCY = "car_emergency"; 1132 1133 /** 1134 * Notification category: car warnings. 1135 * @hide 1136 */ 1137 @SystemApi 1138 public static final String CATEGORY_CAR_WARNING = "car_warning"; 1139 1140 /** 1141 * Notification category: general car system information. 1142 * @hide 1143 */ 1144 @SystemApi 1145 public static final String CATEGORY_CAR_INFORMATION = "car_information"; 1146 1147 /** 1148 * Notification category: tracking a user's workout. 1149 */ 1150 public static final String CATEGORY_WORKOUT = "workout"; 1151 1152 /** 1153 * Notification category: temporarily sharing location. 1154 */ 1155 public static final String CATEGORY_LOCATION_SHARING = "location_sharing"; 1156 1157 /** 1158 * Notification category: running stopwatch. 1159 */ 1160 public static final String CATEGORY_STOPWATCH = "stopwatch"; 1161 1162 /** 1163 * Notification category: missed call. 1164 */ 1165 public static final String CATEGORY_MISSED_CALL = "missed_call"; 1166 1167 /** 1168 * Notification category: voicemail. 1169 */ 1170 @FlaggedApi(Flags.FLAG_CATEGORY_VOICEMAIL) 1171 public static final String CATEGORY_VOICEMAIL = "voicemail"; 1172 1173 /** 1174 * One of the predefined notification categories (see the <code>CATEGORY_*</code> constants) 1175 * that best describes this Notification. May be used by the system for ranking and filtering. 1176 */ 1177 public String category; 1178 1179 @UnsupportedAppUsage 1180 private String mGroupKey; 1181 1182 /** 1183 * Get the key used to group this notification into a cluster or stack 1184 * with other notifications on devices which support such rendering. 1185 */ getGroup()1186 public String getGroup() { 1187 return mGroupKey; 1188 } 1189 1190 private String mSortKey; 1191 1192 /** 1193 * Get a sort key that orders this notification among other notifications from the 1194 * same package. This can be useful if an external sort was already applied and an app 1195 * would like to preserve this. Notifications will be sorted lexicographically using this 1196 * value, although providing different priorities in addition to providing sort key may 1197 * cause this value to be ignored. 1198 * 1199 * <p>This sort key can also be used to order members of a notification group. See 1200 * {@link Builder#setGroup}. 1201 * 1202 * @see String#compareTo(String) 1203 */ getSortKey()1204 public String getSortKey() { 1205 return mSortKey; 1206 } 1207 1208 /** 1209 * Additional semantic data to be carried around with this Notification. 1210 * <p> 1211 * The extras keys defined here are intended to capture the original inputs to {@link Builder} 1212 * APIs, and are intended to be used by 1213 * {@link android.service.notification.NotificationListenerService} implementations to extract 1214 * detailed information from notification objects. 1215 */ 1216 public Bundle extras = new Bundle(); 1217 1218 /** 1219 * All pending intents in the notification as the system needs to be able to access them but 1220 * touching the extras bundle in the system process is not safe because the bundle may contain 1221 * custom parcelable objects. 1222 * 1223 * @hide 1224 */ 1225 @UnsupportedAppUsage 1226 public ArraySet<PendingIntent> allPendingIntents; 1227 1228 /** 1229 * Token identifying the notification that is applying doze/bgcheck allowlisting to the 1230 * pending intents inside of it, so only those will get the behavior. 1231 * 1232 * @hide 1233 */ 1234 private IBinder mAllowlistToken; 1235 1236 /** 1237 * Must be set by a process to start associating tokens with Notification objects 1238 * coming in to it. This is set by NotificationManagerService. 1239 * 1240 * @hide 1241 */ 1242 static public IBinder processAllowlistToken; 1243 1244 /** 1245 * {@link #extras} key: this is the title of the notification, 1246 * as supplied to {@link Builder#setContentTitle(CharSequence)}. 1247 */ 1248 public static final String EXTRA_TITLE = "android.title"; 1249 1250 /** 1251 * {@link #extras} key: this is the title of the notification when shown in expanded form, 1252 * e.g. as supplied to {@link BigTextStyle#setBigContentTitle(CharSequence)}. 1253 */ 1254 public static final String EXTRA_TITLE_BIG = EXTRA_TITLE + ".big"; 1255 1256 /** 1257 * {@link #extras} key: this is the main text payload, as supplied to 1258 * {@link Builder#setContentText(CharSequence)}. 1259 */ 1260 public static final String EXTRA_TEXT = "android.text"; 1261 1262 /** 1263 * {@link #extras} key: this is a third line of text, as supplied to 1264 * {@link Builder#setSubText(CharSequence)}. 1265 */ 1266 public static final String EXTRA_SUB_TEXT = "android.subText"; 1267 1268 /** 1269 * {@link #extras} key: this is the remote input history, as supplied to 1270 * {@link Builder#setRemoteInputHistory(CharSequence[])}. 1271 * 1272 * Apps can fill this through {@link Builder#setRemoteInputHistory(CharSequence[])} 1273 * with the most recent inputs that have been sent through a {@link RemoteInput} of this 1274 * Notification and are expected to clear it once the it is no longer relevant (e.g. for chat 1275 * notifications once the other party has responded). 1276 * 1277 * The extra with this key is of type CharSequence[] and contains the most recent entry at 1278 * the 0 index, the second most recent at the 1 index, etc. 1279 * 1280 * @see Builder#setRemoteInputHistory(CharSequence[]) 1281 */ 1282 public static final String EXTRA_REMOTE_INPUT_HISTORY = "android.remoteInputHistory"; 1283 1284 1285 /** 1286 * {@link #extras} key: this is a remote input history which can include media messages 1287 * in addition to text, as supplied to 1288 * {@link Builder#setRemoteInputHistory(RemoteInputHistoryItem[])} or 1289 * {@link Builder#setRemoteInputHistory(CharSequence[])}. 1290 * 1291 * SystemUI can populate this through 1292 * {@link Builder#setRemoteInputHistory(RemoteInputHistoryItem[])} with the most recent inputs 1293 * that have been sent through a {@link RemoteInput} of this Notification. These items can 1294 * represent either media content (specified by a URI and a MIME type) or a text message 1295 * (described by a CharSequence). 1296 * 1297 * To maintain compatibility, this can also be set by apps with 1298 * {@link Builder#setRemoteInputHistory(CharSequence[])}, which will create a 1299 * {@link RemoteInputHistoryItem} for each of the provided text-only messages. 1300 * 1301 * The extra with this key is of type {@link RemoteInputHistoryItem[]} and contains the most 1302 * recent entry at the 0 index, the second most recent at the 1 index, etc. 1303 * 1304 * @see Builder#setRemoteInputHistory(RemoteInputHistoryItem[]) 1305 * @hide 1306 */ 1307 public static final String EXTRA_REMOTE_INPUT_HISTORY_ITEMS = "android.remoteInputHistoryItems"; 1308 1309 /** 1310 * {@link #extras} key: boolean as supplied to 1311 * {@link Builder#setShowRemoteInputSpinner(boolean)}. 1312 * 1313 * If set to true, then the view displaying the remote input history from 1314 * {@link Builder#setRemoteInputHistory(CharSequence[])} will have a progress spinner. 1315 * 1316 * @see Builder#setShowRemoteInputSpinner(boolean) 1317 * @hide 1318 */ 1319 public static final String EXTRA_SHOW_REMOTE_INPUT_SPINNER = "android.remoteInputSpinner"; 1320 1321 /** 1322 * {@link #extras} key: boolean as supplied to 1323 * {@link Builder#setHideSmartReplies(boolean)}. 1324 * 1325 * If set to true, then any smart reply buttons will be hidden. 1326 * 1327 * @see Builder#setHideSmartReplies(boolean) 1328 * @hide 1329 */ 1330 public static final String EXTRA_HIDE_SMART_REPLIES = "android.hideSmartReplies"; 1331 1332 /** 1333 * {@link #extras} key: this is a small piece of additional text as supplied to 1334 * {@link Builder#setContentInfo(CharSequence)}. 1335 */ 1336 public static final String EXTRA_INFO_TEXT = "android.infoText"; 1337 1338 /** 1339 * {@link #extras} key: this is a line of summary information intended to be shown 1340 * alongside expanded notifications, as supplied to (e.g.) 1341 * {@link BigTextStyle#setSummaryText(CharSequence)}. 1342 */ 1343 public static final String EXTRA_SUMMARY_TEXT = "android.summaryText"; 1344 1345 /** 1346 * {@link #extras} key: this is the longer text shown in the expanded form of a 1347 * {@link BigTextStyle} notification, as supplied to 1348 * {@link BigTextStyle#bigText(CharSequence)}. 1349 */ 1350 public static final String EXTRA_BIG_TEXT = "android.bigText"; 1351 1352 /** 1353 * {@link #extras} key: very short text summarizing the most critical information contained in 1354 * the notification. 1355 * 1356 * @hide 1357 */ 1358 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 1359 public static final String EXTRA_SHORT_CRITICAL_TEXT = "android.shortCriticalText"; 1360 1361 /** 1362 * {@link #extras} key: this is the resource ID of the notification's main small icon, as 1363 * supplied to {@link Builder#setSmallIcon(int)}. 1364 * 1365 * @deprecated Use {@link #getSmallIcon()}, which supports a wider variety of icon sources. 1366 */ 1367 @Deprecated 1368 public static final String EXTRA_SMALL_ICON = "android.icon"; 1369 1370 /** 1371 * {@link #extras} key: this is a bitmap to be used instead of the small icon when showing the 1372 * notification payload, as 1373 * supplied to {@link Builder#setLargeIcon(android.graphics.Bitmap)}. 1374 * 1375 * @deprecated Use {@link #getLargeIcon()}, which supports a wider variety of icon sources. 1376 */ 1377 @Deprecated 1378 public static final String EXTRA_LARGE_ICON = "android.largeIcon"; 1379 1380 /** 1381 * {@link #extras} key: this is a bitmap to be used instead of the one from 1382 * {@link Builder#setLargeIcon(android.graphics.Bitmap)} when the notification is 1383 * shown in its expanded form, as supplied to 1384 * {@link BigPictureStyle#bigLargeIcon(android.graphics.Bitmap)}. 1385 */ 1386 public static final String EXTRA_LARGE_ICON_BIG = EXTRA_LARGE_ICON + ".big"; 1387 1388 /** 1389 * {@link #extras} key: this is the progress value supplied to 1390 * {@link Builder#setProgress(int, int, boolean)}. 1391 */ 1392 public static final String EXTRA_PROGRESS = "android.progress"; 1393 1394 /** 1395 * {@link #extras} key: this is the maximum value supplied to 1396 * {@link Builder#setProgress(int, int, boolean)}. 1397 */ 1398 public static final String EXTRA_PROGRESS_MAX = "android.progressMax"; 1399 1400 /** 1401 * {@link #extras} key: whether the progress bar is indeterminate, supplied to 1402 * {@link Builder#setProgress(int, int, boolean)}. 1403 */ 1404 public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate"; 1405 1406 /** 1407 * {@link #extras} key: whether {@link #when} should be shown as a count-up timer (specifically 1408 * a {@link android.widget.Chronometer}) instead of a timestamp, as supplied to 1409 * {@link Builder#setUsesChronometer(boolean)}. 1410 */ 1411 public static final String EXTRA_SHOW_CHRONOMETER = "android.showChronometer"; 1412 1413 /** 1414 * {@link #extras} key: whether the chronometer set on the notification should count down 1415 * instead of counting up. Is only relevant if key {@link #EXTRA_SHOW_CHRONOMETER} is present. 1416 * This extra is a boolean. The default is false. 1417 */ 1418 public static final String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown"; 1419 1420 /** 1421 * {@link #extras} key: whether {@link #when} should be shown, 1422 * as supplied to {@link Builder#setShowWhen(boolean)}. 1423 */ 1424 public static final String EXTRA_SHOW_WHEN = "android.showWhen"; 1425 1426 /** 1427 * {@link #extras} key: this is a bitmap to be shown in {@link BigPictureStyle} expanded 1428 * notifications, supplied to {@link BigPictureStyle#bigPicture(android.graphics.Bitmap)}. 1429 */ 1430 public static final String EXTRA_PICTURE = "android.picture"; 1431 1432 /** 1433 * {@link #extras} key: this is an {@link Icon} of an image to be 1434 * shown in {@link BigPictureStyle} expanded notifications, supplied to 1435 * {@link BigPictureStyle#bigPicture(Icon)}. 1436 */ 1437 public static final String EXTRA_PICTURE_ICON = "android.pictureIcon"; 1438 1439 /** 1440 * {@link #extras} key: this is a content description of the big picture supplied from 1441 * {@link BigPictureStyle#bigPicture(Bitmap)}, supplied to 1442 * {@link BigPictureStyle#setContentDescription(CharSequence)}. 1443 */ 1444 public static final String EXTRA_PICTURE_CONTENT_DESCRIPTION = 1445 "android.pictureContentDescription"; 1446 1447 /** 1448 * {@link #extras} key: this is a boolean to indicate that the 1449 * {@link BigPictureStyle#bigPicture(Bitmap) big picture} is to be shown in the collapsed state 1450 * of a {@link BigPictureStyle} notification. This will replace a 1451 * {@link Builder#setLargeIcon(Icon) large icon} in that state if one was provided. 1452 */ 1453 public static final String EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED = 1454 "android.showBigPictureWhenCollapsed"; 1455 1456 /** 1457 * {@link #extras} key: An array of CharSequences to show in {@link InboxStyle} expanded 1458 * notifications, each of which was supplied to {@link InboxStyle#addLine(CharSequence)}. 1459 */ 1460 public static final String EXTRA_TEXT_LINES = "android.textLines"; 1461 1462 /** 1463 * {@link #extras} key: A string representing the name of the specific 1464 * {@link android.app.Notification.Style} used to create this notification. 1465 */ 1466 public static final String EXTRA_TEMPLATE = "android.template"; 1467 1468 /** 1469 * {@link #extras} key: A String array containing the people that this notification relates to, 1470 * each of which was supplied to {@link Builder#addPerson(String)}. 1471 * 1472 * @deprecated the actual objects are now in {@link #EXTRA_PEOPLE_LIST} 1473 */ 1474 public static final String EXTRA_PEOPLE = "android.people"; 1475 1476 /** 1477 * {@link #extras} key: An arrayList of {@link Person} objects containing the people that 1478 * this notification relates to. 1479 */ 1480 public static final String EXTRA_PEOPLE_LIST = "android.people.list"; 1481 1482 /** 1483 * Allow certain system-generated notifications to appear before the device is provisioned. 1484 * Only available to notifications coming from the android package. 1485 * @hide 1486 */ 1487 @SystemApi 1488 @RequiresPermission(android.Manifest.permission.NOTIFICATION_DURING_SETUP) 1489 public static final String EXTRA_ALLOW_DURING_SETUP = "android.allowDuringSetup"; 1490 1491 /** 1492 * {@link #extras} key: 1493 * flat {@link String} representation of a {@link android.content.ContentUris content URI} 1494 * pointing to an image that can be displayed in the background when the notification is 1495 * selected. Used on television platforms. The URI must point to an image stream suitable for 1496 * passing into {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) 1497 * BitmapFactory.decodeStream}; all other content types will be ignored. 1498 */ 1499 public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; 1500 1501 /** 1502 * {@link #extras} key: A 1503 * {@link android.media.session.MediaSession.Token} associated with a 1504 * {@link android.app.Notification.MediaStyle} notification. 1505 */ 1506 public static final String EXTRA_MEDIA_SESSION = "android.mediaSession"; 1507 1508 /** 1509 * {@link #extras} key: A {@code CharSequence} name of a remote device used for a media session 1510 * associated with a {@link Notification.MediaStyle} notification. This will show in the media 1511 * controls output switcher instead of the local device name. 1512 * @hide 1513 */ 1514 @TestApi 1515 public static final String EXTRA_MEDIA_REMOTE_DEVICE = "android.mediaRemoteDevice"; 1516 1517 /** 1518 * {@link #extras} key: A {@code int} resource ID for an icon that should show in the output 1519 * switcher of the media controls for a {@link Notification.MediaStyle} notification. 1520 * @hide 1521 */ 1522 @TestApi 1523 public static final String EXTRA_MEDIA_REMOTE_ICON = "android.mediaRemoteIcon"; 1524 1525 /** 1526 * {@link #extras} key: A {@code PendingIntent} that will replace the default action for the 1527 * media controls output switcher chip, associated with a {@link Notification.MediaStyle} 1528 * notification. This should launch an activity. 1529 * @hide 1530 */ 1531 @TestApi 1532 public static final String EXTRA_MEDIA_REMOTE_INTENT = "android.mediaRemoteIntent"; 1533 1534 /** 1535 * {@link #extras} key: the indices of actions to be shown in the compact view, 1536 * as supplied to (e.g.) {@link MediaStyle#setShowActionsInCompactView(int...)}. 1537 */ 1538 public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions"; 1539 1540 /** 1541 * {@link #extras} key: the username to be displayed for all messages sent by the user including 1542 * direct replies 1543 * {@link android.app.Notification.MessagingStyle} notification. This extra is a 1544 * {@link CharSequence} 1545 * 1546 * @deprecated use {@link #EXTRA_MESSAGING_PERSON} 1547 */ 1548 public static final String EXTRA_SELF_DISPLAY_NAME = "android.selfDisplayName"; 1549 1550 /** 1551 * {@link #extras} key: the person to be displayed for all messages sent by the user including 1552 * direct replies 1553 * {@link android.app.Notification.MessagingStyle} notification. This extra is a 1554 * {@link Person} 1555 */ 1556 public static final String EXTRA_MESSAGING_PERSON = "android.messagingUser"; 1557 1558 /** 1559 * {@link #extras} key: a {@link CharSequence} to be displayed as the title to a conversation 1560 * represented by a {@link android.app.Notification.MessagingStyle} 1561 */ 1562 public static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle"; 1563 1564 /** @hide */ 1565 public static final String EXTRA_CONVERSATION_ICON = "android.conversationIcon"; 1566 1567 /** @hide */ 1568 public static final String EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT = 1569 "android.conversationUnreadMessageCount"; 1570 1571 /** 1572 * {@link #extras} key: an array of {@link android.app.Notification.MessagingStyle.Message} 1573 * bundles provided by a 1574 * {@link android.app.Notification.MessagingStyle} notification. This extra is a parcelable 1575 * array of bundles. 1576 */ 1577 public static final String EXTRA_MESSAGES = "android.messages"; 1578 1579 /** 1580 * {@link #extras} key: an array of 1581 * {@link android.app.Notification.MessagingStyle#addHistoricMessage historic} 1582 * {@link android.app.Notification.MessagingStyle.Message} bundles provided by a 1583 * {@link android.app.Notification.MessagingStyle} notification. This extra is a parcelable 1584 * array of bundles. 1585 */ 1586 public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic"; 1587 1588 /** 1589 * {@link #extras} key: whether the {@link android.app.Notification.MessagingStyle} notification 1590 * represents a group conversation. 1591 */ 1592 public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation"; 1593 1594 /** 1595 * {@link #extras} key: the type of call represented by the 1596 * {@link android.app.Notification.CallStyle} notification. This extra is an int. 1597 */ 1598 public static final String EXTRA_CALL_TYPE = "android.callType"; 1599 1600 /** 1601 * {@link #extras} key: whether the {@link android.app.Notification.CallStyle} notification 1602 * is for a call that will activate video when answered. This extra is a boolean. 1603 */ 1604 public static final String EXTRA_CALL_IS_VIDEO = "android.callIsVideo"; 1605 1606 /** 1607 * {@link #extras} key: the person to be displayed as calling for the 1608 * {@link android.app.Notification.CallStyle} notification. This extra is a {@link Person}. 1609 */ 1610 public static final String EXTRA_CALL_PERSON = "android.callPerson"; 1611 1612 /** 1613 * {@link #extras} key: the icon to be displayed as a verification status of the caller on a 1614 * {@link android.app.Notification.CallStyle} notification. This extra is an {@link Icon}. 1615 */ 1616 public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon"; 1617 1618 /** 1619 * {@link #extras} key: the text to be displayed as a verification status of the caller on a 1620 * {@link android.app.Notification.CallStyle} notification. This extra is a 1621 * {@link CharSequence}. 1622 */ 1623 public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText"; 1624 1625 /** 1626 * {@link #extras} key: the intent to be sent when the users answers a 1627 * {@link android.app.Notification.CallStyle} notification. This extra is a 1628 * {@link PendingIntent}. 1629 */ 1630 public static final String EXTRA_ANSWER_INTENT = "android.answerIntent"; 1631 1632 /** 1633 * {@link #extras} key: the intent to be sent when the users declines a 1634 * {@link android.app.Notification.CallStyle} notification. This extra is a 1635 * {@link PendingIntent}. 1636 */ 1637 public static final String EXTRA_DECLINE_INTENT = "android.declineIntent"; 1638 1639 /** 1640 * {@link #extras} key: the intent to be sent when the users hangs up a 1641 * {@link android.app.Notification.CallStyle} notification. This extra is a 1642 * {@link PendingIntent}. 1643 */ 1644 public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent"; 1645 1646 /** 1647 * {@link #extras} key: the color used as a hint for the Answer action button of a 1648 * {@link android.app.Notification.CallStyle} notification. This extra is a {@code ColorInt}. 1649 */ 1650 public static final String EXTRA_ANSWER_COLOR = "android.answerColor"; 1651 1652 /** 1653 * {@link #extras} key: the color used as a hint for the Decline or Hang Up action button of a 1654 * {@link android.app.Notification.CallStyle} notification. This extra is a {@code ColorInt}. 1655 */ 1656 public static final String EXTRA_DECLINE_COLOR = "android.declineColor"; 1657 1658 /** 1659 * {@link #extras} key: whether the notification should be colorized as 1660 * supplied to {@link Builder#setColorized(boolean)}. 1661 */ 1662 public static final String EXTRA_COLORIZED = "android.colorized"; 1663 1664 /** 1665 * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Segment} 1666 * bundles provided by a 1667 * {@link android.app.Notification.ProgressStyle} notification as supplied to 1668 * {@link ProgressStyle#setProgressSegments} 1669 * or {@link ProgressStyle#addProgressSegment(ProgressStyle.Segment)}. 1670 * This extra is a parcelable array list of bundles. 1671 * @hide 1672 */ 1673 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 1674 public static final String EXTRA_PROGRESS_SEGMENTS = "android.progressSegments"; 1675 1676 /** 1677 * {@link #extras} key: an arraylist of {@link ProgressStyle.Point} 1678 * bundles provided by a 1679 * {@link android.app.Notification.ProgressStyle} notification as supplied to 1680 * {@link ProgressStyle#setProgressPoints} 1681 * or {@link ProgressStyle#addProgressPoint(ProgressStyle.Point)}. 1682 * This extra is a parcelable array list of bundles. 1683 * @hide 1684 */ 1685 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 1686 public static final String EXTRA_PROGRESS_POINTS = "android.progressPoints"; 1687 1688 /** 1689 * {@link #extras} key: whether the progress bar should be styled by its progress as 1690 * supplied to {@link ProgressStyle#setStyledByProgress}. 1691 * This extra is a boolean. 1692 * @hide 1693 */ 1694 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 1695 public static final String EXTRA_STYLED_BY_PROGRESS = "android.styledByProgress"; 1696 1697 /** 1698 * {@link #extras} key: this is an {@link Icon} of an image to be 1699 * shown as progress bar progress tracker icon in {@link ProgressStyle}, supplied to 1700 *{@link ProgressStyle#setProgressTrackerIcon(Icon)}. 1701 * @hide 1702 */ 1703 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 1704 public static final String EXTRA_PROGRESS_TRACKER_ICON = "android.progressTrackerIcon"; 1705 1706 /** 1707 * {@link #extras} key: this is an {@link Icon} of an image to be 1708 * shown at the beginning of the progress bar in {@link ProgressStyle}, supplied to 1709 *{@link ProgressStyle#setProgressStartIcon(Icon)}. 1710 * @hide 1711 */ 1712 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 1713 public static final String EXTRA_PROGRESS_START_ICON = "android.progressStartIcon"; 1714 1715 /** 1716 * {@link #extras} key: this is an {@link Icon} of an image to be 1717 * shown at the end of the progress bar in {@link ProgressStyle}, supplied to 1718 *{@link ProgressStyle#setProgressEndIcon(Icon)}. 1719 * @hide 1720 */ 1721 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 1722 public static final String EXTRA_PROGRESS_END_ICON = "android.progressEndIcon"; 1723 1724 /** 1725 * @hide 1726 */ 1727 public static final String EXTRA_BUILDER_APPLICATION_INFO = "android.appInfo"; 1728 1729 /** 1730 * @hide 1731 */ 1732 public static final String EXTRA_CONTAINS_CUSTOM_VIEW = "android.contains.customView"; 1733 1734 /** 1735 * @hide 1736 */ 1737 public static final String EXTRA_REDUCED_IMAGES = "android.reduced.images"; 1738 1739 /** 1740 * {@link #extras} key: the audio contents of this notification. 1741 * 1742 * This is for use when rendering the notification on an audio-focused interface; 1743 * the audio contents are a complete sound sample that contains the contents/body of the 1744 * notification. This may be used in substitute of a Text-to-Speech reading of the 1745 * notification. For example if the notification represents a voice message this should point 1746 * to the audio of that message. 1747 * 1748 * The data stored under this key should be a String representation of a Uri that contains the 1749 * audio contents in one of the following formats: WAV, PCM 16-bit, AMR-WB. 1750 * 1751 * This extra is unnecessary if you are using {@code MessagingStyle} since each {@code Message} 1752 * has a field for holding data URI. That field can be used for audio. 1753 * See {@code Message#setData}. 1754 * 1755 * Example usage: 1756 * <pre> 1757 * {@code 1758 * Notification.Builder myBuilder = (build your Notification as normal); 1759 * myBuilder.getExtras().putString(EXTRA_AUDIO_CONTENTS_URI, myAudioUri.toString()); 1760 * } 1761 * </pre> 1762 */ 1763 public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents"; 1764 1765 /** @hide */ 1766 @SystemApi 1767 @RequiresPermission(android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME) 1768 public static final String EXTRA_SUBSTITUTE_APP_NAME = "android.substName"; 1769 1770 /** 1771 * This is set on the notifications shown by system_server about apps running foreground 1772 * services. It indicates that the notification should be shown 1773 * only if any of the given apps do not already have a properly tagged 1774 * {@link #FLAG_FOREGROUND_SERVICE} notification currently visible to the user. 1775 * This is a string array of all package names of the apps. 1776 * @hide 1777 */ 1778 public static final String EXTRA_FOREGROUND_APPS = "android.foregroundApps"; 1779 1780 /** 1781 * @hide 1782 */ 1783 public static final String EXTRA_SUMMARIZED_CONTENT = "android.summarization"; 1784 1785 @UnsupportedAppUsage 1786 private Icon mSmallIcon; 1787 @UnsupportedAppUsage 1788 private Icon mLargeIcon; 1789 1790 @UnsupportedAppUsage 1791 private String mChannelId; 1792 private long mTimeout; 1793 1794 private String mShortcutId; 1795 private LocusId mLocusId; 1796 private CharSequence mSettingsText; 1797 1798 private BubbleMetadata mBubbleMetadata; 1799 1800 /** @hide */ 1801 @IntDef(prefix = { "GROUP_ALERT_" }, value = { 1802 GROUP_ALERT_ALL, GROUP_ALERT_CHILDREN, GROUP_ALERT_SUMMARY 1803 }) 1804 @Retention(RetentionPolicy.SOURCE) 1805 public @interface GroupAlertBehavior {} 1806 1807 /** 1808 * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all notifications in a 1809 * group with sound or vibration ought to make sound or vibrate (respectively), so this 1810 * notification will not be muted when it is in a group. 1811 */ 1812 public static final int GROUP_ALERT_ALL = 0; 1813 1814 /** 1815 * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all children 1816 * notification in a group should be silenced (no sound or vibration) even if they are posted 1817 * to a {@link NotificationChannel} that has sound and/or vibration. Use this constant to 1818 * mute this notification if this notification is a group child. This must be applied to all 1819 * children notifications you want to mute. 1820 * 1821 * <p> For example, you might want to use this constant if you post a number of children 1822 * notifications at once (say, after a periodic sync), and only need to notify the user 1823 * audibly once. 1824 */ 1825 public static final int GROUP_ALERT_SUMMARY = 1; 1826 1827 /** 1828 * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that the summary 1829 * notification in a group should be silenced (no sound or vibration) even if they are 1830 * posted to a {@link NotificationChannel} that has sound and/or vibration. Use this constant 1831 * to mute this notification if this notification is a group summary. 1832 * 1833 * <p>For example, you might want to use this constant if only the children notifications 1834 * in your group have content and the summary is only used to visually group notifications 1835 * rather than to alert the user that new information is available. 1836 */ 1837 public static final int GROUP_ALERT_CHILDREN = 2; 1838 1839 /** 1840 * Constant for the {@link Builder#setGroup(String) group key} that is added to notifications 1841 * that are not already grouped when {@link Builder#setSilent()} is used. 1842 * 1843 * @hide 1844 */ 1845 @Deprecated 1846 public static final String GROUP_KEY_SILENT = "silent"; 1847 1848 private int mGroupAlertBehavior = GROUP_ALERT_ALL; 1849 1850 /** 1851 * If this notification is being shown as a badge, always show as a number. 1852 */ 1853 public static final int BADGE_ICON_NONE = 0; 1854 1855 /** 1856 * If this notification is being shown as a badge, use the {@link #getSmallIcon()} to 1857 * represent this notification. 1858 */ 1859 public static final int BADGE_ICON_SMALL = 1; 1860 1861 /** 1862 * If this notification is being shown as a badge, use the {@link #getLargeIcon()} to 1863 * represent this notification. 1864 */ 1865 public static final int BADGE_ICON_LARGE = 2; 1866 private int mBadgeIcon = BADGE_ICON_NONE; 1867 1868 /** 1869 * Determines whether the platform can generate contextual actions for a notification. 1870 */ 1871 private boolean mAllowSystemGeneratedContextualActions = true; 1872 1873 /** 1874 * Structure to encapsulate a named action that can be shown as part of this notification. 1875 * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is 1876 * selected by the user. 1877 * <p> 1878 * Apps should use {@link Notification.Builder#addAction(int, CharSequence, PendingIntent)} 1879 * or {@link Notification.Builder#addAction(Notification.Action)} 1880 * to attach actions. 1881 * <p> 1882 * As of Android {@link android.os.Build.VERSION_CODES#S}, apps targeting API level {@link 1883 * android.os.Build.VERSION_CODES#S} or higher won't be able to start activities while 1884 * processing broadcast receivers or services in response to notification action clicks. To 1885 * launch an activity in those cases, provide a {@link PendingIntent} for the activity itself. 1886 */ 1887 public static class Action implements Parcelable { 1888 /** 1889 * {@link #extras} key: Keys to a {@link Parcelable} {@link ArrayList} of 1890 * {@link RemoteInput}s. 1891 * 1892 * This is intended for {@link RemoteInput}s that only accept data, meaning 1893 * {@link RemoteInput#getAllowFreeFormInput} is false, {@link RemoteInput#getChoices} 1894 * is null or empty, and {@link RemoteInput#getAllowedDataTypes} is non-null and not 1895 * empty. These {@link RemoteInput}s will be ignored by devices that do not 1896 * support non-text-based {@link RemoteInput}s. See {@link Builder#build}. 1897 * 1898 * You can test if a RemoteInput matches these constraints using 1899 * {@link RemoteInput#isDataOnly}. 1900 */ 1901 private static final String EXTRA_DATA_ONLY_INPUTS = "android.extra.DATA_ONLY_INPUTS"; 1902 1903 /** 1904 * No semantic action defined. 1905 */ 1906 public static final int SEMANTIC_ACTION_NONE = 0; 1907 1908 /** 1909 * {@code SemanticAction}: Reply to a conversation, chat, group, or wherever replies 1910 * may be appropriate. 1911 */ 1912 public static final int SEMANTIC_ACTION_REPLY = 1; 1913 1914 /** 1915 * {@code SemanticAction}: Mark content as read. 1916 */ 1917 public static final int SEMANTIC_ACTION_MARK_AS_READ = 2; 1918 1919 /** 1920 * {@code SemanticAction}: Mark content as unread. 1921 */ 1922 public static final int SEMANTIC_ACTION_MARK_AS_UNREAD = 3; 1923 1924 /** 1925 * {@code SemanticAction}: Delete the content associated with the notification. This 1926 * could mean deleting an email, message, etc. 1927 */ 1928 public static final int SEMANTIC_ACTION_DELETE = 4; 1929 1930 /** 1931 * {@code SemanticAction}: Archive the content associated with the notification. This 1932 * could mean archiving an email, message, etc. 1933 */ 1934 public static final int SEMANTIC_ACTION_ARCHIVE = 5; 1935 1936 /** 1937 * {@code SemanticAction}: Mute the content associated with the notification. This could 1938 * mean silencing a conversation or currently playing media. 1939 */ 1940 public static final int SEMANTIC_ACTION_MUTE = 6; 1941 1942 /** 1943 * {@code SemanticAction}: Unmute the content associated with the notification. This could 1944 * mean un-silencing a conversation or currently playing media. 1945 */ 1946 public static final int SEMANTIC_ACTION_UNMUTE = 7; 1947 1948 /** 1949 * {@code SemanticAction}: Mark content with a thumbs up. 1950 */ 1951 public static final int SEMANTIC_ACTION_THUMBS_UP = 8; 1952 1953 /** 1954 * {@code SemanticAction}: Mark content with a thumbs down. 1955 */ 1956 public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9; 1957 1958 /** 1959 * {@code SemanticAction}: Call a contact, group, etc. 1960 */ 1961 public static final int SEMANTIC_ACTION_CALL = 10; 1962 1963 /** 1964 * {@code SemanticAction}: Mark the conversation associated with the notification as a 1965 * priority. Note that this is only for use by the notification assistant services. The 1966 * type will be ignored for actions an app adds to its own notifications. 1967 * @hide 1968 */ 1969 @SystemApi 1970 public static final int SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY = 11; 1971 1972 /** 1973 * {@code SemanticAction}: Mark content as a potential phishing attempt. 1974 * Note that this is only for use by the notification assistant services. The type will 1975 * be ignored for actions an app adds to its own notifications. 1976 * @hide 1977 */ 1978 @SystemApi 1979 public static final int SEMANTIC_ACTION_CONVERSATION_IS_PHISHING = 12; 1980 1981 /** 1982 * {@link #extras} key to a boolean defining if this action requires special visual 1983 * treatment. 1984 * @hide 1985 */ 1986 public static final String EXTRA_IS_ANIMATED = "android.extra.IS_ANIMATED"; 1987 1988 private final Bundle mExtras; 1989 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 1990 private Icon mIcon; 1991 private final RemoteInput[] mRemoteInputs; 1992 private boolean mAllowGeneratedReplies = true; 1993 private final @SemanticAction int mSemanticAction; 1994 private final boolean mIsContextual; 1995 private boolean mAuthenticationRequired; 1996 1997 /** 1998 * Small icon representing the action. 1999 * 2000 * @deprecated Use {@link Action#getIcon()} instead. 2001 */ 2002 @Deprecated 2003 public int icon; 2004 2005 /** 2006 * Title of the action. 2007 */ 2008 public CharSequence title; 2009 2010 /** 2011 * Intent to send when the user invokes this action. May be null, in which case the action 2012 * may be rendered in a disabled presentation by the system UI. 2013 */ 2014 public PendingIntent actionIntent; 2015 Action(Parcel in)2016 private Action(Parcel in) { 2017 if (in.readInt() != 0) { 2018 mIcon = Icon.CREATOR.createFromParcel(in); 2019 if (mIcon.getType() == Icon.TYPE_RESOURCE) { 2020 icon = mIcon.getResId(); 2021 } 2022 } 2023 title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 2024 if (in.readInt() == 1) { 2025 actionIntent = PendingIntent.CREATOR.createFromParcel(in); 2026 } 2027 mExtras = Bundle.setDefusable(in.readBundle(), true); 2028 mRemoteInputs = in.createTypedArray(RemoteInput.CREATOR); 2029 mAllowGeneratedReplies = in.readInt() == 1; 2030 mSemanticAction = in.readInt(); 2031 mIsContextual = in.readInt() == 1; 2032 mAuthenticationRequired = in.readInt() == 1; 2033 } 2034 2035 /** 2036 * @deprecated Use {@link android.app.Notification.Action.Builder}. 2037 */ 2038 @Deprecated Action(int icon, CharSequence title, @Nullable PendingIntent intent)2039 public Action(int icon, CharSequence title, @Nullable PendingIntent intent) { 2040 this(Icon.createWithResource("", icon), title, intent, new Bundle(), null, true, 2041 SEMANTIC_ACTION_NONE, false /* isContextual */, false /* requireAuth */); 2042 } 2043 2044 /** Keep in sync with {@link Notification.Action.Builder#Builder(Action)}! */ Action(Icon icon, CharSequence title, PendingIntent intent, Bundle extras, RemoteInput[] remoteInputs, boolean allowGeneratedReplies, @SemanticAction int semanticAction, boolean isContextual, boolean requireAuth)2045 private Action(Icon icon, CharSequence title, PendingIntent intent, Bundle extras, 2046 RemoteInput[] remoteInputs, boolean allowGeneratedReplies, 2047 @SemanticAction int semanticAction, boolean isContextual, 2048 boolean requireAuth) { 2049 this.mIcon = icon; 2050 if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) { 2051 this.icon = icon.getResId(); 2052 } 2053 this.title = title; 2054 this.actionIntent = intent; 2055 this.mExtras = extras != null ? extras : new Bundle(); 2056 this.mRemoteInputs = remoteInputs; 2057 this.mAllowGeneratedReplies = allowGeneratedReplies; 2058 this.mSemanticAction = semanticAction; 2059 this.mIsContextual = isContextual; 2060 this.mAuthenticationRequired = requireAuth; 2061 } 2062 2063 /** 2064 * Return an icon representing the action. 2065 */ getIcon()2066 public Icon getIcon() { 2067 if (mIcon == null && icon != 0) { 2068 // you snuck an icon in here without using the builder; let's try to keep it 2069 mIcon = Icon.createWithResource("", icon); 2070 } 2071 return mIcon; 2072 } 2073 2074 /** 2075 * Get additional metadata carried around with this Action. 2076 */ getExtras()2077 public Bundle getExtras() { 2078 return mExtras; 2079 } 2080 2081 /** 2082 * Return whether the platform should automatically generate possible replies for this 2083 * {@link Action} 2084 */ getAllowGeneratedReplies()2085 public boolean getAllowGeneratedReplies() { 2086 return mAllowGeneratedReplies; 2087 } 2088 2089 /** 2090 * Get the list of inputs to be collected from the user when this action is sent. 2091 * May return null if no remote inputs were added. Only returns inputs which accept 2092 * a text input. For inputs which only accept data use {@link #getDataOnlyRemoteInputs}. 2093 */ getRemoteInputs()2094 public RemoteInput[] getRemoteInputs() { 2095 return mRemoteInputs; 2096 } 2097 2098 /** 2099 * Returns the {@code SemanticAction} associated with this {@link Action}. A 2100 * {@code SemanticAction} denotes what an {@link Action}'s {@link PendingIntent} will do 2101 * (eg. reply, mark as read, delete, etc). 2102 */ getSemanticAction()2103 public @SemanticAction int getSemanticAction() { 2104 return mSemanticAction; 2105 } 2106 2107 /** 2108 * Returns whether this is a contextual Action, i.e. whether the action is dependent on the 2109 * notification message body. An example of a contextual action could be an action opening a 2110 * map application with an address shown in the notification. 2111 */ isContextual()2112 public boolean isContextual() { 2113 return mIsContextual; 2114 } 2115 2116 /** 2117 * Get the list of inputs to be collected from the user that ONLY accept data when this 2118 * action is sent. These remote inputs are guaranteed to return true on a call to 2119 * {@link RemoteInput#isDataOnly}. 2120 * 2121 * Returns null if there are no data-only remote inputs. 2122 * 2123 * This method exists so that legacy RemoteInput collectors that pre-date the addition 2124 * of non-textual RemoteInputs do not access these remote inputs. 2125 */ getDataOnlyRemoteInputs()2126 public RemoteInput[] getDataOnlyRemoteInputs() { 2127 return getParcelableArrayFromBundle(mExtras, EXTRA_DATA_ONLY_INPUTS, RemoteInput.class); 2128 } 2129 2130 /** 2131 * Returns whether the OS should only send this action's {@link PendingIntent} on an 2132 * unlocked device. 2133 * 2134 * If the device is locked when the action is invoked, the OS should show the keyguard and 2135 * require successful authentication before invoking the intent. 2136 */ isAuthenticationRequired()2137 public boolean isAuthenticationRequired() { 2138 return mAuthenticationRequired; 2139 } 2140 2141 /** 2142 * Builder class for {@link Action} objects. 2143 */ 2144 public static final class Builder { 2145 @Nullable private final Icon mIcon; 2146 @Nullable private final CharSequence mTitle; 2147 @Nullable private final PendingIntent mIntent; 2148 private boolean mAllowGeneratedReplies = true; 2149 @NonNull private final Bundle mExtras; 2150 @Nullable private ArrayList<RemoteInput> mRemoteInputs; 2151 private @SemanticAction int mSemanticAction; 2152 private boolean mIsContextual; 2153 private boolean mAuthenticationRequired; 2154 2155 /** 2156 * Construct a new builder for {@link Action} object. 2157 * <p>As of Android {@link android.os.Build.VERSION_CODES#N}, 2158 * action button icons will not be displayed on action buttons, but are still required 2159 * and are available to 2160 * {@link android.service.notification.NotificationListenerService notification listeners}, 2161 * which may display them in other contexts, for example on a wearable device. 2162 * @param icon icon to show for this action 2163 * @param title the title of the action 2164 * @param intent the {@link PendingIntent} to fire when users trigger this action. May 2165 * be null, in which case the action may be rendered in a disabled presentation by the 2166 * system UI. 2167 */ 2168 @Deprecated Builder(int icon, CharSequence title, @Nullable PendingIntent intent)2169 public Builder(int icon, CharSequence title, @Nullable PendingIntent intent) { 2170 this(Icon.createWithResource("", icon), title, intent); 2171 } 2172 2173 /** 2174 * Construct a new builder for {@link Action} object. 2175 * 2176 * <p>As of Android {@link android.os.Build.VERSION_CODES#S}, apps targeting API level 2177 * {@link android.os.Build.VERSION_CODES#S} or higher won't be able to start activities 2178 * while processing broadcast receivers or services in response to notification action 2179 * clicks. To launch an activity in those cases, provide a {@link PendingIntent} for the 2180 * activity itself. 2181 * 2182 * <p>How an Action is displayed, including whether the {@code icon}, {@code text}, or 2183 * both are displayed or required, depends on where and how the action is used, and the 2184 * {@link Style} applied to the Notification. 2185 * 2186 * <p>As of Android {@link android.os.Build.VERSION_CODES#N}, action button icons 2187 * will not be displayed on action buttons, but are still required and are available 2188 * to {@link android.service.notification.NotificationListenerService notification 2189 * listeners}, which may display them in other contexts, for example on a wearable 2190 * device. 2191 * 2192 * <p>When the {@code title} is a {@link android.text.Spanned}, any colors set by a 2193 * {@link ForegroundColorSpan} or {@link TextAppearanceSpan} may be removed or displayed 2194 * with an altered in luminance to ensure proper contrast within the Notification. 2195 * 2196 * @param icon icon to show for this action 2197 * @param title the title of the action 2198 * @param intent the {@link PendingIntent} to fire when users trigger this action. May 2199 * be null, in which case the action may be rendered in a disabled presentation by the 2200 * system UI. 2201 */ Builder(Icon icon, CharSequence title, @Nullable PendingIntent intent)2202 public Builder(Icon icon, CharSequence title, @Nullable PendingIntent intent) { 2203 this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE, false); 2204 } 2205 2206 /** 2207 * Construct a new builder for {@link Action} object using the fields from an 2208 * {@link Action}. 2209 * @param action the action to read fields from. 2210 */ Builder(Action action)2211 public Builder(Action action) { 2212 this(action.getIcon(), action.title, action.actionIntent, 2213 new Bundle(action.mExtras), action.getRemoteInputs(), 2214 action.getAllowGeneratedReplies(), action.getSemanticAction(), 2215 action.isAuthenticationRequired()); 2216 } 2217 Builder(@ullable Icon icon, @Nullable CharSequence title, @Nullable PendingIntent intent, @NonNull Bundle extras, @Nullable RemoteInput[] remoteInputs, boolean allowGeneratedReplies, @SemanticAction int semanticAction, boolean authRequired)2218 private Builder(@Nullable Icon icon, @Nullable CharSequence title, 2219 @Nullable PendingIntent intent, @NonNull Bundle extras, 2220 @Nullable RemoteInput[] remoteInputs, boolean allowGeneratedReplies, 2221 @SemanticAction int semanticAction, boolean authRequired) { 2222 mIcon = icon; 2223 mTitle = title; 2224 mIntent = intent; 2225 mExtras = extras; 2226 if (remoteInputs != null) { 2227 mRemoteInputs = new ArrayList<>(remoteInputs.length); 2228 Collections.addAll(mRemoteInputs, remoteInputs); 2229 } 2230 mAllowGeneratedReplies = allowGeneratedReplies; 2231 mSemanticAction = semanticAction; 2232 mAuthenticationRequired = authRequired; 2233 } 2234 2235 /** 2236 * Merge additional metadata into this builder. 2237 * 2238 * <p>Values within the Bundle will replace existing extras values in this Builder. 2239 * 2240 * @see Notification.Action#extras 2241 */ 2242 @NonNull addExtras(Bundle extras)2243 public Builder addExtras(Bundle extras) { 2244 if (extras != null) { 2245 mExtras.putAll(extras); 2246 } 2247 return this; 2248 } 2249 2250 /** 2251 * Get the metadata Bundle used by this Builder. 2252 * 2253 * <p>The returned Bundle is shared with this Builder. 2254 */ 2255 @NonNull getExtras()2256 public Bundle getExtras() { 2257 return mExtras; 2258 } 2259 2260 /** 2261 * Add an input to be collected from the user when this action is sent. 2262 * Response values can be retrieved from the fired intent by using the 2263 * {@link RemoteInput#getResultsFromIntent} function. 2264 * @param remoteInput a {@link RemoteInput} to add to the action 2265 * @return this object for method chaining 2266 */ 2267 @NonNull addRemoteInput(RemoteInput remoteInput)2268 public Builder addRemoteInput(RemoteInput remoteInput) { 2269 if (mRemoteInputs == null) { 2270 mRemoteInputs = new ArrayList<RemoteInput>(); 2271 } 2272 mRemoteInputs.add(remoteInput); 2273 return this; 2274 } 2275 2276 /** 2277 * Set whether the platform should automatically generate possible replies to add to 2278 * {@link RemoteInput#getChoices()}. If the {@link Action} doesn't have a 2279 * {@link RemoteInput}, this has no effect. 2280 * @param allowGeneratedReplies {@code true} to allow generated replies, {@code false} 2281 * otherwise 2282 * @return this object for method chaining 2283 * The default value is {@code true} 2284 */ 2285 @NonNull setAllowGeneratedReplies(boolean allowGeneratedReplies)2286 public Builder setAllowGeneratedReplies(boolean allowGeneratedReplies) { 2287 mAllowGeneratedReplies = allowGeneratedReplies; 2288 return this; 2289 } 2290 2291 /** 2292 * Sets the {@code SemanticAction} for this {@link Action}. A 2293 * {@code SemanticAction} denotes what an {@link Action}'s 2294 * {@link PendingIntent} will do (eg. reply, mark as read, delete, etc). 2295 * @param semanticAction a SemanticAction defined within {@link Action} with 2296 * {@code SEMANTIC_ACTION_} prefixes 2297 * @return this object for method chaining 2298 */ 2299 @NonNull setSemanticAction(@emanticAction int semanticAction)2300 public Builder setSemanticAction(@SemanticAction int semanticAction) { 2301 mSemanticAction = semanticAction; 2302 return this; 2303 } 2304 2305 /** 2306 * Sets whether this {@link Action} is a contextual action, i.e. whether the action is 2307 * dependent on the notification message body. An example of a contextual action could 2308 * be an action opening a map application with an address shown in the notification. 2309 */ 2310 @NonNull setContextual(boolean isContextual)2311 public Builder setContextual(boolean isContextual) { 2312 mIsContextual = isContextual; 2313 return this; 2314 } 2315 2316 /** 2317 * Apply an extender to this action builder. Extenders may be used to add 2318 * metadata or change options on this builder. 2319 */ 2320 @NonNull extend(Extender extender)2321 public Builder extend(Extender extender) { 2322 extender.extend(this); 2323 return this; 2324 } 2325 2326 /** 2327 * Sets whether the OS should only send this action's {@link PendingIntent} on an 2328 * unlocked device. 2329 * 2330 * If this is true and the device is locked when the action is invoked, the OS will 2331 * show the keyguard and require successful authentication before invoking the intent. 2332 * If this is false and the device is locked, the OS will decide whether authentication 2333 * should be required. 2334 */ 2335 @NonNull setAuthenticationRequired(boolean authenticationRequired)2336 public Builder setAuthenticationRequired(boolean authenticationRequired) { 2337 mAuthenticationRequired = authenticationRequired; 2338 return this; 2339 } 2340 2341 /** 2342 * Throws an NPE if we are building a contextual action missing one of the fields 2343 * necessary to display the action. 2344 */ checkContextualActionNullFields()2345 private void checkContextualActionNullFields() { 2346 if (!mIsContextual) return; 2347 2348 if (mIcon == null) { 2349 throw new NullPointerException("Contextual Actions must contain a valid icon"); 2350 } 2351 2352 if (mIntent == null) { 2353 throw new NullPointerException( 2354 "Contextual Actions must contain a valid PendingIntent"); 2355 } 2356 } 2357 2358 /** 2359 * Combine all of the options that have been set and return a new {@link Action} 2360 * object. 2361 * @return the built action 2362 */ 2363 @NonNull build()2364 public Action build() { 2365 checkContextualActionNullFields(); 2366 2367 ArrayList<RemoteInput> dataOnlyInputs = new ArrayList<>(); 2368 RemoteInput[] previousDataInputs = getParcelableArrayFromBundle( 2369 mExtras, EXTRA_DATA_ONLY_INPUTS, RemoteInput.class); 2370 if (previousDataInputs != null) { 2371 for (RemoteInput input : previousDataInputs) { 2372 dataOnlyInputs.add(input); 2373 } 2374 } 2375 List<RemoteInput> textInputs = new ArrayList<>(); 2376 if (mRemoteInputs != null) { 2377 for (RemoteInput input : mRemoteInputs) { 2378 if (input.isDataOnly()) { 2379 dataOnlyInputs.add(input); 2380 } else { 2381 textInputs.add(input); 2382 } 2383 } 2384 } 2385 if (!dataOnlyInputs.isEmpty()) { 2386 RemoteInput[] dataInputsArr = 2387 dataOnlyInputs.toArray(new RemoteInput[dataOnlyInputs.size()]); 2388 mExtras.putParcelableArray(EXTRA_DATA_ONLY_INPUTS, dataInputsArr); 2389 } 2390 RemoteInput[] textInputsArr = textInputs.isEmpty() 2391 ? null : textInputs.toArray(new RemoteInput[textInputs.size()]); 2392 return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr, 2393 mAllowGeneratedReplies, mSemanticAction, mIsContextual, 2394 mAuthenticationRequired); 2395 } 2396 } 2397 visitUris(@onNull Consumer<Uri> visitor)2398 private void visitUris(@NonNull Consumer<Uri> visitor) { 2399 visitIconUri(visitor, getIcon()); 2400 } 2401 2402 @Override clone()2403 public Action clone() { 2404 return new Action( 2405 getIcon(), 2406 title, 2407 actionIntent, // safe to alias 2408 mExtras == null ? new Bundle() : new Bundle(mExtras), 2409 getRemoteInputs(), 2410 getAllowGeneratedReplies(), 2411 getSemanticAction(), 2412 isContextual(), 2413 isAuthenticationRequired()); 2414 } 2415 2416 @Override describeContents()2417 public int describeContents() { 2418 return 0; 2419 } 2420 2421 @Override writeToParcel(Parcel out, int flags)2422 public void writeToParcel(Parcel out, int flags) { 2423 final Icon ic = getIcon(); 2424 if (ic != null) { 2425 out.writeInt(1); 2426 ic.writeToParcel(out, 0); 2427 } else { 2428 out.writeInt(0); 2429 } 2430 TextUtils.writeToParcel(title, out, flags); 2431 if (actionIntent != null) { 2432 out.writeInt(1); 2433 actionIntent.writeToParcel(out, flags); 2434 } else { 2435 out.writeInt(0); 2436 } 2437 out.writeBundle(mExtras); 2438 out.writeTypedArray(mRemoteInputs, flags); 2439 out.writeInt(mAllowGeneratedReplies ? 1 : 0); 2440 out.writeInt(mSemanticAction); 2441 out.writeInt(mIsContextual ? 1 : 0); 2442 out.writeInt(mAuthenticationRequired ? 1 : 0); 2443 } 2444 2445 public static final @android.annotation.NonNull Parcelable.Creator<Action> CREATOR = 2446 new Parcelable.Creator<Action>() { 2447 public Action createFromParcel(Parcel in) { 2448 return new Action(in); 2449 } 2450 public Action[] newArray(int size) { 2451 return new Action[size]; 2452 } 2453 }; 2454 2455 /** 2456 * Extender interface for use with {@link Builder#extend}. Extenders may be used to add 2457 * metadata or change options on an action builder. 2458 */ 2459 public interface Extender { 2460 /** 2461 * Apply this extender to a notification action builder. 2462 * @param builder the builder to be modified. 2463 * @return the build object for chaining. 2464 */ extend(Builder builder)2465 public Builder extend(Builder builder); 2466 } 2467 2468 /** 2469 * Wearable extender for notification actions. To add extensions to an action, 2470 * create a new {@link android.app.Notification.Action.WearableExtender} object using 2471 * the {@code WearableExtender()} constructor and apply it to a 2472 * {@link android.app.Notification.Action.Builder} using 2473 * {@link android.app.Notification.Action.Builder#extend}. 2474 * 2475 * <pre class="prettyprint"> 2476 * Notification.Action action = new Notification.Action.Builder( 2477 * R.drawable.archive_all, "Archive all", actionIntent) 2478 * .extend(new Notification.Action.WearableExtender() 2479 * .setAvailableOffline(false)) 2480 * .build();</pre> 2481 */ 2482 public static final class WearableExtender implements Extender { 2483 /** Notification action extra which contains wearable extensions */ 2484 private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; 2485 2486 // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. 2487 private static final String KEY_FLAGS = "flags"; 2488 private static final String KEY_IN_PROGRESS_LABEL = "inProgressLabel"; 2489 private static final String KEY_CONFIRM_LABEL = "confirmLabel"; 2490 private static final String KEY_CANCEL_LABEL = "cancelLabel"; 2491 2492 // Flags bitwise-ored to mFlags 2493 private static final int FLAG_AVAILABLE_OFFLINE = 0x1; 2494 private static final int FLAG_HINT_LAUNCHES_ACTIVITY = 1 << 1; 2495 private static final int FLAG_HINT_DISPLAY_INLINE = 1 << 2; 2496 2497 // Default value for flags integer 2498 private static final int DEFAULT_FLAGS = FLAG_AVAILABLE_OFFLINE; 2499 2500 private int mFlags = DEFAULT_FLAGS; 2501 2502 private CharSequence mInProgressLabel; 2503 private CharSequence mConfirmLabel; 2504 private CharSequence mCancelLabel; 2505 2506 /** 2507 * Create a {@link android.app.Notification.Action.WearableExtender} with default 2508 * options. 2509 */ WearableExtender()2510 public WearableExtender() { 2511 } 2512 2513 /** 2514 * Create a {@link android.app.Notification.Action.WearableExtender} by reading 2515 * wearable options present in an existing notification action. 2516 * @param action the notification action to inspect. 2517 */ WearableExtender(Action action)2518 public WearableExtender(Action action) { 2519 Bundle wearableBundle = action.getExtras().getBundle(EXTRA_WEARABLE_EXTENSIONS); 2520 if (wearableBundle != null) { 2521 mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); 2522 mInProgressLabel = wearableBundle.getCharSequence(KEY_IN_PROGRESS_LABEL); 2523 mConfirmLabel = wearableBundle.getCharSequence(KEY_CONFIRM_LABEL); 2524 mCancelLabel = wearableBundle.getCharSequence(KEY_CANCEL_LABEL); 2525 } 2526 } 2527 2528 /** 2529 * Apply wearable extensions to a notification action that is being built. This is 2530 * typically called by the {@link android.app.Notification.Action.Builder#extend} 2531 * method of {@link android.app.Notification.Action.Builder}. 2532 */ 2533 @Override extend(Action.Builder builder)2534 public Action.Builder extend(Action.Builder builder) { 2535 Bundle wearableBundle = new Bundle(); 2536 2537 if (mFlags != DEFAULT_FLAGS) { 2538 wearableBundle.putInt(KEY_FLAGS, mFlags); 2539 } 2540 if (mInProgressLabel != null) { 2541 wearableBundle.putCharSequence(KEY_IN_PROGRESS_LABEL, mInProgressLabel); 2542 } 2543 if (mConfirmLabel != null) { 2544 wearableBundle.putCharSequence(KEY_CONFIRM_LABEL, mConfirmLabel); 2545 } 2546 if (mCancelLabel != null) { 2547 wearableBundle.putCharSequence(KEY_CANCEL_LABEL, mCancelLabel); 2548 } 2549 2550 builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); 2551 return builder; 2552 } 2553 2554 @Override clone()2555 public WearableExtender clone() { 2556 WearableExtender that = new WearableExtender(); 2557 that.mFlags = this.mFlags; 2558 that.mInProgressLabel = this.mInProgressLabel; 2559 that.mConfirmLabel = this.mConfirmLabel; 2560 that.mCancelLabel = this.mCancelLabel; 2561 return that; 2562 } 2563 2564 /** 2565 * Set whether this action is available when the wearable device is not connected to 2566 * a companion device. The user can still trigger this action when the wearable device is 2567 * offline, but a visual hint will indicate that the action may not be available. 2568 * Defaults to true. 2569 */ setAvailableOffline(boolean availableOffline)2570 public WearableExtender setAvailableOffline(boolean availableOffline) { 2571 setFlag(FLAG_AVAILABLE_OFFLINE, availableOffline); 2572 return this; 2573 } 2574 2575 /** 2576 * Get whether this action is available when the wearable device is not connected to 2577 * a companion device. The user can still trigger this action when the wearable device is 2578 * offline, but a visual hint will indicate that the action may not be available. 2579 * Defaults to true. 2580 */ isAvailableOffline()2581 public boolean isAvailableOffline() { 2582 return (mFlags & FLAG_AVAILABLE_OFFLINE) != 0; 2583 } 2584 setFlag(int mask, boolean value)2585 private void setFlag(int mask, boolean value) { 2586 if (value) { 2587 mFlags |= mask; 2588 } else { 2589 mFlags &= ~mask; 2590 } 2591 } 2592 2593 /** 2594 * Set a label to display while the wearable is preparing to automatically execute the 2595 * action. This is usually a 'ing' verb ending in ellipsis like "Sending..." 2596 * 2597 * @param label the label to display while the action is being prepared to execute 2598 * @return this object for method chaining 2599 */ 2600 @Deprecated setInProgressLabel(CharSequence label)2601 public WearableExtender setInProgressLabel(CharSequence label) { 2602 mInProgressLabel = label; 2603 return this; 2604 } 2605 2606 /** 2607 * Get the label to display while the wearable is preparing to automatically execute 2608 * the action. This is usually a 'ing' verb ending in ellipsis like "Sending..." 2609 * 2610 * @return the label to display while the action is being prepared to execute 2611 */ 2612 @Deprecated getInProgressLabel()2613 public CharSequence getInProgressLabel() { 2614 return mInProgressLabel; 2615 } 2616 2617 /** 2618 * Set a label to display to confirm that the action should be executed. 2619 * This is usually an imperative verb like "Send". 2620 * 2621 * @param label the label to confirm the action should be executed 2622 * @return this object for method chaining 2623 */ 2624 @Deprecated setConfirmLabel(CharSequence label)2625 public WearableExtender setConfirmLabel(CharSequence label) { 2626 mConfirmLabel = label; 2627 return this; 2628 } 2629 2630 /** 2631 * Get the label to display to confirm that the action should be executed. 2632 * This is usually an imperative verb like "Send". 2633 * 2634 * @return the label to confirm the action should be executed 2635 */ 2636 @Deprecated getConfirmLabel()2637 public CharSequence getConfirmLabel() { 2638 return mConfirmLabel; 2639 } 2640 2641 /** 2642 * Set a label to display to cancel the action. 2643 * This is usually an imperative verb, like "Cancel". 2644 * 2645 * @param label the label to display to cancel the action 2646 * @return this object for method chaining 2647 */ 2648 @Deprecated setCancelLabel(CharSequence label)2649 public WearableExtender setCancelLabel(CharSequence label) { 2650 mCancelLabel = label; 2651 return this; 2652 } 2653 2654 /** 2655 * Get the label to display to cancel the action. 2656 * This is usually an imperative verb like "Cancel". 2657 * 2658 * @return the label to display to cancel the action 2659 */ 2660 @Deprecated getCancelLabel()2661 public CharSequence getCancelLabel() { 2662 return mCancelLabel; 2663 } 2664 2665 /** 2666 * Set a hint that this Action will launch an {@link Activity} directly, telling the 2667 * platform that it can generate the appropriate transitions. 2668 * @param hintLaunchesActivity {@code true} if the content intent will launch 2669 * an activity and transitions should be generated, false otherwise. 2670 * @return this object for method chaining 2671 */ setHintLaunchesActivity( boolean hintLaunchesActivity)2672 public WearableExtender setHintLaunchesActivity( 2673 boolean hintLaunchesActivity) { 2674 setFlag(FLAG_HINT_LAUNCHES_ACTIVITY, hintLaunchesActivity); 2675 return this; 2676 } 2677 2678 /** 2679 * Get a hint that this Action will launch an {@link Activity} directly, telling the 2680 * platform that it can generate the appropriate transitions 2681 * @return {@code true} if the content intent will launch an activity and transitions 2682 * should be generated, false otherwise. The default value is {@code false} if this was 2683 * never set. 2684 */ getHintLaunchesActivity()2685 public boolean getHintLaunchesActivity() { 2686 return (mFlags & FLAG_HINT_LAUNCHES_ACTIVITY) != 0; 2687 } 2688 2689 /** 2690 * Set a hint that this Action should be displayed inline. 2691 * 2692 * @param hintDisplayInline {@code true} if action should be displayed inline, false 2693 * otherwise 2694 * @return this object for method chaining 2695 */ setHintDisplayActionInline( boolean hintDisplayInline)2696 public WearableExtender setHintDisplayActionInline( 2697 boolean hintDisplayInline) { 2698 setFlag(FLAG_HINT_DISPLAY_INLINE, hintDisplayInline); 2699 return this; 2700 } 2701 2702 /** 2703 * Get a hint that this Action should be displayed inline. 2704 * 2705 * @return {@code true} if the Action should be displayed inline, {@code false} 2706 * otherwise. The default value is {@code false} if this was never set. 2707 */ getHintDisplayActionInline()2708 public boolean getHintDisplayActionInline() { 2709 return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0; 2710 } 2711 } 2712 2713 /** 2714 * Provides meaning to an {@link Action} that hints at what the associated 2715 * {@link PendingIntent} will do. For example, an {@link Action} with a 2716 * {@link PendingIntent} that replies to a text message notification may have the 2717 * {@link #SEMANTIC_ACTION_REPLY} {@code SemanticAction} set within it. 2718 * 2719 * @hide 2720 */ 2721 @IntDef(prefix = { "SEMANTIC_ACTION_" }, value = { 2722 SEMANTIC_ACTION_NONE, 2723 SEMANTIC_ACTION_REPLY, 2724 SEMANTIC_ACTION_MARK_AS_READ, 2725 SEMANTIC_ACTION_MARK_AS_UNREAD, 2726 SEMANTIC_ACTION_DELETE, 2727 SEMANTIC_ACTION_ARCHIVE, 2728 SEMANTIC_ACTION_MUTE, 2729 SEMANTIC_ACTION_UNMUTE, 2730 SEMANTIC_ACTION_THUMBS_UP, 2731 SEMANTIC_ACTION_THUMBS_DOWN, 2732 SEMANTIC_ACTION_CALL, 2733 SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY, 2734 SEMANTIC_ACTION_CONVERSATION_IS_PHISHING 2735 }) 2736 @Retention(RetentionPolicy.SOURCE) 2737 public @interface SemanticAction {} 2738 } 2739 2740 /** 2741 * Array of all {@link Action} structures attached to this notification by 2742 * {@link Builder#addAction(int, CharSequence, PendingIntent)}. Mostly useful for instances of 2743 * {@link android.service.notification.NotificationListenerService} that provide an alternative 2744 * interface for invoking actions. 2745 */ 2746 public Action[] actions; 2747 2748 /** 2749 * Replacement version of this notification whose content will be shown 2750 * in an insecure context such as atop a secure keyguard. See {@link #visibility} 2751 * and {@link #VISIBILITY_PUBLIC}. 2752 */ 2753 public Notification publicVersion; 2754 2755 /** 2756 * Constructs a Notification object with default values. 2757 * You might want to consider using {@link Builder} instead. 2758 */ Notification()2759 public Notification() 2760 { 2761 this.when = System.currentTimeMillis(); 2762 if (Flags.sortSectionByTime()) { 2763 creationTime = when; 2764 extras.putBoolean(EXTRA_SHOW_WHEN, true); 2765 } else { 2766 this.creationTime = System.currentTimeMillis(); 2767 } 2768 this.priority = PRIORITY_DEFAULT; 2769 } 2770 2771 /** 2772 * @hide 2773 */ 2774 @UnsupportedAppUsage Notification(Context context, int icon, CharSequence tickerText, long when, CharSequence contentTitle, CharSequence contentText, Intent contentIntent)2775 public Notification(Context context, int icon, CharSequence tickerText, long when, 2776 CharSequence contentTitle, CharSequence contentText, Intent contentIntent) 2777 { 2778 if (Flags.sortSectionByTime()) { 2779 creationTime = when; 2780 extras.putBoolean(EXTRA_SHOW_WHEN, true); 2781 } 2782 new Builder(context) 2783 .setWhen(when) 2784 .setSmallIcon(icon) 2785 .setTicker(tickerText) 2786 .setContentTitle(contentTitle) 2787 .setContentText(contentText) 2788 .setContentIntent(PendingIntent.getActivity( 2789 context, 0, contentIntent, PendingIntent.FLAG_MUTABLE)) 2790 .buildInto(this); 2791 } 2792 2793 /** 2794 * Constructs a Notification object with the information needed to 2795 * have a status bar icon without the standard expanded view. 2796 * 2797 * @param icon The resource id of the icon to put in the status bar. 2798 * @param tickerText The text that flows by in the status bar when the notification first 2799 * activates. 2800 * @param when The time to show in the time field. In the System.currentTimeMillis 2801 * timebase. 2802 * 2803 * @deprecated Use {@link Builder} instead. 2804 */ 2805 @Deprecated Notification(int icon, CharSequence tickerText, long when)2806 public Notification(int icon, CharSequence tickerText, long when) 2807 { 2808 this.icon = icon; 2809 this.tickerText = tickerText; 2810 this.when = when; 2811 if (Flags.sortSectionByTime()) { 2812 creationTime = when; 2813 extras.putBoolean(EXTRA_SHOW_WHEN, true); 2814 } else { 2815 this.creationTime = System.currentTimeMillis(); 2816 } 2817 } 2818 2819 /** 2820 * Unflatten the notification from a parcel. 2821 */ 2822 @SuppressWarnings("unchecked") Notification(Parcel parcel)2823 public Notification(Parcel parcel) { 2824 // IMPORTANT: Add unmarshaling code in readFromParcel as the pending 2825 // intents in extras are always written as the last entry. 2826 readFromParcelImpl(parcel); 2827 // Must be read last! 2828 allPendingIntents = (ArraySet<PendingIntent>) parcel.readArraySet(null); 2829 } 2830 readFromParcelImpl(Parcel parcel)2831 private void readFromParcelImpl(Parcel parcel) 2832 { 2833 int version = parcel.readInt(); 2834 2835 mAllowlistToken = parcel.readStrongBinder(); 2836 if (mAllowlistToken == null) { 2837 mAllowlistToken = processAllowlistToken; 2838 } 2839 // Propagate this token to all pending intents that are unmarshalled from the parcel, 2840 // or keep the one we're already propagating, if that's the case. 2841 if (!parcel.hasClassCookie(PendingIntent.class)) { 2842 parcel.setClassCookie(PendingIntent.class, mAllowlistToken); 2843 } 2844 2845 when = parcel.readLong(); 2846 creationTime = parcel.readLong(); 2847 if (parcel.readInt() != 0) { 2848 mSmallIcon = Icon.CREATOR.createFromParcel(parcel); 2849 if (mSmallIcon.getType() == Icon.TYPE_RESOURCE) { 2850 icon = mSmallIcon.getResId(); 2851 } 2852 } 2853 number = parcel.readInt(); 2854 if (parcel.readInt() != 0) { 2855 contentIntent = PendingIntent.CREATOR.createFromParcel(parcel); 2856 } 2857 if (parcel.readInt() != 0) { 2858 deleteIntent = PendingIntent.CREATOR.createFromParcel(parcel); 2859 } 2860 if (parcel.readInt() != 0) { 2861 tickerText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); 2862 } 2863 if (parcel.readInt() != 0) { 2864 tickerView = RemoteViews.CREATOR.createFromParcel(parcel); 2865 } 2866 if (parcel.readInt() != 0) { 2867 contentView = RemoteViews.CREATOR.createFromParcel(parcel); 2868 } 2869 if (parcel.readInt() != 0) { 2870 mLargeIcon = Icon.CREATOR.createFromParcel(parcel); 2871 } 2872 defaults = parcel.readInt(); 2873 flags = parcel.readInt(); 2874 if (parcel.readInt() != 0) { 2875 sound = Uri.CREATOR.createFromParcel(parcel); 2876 } 2877 2878 audioStreamType = parcel.readInt(); 2879 if (parcel.readInt() != 0) { 2880 audioAttributes = AudioAttributes.CREATOR.createFromParcel(parcel); 2881 } 2882 vibrate = parcel.createLongArray(); 2883 ledARGB = parcel.readInt(); 2884 ledOnMS = parcel.readInt(); 2885 ledOffMS = parcel.readInt(); 2886 iconLevel = parcel.readInt(); 2887 2888 if (parcel.readInt() != 0) { 2889 fullScreenIntent = PendingIntent.CREATOR.createFromParcel(parcel); 2890 } 2891 2892 priority = parcel.readInt(); 2893 2894 category = parcel.readString8(); 2895 2896 mGroupKey = parcel.readString8(); 2897 2898 mSortKey = parcel.readString8(); 2899 2900 extras = Bundle.setDefusable(parcel.readBundle(), true); // may be null 2901 fixDuplicateExtras(); 2902 2903 actions = parcel.createTypedArray(Action.CREATOR); // may be null 2904 2905 if (parcel.readInt() != 0) { 2906 bigContentView = RemoteViews.CREATOR.createFromParcel(parcel); 2907 } 2908 2909 if (parcel.readInt() != 0) { 2910 headsUpContentView = RemoteViews.CREATOR.createFromParcel(parcel); 2911 } 2912 2913 visibility = parcel.readInt(); 2914 2915 if (parcel.readInt() != 0) { 2916 publicVersion = Notification.CREATOR.createFromParcel(parcel); 2917 } 2918 2919 color = parcel.readInt(); 2920 2921 if (parcel.readInt() != 0) { 2922 mChannelId = parcel.readString8(); 2923 } 2924 mTimeout = parcel.readLong(); 2925 2926 if (parcel.readInt() != 0) { 2927 mShortcutId = parcel.readString8(); 2928 } 2929 2930 if (parcel.readInt() != 0) { 2931 mLocusId = LocusId.CREATOR.createFromParcel(parcel); 2932 } 2933 2934 mBadgeIcon = parcel.readInt(); 2935 2936 if (parcel.readInt() != 0) { 2937 mSettingsText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); 2938 } 2939 2940 mGroupAlertBehavior = parcel.readInt(); 2941 if (parcel.readInt() != 0) { 2942 mBubbleMetadata = BubbleMetadata.CREATOR.createFromParcel(parcel); 2943 } 2944 2945 mAllowSystemGeneratedContextualActions = parcel.readBoolean(); 2946 2947 mFgsDeferBehavior = parcel.readInt(); 2948 } 2949 2950 @Override clone()2951 public Notification clone() { 2952 Notification that = new Notification(); 2953 cloneInto(that, true); 2954 return that; 2955 } 2956 2957 /** 2958 * Copy all (or if heavy is false, all except Bitmaps and RemoteViews) members 2959 * of this into that. 2960 * @hide 2961 */ cloneInto(Notification that, boolean heavy)2962 public void cloneInto(Notification that, boolean heavy) { 2963 that.mAllowlistToken = this.mAllowlistToken; 2964 that.when = this.when; 2965 that.creationTime = this.creationTime; 2966 that.mSmallIcon = this.mSmallIcon; 2967 that.number = this.number; 2968 2969 // PendingIntents are global, so there's no reason (or way) to clone them. 2970 that.contentIntent = this.contentIntent; 2971 that.deleteIntent = this.deleteIntent; 2972 that.fullScreenIntent = this.fullScreenIntent; 2973 2974 if (this.tickerText != null) { 2975 that.tickerText = this.tickerText.toString(); 2976 } 2977 if (heavy && this.tickerView != null) { 2978 that.tickerView = this.tickerView.clone(); 2979 } 2980 if (heavy && this.contentView != null) { 2981 that.contentView = this.contentView.clone(); 2982 } 2983 if (heavy && this.mLargeIcon != null) { 2984 that.mLargeIcon = this.mLargeIcon; 2985 } 2986 that.iconLevel = this.iconLevel; 2987 that.sound = this.sound; // android.net.Uri is immutable 2988 that.audioStreamType = this.audioStreamType; 2989 if (this.audioAttributes != null) { 2990 that.audioAttributes = new AudioAttributes.Builder(this.audioAttributes).build(); 2991 } 2992 2993 final long[] vibrate = this.vibrate; 2994 if (vibrate != null) { 2995 final int N = vibrate.length; 2996 final long[] vib = that.vibrate = new long[N]; 2997 System.arraycopy(vibrate, 0, vib, 0, N); 2998 } 2999 3000 that.ledARGB = this.ledARGB; 3001 that.ledOnMS = this.ledOnMS; 3002 that.ledOffMS = this.ledOffMS; 3003 that.defaults = this.defaults; 3004 3005 that.flags = this.flags; 3006 3007 that.priority = this.priority; 3008 3009 that.category = this.category; 3010 3011 that.mGroupKey = this.mGroupKey; 3012 3013 that.mSortKey = this.mSortKey; 3014 3015 if (this.extras != null) { 3016 try { 3017 that.extras = new Bundle(this.extras); 3018 // will unparcel 3019 that.extras.size(); 3020 } catch (BadParcelableException e) { 3021 Log.e(TAG, "could not unparcel extras from notification: " + this, e); 3022 that.extras = null; 3023 } 3024 } 3025 3026 if (!ArrayUtils.isEmpty(allPendingIntents)) { 3027 that.allPendingIntents = new ArraySet<>(allPendingIntents); 3028 } 3029 3030 if (this.actions != null) { 3031 that.actions = new Action[this.actions.length]; 3032 for(int i=0; i<this.actions.length; i++) { 3033 if ( this.actions[i] != null) { 3034 that.actions[i] = this.actions[i].clone(); 3035 } 3036 } 3037 } 3038 3039 if (heavy && this.bigContentView != null) { 3040 that.bigContentView = this.bigContentView.clone(); 3041 } 3042 3043 if (heavy && this.headsUpContentView != null) { 3044 that.headsUpContentView = this.headsUpContentView.clone(); 3045 } 3046 3047 that.visibility = this.visibility; 3048 3049 if (this.publicVersion != null) { 3050 that.publicVersion = new Notification(); 3051 this.publicVersion.cloneInto(that.publicVersion, heavy); 3052 } 3053 3054 that.color = this.color; 3055 3056 that.mChannelId = this.mChannelId; 3057 that.mTimeout = this.mTimeout; 3058 that.mShortcutId = this.mShortcutId; 3059 that.mLocusId = this.mLocusId; 3060 that.mBadgeIcon = this.mBadgeIcon; 3061 that.mSettingsText = this.mSettingsText; 3062 that.mGroupAlertBehavior = this.mGroupAlertBehavior; 3063 that.mFgsDeferBehavior = this.mFgsDeferBehavior; 3064 that.mBubbleMetadata = this.mBubbleMetadata; 3065 that.mAllowSystemGeneratedContextualActions = this.mAllowSystemGeneratedContextualActions; 3066 3067 if (!heavy) { 3068 that.lightenPayload(); // will clean out extras 3069 } 3070 } 3071 visitIconUri(@onNull Consumer<Uri> visitor, @Nullable Icon icon)3072 private static void visitIconUri(@NonNull Consumer<Uri> visitor, @Nullable Icon icon) { 3073 if (icon == null) return; 3074 final int iconType = icon.getType(); 3075 if (iconType == TYPE_URI || iconType == TYPE_URI_ADAPTIVE_BITMAP) { 3076 visitor.accept(icon.getUri()); 3077 } 3078 } 3079 3080 /** 3081 * Note all {@link Uri} that are referenced internally, with the expectation that Uri permission 3082 * grants will need to be issued to ensure the recipient of this object is able to render its 3083 * contents. 3084 * See b/281044385 for more context and examples about what happens when this isn't done 3085 * correctly. 3086 * 3087 * @hide 3088 */ visitUris(@onNull Consumer<Uri> visitor)3089 public void visitUris(@NonNull Consumer<Uri> visitor) { 3090 if (publicVersion != null) { 3091 publicVersion.visitUris(visitor); 3092 } 3093 3094 visitor.accept(sound); 3095 3096 if (tickerView != null) tickerView.visitUris(visitor); 3097 if (contentView != null) contentView.visitUris(visitor); 3098 if (bigContentView != null) bigContentView.visitUris(visitor); 3099 if (headsUpContentView != null) headsUpContentView.visitUris(visitor); 3100 3101 visitIconUri(visitor, mSmallIcon); 3102 visitIconUri(visitor, mLargeIcon); 3103 3104 if (actions != null) { 3105 for (Action action : actions) { 3106 action.visitUris(visitor); 3107 } 3108 } 3109 3110 if (extras != null) { 3111 visitIconUri(visitor, extras.getParcelable(EXTRA_LARGE_ICON_BIG, Icon.class)); 3112 visitIconUri(visitor, extras.getParcelable(EXTRA_PICTURE_ICON, Icon.class)); 3113 3114 // NOTE: The documentation of EXTRA_AUDIO_CONTENTS_URI explicitly says that it is a 3115 // String representation of a Uri, but the previous implementation (and unit test) of 3116 // this method has always treated it as a Uri object. Given the inconsistency, 3117 // supporting both going forward is the safest choice. 3118 Object audioContentsUri = extras.get(EXTRA_AUDIO_CONTENTS_URI); 3119 if (audioContentsUri instanceof Uri) { 3120 visitor.accept((Uri) audioContentsUri); 3121 } else if (audioContentsUri instanceof String) { 3122 visitor.accept(Uri.parse((String) audioContentsUri)); 3123 } 3124 3125 if (extras.containsKey(EXTRA_BACKGROUND_IMAGE_URI)) { 3126 visitor.accept(Uri.parse(extras.getString(EXTRA_BACKGROUND_IMAGE_URI))); 3127 } 3128 3129 ArrayList<Person> people = extras.getParcelableArrayList(EXTRA_PEOPLE_LIST, android.app.Person.class); 3130 if (people != null && !people.isEmpty()) { 3131 for (Person p : people) { 3132 p.visitUris(visitor); 3133 } 3134 } 3135 3136 final RemoteInputHistoryItem[] history = extras.getParcelableArray( 3137 Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, 3138 RemoteInputHistoryItem.class); 3139 if (history != null) { 3140 for (int i = 0; i < history.length; i++) { 3141 RemoteInputHistoryItem item = history[i]; 3142 if (item.getUri() != null) { 3143 visitor.accept(item.getUri()); 3144 } 3145 } 3146 } 3147 3148 // Extras for MessagingStyle. We visit them even if not isStyle(MessagingStyle), since 3149 // Notification Listeners might use directly (without the isStyle check). 3150 final Person person = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class); 3151 if (person != null) { 3152 person.visitUris(visitor); 3153 } 3154 3155 final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, 3156 Parcelable.class); 3157 if (!ArrayUtils.isEmpty(messages)) { 3158 for (MessagingStyle.Message message : MessagingStyle.Message 3159 .getMessagesFromBundleArray(messages)) { 3160 message.visitUris(visitor); 3161 } 3162 } 3163 3164 final Parcelable[] historic = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES, 3165 Parcelable.class); 3166 if (!ArrayUtils.isEmpty(historic)) { 3167 for (MessagingStyle.Message message : MessagingStyle.Message 3168 .getMessagesFromBundleArray(historic)) { 3169 message.visitUris(visitor); 3170 } 3171 } 3172 3173 visitIconUri(visitor, extras.getParcelable(EXTRA_CONVERSATION_ICON, Icon.class)); 3174 3175 // Extras for CallStyle (same reason for visiting without checking isStyle). 3176 Person callPerson = extras.getParcelable(EXTRA_CALL_PERSON, Person.class); 3177 if (callPerson != null) { 3178 callPerson.visitUris(visitor); 3179 } 3180 visitIconUri(visitor, extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon.class)); 3181 3182 3183 if (Flags.apiRichOngoing()) { 3184 visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, 3185 Icon.class)); 3186 visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, 3187 Icon.class)); 3188 visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, 3189 Icon.class)); 3190 } 3191 } 3192 3193 if (mBubbleMetadata != null) { 3194 visitIconUri(visitor, mBubbleMetadata.getIcon()); 3195 } 3196 3197 if (extras != null && extras.containsKey(WearableExtender.EXTRA_WEARABLE_EXTENSIONS)) { 3198 WearableExtender extender = new WearableExtender(this); 3199 extender.visitUris(visitor); 3200 } 3201 } 3202 3203 /** 3204 * @hide 3205 */ loadHeaderAppName(Context context)3206 public String loadHeaderAppName(Context context) { 3207 Trace.beginSection("Notification#loadHeaderAppName"); 3208 3209 try { 3210 CharSequence name = null; 3211 // Check if there is a non-empty substitute app name and return that. 3212 if (extras.containsKey(EXTRA_SUBSTITUTE_APP_NAME)) { 3213 name = extras.getString(EXTRA_SUBSTITUTE_APP_NAME); 3214 if (!TextUtils.isEmpty(name)) { 3215 return name.toString(); 3216 } 3217 } 3218 // If not, try getting the name from the app info. 3219 if (context == null) { 3220 return null; 3221 } 3222 if (TextUtils.isEmpty(name)) { 3223 ApplicationInfo info = getApplicationInfo(context); 3224 if (info != null) { 3225 final PackageManager pm = context.getPackageManager(); 3226 name = pm.getApplicationLabel(getApplicationInfo(context)); 3227 } 3228 } 3229 // If there's still nothing, ¯\_(ツ)_/¯ 3230 if (TextUtils.isEmpty(name)) { 3231 return null; 3232 } 3233 return name.toString(); 3234 } finally { 3235 Trace.endSection(); 3236 } 3237 } 3238 3239 /** 3240 * @hide 3241 */ containsCustomViews()3242 public boolean containsCustomViews() { 3243 return contentView != null 3244 || bigContentView != null 3245 || headsUpContentView != null 3246 || (publicVersion != null 3247 && (publicVersion.contentView != null 3248 || publicVersion.bigContentView != null 3249 || publicVersion.headsUpContentView != null)); 3250 } 3251 3252 /** 3253 * @hide 3254 */ hasTitle()3255 public boolean hasTitle() { 3256 if (extras == null) { 3257 return false; 3258 } 3259 // CallStyle notifications only use the other person's name as the title. 3260 if (isStyle(CallStyle.class)) { 3261 Person person = extras.getParcelable(EXTRA_CALL_PERSON, Person.class); 3262 return person != null && !TextUtils.isEmpty(person.getName()); 3263 } 3264 // non-CallStyle notifications can use EXTRA_TITLE 3265 if (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE))) { 3266 return true; 3267 } 3268 // BigTextStyle notifications first use EXTRA_TITLE_BIG 3269 if (isStyle(BigTextStyle.class)) { 3270 return !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG)); 3271 } else { 3272 return false; 3273 } 3274 } 3275 3276 /** 3277 * @hide 3278 */ 3279 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) hasPromotableStyle()3280 public boolean hasPromotableStyle() { 3281 final Class<? extends Style> notificationStyle = getNotificationStyle(); 3282 3283 return notificationStyle == null 3284 || BigTextStyle.class.equals(notificationStyle) 3285 || CallStyle.class.equals(notificationStyle) 3286 || ProgressStyle.class.equals(notificationStyle); 3287 } 3288 3289 /** 3290 * Returns whether the notification has all the characteristics that make it eligible for 3291 * {@link #FLAG_PROMOTED_ONGOING}. This method does not factor in other criteria such user 3292 * preferences for the app or channel. If this returns true, it does not guarantee that the 3293 * notification will be assigned FLAG_PROMOTED_ONGOING by the system, but if this returns false, 3294 * it will not. 3295 */ 3296 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) hasPromotableCharacteristics()3297 public boolean hasPromotableCharacteristics() { 3298 if (!isOngoingEvent() || isGroupSummary() || containsCustomViews() || !hasTitle()) { 3299 return false; 3300 } 3301 // Only "Ongoing CallStyle" notifications are promotable without EXTRA_COLORIZED 3302 if (isOngoingCallStyle()) { 3303 return true; 3304 } 3305 return isColorizedRequested() && hasPromotableStyle(); 3306 } 3307 3308 /** Returns whether the notification is CallStyle.forOngoingCall(). */ isOngoingCallStyle()3309 private boolean isOngoingCallStyle() { 3310 if (!isStyle(CallStyle.class)) { 3311 return false; 3312 } 3313 int callType = extras.getInt(EXTRA_CALL_TYPE, CallStyle.CALL_TYPE_UNKNOWN); 3314 return callType == CallStyle.CALL_TYPE_ONGOING; 3315 } 3316 3317 /** 3318 * Fetch the application info from the notification, or the context if that isn't available. 3319 */ getApplicationInfo(Context context)3320 private ApplicationInfo getApplicationInfo(Context context) { 3321 ApplicationInfo info = null; 3322 if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) { 3323 info = extras.getParcelable( 3324 EXTRA_BUILDER_APPLICATION_INFO, 3325 ApplicationInfo.class); 3326 } 3327 if (info == null) { 3328 if (context == null) { 3329 return null; 3330 } 3331 info = context.getApplicationInfo(); 3332 } 3333 return info; 3334 } 3335 3336 /** 3337 * Removes heavyweight parts of the Notification object for archival or for sending to 3338 * listeners when the full contents are not necessary. 3339 * @hide 3340 */ lightenPayload()3341 public final void lightenPayload() { 3342 tickerView = null; 3343 contentView = null; 3344 bigContentView = null; 3345 headsUpContentView = null; 3346 mLargeIcon = null; 3347 if (extras != null && !extras.isEmpty()) { 3348 final Set<String> keyset = extras.keySet(); 3349 final int N = keyset.size(); 3350 final String[] keys = keyset.toArray(new String[N]); 3351 for (int i=0; i<N; i++) { 3352 final String key = keys[i]; 3353 if (TvExtender.EXTRA_TV_EXTENDER.equals(key)) { 3354 continue; 3355 } 3356 final Object obj = extras.get(key); 3357 if (obj != null && 3358 ( obj instanceof Parcelable 3359 || obj instanceof Parcelable[] 3360 || obj instanceof SparseArray 3361 || obj instanceof ArrayList)) { 3362 extras.remove(key); 3363 } 3364 } 3365 } 3366 } 3367 3368 /** 3369 * Make sure this String is safe to put into a bundle. 3370 * @hide 3371 */ safeString(String str)3372 public static String safeString(String str) { 3373 if (str == null) return str; 3374 if (str.length() > MAX_CHARSEQUENCE_LENGTH) { 3375 str = str.substring(0, MAX_CHARSEQUENCE_LENGTH); 3376 } 3377 return str; 3378 } 3379 3380 /** 3381 * Make sure this CharSequence is safe to put into a bundle, which basically 3382 * means it had better not be some custom Parcelable implementation. 3383 * @hide 3384 */ safeCharSequence(CharSequence cs)3385 public static CharSequence safeCharSequence(CharSequence cs) { 3386 if (cs == null) return cs; 3387 if (cs.length() > MAX_CHARSEQUENCE_LENGTH) { 3388 cs = cs.subSequence(0, MAX_CHARSEQUENCE_LENGTH); 3389 } 3390 if (cs instanceof Parcelable) { 3391 Log.e(TAG, "warning: " + cs.getClass().getCanonicalName() 3392 + " instance is a custom Parcelable and not allowed in Notification"); 3393 return cs.toString(); 3394 } 3395 3396 return removeTextSizeSpans(cs); 3397 } 3398 stripStyling(@ullable CharSequence cs)3399 private static CharSequence stripStyling(@Nullable CharSequence cs) { 3400 if (cs == null) { 3401 return cs; 3402 } 3403 3404 return cs.toString(); 3405 } 3406 normalizeBigText(@ullable CharSequence charSequence)3407 private static CharSequence normalizeBigText(@Nullable CharSequence charSequence) { 3408 if (charSequence == null) { 3409 return charSequence; 3410 } 3411 3412 return NotificationBigTextNormalizer.normalizeBigText(charSequence.toString()); 3413 } 3414 removeTextSizeSpans(CharSequence charSequence)3415 private static CharSequence removeTextSizeSpans(CharSequence charSequence) { 3416 if (charSequence instanceof Spanned) { 3417 Spanned ss = (Spanned) charSequence; 3418 Object[] spans = ss.getSpans(0, ss.length(), Object.class); 3419 SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); 3420 for (Object span : spans) { 3421 Object resultSpan = span; 3422 if (resultSpan instanceof CharacterStyle) { 3423 resultSpan = ((CharacterStyle) span).getUnderlying(); 3424 } 3425 if (resultSpan instanceof TextAppearanceSpan) { 3426 TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan; 3427 resultSpan = new TextAppearanceSpan( 3428 originalSpan.getFamily(), 3429 originalSpan.getTextStyle(), 3430 -1, 3431 originalSpan.getTextColor(), 3432 originalSpan.getLinkTextColor()); 3433 } else if (resultSpan instanceof RelativeSizeSpan 3434 || resultSpan instanceof AbsoluteSizeSpan) { 3435 continue; 3436 } else { 3437 resultSpan = span; 3438 } 3439 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), 3440 ss.getSpanFlags(span)); 3441 } 3442 return builder; 3443 } 3444 return charSequence; 3445 } 3446 describeContents()3447 public int describeContents() { 3448 return 0; 3449 } 3450 3451 /** 3452 * Flatten this notification into a parcel. 3453 */ writeToParcel(Parcel parcel, int flags)3454 public void writeToParcel(Parcel parcel, int flags) { 3455 // We need to mark all pending intents getting into the notification 3456 // system as being put there to later allow the notification ranker 3457 // to launch them and by doing so add the app to the battery saver white 3458 // list for a short period of time. The problem is that the system 3459 // cannot look into the extras as there may be parcelables there that 3460 // the platform does not know how to handle. To go around that we have 3461 // an explicit list of the pending intents in the extras bundle. 3462 PendingIntent.OnMarshaledListener addedListener = null; 3463 if (allPendingIntents == null) { 3464 addedListener = (PendingIntent intent, Parcel out, int outFlags) -> { 3465 if (parcel == out) { 3466 synchronized (this) { 3467 if (allPendingIntents == null) { 3468 allPendingIntents = new ArraySet<>(); 3469 } 3470 allPendingIntents.add(intent); 3471 } 3472 } 3473 }; 3474 PendingIntent.addOnMarshaledListener(addedListener); 3475 } 3476 try { 3477 boolean mustClearCookie = false; 3478 if (!parcel.hasClassCookie(Notification.class)) { 3479 // This is the "root" notification, and not an "inner" notification (including 3480 // publicVersion or anything else that might be embedded in extras). So we want 3481 // to use its token for every inner notification (might be null). 3482 parcel.setClassCookie(Notification.class, mAllowlistToken); 3483 mustClearCookie = true; 3484 } 3485 try { 3486 // IMPORTANT: Add marshaling code in writeToParcelImpl as we 3487 // want to intercept all pending events written to the parcel. 3488 writeToParcelImpl(parcel, flags); 3489 } finally { 3490 if (mustClearCookie) { 3491 parcel.removeClassCookie(Notification.class, mAllowlistToken); 3492 } 3493 } 3494 3495 synchronized (this) { 3496 // Must be written last! 3497 parcel.writeArraySet(allPendingIntents); 3498 } 3499 } finally { 3500 if (addedListener != null) { 3501 PendingIntent.removeOnMarshaledListener(addedListener); 3502 } 3503 } 3504 } 3505 writeToParcelImpl(Parcel parcel, int flags)3506 private void writeToParcelImpl(Parcel parcel, int flags) { 3507 parcel.writeInt(1); 3508 3509 // Always use the same token as the root notification (might be null). 3510 IBinder rootNotificationToken = (IBinder) parcel.getClassCookie(Notification.class); 3511 parcel.writeStrongBinder(rootNotificationToken); 3512 3513 parcel.writeLong(when); 3514 parcel.writeLong(creationTime); 3515 if (mSmallIcon == null && icon != 0) { 3516 // you snuck an icon in here without using the builder; let's try to keep it 3517 mSmallIcon = Icon.createWithResource("", icon); 3518 } 3519 if (mSmallIcon != null) { 3520 parcel.writeInt(1); 3521 mSmallIcon.writeToParcel(parcel, 0); 3522 } else { 3523 parcel.writeInt(0); 3524 } 3525 parcel.writeInt(number); 3526 if (contentIntent != null) { 3527 parcel.writeInt(1); 3528 contentIntent.writeToParcel(parcel, 0); 3529 } else { 3530 parcel.writeInt(0); 3531 } 3532 if (deleteIntent != null) { 3533 parcel.writeInt(1); 3534 deleteIntent.writeToParcel(parcel, 0); 3535 } else { 3536 parcel.writeInt(0); 3537 } 3538 if (tickerText != null) { 3539 parcel.writeInt(1); 3540 TextUtils.writeToParcel(tickerText, parcel, flags); 3541 } else { 3542 parcel.writeInt(0); 3543 } 3544 if (tickerView != null) { 3545 parcel.writeInt(1); 3546 tickerView.writeToParcel(parcel, 0); 3547 } else { 3548 parcel.writeInt(0); 3549 } 3550 if (contentView != null) { 3551 parcel.writeInt(1); 3552 contentView.writeToParcel(parcel, 0); 3553 } else { 3554 parcel.writeInt(0); 3555 } 3556 if (mLargeIcon == null && largeIcon != null) { 3557 // you snuck an icon in here without using the builder; let's try to keep it 3558 mLargeIcon = Icon.createWithBitmap(largeIcon); 3559 } 3560 if (mLargeIcon != null) { 3561 parcel.writeInt(1); 3562 mLargeIcon.writeToParcel(parcel, 0); 3563 } else { 3564 parcel.writeInt(0); 3565 } 3566 3567 parcel.writeInt(defaults); 3568 parcel.writeInt(this.flags); 3569 3570 if (sound != null) { 3571 parcel.writeInt(1); 3572 sound.writeToParcel(parcel, 0); 3573 } else { 3574 parcel.writeInt(0); 3575 } 3576 parcel.writeInt(audioStreamType); 3577 3578 if (audioAttributes != null) { 3579 parcel.writeInt(1); 3580 audioAttributes.writeToParcel(parcel, 0); 3581 } else { 3582 parcel.writeInt(0); 3583 } 3584 3585 parcel.writeLongArray(vibrate); 3586 parcel.writeInt(ledARGB); 3587 parcel.writeInt(ledOnMS); 3588 parcel.writeInt(ledOffMS); 3589 parcel.writeInt(iconLevel); 3590 3591 if (fullScreenIntent != null) { 3592 parcel.writeInt(1); 3593 fullScreenIntent.writeToParcel(parcel, 0); 3594 } else { 3595 parcel.writeInt(0); 3596 } 3597 3598 parcel.writeInt(priority); 3599 3600 parcel.writeString8(category); 3601 3602 parcel.writeString8(mGroupKey); 3603 3604 parcel.writeString8(mSortKey); 3605 3606 parcel.writeBundle(extras); // null ok 3607 3608 parcel.writeTypedArray(actions, 0); // null ok 3609 3610 if (bigContentView != null) { 3611 parcel.writeInt(1); 3612 bigContentView.writeToParcel(parcel, 0); 3613 } else { 3614 parcel.writeInt(0); 3615 } 3616 3617 if (headsUpContentView != null) { 3618 parcel.writeInt(1); 3619 headsUpContentView.writeToParcel(parcel, 0); 3620 } else { 3621 parcel.writeInt(0); 3622 } 3623 3624 parcel.writeInt(visibility); 3625 3626 if (publicVersion != null) { 3627 parcel.writeInt(1); 3628 publicVersion.writeToParcel(parcel, 0); 3629 } else { 3630 parcel.writeInt(0); 3631 } 3632 3633 parcel.writeInt(color); 3634 3635 if (mChannelId != null) { 3636 parcel.writeInt(1); 3637 parcel.writeString8(mChannelId); 3638 } else { 3639 parcel.writeInt(0); 3640 } 3641 parcel.writeLong(mTimeout); 3642 3643 if (mShortcutId != null) { 3644 parcel.writeInt(1); 3645 parcel.writeString8(mShortcutId); 3646 } else { 3647 parcel.writeInt(0); 3648 } 3649 3650 if (mLocusId != null) { 3651 parcel.writeInt(1); 3652 mLocusId.writeToParcel(parcel, 0); 3653 } else { 3654 parcel.writeInt(0); 3655 } 3656 3657 parcel.writeInt(mBadgeIcon); 3658 3659 if (mSettingsText != null) { 3660 parcel.writeInt(1); 3661 TextUtils.writeToParcel(mSettingsText, parcel, flags); 3662 } else { 3663 parcel.writeInt(0); 3664 } 3665 3666 parcel.writeInt(mGroupAlertBehavior); 3667 3668 if (mBubbleMetadata != null) { 3669 parcel.writeInt(1); 3670 mBubbleMetadata.writeToParcel(parcel, 0); 3671 } else { 3672 parcel.writeInt(0); 3673 } 3674 3675 parcel.writeBoolean(mAllowSystemGeneratedContextualActions); 3676 3677 parcel.writeInt(mFgsDeferBehavior); 3678 3679 // mUsesStandardHeader is not written because it should be recomputed in listeners 3680 } 3681 3682 /** 3683 * Parcelable.Creator that instantiates Notification objects 3684 */ 3685 public static final @android.annotation.NonNull Parcelable.Creator<Notification> CREATOR 3686 = new Parcelable.Creator<Notification>() 3687 { 3688 public Notification createFromParcel(Parcel parcel) 3689 { 3690 return new Notification(parcel); 3691 } 3692 3693 public Notification[] newArray(int size) 3694 { 3695 return new Notification[size]; 3696 } 3697 }; 3698 3699 /** 3700 * @hide 3701 */ areActionsVisiblyDifferent(Notification first, Notification second)3702 public static boolean areActionsVisiblyDifferent(Notification first, Notification second) { 3703 Notification.Action[] firstAs = first.actions; 3704 Notification.Action[] secondAs = second.actions; 3705 if (firstAs == null && secondAs != null || firstAs != null && secondAs == null) { 3706 return true; 3707 } 3708 if (firstAs != null && secondAs != null) { 3709 if (firstAs.length != secondAs.length) { 3710 return true; 3711 } 3712 for (int i = 0; i < firstAs.length; i++) { 3713 if (!Objects.equals(String.valueOf(firstAs[i].title), 3714 String.valueOf(secondAs[i].title))) { 3715 return true; 3716 } 3717 RemoteInput[] firstRs = firstAs[i].getRemoteInputs(); 3718 RemoteInput[] secondRs = secondAs[i].getRemoteInputs(); 3719 if (firstRs == null) { 3720 firstRs = new RemoteInput[0]; 3721 } 3722 if (secondRs == null) { 3723 secondRs = new RemoteInput[0]; 3724 } 3725 if (firstRs.length != secondRs.length) { 3726 return true; 3727 } 3728 for (int j = 0; j < firstRs.length; j++) { 3729 if (!Objects.equals(String.valueOf(firstRs[j].getLabel()), 3730 String.valueOf(secondRs[j].getLabel()))) { 3731 return true; 3732 } 3733 } 3734 } 3735 } 3736 return false; 3737 } 3738 3739 /** 3740 * @hide 3741 */ areIconsDifferent(Notification first, Notification second)3742 public static boolean areIconsDifferent(Notification first, Notification second) { 3743 return areIconsMaybeDifferent(first.getSmallIcon(), second.getSmallIcon()) 3744 || areIconsMaybeDifferent(first.getLargeIcon(), second.getLargeIcon()); 3745 } 3746 3747 /** 3748 * Note that we aren't actually comparing the contents of the bitmaps here; this is only a 3749 * cursory inspection. We will not return false negatives, but false positives are likely. 3750 */ areIconsMaybeDifferent(Icon a, Icon b)3751 private static boolean areIconsMaybeDifferent(Icon a, Icon b) { 3752 if (a == b) { 3753 return false; 3754 } 3755 if (a == null || b == null) { 3756 return true; 3757 } 3758 if (a.sameAs(b)) { 3759 return false; 3760 } 3761 final int aType = a.getType(); 3762 if (aType != b.getType()) { 3763 return true; 3764 } 3765 if (aType == Icon.TYPE_BITMAP || aType == Icon.TYPE_ADAPTIVE_BITMAP) { 3766 final Bitmap aBitmap = a.getBitmap(); 3767 final Bitmap bBitmap = b.getBitmap(); 3768 return aBitmap.getWidth() != bBitmap.getWidth() 3769 || aBitmap.getHeight() != bBitmap.getHeight() 3770 || aBitmap.getConfig() != bBitmap.getConfig() 3771 || aBitmap.getGenerationId() != bBitmap.getGenerationId(); 3772 } 3773 return true; 3774 } 3775 3776 /** 3777 * @hide 3778 */ areStyledNotificationsVisiblyDifferent(Builder first, Builder second)3779 public static boolean areStyledNotificationsVisiblyDifferent(Builder first, Builder second) { 3780 if (first.getStyle() == null) { 3781 return second.getStyle() != null; 3782 } 3783 if (second.getStyle() == null) { 3784 return true; 3785 } 3786 return first.getStyle().areNotificationsVisiblyDifferent(second.getStyle()); 3787 } 3788 3789 /** 3790 * @hide 3791 */ areRemoteViewsChanged(Builder first, Builder second)3792 public static boolean areRemoteViewsChanged(Builder first, Builder second) { 3793 if (!Objects.equals(first.usesStandardHeader(), second.usesStandardHeader())) { 3794 return true; 3795 } 3796 3797 if (areRemoteViewsChanged(first.mN.contentView, second.mN.contentView)) { 3798 return true; 3799 } 3800 if (areRemoteViewsChanged(first.mN.bigContentView, second.mN.bigContentView)) { 3801 return true; 3802 } 3803 if (areRemoteViewsChanged(first.mN.headsUpContentView, second.mN.headsUpContentView)) { 3804 return true; 3805 } 3806 3807 return false; 3808 } 3809 areRemoteViewsChanged(RemoteViews first, RemoteViews second)3810 private static boolean areRemoteViewsChanged(RemoteViews first, RemoteViews second) { 3811 if (first == null && second == null) { 3812 return false; 3813 } 3814 if (first == null && second != null || first != null && second == null) { 3815 return true; 3816 } 3817 3818 if (!Objects.equals(first.getLayoutId(), second.getLayoutId())) { 3819 return true; 3820 } 3821 3822 if (!Objects.equals(first.getSequenceNumber(), second.getSequenceNumber())) { 3823 return true; 3824 } 3825 3826 return false; 3827 } 3828 3829 /** 3830 * Parcelling creates multiple copies of objects in {@code extras}. Fix them. 3831 * <p> 3832 * For backwards compatibility {@code extras} holds some references to "real" member data such 3833 * as {@link getLargeIcon()} which is mirrored by {@link #EXTRA_LARGE_ICON}. This is mostly 3834 * fine as long as the object stays in one process. 3835 * <p> 3836 * However, once the notification goes into a parcel each reference gets marshalled separately, 3837 * wasting memory. Especially with large images on Auto and TV, this is worth fixing. 3838 */ fixDuplicateExtras()3839 private void fixDuplicateExtras() { 3840 if (extras != null) { 3841 fixDuplicateExtra(mLargeIcon, EXTRA_LARGE_ICON); 3842 } 3843 } 3844 3845 /** 3846 * If we find an extra that's exactly the same as one of the "real" fields but refers to a 3847 * separate object, replace it with the field's version to avoid holding duplicate copies. 3848 */ fixDuplicateExtra(@ullable Parcelable original, @NonNull String extraName)3849 private void fixDuplicateExtra(@Nullable Parcelable original, @NonNull String extraName) { 3850 if (original != null && extras.getParcelable(extraName, Parcelable.class) != null) { 3851 extras.putParcelable(extraName, original); 3852 } 3853 } 3854 3855 /** 3856 * Sets the {@link #contentView} field to be a view with the standard "Latest Event" 3857 * layout. 3858 * 3859 * <p>Uses the {@link #icon} and {@link #when} fields to set the icon and time fields 3860 * in the view.</p> 3861 * @param context The context for your application / activity. 3862 * @param contentTitle The title that goes in the expanded entry. 3863 * @param contentText The text that goes in the expanded entry. 3864 * @param contentIntent The intent to launch when the user clicks the expanded notification. 3865 * If this is an activity, it must include the 3866 * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} flag, which requires 3867 * that you take care of task management as described in the 3868 * <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and Back 3869 * Stack</a> document. 3870 * 3871 * @deprecated Use {@link Builder} instead. 3872 * @removed 3873 */ 3874 @Deprecated setLatestEventInfo(Context context, CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent)3875 public void setLatestEventInfo(Context context, 3876 CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent) { 3877 if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1){ 3878 Log.e(TAG, "setLatestEventInfo() is deprecated and you should feel deprecated.", 3879 new Throwable()); 3880 } 3881 3882 if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { 3883 extras.putBoolean(EXTRA_SHOW_WHEN, true); 3884 } 3885 3886 // ensure that any information already set directly is preserved 3887 final Notification.Builder builder = new Notification.Builder(context, this); 3888 3889 // now apply the latestEventInfo fields 3890 if (contentTitle != null) { 3891 builder.setContentTitle(contentTitle); 3892 } 3893 if (contentText != null) { 3894 builder.setContentText(contentText); 3895 } 3896 builder.setContentIntent(contentIntent); 3897 3898 builder.build(); // callers expect this notification to be ready to use 3899 } 3900 3901 /** 3902 * Sets the token used for background operations for the pending intents associated with this 3903 * notification. 3904 * 3905 * Note: Should <em>only</em> be invoked by NotificationManagerService, since this is normally 3906 * populated by unparceling (and also used there). Any other usage is suspect. 3907 * 3908 * @hide 3909 */ overrideAllowlistToken(IBinder token)3910 public void overrideAllowlistToken(IBinder token) { 3911 mAllowlistToken = token; 3912 if (publicVersion != null) { 3913 publicVersion.overrideAllowlistToken(token); 3914 } 3915 } 3916 3917 /** @hide */ getAllowlistToken()3918 public IBinder getAllowlistToken() { 3919 return mAllowlistToken; 3920 } 3921 3922 /** 3923 * @hide 3924 */ addFieldsFromContext(Context context, Notification notification)3925 public static void addFieldsFromContext(Context context, Notification notification) { 3926 addFieldsFromContext(context.getApplicationInfo(), notification); 3927 } 3928 3929 /** 3930 * @hide 3931 */ addFieldsFromContext(ApplicationInfo ai, Notification notification)3932 public static void addFieldsFromContext(ApplicationInfo ai, Notification notification) { 3933 notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, ai); 3934 } 3935 3936 /** 3937 * @hide 3938 */ dumpDebug(ProtoOutputStream proto, long fieldId)3939 public void dumpDebug(ProtoOutputStream proto, long fieldId) { 3940 long token = proto.start(fieldId); 3941 proto.write(NotificationProto.CHANNEL_ID, getChannelId()); 3942 proto.write(NotificationProto.HAS_TICKER_TEXT, this.tickerText != null); 3943 proto.write(NotificationProto.FLAGS, this.flags); 3944 proto.write(NotificationProto.COLOR, this.color); 3945 proto.write(NotificationProto.CATEGORY, this.category); 3946 proto.write(NotificationProto.GROUP_KEY, this.mGroupKey); 3947 proto.write(NotificationProto.SORT_KEY, this.mSortKey); 3948 if (this.actions != null) { 3949 proto.write(NotificationProto.ACTION_LENGTH, this.actions.length); 3950 } 3951 if (this.visibility >= VISIBILITY_SECRET && this.visibility <= VISIBILITY_PUBLIC) { 3952 proto.write(NotificationProto.VISIBILITY, this.visibility); 3953 } 3954 if (publicVersion != null) { 3955 publicVersion.dumpDebug(proto, NotificationProto.PUBLIC_VERSION); 3956 } 3957 proto.end(token); 3958 } 3959 3960 @Override toString()3961 public String toString() { 3962 StringBuilder sb = new StringBuilder(); 3963 sb.append("Notification(channel="); 3964 sb.append(getChannelId()); 3965 sb.append(" shortcut="); 3966 sb.append(getShortcutId()); 3967 sb.append(" contentView="); 3968 if (contentView != null) { 3969 sb.append(contentView.getPackage()); 3970 sb.append("/0x"); 3971 sb.append(Integer.toHexString(contentView.getLayoutId())); 3972 } else { 3973 sb.append("null"); 3974 } 3975 sb.append(" vibrate="); 3976 if ((this.defaults & DEFAULT_VIBRATE) != 0) { 3977 sb.append("default"); 3978 } else if (this.vibrate != null) { 3979 int N = this.vibrate.length-1; 3980 sb.append("["); 3981 for (int i=0; i<N; i++) { 3982 sb.append(this.vibrate[i]); 3983 sb.append(','); 3984 } 3985 if (N != -1) { 3986 sb.append(this.vibrate[N]); 3987 } 3988 sb.append("]"); 3989 } else { 3990 sb.append("null"); 3991 } 3992 sb.append(" sound="); 3993 if ((this.defaults & DEFAULT_SOUND) != 0) { 3994 sb.append("default"); 3995 } else if (this.sound != null) { 3996 sb.append(this.sound.toString()); 3997 } else { 3998 sb.append("null"); 3999 } 4000 if (this.tickerText != null) { 4001 sb.append(" tick"); 4002 } 4003 sb.append(" defaults="); 4004 sb.append(defaultsToString(this.defaults)); 4005 sb.append(" flags="); 4006 sb.append(flagsToString(this.flags)); 4007 sb.append(String.format(" color=0x%08x", this.color)); 4008 if (this.category != null) { 4009 sb.append(" category="); 4010 sb.append(this.category); 4011 } 4012 if (this.mGroupKey != null) { 4013 sb.append(" groupKey="); 4014 sb.append(this.mGroupKey); 4015 } 4016 if (this.mSortKey != null) { 4017 sb.append(" sortKey="); 4018 sb.append(this.mSortKey); 4019 } 4020 if (actions != null) { 4021 sb.append(" actions="); 4022 sb.append(actions.length); 4023 } 4024 sb.append(" vis="); 4025 sb.append(visibilityToString(this.visibility)); 4026 if (this.publicVersion != null) { 4027 sb.append(" publicVersion="); 4028 sb.append(publicVersion.toString()); 4029 } 4030 if (this.mLocusId != null) { 4031 sb.append(" locusId="); 4032 sb.append(this.mLocusId); // LocusId.toString() is PII safe. 4033 } 4034 sb.append(")"); 4035 return sb.toString(); 4036 } 4037 4038 /** 4039 * {@hide} 4040 */ visibilityToString(int vis)4041 public static String visibilityToString(int vis) { 4042 switch (vis) { 4043 case VISIBILITY_PRIVATE: 4044 return "PRIVATE"; 4045 case VISIBILITY_PUBLIC: 4046 return "PUBLIC"; 4047 case VISIBILITY_SECRET: 4048 return "SECRET"; 4049 default: 4050 return "UNKNOWN(" + String.valueOf(vis) + ")"; 4051 } 4052 } 4053 4054 /** 4055 * {@hide} 4056 */ priorityToString(@riority int pri)4057 public static String priorityToString(@Priority int pri) { 4058 switch (pri) { 4059 case PRIORITY_MIN: 4060 return "MIN"; 4061 case PRIORITY_LOW: 4062 return "LOW"; 4063 case PRIORITY_DEFAULT: 4064 return "DEFAULT"; 4065 case PRIORITY_HIGH: 4066 return "HIGH"; 4067 case PRIORITY_MAX: 4068 return "MAX"; 4069 default: 4070 return "UNKNOWN(" + String.valueOf(pri) + ")"; 4071 } 4072 } 4073 4074 /** 4075 * {@hide} 4076 */ flagsToString(@otificationFlags int flags)4077 public static String flagsToString(@NotificationFlags int flags) { 4078 final List<String> flagStrings = new ArrayList<String>(); 4079 if ((flags & FLAG_SHOW_LIGHTS) != 0) { 4080 flagStrings.add("SHOW_LIGHTS"); 4081 flags &= ~FLAG_SHOW_LIGHTS; 4082 } 4083 if ((flags & FLAG_ONGOING_EVENT) != 0) { 4084 flagStrings.add("ONGOING_EVENT"); 4085 flags &= ~FLAG_ONGOING_EVENT; 4086 } 4087 if ((flags & FLAG_INSISTENT) != 0) { 4088 flagStrings.add("INSISTENT"); 4089 flags &= ~FLAG_INSISTENT; 4090 } 4091 if ((flags & FLAG_ONLY_ALERT_ONCE) != 0) { 4092 flagStrings.add("ONLY_ALERT_ONCE"); 4093 flags &= ~FLAG_ONLY_ALERT_ONCE; 4094 } 4095 if ((flags & FLAG_AUTO_CANCEL) != 0) { 4096 flagStrings.add("AUTO_CANCEL"); 4097 flags &= ~FLAG_AUTO_CANCEL; 4098 } 4099 if ((flags & FLAG_NO_CLEAR) != 0) { 4100 flagStrings.add("NO_CLEAR"); 4101 flags &= ~FLAG_NO_CLEAR; 4102 } 4103 if ((flags & FLAG_FOREGROUND_SERVICE) != 0) { 4104 flagStrings.add("FOREGROUND_SERVICE"); 4105 flags &= ~FLAG_FOREGROUND_SERVICE; 4106 } 4107 if ((flags & FLAG_HIGH_PRIORITY) != 0) { 4108 flagStrings.add("HIGH_PRIORITY"); 4109 flags &= ~FLAG_HIGH_PRIORITY; 4110 } 4111 if ((flags & FLAG_LOCAL_ONLY) != 0) { 4112 flagStrings.add("LOCAL_ONLY"); 4113 flags &= ~FLAG_LOCAL_ONLY; 4114 } 4115 if ((flags & FLAG_GROUP_SUMMARY) != 0) { 4116 flagStrings.add("GROUP_SUMMARY"); 4117 flags &= ~FLAG_GROUP_SUMMARY; 4118 } 4119 if ((flags & FLAG_AUTOGROUP_SUMMARY) != 0) { 4120 flagStrings.add("AUTOGROUP_SUMMARY"); 4121 flags &= ~FLAG_AUTOGROUP_SUMMARY; 4122 } 4123 if ((flags & FLAG_CAN_COLORIZE) != 0) { 4124 flagStrings.add("CAN_COLORIZE"); 4125 flags &= ~FLAG_CAN_COLORIZE; 4126 } 4127 if ((flags & FLAG_BUBBLE) != 0) { 4128 flagStrings.add("BUBBLE"); 4129 flags &= ~FLAG_BUBBLE; 4130 } 4131 if ((flags & FLAG_NO_DISMISS) != 0) { 4132 flagStrings.add("NO_DISMISS"); 4133 flags &= ~FLAG_NO_DISMISS; 4134 } 4135 if ((flags & FLAG_FSI_REQUESTED_BUT_DENIED) != 0) { 4136 flagStrings.add("FSI_REQUESTED_BUT_DENIED"); 4137 flags &= ~FLAG_FSI_REQUESTED_BUT_DENIED; 4138 } 4139 if ((flags & FLAG_USER_INITIATED_JOB) != 0) { 4140 flagStrings.add("USER_INITIATED_JOB"); 4141 flags &= ~FLAG_USER_INITIATED_JOB; 4142 } 4143 if (Flags.lifetimeExtensionRefactor()) { 4144 if ((flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) != 0) { 4145 flagStrings.add("LIFETIME_EXTENDED_BY_DIRECT_REPLY"); 4146 flags &= ~FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; 4147 } 4148 } 4149 if (Flags.apiRichOngoing()) { 4150 if ((flags & FLAG_PROMOTED_ONGOING) != 0) { 4151 flagStrings.add("PROMOTED_ONGOING"); 4152 flags &= ~FLAG_PROMOTED_ONGOING; 4153 } 4154 } 4155 4156 if (android.service.notification.Flags.notificationSilentFlag()) { 4157 if ((flags & FLAG_SILENT) != 0) { 4158 flagStrings.add("SILENT"); 4159 flags &= ~FLAG_SILENT; 4160 } 4161 } 4162 4163 if (flagStrings.isEmpty()) { 4164 return "0"; 4165 } 4166 4167 if (flags != 0) { 4168 flagStrings.add(String.format("UNKNOWN(0x%08x)", flags)); 4169 } 4170 4171 return String.join("|", flagStrings); 4172 } 4173 4174 /** @hide */ defaultsToString(int defaults)4175 public static String defaultsToString(int defaults) { 4176 final List<String> defaultStrings = new ArrayList<String>(); 4177 if ((defaults & DEFAULT_ALL) == DEFAULT_ALL) { 4178 defaultStrings.add("ALL"); 4179 defaults &= ~DEFAULT_ALL; 4180 } 4181 if ((defaults & DEFAULT_SOUND) != 0) { 4182 defaultStrings.add("SOUND"); 4183 defaults &= ~DEFAULT_SOUND; 4184 } 4185 if ((defaults & DEFAULT_VIBRATE) != 0) { 4186 defaultStrings.add("VIBRATE"); 4187 defaults &= ~DEFAULT_VIBRATE; 4188 } 4189 if ((defaults & DEFAULT_LIGHTS) != 0) { 4190 defaultStrings.add("LIGHTS"); 4191 defaults &= ~DEFAULT_LIGHTS; 4192 } 4193 4194 if (defaultStrings.isEmpty()) { 4195 return "0"; 4196 } 4197 4198 if (defaults != 0) { 4199 defaultStrings.add(String.format("UNKNOWN(0x%08x)", defaults)); 4200 } 4201 4202 return String.join("|", defaultStrings); 4203 } 4204 4205 4206 /** 4207 * Returns the very short text summarizing the most critical information contained in the 4208 * notification, or null if this field was not set. 4209 */ 4210 @Nullable 4211 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) getShortCriticalText()4212 public String getShortCriticalText() { 4213 return extras.getString(EXTRA_SHORT_CRITICAL_TEXT); 4214 } 4215 4216 /** 4217 * @hide 4218 */ isOngoingEvent()4219 public boolean isOngoingEvent() { 4220 return (flags & FLAG_ONGOING_EVENT) != 0; 4221 } 4222 4223 /** 4224 * @hide 4225 */ hasCompletedProgress()4226 public boolean hasCompletedProgress() { 4227 // not a progress notification; can't be complete 4228 if (!extras.containsKey(EXTRA_PROGRESS) 4229 || !extras.containsKey(EXTRA_PROGRESS_MAX)) { 4230 return false; 4231 } 4232 // many apps use max 0 for 'indeterminate'; not complete 4233 if (extras.getInt(EXTRA_PROGRESS_MAX) == 0) { 4234 return false; 4235 } 4236 return extras.getInt(EXTRA_PROGRESS) == extras.getInt(EXTRA_PROGRESS_MAX); 4237 } 4238 4239 /** @removed */ 4240 @Deprecated getChannel()4241 public String getChannel() { 4242 return mChannelId; 4243 } 4244 4245 /** 4246 * Returns the id of the channel this notification posts to. 4247 */ getChannelId()4248 public String getChannelId() { 4249 return mChannelId; 4250 } 4251 4252 /** @removed */ 4253 @Deprecated getTimeout()4254 public long getTimeout() { 4255 return mTimeout; 4256 } 4257 4258 /** 4259 * Returns the duration from posting after which this notification should be canceled by the 4260 * system, if it's not canceled already. 4261 */ getTimeoutAfter()4262 public long getTimeoutAfter() { 4263 return mTimeout; 4264 } 4265 4266 /** 4267 * @hide 4268 */ setTimeoutAfter(long timeout)4269 public void setTimeoutAfter(long timeout) { 4270 mTimeout = timeout; 4271 } 4272 4273 /** 4274 * Returns what icon should be shown for this notification if it is being displayed in a 4275 * Launcher that supports badging. Will be one of {@link #BADGE_ICON_NONE}, 4276 * {@link #BADGE_ICON_SMALL}, or {@link #BADGE_ICON_LARGE}. 4277 */ getBadgeIconType()4278 public int getBadgeIconType() { 4279 return mBadgeIcon; 4280 } 4281 4282 /** 4283 * Returns the {@link ShortcutInfo#getId() id} that this notification supersedes, if any. 4284 * 4285 * <p>Used by some Launchers that display notification content to hide shortcuts that duplicate 4286 * notifications. 4287 */ getShortcutId()4288 public String getShortcutId() { 4289 return mShortcutId; 4290 } 4291 4292 /** 4293 * Gets the {@link LocusId} associated with this notification. 4294 * 4295 * <p>Used by the device's intelligence services to correlate objects (such as 4296 * {@link ShortcutInfo} and {@link ContentCaptureContext}) that are correlated. 4297 */ 4298 @Nullable getLocusId()4299 public LocusId getLocusId() { 4300 return mLocusId; 4301 } 4302 4303 /** 4304 * Returns the settings text provided to {@link Builder#setSettingsText(CharSequence)}. 4305 */ getSettingsText()4306 public CharSequence getSettingsText() { 4307 return mSettingsText; 4308 } 4309 4310 /** 4311 * Returns which type of notifications in a group are responsible for audibly alerting the 4312 * user. See {@link #GROUP_ALERT_ALL}, {@link #GROUP_ALERT_CHILDREN}, 4313 * {@link #GROUP_ALERT_SUMMARY}. 4314 */ getGroupAlertBehavior()4315 public @GroupAlertBehavior int getGroupAlertBehavior() { 4316 return mGroupAlertBehavior; 4317 } 4318 4319 /** 4320 * Sets which type of notifications in a group are responsible for audibly alerting the 4321 * user. See {@link #GROUP_ALERT_ALL}, {@link #GROUP_ALERT_CHILDREN}, 4322 * {@link #GROUP_ALERT_SUMMARY}. 4323 * @param groupAlertBehavior 4324 * @hide 4325 */ setGroupAlertBehavior(@roupAlertBehavior int groupAlertBehavior)4326 public void setGroupAlertBehavior(@GroupAlertBehavior int groupAlertBehavior) { 4327 mGroupAlertBehavior = groupAlertBehavior; 4328 } 4329 4330 /** 4331 * Returns the bubble metadata that will be used to display app content in a floating window 4332 * over the existing foreground activity. 4333 */ 4334 @Nullable getBubbleMetadata()4335 public BubbleMetadata getBubbleMetadata() { 4336 return mBubbleMetadata; 4337 } 4338 4339 /** 4340 * Sets the {@link BubbleMetadata} for this notification. 4341 * @hide 4342 */ setBubbleMetadata(BubbleMetadata data)4343 public void setBubbleMetadata(BubbleMetadata data) { 4344 mBubbleMetadata = data; 4345 } 4346 4347 /** 4348 * Returns whether the platform is allowed (by the app developer) to generate contextual actions 4349 * for this notification. 4350 */ getAllowSystemGeneratedContextualActions()4351 public boolean getAllowSystemGeneratedContextualActions() { 4352 return mAllowSystemGeneratedContextualActions; 4353 } 4354 4355 /** 4356 * The small icon representing this notification in the status bar and content view. 4357 * 4358 * @return the small icon representing this notification. 4359 * 4360 * @see Builder#getSmallIcon() 4361 * @see Builder#setSmallIcon(Icon) 4362 */ getSmallIcon()4363 public Icon getSmallIcon() { 4364 return mSmallIcon; 4365 } 4366 4367 /** 4368 * Used when notifying to clean up legacy small icons. 4369 * @hide 4370 */ 4371 @UnsupportedAppUsage setSmallIcon(Icon icon)4372 public void setSmallIcon(Icon icon) { 4373 mSmallIcon = icon; 4374 } 4375 4376 /** 4377 * The large icon shown in this notification's content view. 4378 * @see Builder#getLargeIcon() 4379 * @see Builder#setLargeIcon(Icon) 4380 */ getLargeIcon()4381 public Icon getLargeIcon() { 4382 return mLargeIcon; 4383 } 4384 4385 /** 4386 * @hide 4387 */ hasAppProvidedWhen()4388 public boolean hasAppProvidedWhen() { 4389 return when != 0 && when != creationTime; 4390 } 4391 4392 /** 4393 * @hide 4394 */ 4395 @UnsupportedAppUsage isGroupSummary()4396 public boolean isGroupSummary() { 4397 return mGroupKey != null && (flags & FLAG_GROUP_SUMMARY) != 0; 4398 } 4399 4400 /** 4401 * @hide 4402 */ 4403 @UnsupportedAppUsage isGroupChild()4404 public boolean isGroupChild() { 4405 return mGroupKey != null && (flags & FLAG_GROUP_SUMMARY) == 0; 4406 } 4407 4408 /** 4409 * @hide 4410 */ suppressAlertingDueToGrouping()4411 public boolean suppressAlertingDueToGrouping() { 4412 if (isGroupSummary() 4413 && getGroupAlertBehavior() == Notification.GROUP_ALERT_CHILDREN) { 4414 return true; 4415 } else if (isGroupChild() 4416 && getGroupAlertBehavior() == Notification.GROUP_ALERT_SUMMARY) { 4417 return true; 4418 } 4419 return false; 4420 } 4421 4422 4423 /** 4424 * Finds and returns a remote input and its corresponding action. 4425 * 4426 * @param requiresFreeform requires the remoteinput to allow freeform or not. 4427 * @return the result pair, {@code null} if no result is found. 4428 */ 4429 @Nullable findRemoteInputActionPair(boolean requiresFreeform)4430 public Pair<RemoteInput, Action> findRemoteInputActionPair(boolean requiresFreeform) { 4431 if (isPromotedOngoing()) { 4432 return null; 4433 } 4434 if (actions == null) { 4435 return null; 4436 } 4437 for (Notification.Action action : actions) { 4438 if (action.getRemoteInputs() == null) { 4439 continue; 4440 } 4441 RemoteInput resultRemoteInput = null; 4442 for (RemoteInput remoteInput : action.getRemoteInputs()) { 4443 if (remoteInput.getAllowFreeFormInput() || !requiresFreeform) { 4444 resultRemoteInput = remoteInput; 4445 } 4446 } 4447 if (resultRemoteInput != null) { 4448 return Pair.create(resultRemoteInput, action); 4449 } 4450 } 4451 return null; 4452 } 4453 4454 /** 4455 * Returns the actions that are contextual (that is, suggested because of the content of the 4456 * notification) out of the actions in this notification. 4457 */ getContextualActions()4458 public @NonNull List<Notification.Action> getContextualActions() { 4459 if (actions == null || isPromotedOngoing()) return Collections.emptyList(); 4460 List<Notification.Action> contextualActions = new ArrayList<>(); 4461 for (Notification.Action action : actions) { 4462 if (action.isContextual()) { 4463 contextualActions.add(action); 4464 } 4465 } 4466 return contextualActions; 4467 } 4468 4469 /** 4470 * Sets the FLAG_SILENT flag to mark the notification as silent and clears the group key. 4471 * @hide 4472 */ fixSilentGroup()4473 public void fixSilentGroup() { 4474 if (android.service.notification.Flags.notificationSilentFlag()) { 4475 if (GROUP_KEY_SILENT.equals(mGroupKey)) { 4476 mGroupKey = null; 4477 flags |= FLAG_SILENT; 4478 } 4479 } 4480 } 4481 4482 /** 4483 * @return whether this notification is silent. See {@link Builder#setSilent()} 4484 * @hide 4485 */ isSilent()4486 public boolean isSilent() { 4487 if (android.service.notification.Flags.notificationSilentFlag()) { 4488 return (flags & Notification.FLAG_SILENT) != 0; 4489 } else { 4490 return GROUP_KEY_SILENT.equals(getGroup()) && suppressAlertingDueToGrouping(); 4491 } 4492 } 4493 4494 /** 4495 * Builder class for {@link Notification} objects. 4496 * 4497 * Provides a convenient way to set the various fields of a {@link Notification} and generate 4498 * content views using the platform's notification layout template. If your app supports 4499 * versions of Android as old as API level 4, you can instead use 4500 * {@link androidx.core.app.NotificationCompat.Builder NotificationCompat.Builder}, 4501 * available in the <a href="{@docRoot}tools/extras/support-library.html">Android Support 4502 * library</a>. 4503 * 4504 * <p>Example: 4505 * 4506 * <pre class="prettyprint"> 4507 * Notification noti = new Notification.Builder(mContext) 4508 * .setContentTitle("New mail from " + sender.toString()) 4509 * .setContentText(subject) 4510 * .setSmallIcon(R.drawable.new_mail) 4511 * .setLargeIcon(aBitmap) 4512 * .build(); 4513 * </pre> 4514 */ 4515 public static class Builder { 4516 /** 4517 * @hide 4518 */ 4519 public static final String EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT = 4520 "android.rebuild.contentViewActionCount"; 4521 /** 4522 * @hide 4523 */ 4524 public static final String EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT 4525 = "android.rebuild.bigViewActionCount"; 4526 /** 4527 * @hide 4528 */ 4529 public static final String EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT 4530 = "android.rebuild.hudViewActionCount"; 4531 4532 private static final boolean USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY = 4533 SystemProperties.getBoolean("notifications.only_title", true); 4534 4535 private Context mContext; 4536 private Notification mN; 4537 private Bundle mUserExtras = new Bundle(); 4538 private Style mStyle; 4539 @UnsupportedAppUsage 4540 private ArrayList<Action> mActions = new ArrayList<>(MAX_ACTION_BUTTONS); 4541 private ArrayList<Person> mPersonList = new ArrayList<>(); 4542 private ContrastColorUtil mColorUtil; 4543 private boolean mIsLegacy; 4544 private boolean mIsLegacyInitialized; 4545 4546 /** 4547 * Caches an instance of StandardTemplateParams. Note that this may have been used before, 4548 * so make sure to call {@link StandardTemplateParams#reset()} before using it. 4549 */ 4550 StandardTemplateParams mParams = new StandardTemplateParams(); 4551 Colors mColors = new Colors(); 4552 4553 private boolean mTintActionButtons; 4554 private boolean mInNightMode; 4555 4556 /** 4557 * Constructs a new Builder with the defaults: 4558 * 4559 * @param context 4560 * A {@link Context} that will be used by the Builder to construct the 4561 * RemoteViews. The Context will not be held past the lifetime of this Builder 4562 * object. 4563 * @param channelId 4564 * The constructed Notification will be posted on this 4565 * {@link NotificationChannel}. To use a NotificationChannel, it must first be 4566 * created using {@link NotificationManager#createNotificationChannel}. 4567 */ Builder(Context context, String channelId)4568 public Builder(Context context, String channelId) { 4569 this(context, (Notification) null); 4570 mN.mChannelId = channelId; 4571 } 4572 4573 /** 4574 * @deprecated use {@link #Builder(Context, String)} 4575 * instead. All posted Notifications must specify a NotificationChannel Id. 4576 */ 4577 @Deprecated Builder(Context context)4578 public Builder(Context context) { 4579 this(context, (Notification) null); 4580 } 4581 4582 /** 4583 * @hide 4584 */ Builder(Context context, Notification toAdopt)4585 public Builder(Context context, Notification toAdopt) { 4586 mContext = context; 4587 Resources res = mContext.getResources(); 4588 mTintActionButtons = res.getBoolean(R.bool.config_tintNotificationActionButtons); 4589 4590 if (res.getBoolean(R.bool.config_enableNightMode)) { 4591 Configuration currentConfig = res.getConfiguration(); 4592 mInNightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) 4593 == Configuration.UI_MODE_NIGHT_YES; 4594 } 4595 4596 if (toAdopt == null) { 4597 mN = new Notification(); 4598 if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { 4599 mN.extras.putBoolean(EXTRA_SHOW_WHEN, true); 4600 } 4601 mN.priority = PRIORITY_DEFAULT; 4602 mN.visibility = VISIBILITY_PRIVATE; 4603 } else { 4604 mN = toAdopt; 4605 if (mN.actions != null) { 4606 Collections.addAll(mActions, mN.actions); 4607 } 4608 4609 if (mN.extras.containsKey(EXTRA_PEOPLE_LIST)) { 4610 ArrayList<Person> people = mN.extras.getParcelableArrayList(EXTRA_PEOPLE_LIST, 4611 android.app.Person.class); 4612 if (people != null && !people.isEmpty()) { 4613 mPersonList.addAll(people); 4614 } 4615 } 4616 4617 if (mN.getSmallIcon() == null && mN.icon != 0) { 4618 setSmallIcon(mN.icon); 4619 } 4620 4621 if (mN.getLargeIcon() == null && mN.largeIcon != null) { 4622 setLargeIcon(mN.largeIcon); 4623 } 4624 4625 String templateClass = mN.extras.getString(EXTRA_TEMPLATE); 4626 if (!TextUtils.isEmpty(templateClass)) { 4627 final Class<? extends Style> styleClass 4628 = getNotificationStyleClass(templateClass); 4629 if (styleClass == null) { 4630 Log.d(TAG, "Unknown style class: " + templateClass); 4631 } else { 4632 try { 4633 final Constructor<? extends Style> ctor = 4634 styleClass.getDeclaredConstructor(); 4635 ctor.setAccessible(true); 4636 final Style style = ctor.newInstance(); 4637 style.restoreFromExtras(mN.extras); 4638 4639 if (style != null) { 4640 setStyle(style); 4641 } 4642 } catch (Throwable t) { 4643 Log.e(TAG, "Could not create Style", t); 4644 } 4645 } 4646 } 4647 } 4648 } 4649 getColorUtil()4650 private ContrastColorUtil getColorUtil() { 4651 if (mColorUtil == null) { 4652 mColorUtil = ContrastColorUtil.getInstance(mContext); 4653 } 4654 return mColorUtil; 4655 } 4656 4657 /** 4658 * From Android 11, messaging notifications (those that use {@link MessagingStyle}) that 4659 * use this method to link to a published long-lived sharing shortcut may appear in a 4660 * dedicated Conversation section of the shade and may show configuration options that 4661 * are unique to conversations. This behavior should be reserved for person to person(s) 4662 * conversations where there is a likely social obligation for an individual to respond. 4663 * <p> 4664 * For example, the following are some examples of notifications that belong in the 4665 * conversation space: 4666 * <ul> 4667 * <li>1:1 conversations between two individuals</li> 4668 * <li>Group conversations between individuals where everyone can contribute</li> 4669 * </ul> 4670 * And the following are some examples of notifications that do not belong in the 4671 * conversation space: 4672 * <ul> 4673 * <li>Advertisements from a bot (even if personal and contextualized)</li> 4674 * <li>Engagement notifications from a bot</li> 4675 * <li>Directional conversations where there is an active speaker and many passive 4676 * individuals</li> 4677 * <li>Stream / posting updates from other individuals</li> 4678 * <li>Email, document comments, or other conversation types that are not real-time</li> 4679 * </ul> 4680 * </p> 4681 * 4682 * <p> 4683 * Additionally, this method can be used for all types of notifications to mark this 4684 * notification as duplicative of a Launcher shortcut. Launchers that show badges or 4685 * notification content may then suppress the shortcut in favor of the content of this 4686 * notification. 4687 * <p> 4688 * If this notification has {@link BubbleMetadata} attached that was created with 4689 * a shortcutId a check will be performed to ensure the shortcutId supplied to bubble 4690 * metadata matches the shortcutId set here, if one was set. If the shortcutId's were 4691 * specified but do not match, an exception is thrown. 4692 * 4693 * @param shortcutId the {@link ShortcutInfo#getId() id} of the shortcut this notification 4694 * is linked to 4695 * 4696 * @see BubbleMetadata.Builder#Builder(String) 4697 */ 4698 @NonNull setShortcutId(String shortcutId)4699 public Builder setShortcutId(String shortcutId) { 4700 mN.mShortcutId = shortcutId; 4701 return this; 4702 } 4703 4704 /** 4705 * Sets the {@link LocusId} associated with this notification. 4706 * 4707 * <p>This method should be called when the {@link LocusId} is used in other places (such 4708 * as {@link ShortcutInfo} and {@link ContentCaptureContext}) so the device's intelligence 4709 * services can correlate them. 4710 */ 4711 @NonNull setLocusId(@ullable LocusId locusId)4712 public Builder setLocusId(@Nullable LocusId locusId) { 4713 mN.mLocusId = locusId; 4714 return this; 4715 } 4716 4717 /** 4718 * Sets which icon to display as a badge for this notification. 4719 * 4720 * Must be one of {@link #BADGE_ICON_NONE}, {@link #BADGE_ICON_SMALL}, 4721 * {@link #BADGE_ICON_LARGE}. 4722 * 4723 * Note: This value might be ignored, for launchers that don't support badge icons. 4724 */ 4725 @NonNull setBadgeIconType(int icon)4726 public Builder setBadgeIconType(int icon) { 4727 mN.mBadgeIcon = icon; 4728 return this; 4729 } 4730 4731 /** 4732 * Sets the group alert behavior for this notification. Use this method to mute this 4733 * notification if alerts for this notification's group should be handled by a different 4734 * notification. This is only applicable for notifications that belong to a 4735 * {@link #setGroup(String) group}. This must be called on all notifications you want to 4736 * mute. For example, if you want only the summary of your group to make noise and/or peek 4737 * on screen, all children in the group should have the group alert behavior 4738 * {@link #GROUP_ALERT_SUMMARY}. 4739 * 4740 * <p> The default value is {@link #GROUP_ALERT_ALL}.</p> 4741 */ 4742 @NonNull setGroupAlertBehavior(@roupAlertBehavior int groupAlertBehavior)4743 public Builder setGroupAlertBehavior(@GroupAlertBehavior int groupAlertBehavior) { 4744 mN.mGroupAlertBehavior = groupAlertBehavior; 4745 return this; 4746 } 4747 4748 /** 4749 * Sets the {@link BubbleMetadata} that will be used to display app content in a floating 4750 * window over the existing foreground activity. 4751 * 4752 * <p>This data will be ignored unless the notification is posted to a channel that 4753 * allows {@link NotificationChannel#canBubble() bubbles}.</p> 4754 * 4755 * <p>Notifications allowed to bubble that have valid bubble metadata will display in 4756 * collapsed state outside of the notification shade on unlocked devices. When a user 4757 * interacts with the collapsed state, the bubble intent will be invoked and displayed.</p> 4758 */ 4759 @NonNull setBubbleMetadata(@ullable BubbleMetadata data)4760 public Builder setBubbleMetadata(@Nullable BubbleMetadata data) { 4761 mN.mBubbleMetadata = data; 4762 return this; 4763 } 4764 4765 /** @removed */ 4766 @Deprecated setChannel(String channelId)4767 public Builder setChannel(String channelId) { 4768 mN.mChannelId = channelId; 4769 return this; 4770 } 4771 4772 /** 4773 * Specifies the channel the notification should be delivered on. 4774 */ 4775 @NonNull setChannelId(String channelId)4776 public Builder setChannelId(String channelId) { 4777 mN.mChannelId = channelId; 4778 return this; 4779 } 4780 4781 /** @removed */ 4782 @Deprecated setTimeout(long durationMs)4783 public Builder setTimeout(long durationMs) { 4784 mN.mTimeout = durationMs; 4785 return this; 4786 } 4787 4788 /** 4789 * Specifies a duration in milliseconds after which this notification should be canceled, 4790 * if it is not already canceled. 4791 */ 4792 @NonNull setTimeoutAfter(long durationMs)4793 public Builder setTimeoutAfter(long durationMs) { 4794 mN.mTimeout = durationMs; 4795 return this; 4796 } 4797 4798 /** 4799 * Add a timestamp pertaining to the notification (usually the time the event occurred). 4800 * 4801 * @see Notification#when 4802 */ 4803 @NonNull setWhen(long when)4804 public Builder setWhen(long when) { 4805 mN.when = when; 4806 return this; 4807 } 4808 4809 /** 4810 * Control whether the timestamp set with {@link #setWhen(long) setWhen} is shown 4811 * in the content view. 4812 */ 4813 @NonNull setShowWhen(boolean show)4814 public Builder setShowWhen(boolean show) { 4815 mN.extras.putBoolean(EXTRA_SHOW_WHEN, show); 4816 return this; 4817 } 4818 4819 /** 4820 * Show the {@link Notification#when} field as a stopwatch. 4821 * 4822 * Instead of presenting <code>when</code> as a timestamp, the notification will show an 4823 * automatically updating display of the minutes and seconds since <code>when</code>. 4824 * 4825 * Useful when showing an elapsed time (like an ongoing phone call). 4826 * 4827 * The counter can also be set to count down to <code>when</code> when using 4828 * {@link #setChronometerCountDown(boolean)}. 4829 * 4830 * @see android.widget.Chronometer 4831 * @see Notification#when 4832 * @see #setChronometerCountDown(boolean) 4833 */ 4834 @NonNull setUsesChronometer(boolean b)4835 public Builder setUsesChronometer(boolean b) { 4836 mN.extras.putBoolean(EXTRA_SHOW_CHRONOMETER, b); 4837 return this; 4838 } 4839 4840 /** 4841 * Sets the Chronometer to count down instead of counting up. 4842 * 4843 * <p>This is only relevant if {@link #setUsesChronometer(boolean)} has been set to true. 4844 * If it isn't set the chronometer will count up. 4845 * 4846 * @see #setUsesChronometer(boolean) 4847 */ 4848 @NonNull setChronometerCountDown(boolean countDown)4849 public Builder setChronometerCountDown(boolean countDown) { 4850 mN.extras.putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, countDown); 4851 return this; 4852 } 4853 4854 /** 4855 * Set the small icon resource, which will be used to represent the notification in the 4856 * status bar. 4857 * 4858 4859 * The platform template for the expanded view will draw this icon in the left, unless a 4860 * {@link #setLargeIcon(Bitmap) large icon} has also been specified, in which case the small 4861 * icon will be moved to the right-hand side. 4862 * 4863 4864 * @param icon 4865 * A resource ID in the application's package of the drawable to use. 4866 * @see Notification#icon 4867 */ 4868 @NonNull setSmallIcon(@rawableRes int icon)4869 public Builder setSmallIcon(@DrawableRes int icon) { 4870 return setSmallIcon(icon != 0 4871 ? Icon.createWithResource(mContext, icon) 4872 : null); 4873 } 4874 4875 /** 4876 * A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional 4877 * level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable 4878 * LevelListDrawable}. 4879 * 4880 * @param icon A resource ID in the application's package of the drawable to use. 4881 * @param level The level to use for the icon. 4882 * 4883 * @see Notification#icon 4884 * @see Notification#iconLevel 4885 */ 4886 @NonNull setSmallIcon(@rawableRes int icon, int level)4887 public Builder setSmallIcon(@DrawableRes int icon, int level) { 4888 mN.iconLevel = level; 4889 return setSmallIcon(icon); 4890 } 4891 4892 /** 4893 * Set the small icon, which will be used to represent the notification in the 4894 * status bar and content view (unless overridden there by a 4895 * {@link #setLargeIcon(Bitmap) large icon}). 4896 * 4897 * @param icon An Icon object to use. 4898 * @see Notification#icon 4899 */ 4900 @NonNull setSmallIcon(Icon icon)4901 public Builder setSmallIcon(Icon icon) { 4902 mN.setSmallIcon(icon); 4903 if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) { 4904 mN.icon = icon.getResId(); 4905 } 4906 return this; 4907 } 4908 4909 /** 4910 * If {@code true}, silences this instance of the notification, regardless of the sounds or 4911 * vibrations set on the notification or notification channel. If {@code false}, then the 4912 * normal sound and vibration logic applies. 4913 * 4914 * @hide 4915 */ setSilent(boolean silent)4916 public @NonNull Builder setSilent(boolean silent) { 4917 if (!silent) { 4918 return this; 4919 } 4920 if (mN.isGroupSummary()) { 4921 setGroupAlertBehavior(GROUP_ALERT_CHILDREN); 4922 } else { 4923 setGroupAlertBehavior(GROUP_ALERT_SUMMARY); 4924 } 4925 4926 setVibrate(null); 4927 setSound(null); 4928 mN.defaults &= ~DEFAULT_SOUND; 4929 mN.defaults &= ~DEFAULT_VIBRATE; 4930 setDefaults(mN.defaults); 4931 4932 if (android.service.notification.Flags.notificationSilentFlag()) { 4933 mN.flags |= FLAG_SILENT; 4934 } else { 4935 if (TextUtils.isEmpty(mN.mGroupKey)) { 4936 setGroup(GROUP_KEY_SILENT); 4937 } 4938 } 4939 return this; 4940 } 4941 4942 /** 4943 * Set the first line of text in the platform notification template. 4944 */ 4945 @NonNull setContentTitle(CharSequence title)4946 public Builder setContentTitle(CharSequence title) { 4947 mN.extras.putCharSequence(EXTRA_TITLE, safeCharSequence(title)); 4948 return this; 4949 } 4950 4951 /** 4952 * Set the second line of text in the platform notification template. 4953 */ 4954 @NonNull setContentText(CharSequence text)4955 public Builder setContentText(CharSequence text) { 4956 mN.extras.putCharSequence(EXTRA_TEXT, safeCharSequence(text)); 4957 return this; 4958 } 4959 4960 /** 4961 * This provides some additional information that is displayed in the notification. No 4962 * guarantees are given where exactly it is displayed. 4963 * 4964 * <p>This information should only be provided if it provides an essential 4965 * benefit to the understanding of the notification. The more text you provide the 4966 * less readable it becomes. For example, an email client should only provide the account 4967 * name here if more than one email account has been added.</p> 4968 * 4969 * <p>As of {@link android.os.Build.VERSION_CODES#N} this information is displayed in the 4970 * notification header area. 4971 * 4972 * On Android versions before {@link android.os.Build.VERSION_CODES#N} 4973 * this will be shown in the third line of text in the platform notification template. 4974 * You should not be using {@link #setProgress(int, int, boolean)} at the 4975 * same time on those versions; they occupy the same place. 4976 * </p> 4977 */ 4978 @NonNull setSubText(CharSequence text)4979 public Builder setSubText(CharSequence text) { 4980 mN.extras.putCharSequence(EXTRA_SUB_TEXT, safeCharSequence(text)); 4981 return this; 4982 } 4983 4984 /** 4985 * Provides text that will appear as a link to your application's settings. 4986 * 4987 * <p>This text does not appear within notification {@link Style templates} but may 4988 * appear when the user uses an affordance to learn more about the notification. 4989 * Additionally, this text will not appear unless you provide a valid link target by 4990 * handling {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. 4991 * 4992 * <p>This text is meant to be concise description about what the user can customize 4993 * when they click on this link. The recommended maximum length is 40 characters. 4994 * @param text 4995 * @return 4996 */ 4997 @NonNull setSettingsText(CharSequence text)4998 public Builder setSettingsText(CharSequence text) { 4999 mN.mSettingsText = safeCharSequence(text); 5000 return this; 5001 } 5002 5003 /** 5004 * Set the remote input history. 5005 * 5006 * This should be set to the most recent inputs that have been sent 5007 * through a {@link RemoteInput} of this Notification and cleared once the it is no 5008 * longer relevant (e.g. for chat notifications once the other party has responded). 5009 * 5010 * The most recent input must be stored at the 0 index, the second most recent at the 5011 * 1 index, etc. Note that the system will limit both how far back the inputs will be shown 5012 * and how much of each individual input is shown. 5013 * 5014 * <p>Note: The reply text will only be shown on notifications that have least one action 5015 * with a {@code RemoteInput}.</p> 5016 */ 5017 @NonNull setRemoteInputHistory(CharSequence[] text)5018 public Builder setRemoteInputHistory(CharSequence[] text) { 5019 if (text == null) { 5020 mN.extras.putCharSequenceArray(EXTRA_REMOTE_INPUT_HISTORY, null); 5021 } else { 5022 final int itemCount = Math.min(MAX_REPLY_HISTORY, text.length); 5023 CharSequence[] safe = new CharSequence[itemCount]; 5024 RemoteInputHistoryItem[] items = new RemoteInputHistoryItem[itemCount]; 5025 for (int i = 0; i < itemCount; i++) { 5026 safe[i] = safeCharSequence(text[i]); 5027 items[i] = new RemoteInputHistoryItem(text[i]); 5028 } 5029 mN.extras.putCharSequenceArray(EXTRA_REMOTE_INPUT_HISTORY, safe); 5030 5031 // Also add these messages as structured history items. 5032 mN.extras.putParcelableArray(EXTRA_REMOTE_INPUT_HISTORY_ITEMS, items); 5033 } 5034 return this; 5035 } 5036 5037 /** 5038 * Set the remote input history, with support for embedding URIs and mime types for 5039 * images and other media. 5040 * @hide 5041 */ 5042 @NonNull setRemoteInputHistory(RemoteInputHistoryItem[] items)5043 public Builder setRemoteInputHistory(RemoteInputHistoryItem[] items) { 5044 if (items == null) { 5045 mN.extras.putParcelableArray(EXTRA_REMOTE_INPUT_HISTORY_ITEMS, null); 5046 } else { 5047 final int itemCount = Math.min(MAX_REPLY_HISTORY, items.length); 5048 RemoteInputHistoryItem[] history = new RemoteInputHistoryItem[itemCount]; 5049 for (int i = 0; i < itemCount; i++) { 5050 history[i] = items[i]; 5051 } 5052 mN.extras.putParcelableArray(EXTRA_REMOTE_INPUT_HISTORY_ITEMS, history); 5053 } 5054 return this; 5055 } 5056 5057 /** 5058 * Sets whether remote history entries view should have a spinner. 5059 * @hide 5060 */ 5061 @NonNull setShowRemoteInputSpinner(boolean showSpinner)5062 public Builder setShowRemoteInputSpinner(boolean showSpinner) { 5063 mN.extras.putBoolean(EXTRA_SHOW_REMOTE_INPUT_SPINNER, showSpinner); 5064 return this; 5065 } 5066 5067 /** 5068 * Sets whether smart reply buttons should be hidden. 5069 * @hide 5070 */ 5071 @NonNull setHideSmartReplies(boolean hideSmartReplies)5072 public Builder setHideSmartReplies(boolean hideSmartReplies) { 5073 mN.extras.putBoolean(EXTRA_HIDE_SMART_REPLIES, hideSmartReplies); 5074 return this; 5075 } 5076 5077 /** 5078 * Sets the number of items this notification represents. May be displayed as a badge count 5079 * for Launchers that support badging. 5080 */ 5081 @NonNull setNumber(int number)5082 public Builder setNumber(int number) { 5083 mN.number = number; 5084 return this; 5085 } 5086 5087 /** 5088 * A small piece of additional information pertaining to this notification. 5089 * 5090 * The platform template will draw this on the last line of the notification, at the far 5091 * right (to the right of a smallIcon if it has been placed there). 5092 * 5093 * @deprecated use {@link #setSubText(CharSequence)} instead to set a text in the header. 5094 * For legacy apps targeting a version below {@link android.os.Build.VERSION_CODES#N} this 5095 * field will still show up, but the subtext will take precedence. 5096 */ 5097 @Deprecated setContentInfo(CharSequence info)5098 public Builder setContentInfo(CharSequence info) { 5099 mN.extras.putCharSequence(EXTRA_INFO_TEXT, safeCharSequence(info)); 5100 return this; 5101 } 5102 5103 /** 5104 * Sets a very short string summarizing the most critical information contained in the 5105 * notification. Suggested max length is 7 characters, and there is no guarantee how much or 5106 * how little of this text will be shown. 5107 */ 5108 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 5109 @NonNull setShortCriticalText(@ullable String shortCriticalText)5110 public Builder setShortCriticalText(@Nullable String shortCriticalText) { 5111 mN.extras.putString(EXTRA_SHORT_CRITICAL_TEXT, safeString(shortCriticalText)); 5112 return this; 5113 } 5114 5115 /** 5116 * Set the progress this notification represents. 5117 * 5118 * The platform template will represent this using a {@link ProgressBar}. 5119 */ 5120 @NonNull setProgress(int max, int progress, boolean indeterminate)5121 public Builder setProgress(int max, int progress, boolean indeterminate) { 5122 mN.extras.putInt(EXTRA_PROGRESS, progress); 5123 mN.extras.putInt(EXTRA_PROGRESS_MAX, max); 5124 mN.extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, indeterminate); 5125 return this; 5126 } 5127 5128 /** 5129 * Supply a custom RemoteViews to use instead of the platform template. 5130 * 5131 * Use {@link #setCustomContentView(RemoteViews)} instead. 5132 */ 5133 @Deprecated setContent(RemoteViews views)5134 public Builder setContent(RemoteViews views) { 5135 return setCustomContentView(views); 5136 } 5137 5138 /** 5139 * Supply custom RemoteViews to use instead of the platform template. 5140 * 5141 * This will override the layout that would otherwise be constructed by this Builder 5142 * object. 5143 */ 5144 @NonNull setCustomContentView(RemoteViews contentView)5145 public Builder setCustomContentView(RemoteViews contentView) { 5146 mN.contentView = contentView; 5147 return this; 5148 } 5149 5150 /** 5151 * Supply custom RemoteViews to use instead of the platform template in the expanded form. 5152 * 5153 * This will override the expanded layout that would otherwise be constructed by this 5154 * Builder object. 5155 */ 5156 @NonNull setCustomBigContentView(RemoteViews contentView)5157 public Builder setCustomBigContentView(RemoteViews contentView) { 5158 mN.bigContentView = contentView; 5159 return this; 5160 } 5161 5162 /** 5163 * Supply custom RemoteViews to use instead of the platform template in the heads up dialog. 5164 * 5165 * This will override the heads-up layout that would otherwise be constructed by this 5166 * Builder object. 5167 */ 5168 @NonNull setCustomHeadsUpContentView(RemoteViews contentView)5169 public Builder setCustomHeadsUpContentView(RemoteViews contentView) { 5170 mN.headsUpContentView = contentView; 5171 return this; 5172 } 5173 5174 /** 5175 * Supply a {@link PendingIntent} to be sent when the notification is clicked. 5176 * 5177 * <p>As of Android {@link android.os.Build.VERSION_CODES#S}, apps targeting API level 5178 * {@link android.os.Build.VERSION_CODES#S} or higher won't be able to start activities 5179 * while processing broadcast receivers or services in response to notification clicks. To 5180 * launch an activity in those cases, provide a {@link PendingIntent} for the activity 5181 * itself. 5182 * 5183 * <p>As of {@link android.os.Build.VERSION_CODES#HONEYCOMB}, if this field is unset and you 5184 * have specified a custom RemoteViews with {@link #setContent(RemoteViews)}, you can use 5185 * {@link RemoteViews#setOnClickPendingIntent RemoteViews.setOnClickPendingIntent(int,PendingIntent)} 5186 * to assign PendingIntents to individual views in that custom layout (i.e., to create 5187 * clickable buttons inside the notification view). 5188 * 5189 * @see Notification#contentIntent Notification.contentIntent 5190 */ 5191 @NonNull setContentIntent(PendingIntent intent)5192 public Builder setContentIntent(PendingIntent intent) { 5193 mN.contentIntent = intent; 5194 return this; 5195 } 5196 5197 /** 5198 * Supply a {@link PendingIntent} to send when the notification is cleared explicitly by the user. 5199 * 5200 * @see Notification#deleteIntent 5201 */ 5202 @NonNull setDeleteIntent(PendingIntent intent)5203 public Builder setDeleteIntent(PendingIntent intent) { 5204 mN.deleteIntent = intent; 5205 return this; 5206 } 5207 5208 /** 5209 * An intent to launch instead of posting the notification to the status bar. 5210 * Only for use with extremely high-priority notifications demanding the user's 5211 * <strong>immediate</strong> attention, such as an incoming phone call or 5212 * alarm clock that the user has explicitly set to a particular time. 5213 * If this facility is used for something else, please give the user an option 5214 * to turn it off and use a normal notification, as this can be extremely 5215 * disruptive. 5216 * 5217 * <p>Apps targeting {@link Build.VERSION_CODES#Q} and above will have to request 5218 * a permission ({@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}) in order to 5219 * use full screen intents. </p> 5220 * <p> 5221 * Prior to {@link Build.VERSION_CODES#TIRAMISU}, the system may display a 5222 * heads up notification (which may display on screen longer than other heads up 5223 * notifications), instead of launching the intent, while the user is using the device. 5224 * From {@link Build.VERSION_CODES#TIRAMISU}, 5225 * the system UI will display a heads up notification, instead of launching this intent, 5226 * while the user is using the device. This notification will display with emphasized 5227 * action buttons. If the posting app holds 5228 * {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}, then the heads 5229 * up notification will appear persistently until the user dismisses or snoozes it, or 5230 * the app cancels it. If the posting app does not hold 5231 * {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}, then the notification will 5232 * appear as heads up notification even when the screen is locked or turned off, and this 5233 * notification will only be persistent for 60 seconds. 5234 * </p> 5235 * <p> 5236 * To be launched as a full screen intent, the notification must also be posted to a 5237 * channel with importance level set to IMPORTANCE_HIGH or higher. 5238 * </p> 5239 * 5240 * @param intent The pending intent to launch. 5241 * @param highPriority Passing true will cause this notification to be sent 5242 * even if other notifications are suppressed. 5243 * 5244 * @see Notification#fullScreenIntent 5245 */ 5246 @NonNull 5247 @RequiresPermission(android.Manifest.permission.USE_FULL_SCREEN_INTENT) setFullScreenIntent(PendingIntent intent, boolean highPriority)5248 public Builder setFullScreenIntent(PendingIntent intent, boolean highPriority) { 5249 mN.fullScreenIntent = intent; 5250 setFlag(FLAG_HIGH_PRIORITY, highPriority); 5251 return this; 5252 } 5253 5254 /** 5255 * Set the "ticker" text which is sent to accessibility services. 5256 * 5257 * @see Notification#tickerText 5258 */ 5259 @NonNull setTicker(CharSequence tickerText)5260 public Builder setTicker(CharSequence tickerText) { 5261 mN.tickerText = safeCharSequence(tickerText); 5262 return this; 5263 } 5264 5265 /** 5266 * Obsolete version of {@link #setTicker(CharSequence)}. 5267 * 5268 */ 5269 @Deprecated setTicker(CharSequence tickerText, RemoteViews views)5270 public Builder setTicker(CharSequence tickerText, RemoteViews views) { 5271 setTicker(tickerText); 5272 // views is ignored 5273 return this; 5274 } 5275 5276 /** 5277 * Add a large icon to the notification content view. 5278 * 5279 * In the platform template, this image will be shown either on the right of the 5280 * notification, with an aspect ratio of up to 16:9, or (when the notification is grouped) 5281 * on the left in place of the {@link #setSmallIcon(Icon) small icon}. 5282 */ 5283 @NonNull setLargeIcon(Bitmap b)5284 public Builder setLargeIcon(Bitmap b) { 5285 return setLargeIcon(b != null ? Icon.createWithBitmap(b) : null); 5286 } 5287 5288 /** 5289 * Add a large icon to the notification content view. 5290 * 5291 * In the platform template, this image will be shown either on the right of the 5292 * notification, with an aspect ratio of up to 16:9, or (when the notification is grouped) 5293 * on the left in place of the {@link #setSmallIcon(Icon) small icon}. 5294 */ 5295 @NonNull setLargeIcon(Icon icon)5296 public Builder setLargeIcon(Icon icon) { 5297 mN.mLargeIcon = icon; 5298 mN.extras.putParcelable(EXTRA_LARGE_ICON, icon); 5299 return this; 5300 } 5301 5302 /** 5303 * Set the sound to play. 5304 * 5305 * It will be played using the {@link #AUDIO_ATTRIBUTES_DEFAULT default audio attributes} 5306 * for notifications. 5307 * 5308 * @deprecated use {@link NotificationChannel#setSound(Uri, AudioAttributes)} instead. 5309 */ 5310 @Deprecated setSound(Uri sound)5311 public Builder setSound(Uri sound) { 5312 mN.sound = sound; 5313 mN.audioAttributes = AUDIO_ATTRIBUTES_DEFAULT; 5314 return this; 5315 } 5316 5317 /** 5318 * Set the sound to play, along with a specific stream on which to play it. 5319 * 5320 * See {@link android.media.AudioManager} for the <code>STREAM_</code> constants. 5321 * 5322 * @deprecated use {@link NotificationChannel#setSound(Uri, AudioAttributes)}. 5323 */ 5324 @Deprecated setSound(Uri sound, int streamType)5325 public Builder setSound(Uri sound, int streamType) { 5326 PlayerBase.deprecateStreamTypeForPlayback(streamType, "Notification", "setSound()"); 5327 mN.sound = sound; 5328 mN.audioStreamType = streamType; 5329 return this; 5330 } 5331 5332 /** 5333 * Set the sound to play, along with specific {@link AudioAttributes audio attributes} to 5334 * use during playback. 5335 * 5336 * @deprecated use {@link NotificationChannel#setSound(Uri, AudioAttributes)} instead. 5337 * @see Notification#sound 5338 */ 5339 @Deprecated setSound(Uri sound, AudioAttributes audioAttributes)5340 public Builder setSound(Uri sound, AudioAttributes audioAttributes) { 5341 mN.sound = sound; 5342 mN.audioAttributes = audioAttributes; 5343 return this; 5344 } 5345 5346 /** 5347 * Set the vibration pattern to use. 5348 * 5349 * See {@link android.os.Vibrator#vibrate(long[], int)} for a discussion of the 5350 * <code>pattern</code> parameter. 5351 * 5352 * <p> 5353 * A notification that vibrates is more likely to be presented as a heads-up notification. 5354 * </p> 5355 * 5356 * @deprecated use {@link NotificationChannel#setVibrationPattern(long[])} instead. 5357 * @see Notification#vibrate 5358 */ 5359 @Deprecated setVibrate(long[] pattern)5360 public Builder setVibrate(long[] pattern) { 5361 mN.vibrate = pattern; 5362 return this; 5363 } 5364 5365 /** 5366 * Set the desired color for the indicator LED on the device, as well as the 5367 * blink duty cycle (specified in milliseconds). 5368 * 5369 5370 * Not all devices will honor all (or even any) of these values. 5371 * 5372 * @deprecated use {@link NotificationChannel#enableLights(boolean)} instead. 5373 * @see Notification#ledARGB 5374 * @see Notification#ledOnMS 5375 * @see Notification#ledOffMS 5376 */ 5377 @Deprecated setLights(@olorInt int argb, int onMs, int offMs)5378 public Builder setLights(@ColorInt int argb, int onMs, int offMs) { 5379 mN.ledARGB = argb; 5380 mN.ledOnMS = onMs; 5381 mN.ledOffMS = offMs; 5382 if (onMs != 0 || offMs != 0) { 5383 mN.flags |= FLAG_SHOW_LIGHTS; 5384 } 5385 return this; 5386 } 5387 5388 /** 5389 * Set whether this is an "ongoing" notification. 5390 * 5391 * Ongoing notifications cannot be dismissed by the user on locked devices, or by 5392 * notification listeners, and some notifications (call, device management, media) cannot 5393 * be dismissed on unlocked devices, so your application or service must take care of 5394 * canceling them. 5395 * 5396 * They are typically used to indicate a background task that the user is actively engaged 5397 * with (e.g., playing music) or is pending in some way and therefore occupying the device 5398 * (e.g., a file download, sync operation, active network connection). 5399 * 5400 * @see Notification#FLAG_ONGOING_EVENT 5401 */ 5402 @NonNull setOngoing(boolean ongoing)5403 public Builder setOngoing(boolean ongoing) { 5404 setFlag(FLAG_ONGOING_EVENT, ongoing); 5405 return this; 5406 } 5407 5408 /** 5409 * Set whether this notification should be colorized. When set, the color set with 5410 * {@link #setColor(int)} will be used as the background color of this notification. 5411 * <p> 5412 * This should only be used for high priority ongoing tasks like navigation, an ongoing 5413 * call, or other similarly high-priority events for the user. 5414 * <p> 5415 * For most styles, the coloring will only be applied if the notification is for a 5416 * foreground service notification. 5417 * However, for {@link MediaStyle} and {@link DecoratedMediaCustomViewStyle} notifications 5418 * that have a media session attached there is no such requirement. 5419 * 5420 * @see #setColor(int) 5421 * @see MediaStyle#setMediaSession(MediaSession.Token) 5422 */ 5423 @NonNull setColorized(boolean colorize)5424 public Builder setColorized(boolean colorize) { 5425 mN.extras.putBoolean(EXTRA_COLORIZED, colorize); 5426 return this; 5427 } 5428 5429 /** 5430 * Set this flag if you would only like the sound, vibrate 5431 * and ticker to be played if the notification is not already showing. 5432 * 5433 * Note that using this flag will stop any ongoing alerting behaviour such 5434 * as sound, vibration or blinking notification LED. 5435 * 5436 * @see Notification#FLAG_ONLY_ALERT_ONCE 5437 */ 5438 @NonNull setOnlyAlertOnce(boolean onlyAlertOnce)5439 public Builder setOnlyAlertOnce(boolean onlyAlertOnce) { 5440 setFlag(FLAG_ONLY_ALERT_ONCE, onlyAlertOnce); 5441 return this; 5442 } 5443 5444 /** 5445 * Specify a desired visibility policy for a Notification associated with a 5446 * foreground service. By default, the system can choose to defer 5447 * visibility of the notification for a short time after the service is 5448 * started. Pass 5449 * {@link Notification#FOREGROUND_SERVICE_IMMEDIATE FOREGROUND_SERVICE_IMMEDIATE} 5450 * to this method in order to guarantee that visibility is never deferred. Pass 5451 * {@link Notification#FOREGROUND_SERVICE_DEFERRED FOREGROUND_SERVICE_DEFERRED} 5452 * to request that visibility is deferred whenever possible. 5453 * 5454 * <p class="note">Note that deferred visibility is not guaranteed. There 5455 * may be some circumstances under which the system will show the foreground 5456 * service's associated Notification immediately even when the app has used 5457 * this method to explicitly request deferred display.</p> 5458 * @param behavior One of 5459 * {@link Notification#FOREGROUND_SERVICE_DEFAULT FOREGROUND_SERVICE_DEFAULT}, 5460 * {@link Notification#FOREGROUND_SERVICE_IMMEDIATE FOREGROUND_SERVICE_IMMEDIATE}, 5461 * or {@link Notification#FOREGROUND_SERVICE_DEFERRED FOREGROUND_SERVICE_DEFERRED} 5462 * @return 5463 */ 5464 @NonNull setForegroundServiceBehavior(@erviceNotificationPolicy int behavior)5465 public Builder setForegroundServiceBehavior(@ServiceNotificationPolicy int behavior) { 5466 mN.mFgsDeferBehavior = behavior; 5467 return this; 5468 } 5469 5470 /** 5471 * Make this notification automatically dismissed when the user touches it. 5472 * 5473 * @see Notification#FLAG_AUTO_CANCEL 5474 */ 5475 @NonNull setAutoCancel(boolean autoCancel)5476 public Builder setAutoCancel(boolean autoCancel) { 5477 setFlag(FLAG_AUTO_CANCEL, autoCancel); 5478 return this; 5479 } 5480 5481 /** 5482 * Set whether or not this notification should not bridge to other devices. 5483 * 5484 * <p>Some notifications can be bridged to other devices for remote display. 5485 * This hint can be set to recommend this notification not be bridged. 5486 */ 5487 @NonNull setLocalOnly(boolean localOnly)5488 public Builder setLocalOnly(boolean localOnly) { 5489 setFlag(FLAG_LOCAL_ONLY, localOnly); 5490 return this; 5491 } 5492 5493 /** 5494 * Set which notification properties will be inherited from system defaults. 5495 * <p> 5496 * The value should be one or more of the following fields combined with 5497 * bitwise-or: 5498 * {@link #DEFAULT_SOUND}, {@link #DEFAULT_VIBRATE}, {@link #DEFAULT_LIGHTS}. 5499 * <p> 5500 * For all default values, use {@link #DEFAULT_ALL}. 5501 * 5502 * @deprecated use {@link NotificationChannel#enableVibration(boolean)} and 5503 * {@link NotificationChannel#enableLights(boolean)} and 5504 * {@link NotificationChannel#setSound(Uri, AudioAttributes)} instead. 5505 */ 5506 @Deprecated setDefaults(int defaults)5507 public Builder setDefaults(int defaults) { 5508 mN.defaults = defaults; 5509 return this; 5510 } 5511 5512 /** 5513 * Set the priority of this notification. 5514 * 5515 * @see Notification#priority 5516 * @deprecated use {@link NotificationChannel#setImportance(int)} instead. 5517 */ 5518 @Deprecated setPriority(@riority int pri)5519 public Builder setPriority(@Priority int pri) { 5520 mN.priority = pri; 5521 return this; 5522 } 5523 5524 /** 5525 * Set the notification category. 5526 * 5527 * @see Notification#category 5528 */ 5529 @NonNull setCategory(String category)5530 public Builder setCategory(String category) { 5531 mN.category = category; 5532 return this; 5533 } 5534 5535 /** 5536 * Add a person that is relevant to this notification. 5537 * 5538 * <P> 5539 * Depending on user preferences, this annotation may allow the notification to pass 5540 * through interruption filters, if this notification is of category {@link #CATEGORY_CALL} 5541 * or {@link #CATEGORY_MESSAGE}. The addition of people may also cause this notification to 5542 * appear more prominently in the user interface. 5543 * </P> 5544 * 5545 * <P> 5546 * The person should be specified by the {@code String} representation of a 5547 * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. 5548 * </P> 5549 * 5550 * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema 5551 * URIs. The path part of these URIs must exist in the contacts database, in the 5552 * appropriate column, or the reference will be discarded as invalid. Telephone schema 5553 * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}. 5554 * It is also possible to provide a URI with the schema {@code name:} in order to uniquely 5555 * identify a person without an entry in the contacts database. 5556 * </P> 5557 * 5558 * @param uri A URI for the person. 5559 * @see Notification#EXTRA_PEOPLE 5560 * @deprecated use {@link #addPerson(Person)} 5561 */ addPerson(String uri)5562 public Builder addPerson(String uri) { 5563 addPerson(new Person.Builder().setUri(uri).build()); 5564 return this; 5565 } 5566 5567 /** 5568 * Add a person that is relevant to this notification. 5569 * 5570 * <P> 5571 * Depending on user preferences, this annotation may allow the notification to pass 5572 * through interruption filters, if this notification is of category {@link #CATEGORY_CALL} 5573 * or {@link #CATEGORY_MESSAGE}. The addition of people may also cause this notification to 5574 * appear more prominently in the user interface. 5575 * </P> 5576 * 5577 * <P> 5578 * A person should usually contain a uri in order to benefit from the ranking boost. 5579 * However, even if no uri is provided, it's beneficial to provide other people in the 5580 * notification, such that listeners and voice only devices can announce and handle them 5581 * properly. 5582 * </P> 5583 * 5584 * @param person the person to add. 5585 * @see Notification#EXTRA_PEOPLE_LIST 5586 */ 5587 @NonNull addPerson(Person person)5588 public Builder addPerson(Person person) { 5589 mPersonList.add(person); 5590 return this; 5591 } 5592 5593 /** 5594 * Set this notification to be part of a group of notifications sharing the same key. 5595 * Grouped notifications may display in a cluster or stack on devices which 5596 * support such rendering. 5597 * 5598 * <p>To make this notification the summary for its group, also call 5599 * {@link #setGroupSummary}. A sort order can be specified for group members by using 5600 * {@link #setSortKey}. 5601 * @param groupKey The group key of the group. 5602 * @return this object for method chaining 5603 */ 5604 @NonNull setGroup(String groupKey)5605 public Builder setGroup(String groupKey) { 5606 mN.mGroupKey = groupKey; 5607 return this; 5608 } 5609 5610 /** 5611 * Set this notification to be the group summary for a group of notifications. 5612 * Grouped notifications may display in a cluster or stack on devices which 5613 * support such rendering. If thereRequires a group key also be set using {@link #setGroup}. 5614 * The group summary may be suppressed if too few notifications are included in the group. 5615 * @param isGroupSummary Whether this notification should be a group summary. 5616 * @return this object for method chaining 5617 */ 5618 @NonNull setGroupSummary(boolean isGroupSummary)5619 public Builder setGroupSummary(boolean isGroupSummary) { 5620 setFlag(FLAG_GROUP_SUMMARY, isGroupSummary); 5621 return this; 5622 } 5623 5624 /** 5625 * Set a sort key that orders this notification among other notifications from the 5626 * same package. This can be useful if an external sort was already applied and an app 5627 * would like to preserve this. Notifications will be sorted lexicographically using this 5628 * value, although providing different priorities in addition to providing sort key may 5629 * cause this value to be ignored. 5630 * 5631 * <p>This sort key can also be used to order members of a notification group. See 5632 * {@link #setGroup}. 5633 * 5634 * @see String#compareTo(String) 5635 */ 5636 @NonNull setSortKey(String sortKey)5637 public Builder setSortKey(String sortKey) { 5638 mN.mSortKey = sortKey; 5639 return this; 5640 } 5641 5642 /** 5643 * Merge additional metadata into this notification. 5644 * 5645 * <p>Values within the Bundle will replace existing extras values in this Builder. 5646 * 5647 * @see Notification#extras 5648 */ 5649 @NonNull addExtras(Bundle extras)5650 public Builder addExtras(Bundle extras) { 5651 if (extras != null) { 5652 mUserExtras.putAll(extras); 5653 } 5654 return this; 5655 } 5656 5657 /** 5658 * Set metadata for this notification. 5659 * 5660 * <p>A reference to the Bundle is held for the lifetime of this Builder, and the Bundle's 5661 * current contents are copied into the Notification each time {@link #build()} is 5662 * called. 5663 * 5664 * <p>Replaces any existing extras values with those from the provided Bundle. 5665 * Use {@link #addExtras} to merge in metadata instead. 5666 * 5667 * @see Notification#extras 5668 */ 5669 @NonNull setExtras(Bundle extras)5670 public Builder setExtras(Bundle extras) { 5671 if (extras != null) { 5672 mUserExtras = extras; 5673 } 5674 return this; 5675 } 5676 5677 /** 5678 * Get the current metadata Bundle used by this notification Builder. 5679 * 5680 * <p>The returned Bundle is shared with this Builder. 5681 * 5682 * <p>The current contents of this Bundle are copied into the Notification each time 5683 * {@link #build()} is called. 5684 * 5685 * @see Notification#extras 5686 */ getExtras()5687 public Bundle getExtras() { 5688 return mUserExtras; 5689 } 5690 5691 /** 5692 * Add an action to this notification. Actions are typically displayed by 5693 * the system as a button adjacent to the notification content. 5694 * <p> 5695 * Every action must have an icon (32dp square and matching the 5696 * <a href="{@docRoot}design/style/iconography.html#action-bar">Holo 5697 * Dark action bar</a> visual style), a textual label, and a {@link PendingIntent}. 5698 * <p> 5699 * A notification in its expanded form can display up to 3 actions, from left to right in 5700 * the order they were added. Actions will not be displayed when the notification is 5701 * collapsed, however, so be sure that any essential functions may be accessed by the user 5702 * in some other way (for example, in the Activity pointed to by {@link #contentIntent}). 5703 * <p> 5704 * As of Android {@link android.os.Build.VERSION_CODES#S}, apps targeting API level 5705 * {@link android.os.Build.VERSION_CODES#S} or higher won't be able to start activities 5706 * while processing broadcast receivers or services in response to notification action 5707 * clicks. To launch an activity in those cases, provide a {@link PendingIntent} to the 5708 * activity itself. 5709 * <p> 5710 * As of Android {@link android.os.Build.VERSION_CODES#N}, 5711 * action button icons will not be displayed on action buttons, but are still required 5712 * and are available to 5713 * {@link android.service.notification.NotificationListenerService notification listeners}, 5714 * which may display them in other contexts, for example on a wearable device. 5715 * 5716 * @param icon Resource ID of a drawable that represents the action. 5717 * @param title Text describing the action. 5718 * @param intent PendingIntent to be fired when the action is invoked. 5719 * 5720 * @deprecated Use {@link #addAction(Action)} instead. 5721 */ 5722 @Deprecated addAction(int icon, CharSequence title, PendingIntent intent)5723 public Builder addAction(int icon, CharSequence title, PendingIntent intent) { 5724 mActions.add(new Action(icon, safeCharSequence(title), intent)); 5725 return this; 5726 } 5727 5728 /** 5729 * Add an action to this notification. Actions are typically displayed by 5730 * the system as a button adjacent to the notification content. 5731 * <p> 5732 * Every action must have an icon (32dp square and matching the 5733 * <a href="{@docRoot}design/style/iconography.html#action-bar">Holo 5734 * Dark action bar</a> visual style), a textual label, and a {@link PendingIntent}. 5735 * <p> 5736 * A notification in its expanded form can display up to 3 actions, from left to right in 5737 * the order they were added. Actions will not be displayed when the notification is 5738 * collapsed, however, so be sure that any essential functions may be accessed by the user 5739 * in some other way (for example, in the Activity pointed to by {@link #contentIntent}). 5740 * 5741 * @param action The action to add. 5742 */ 5743 @NonNull addAction(Action action)5744 public Builder addAction(Action action) { 5745 if (action != null) { 5746 mActions.add(action); 5747 } 5748 return this; 5749 } 5750 5751 /** 5752 * Alter the complete list of actions attached to this notification. 5753 * @see #addAction(Action). 5754 * 5755 * @param actions 5756 * @return 5757 */ 5758 @NonNull setActions(Action... actions)5759 public Builder setActions(Action... actions) { 5760 mActions.clear(); 5761 for (int i = 0; i < actions.length; i++) { 5762 if (actions[i] != null) { 5763 mActions.add(actions[i]); 5764 } 5765 } 5766 return this; 5767 } 5768 5769 /** 5770 * Add a rich notification style to be applied at build time. 5771 * 5772 * @param style Object responsible for modifying the notification style. 5773 */ 5774 @NonNull setStyle(Style style)5775 public Builder setStyle(Style style) { 5776 if (mStyle != style) { 5777 mStyle = style; 5778 if (mStyle != null) { 5779 mStyle.setBuilder(this); 5780 mN.extras.putString(EXTRA_TEMPLATE, style.getClass().getName()); 5781 } else { 5782 mN.extras.remove(EXTRA_TEMPLATE); 5783 } 5784 } 5785 return this; 5786 } 5787 5788 /** 5789 * Returns the style set by {@link #setStyle(Style)}. 5790 */ getStyle()5791 public Style getStyle() { 5792 return mStyle; 5793 } 5794 5795 /** 5796 * Specify the value of {@link #visibility}. 5797 * 5798 * @return The same Builder. 5799 */ 5800 @NonNull setVisibility(@isibility int visibility)5801 public Builder setVisibility(@Visibility int visibility) { 5802 mN.visibility = visibility; 5803 return this; 5804 } 5805 5806 /** 5807 * Supply a replacement Notification whose contents should be shown in insecure contexts 5808 * (i.e. atop the secure lockscreen). See {@link #visibility} and {@link #VISIBILITY_PUBLIC}. 5809 * @param n A replacement notification, presumably with some or all info redacted. 5810 * @return The same Builder. 5811 */ 5812 @NonNull setPublicVersion(Notification n)5813 public Builder setPublicVersion(Notification n) { 5814 if (n != null) { 5815 mN.publicVersion = new Notification(); 5816 n.cloneInto(mN.publicVersion, /*heavy=*/ true); 5817 } else { 5818 mN.publicVersion = null; 5819 } 5820 return this; 5821 } 5822 5823 /** 5824 * Apply an extender to this notification builder. Extenders may be used to add 5825 * metadata or change options on this builder. 5826 */ 5827 @NonNull extend(Extender extender)5828 public Builder extend(Extender extender) { 5829 extender.extend(this); 5830 return this; 5831 } 5832 5833 /** 5834 * Set the value for a notification flag 5835 * 5836 * @param mask Bit mask of the flag 5837 * @param value Status (on/off) of the flag 5838 * 5839 * @return The same Builder. 5840 */ 5841 @NonNull setFlag(@otificationFlags int mask, boolean value)5842 public Builder setFlag(@NotificationFlags int mask, boolean value) { 5843 if (value) { 5844 mN.flags |= mask; 5845 } else { 5846 mN.flags &= ~mask; 5847 } 5848 return this; 5849 } 5850 5851 /** 5852 * Sets {@link Notification#color}. 5853 * 5854 * @param argb The accent color to use 5855 * 5856 * @return The same Builder. 5857 */ 5858 @NonNull setColor(@olorInt int argb)5859 public Builder setColor(@ColorInt int argb) { 5860 mN.color = argb; 5861 sanitizeColor(); 5862 return this; 5863 } 5864 bindPhishingAlertIcon(RemoteViews contentView, StandardTemplateParams p)5865 private void bindPhishingAlertIcon(RemoteViews contentView, StandardTemplateParams p) { 5866 contentView.setDrawableTint( 5867 R.id.phishing_alert, 5868 false /* targetBackground */, 5869 getColors(p).getErrorColor(), 5870 PorterDuff.Mode.SRC_ATOP); 5871 } 5872 getProfileBadgeDrawable()5873 private Drawable getProfileBadgeDrawable() { 5874 if (mContext.getUserId() == UserHandle.USER_SYSTEM) { 5875 // This user can never be a badged profile, 5876 // and also includes USER_ALL system notifications. 5877 return null; 5878 } 5879 // Note: This assumes that the current user can read the profile badge of the 5880 // originating user. 5881 DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); 5882 return dpm.getResources().getDrawable( 5883 getUpdatableProfileBadgeId(), SOLID_COLORED, NOTIFICATION, 5884 this::getDefaultProfileBadgeDrawable); 5885 } 5886 getUpdatableProfileBadgeId()5887 private String getUpdatableProfileBadgeId() { 5888 return mContext.getSystemService(UserManager.class).isManagedProfile() 5889 ? WORK_PROFILE_ICON : UNDEFINED; 5890 } 5891 getDefaultProfileBadgeDrawable()5892 private Drawable getDefaultProfileBadgeDrawable() { 5893 return mContext.getPackageManager().getUserBadgeForDensityNoBackground( 5894 new UserHandle(mContext.getUserId()), 0); 5895 } 5896 getProfileBadge()5897 private Bitmap getProfileBadge() { 5898 Drawable badge = getProfileBadgeDrawable(); 5899 if (badge == null) { 5900 return null; 5901 } 5902 final int size = mContext.getResources().getDimensionPixelSize( 5903 Flags.notificationsRedesignTemplates() 5904 ? R.dimen.notification_2025_badge_size 5905 : R.dimen.notification_badge_size); 5906 Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); 5907 Canvas canvas = new Canvas(bitmap); 5908 badge.setBounds(0, 0, size, size); 5909 badge.draw(canvas); 5910 return bitmap; 5911 } 5912 bindProfileBadge(RemoteViews contentView, StandardTemplateParams p)5913 private void bindProfileBadge(RemoteViews contentView, StandardTemplateParams p) { 5914 Bitmap profileBadge = getProfileBadge(); 5915 5916 if (profileBadge != null) { 5917 contentView.setImageViewBitmap(R.id.profile_badge, profileBadge); 5918 contentView.setViewVisibility(R.id.profile_badge, View.VISIBLE); 5919 if (isBackgroundColorized(p)) { 5920 contentView.setDrawableTint(R.id.profile_badge, false, 5921 getPrimaryTextColor(p), PorterDuff.Mode.SRC_ATOP); 5922 } 5923 contentView.setContentDescription( 5924 R.id.profile_badge, 5925 mContext.getSystemService(UserManager.class) 5926 .getProfileAccessibilityString(mContext.getUserId())); 5927 } 5928 } 5929 bindAlertedIcon(RemoteViews contentView, StandardTemplateParams p)5930 private void bindAlertedIcon(RemoteViews contentView, StandardTemplateParams p) { 5931 contentView.setDrawableTint( 5932 R.id.alerted_icon, 5933 false /* targetBackground */, 5934 getColors(p).getSecondaryTextColor(), 5935 PorterDuff.Mode.SRC_IN); 5936 } 5937 5938 /** 5939 * @hide 5940 */ usesStandardHeader()5941 public boolean usesStandardHeader() { 5942 if (mN.mUsesStandardHeader) { 5943 return true; 5944 } 5945 if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) { 5946 if (mN.contentView == null && mN.bigContentView == null) { 5947 return true; 5948 } 5949 } 5950 boolean contentViewUsesHeader = mN.contentView == null 5951 || isStandardLayout(mN.contentView.getLayoutId()); 5952 boolean bigContentViewUsesHeader = mN.bigContentView == null 5953 || isStandardLayout(mN.bigContentView.getLayoutId()); 5954 return contentViewUsesHeader && bigContentViewUsesHeader; 5955 } 5956 resetStandardTemplate(RemoteViews contentView)5957 private void resetStandardTemplate(RemoteViews contentView) { 5958 resetNotificationHeader(contentView); 5959 contentView.setViewVisibility(R.id.right_icon, View.GONE); 5960 contentView.setViewVisibility(R.id.title, View.GONE); 5961 contentView.setTextViewText(R.id.title, null); 5962 contentView.setViewVisibility(R.id.text, View.GONE); 5963 contentView.setTextViewText(R.id.text, null); 5964 } 5965 5966 /** 5967 * Resets the notification header to its original state 5968 */ resetNotificationHeader(RemoteViews contentView)5969 private void resetNotificationHeader(RemoteViews contentView) { 5970 // Small icon doesn't need to be reset, as it's always set. Resetting would prevent 5971 // re-using the drawable when the notification is updated. 5972 contentView.setBoolean(R.id.expand_button, "setExpanded", false); 5973 contentView.setViewVisibility(R.id.app_name_text, View.GONE); 5974 contentView.setTextViewText(R.id.app_name_text, null); 5975 contentView.setViewVisibility(R.id.chronometer, View.GONE); 5976 contentView.setViewVisibility(R.id.header_text, View.GONE); 5977 contentView.setTextViewText(R.id.header_text, null); 5978 contentView.setViewVisibility(R.id.header_text_secondary, View.GONE); 5979 contentView.setTextViewText(R.id.header_text_secondary, null); 5980 contentView.setViewVisibility(R.id.header_text_divider, View.GONE); 5981 contentView.setViewVisibility(R.id.header_text_secondary_divider, View.GONE); 5982 contentView.setViewVisibility(R.id.time_divider, View.GONE); 5983 contentView.setViewVisibility(R.id.time, View.GONE); 5984 contentView.setImageViewIcon(R.id.profile_badge, null); 5985 contentView.setViewVisibility(R.id.profile_badge, View.GONE); 5986 mN.mUsesStandardHeader = false; 5987 } 5988 applyStandardTemplate(int resId, StandardTemplateParams p, TemplateBindResult result)5989 private RemoteViews applyStandardTemplate(int resId, StandardTemplateParams p, 5990 TemplateBindResult result) { 5991 p.headerless(resId == getCollapsedBaseLayoutResource() 5992 || resId == getHeadsUpBaseLayoutResource() 5993 || resId == getCompactHeadsUpBaseLayoutResource() 5994 || resId == getMessagingCompactHeadsUpLayoutResource() 5995 || resId == getCollapsedMessagingLayoutResource() 5996 || resId == getCollapsedMediaLayoutResource() 5997 || resId == getCollapsedConversationLayoutResource() 5998 || (notificationsRedesignTemplates() 5999 && resId == getCollapsedCallLayoutResource())); 6000 RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId); 6001 6002 resetStandardTemplate(contentView); 6003 6004 final Bundle ex = mN.extras; 6005 updateBackgroundColor(contentView, p); 6006 bindNotificationHeader(contentView, p); 6007 bindLargeIconAndApplyMargin(contentView, p, result); 6008 boolean showProgress = handleProgressBar(contentView, ex, p); 6009 boolean hasSecondLine = showProgress; 6010 if (p.hasTitle()) { 6011 contentView.setViewVisibility(p.mTitleViewId, View.VISIBLE); 6012 contentView.setTextViewText(p.mTitleViewId, 6013 ensureColorSpanContrastOrStripStyling(p.mTitle, p)); 6014 setTextViewColorPrimary(contentView, p.mTitleViewId, p); 6015 } else if (p.mTitleViewId != R.id.title) { 6016 // This alternate title view ID is not cleared by resetStandardTemplate 6017 contentView.setViewVisibility(p.mTitleViewId, View.GONE); 6018 contentView.setTextViewText(p.mTitleViewId, null); 6019 } 6020 if (p.mText != null && p.mText.length() != 0 6021 && (!showProgress || p.mAllowTextWithProgress)) { 6022 contentView.setViewVisibility(p.mTextViewId, View.VISIBLE); 6023 contentView.setTextViewText(p.mTextViewId, 6024 ensureColorSpanContrastOrStripStyling(p.mText, p)); 6025 setTextViewColorSecondary(contentView, p.mTextViewId, p); 6026 hasSecondLine = true; 6027 } else if (p.mTextViewId != R.id.text) { 6028 // This alternate text view ID is not cleared by resetStandardTemplate 6029 contentView.setViewVisibility(p.mTextViewId, View.GONE); 6030 contentView.setTextViewText(p.mTextViewId, null); 6031 } 6032 6033 updateExpanderAlignment(contentView, p, hasSecondLine); 6034 setHeaderlessVerticalMargins(contentView, p, hasSecondLine); 6035 6036 // Update margins to leave space for the top line (but not for headerless views like 6037 // HUNS, which use a different layout that already accounts for that). Templates that 6038 // have content that will be displayed under the small icon also use a different margin. 6039 if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) { 6040 int margin = getContentMarginTop(mContext, 6041 R.dimen.notification_2025_content_margin_top); 6042 contentView.setViewLayoutMargin(R.id.notification_main_column, 6043 RemoteViews.MARGIN_TOP, margin, COMPLEX_UNIT_PX); 6044 } 6045 6046 return contentView; 6047 } 6048 updateExpanderAlignment(RemoteViews contentView, StandardTemplateParams p, boolean hasSecondLine)6049 private static void updateExpanderAlignment(RemoteViews contentView, 6050 StandardTemplateParams p, boolean hasSecondLine) { 6051 if (notificationsRedesignTemplates() && p.mHeaderless) { 6052 if (!hasSecondLine) { 6053 // If there's no text, let's center the expand button vertically to align things 6054 // more nicely. This is handled separately for notifications that use a 6055 // NotificationHeaderView, see NotificationHeaderView#centerTopLine. 6056 contentView.setViewLayoutHeight(R.id.expand_button, MATCH_PARENT, 6057 COMPLEX_UNIT_PX); 6058 } else { 6059 // Otherwise, just use the default height for the button to keep it top-aligned. 6060 contentView.setViewLayoutHeight(R.id.expand_button, WRAP_CONTENT, 6061 COMPLEX_UNIT_PX); 6062 } 6063 } 6064 } 6065 setHeaderlessVerticalMargins(RemoteViews contentView, StandardTemplateParams p, boolean hasSecondLine)6066 private static void setHeaderlessVerticalMargins(RemoteViews contentView, 6067 StandardTemplateParams p, boolean hasSecondLine) { 6068 if (Flags.notificationsRedesignTemplates() || !p.mHeaderless) { 6069 return; 6070 } 6071 int marginDimen = hasSecondLine 6072 ? R.dimen.notification_headerless_margin_twoline 6073 : R.dimen.notification_headerless_margin_oneline; 6074 contentView.setViewLayoutMarginDimen(R.id.notification_headerless_view_column, 6075 RemoteViews.MARGIN_TOP, marginDimen); 6076 contentView.setViewLayoutMarginDimen(R.id.notification_headerless_view_column, 6077 RemoteViews.MARGIN_BOTTOM, marginDimen); 6078 } 6079 setTextViewColorPrimary(RemoteViews contentView, @IdRes int id, StandardTemplateParams p)6080 private void setTextViewColorPrimary(RemoteViews contentView, @IdRes int id, 6081 StandardTemplateParams p) { 6082 contentView.setTextColor(id, getPrimaryTextColor(p)); 6083 } 6084 6085 /** 6086 * @param p the template params to inflate this with 6087 * @return the primary text color 6088 * @hide 6089 */ 6090 @VisibleForTesting getPrimaryTextColor(StandardTemplateParams p)6091 public @ColorInt int getPrimaryTextColor(StandardTemplateParams p) { 6092 return getColors(p).getPrimaryTextColor(); 6093 } 6094 6095 /** 6096 * @param p the template params to inflate this with 6097 * @return the secondary text color 6098 * @hide 6099 */ 6100 @VisibleForTesting getSecondaryTextColor(StandardTemplateParams p)6101 public @ColorInt int getSecondaryTextColor(StandardTemplateParams p) { 6102 return getColors(p).getSecondaryTextColor(); 6103 } 6104 setTextViewColorSecondary(RemoteViews contentView, @IdRes int id, StandardTemplateParams p)6105 private void setTextViewColorSecondary(RemoteViews contentView, @IdRes int id, 6106 StandardTemplateParams p) { 6107 contentView.setTextColor(id, getSecondaryTextColor(p)); 6108 } 6109 getColors(StandardTemplateParams p)6110 private Colors getColors(StandardTemplateParams p) { 6111 mColors.resolvePalette(mContext, mN.color, isBackgroundColorized(p), mInNightMode); 6112 return mColors; 6113 } 6114 6115 /** 6116 * @param isHeader If the notification is a notification header 6117 * @return An instance of mColors after resolving the palette 6118 * @hide 6119 */ getColors(boolean isHeader)6120 public Colors getColors(boolean isHeader) { 6121 mColors.resolvePalette(mContext, mN.color, !isHeader && mN.isColorized(), mInNightMode); 6122 return mColors; 6123 } 6124 updateHeaderBackgroundColor(RemoteViews contentView, StandardTemplateParams p)6125 private void updateHeaderBackgroundColor(RemoteViews contentView, 6126 StandardTemplateParams p) { 6127 if (!Flags.uiRichOngoing()) { 6128 return; 6129 } 6130 if (isBackgroundColorized(p)) { 6131 contentView.setInt(R.id.notification_header, "setBackgroundColor", 6132 getBackgroundColor(p)); 6133 } else { 6134 // Clear it! 6135 contentView.setInt(R.id.notification_header, "setBackgroundResource", 6136 0); 6137 } 6138 } 6139 updateBackgroundColor(RemoteViews contentView, StandardTemplateParams p)6140 private void updateBackgroundColor(RemoteViews contentView, 6141 StandardTemplateParams p) { 6142 if (isBackgroundColorized(p)) { 6143 contentView.setInt(R.id.status_bar_latest_event_content, "setBackgroundColor", 6144 getBackgroundColor(p)); 6145 } else { 6146 // Clear it! 6147 contentView.setInt(R.id.status_bar_latest_event_content, "setBackgroundResource", 6148 0); 6149 } 6150 } 6151 handleProgressBar(RemoteViews contentView, Bundle ex, StandardTemplateParams p)6152 private boolean handleProgressBar(RemoteViews contentView, Bundle ex, 6153 StandardTemplateParams p) { 6154 final int max = ex.getInt(EXTRA_PROGRESS_MAX, 0); 6155 final int progress = ex.getInt(EXTRA_PROGRESS, 0); 6156 final boolean ind = ex.getBoolean(EXTRA_PROGRESS_INDETERMINATE); 6157 if (!p.mHideProgress && (max != 0 || ind)) { 6158 contentView.setViewVisibility(com.android.internal.R.id.progress, View.VISIBLE); 6159 contentView.setProgressBar(R.id.progress, max, progress, ind); 6160 contentView.setProgressBackgroundTintList(R.id.progress, 6161 mContext.getColorStateList(R.color.notification_progress_background_color)); 6162 ColorStateList progressTint = ColorStateList.valueOf(getPrimaryAccentColor(p)); 6163 contentView.setProgressTintList(R.id.progress, progressTint); 6164 contentView.setProgressIndeterminateTintList(R.id.progress, progressTint); 6165 return true; 6166 } else { 6167 contentView.setViewVisibility(R.id.progress, View.GONE); 6168 return false; 6169 } 6170 } 6171 bindLargeIconAndApplyMargin(RemoteViews contentView, @NonNull StandardTemplateParams p, @Nullable TemplateBindResult result)6172 private void bindLargeIconAndApplyMargin(RemoteViews contentView, 6173 @NonNull StandardTemplateParams p, 6174 @Nullable TemplateBindResult result) { 6175 if (result == null) { 6176 result = new TemplateBindResult(); 6177 } 6178 bindLargeIcon(contentView, p, result); 6179 if (!p.mHeaderless) { 6180 // views in states with a header (big states) 6181 result.mHeadingExtraMarginSet.applyToView(contentView, R.id.notification_header); 6182 result.mTitleMarginSet.applyToView(contentView, R.id.title); 6183 // If there is no title, the text (or big_text) needs to wrap around the image 6184 result.mTitleMarginSet.applyToView(contentView, p.mTextViewId); 6185 contentView.setInt(p.mTextViewId, "setNumIndentLines", p.hasTitle() ? 0 : 1); 6186 } 6187 // The expand button uses paddings rather than margins, so we'll adjust it 6188 // separately. 6189 adjustExpandButtonPadding(contentView, result.mRightIconVisible); 6190 } 6191 adjustExpandButtonPadding(RemoteViews contentView, boolean rightIconVisible)6192 private void adjustExpandButtonPadding(RemoteViews contentView, boolean rightIconVisible) { 6193 if (notificationsRedesignTemplates()) { 6194 final Resources res = mContext.getResources(); 6195 int normalPadding = res.getDimensionPixelSize(R.dimen.notification_2025_margin); 6196 int iconSpacing = res.getDimensionPixelSize( 6197 R.dimen.notification_2025_expand_button_right_icon_spacing); 6198 contentView.setInt(R.id.expand_button, "setStartPadding", 6199 rightIconVisible ? iconSpacing : normalPadding); 6200 } 6201 } 6202 6203 // This code is executed on behalf of other apps' notifications, sometimes even by 3p apps, 6204 // a use case that is not supported by the Compat Framework library. Workarounds to resolve 6205 // the change's state in NotificationManagerService were very complex. These behavior 6206 // changes are entirely visual, and should otherwise be undetectable by apps. 6207 @SuppressWarnings("AndroidFrameworkCompatChange") calculateRightIconDimens(Icon rightIcon, boolean isPromotedPicture, @NonNull TemplateBindResult result)6208 private void calculateRightIconDimens(Icon rightIcon, boolean isPromotedPicture, 6209 @NonNull TemplateBindResult result) { 6210 final Resources resources = mContext.getResources(); 6211 final float density = resources.getDisplayMetrics().density; 6212 int iconMarginId = notificationsRedesignTemplates() 6213 ? R.dimen.notification_2025_right_icon_content_margin 6214 : R.dimen.notification_right_icon_content_margin; 6215 final float iconMarginDp = resources.getDimension(iconMarginId) / density; 6216 final float contentMarginDp = resources.getDimension( 6217 R.dimen.notification_content_margin_end) / density; 6218 float spaceForExpanderDp; 6219 if (notificationsRedesignTemplates()) { 6220 spaceForExpanderDp = resources.getDimension( 6221 R.dimen.notification_2025_right_icon_expanded_margin_end) / density 6222 - contentMarginDp; 6223 } else { 6224 spaceForExpanderDp = resources.getDimension( 6225 R.dimen.notification_header_expand_icon_size) / density - contentMarginDp; 6226 } 6227 final float viewHeightDp = resources.getDimension( 6228 R.dimen.notification_right_icon_size) / density; 6229 float viewWidthDp = viewHeightDp; // icons are 1:1 by default 6230 if (rightIcon != null && (isPromotedPicture 6231 || mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S)) { 6232 Drawable drawable = rightIcon.loadDrawable(mContext); 6233 if (drawable != null) { 6234 int iconWidth = drawable.getIntrinsicWidth(); 6235 int iconHeight = drawable.getIntrinsicHeight(); 6236 if (iconWidth > iconHeight && iconHeight > 0) { 6237 final float maxViewWidthDp = viewHeightDp * MAX_LARGE_ICON_ASPECT_RATIO; 6238 viewWidthDp = Math.min(viewHeightDp * iconWidth / iconHeight, 6239 maxViewWidthDp); 6240 } 6241 } 6242 } 6243 // Margin needed for the header to accommodate the icon when shown 6244 final float extraMarginEndDpIfVisible = viewWidthDp + iconMarginDp; 6245 result.setRightIconState(rightIcon != null /* visible */, viewWidthDp, 6246 viewHeightDp, extraMarginEndDpIfVisible, spaceForExpanderDp); 6247 } 6248 6249 /** 6250 * Bind the large icon. 6251 */ bindLargeIcon(RemoteViews contentView, @NonNull StandardTemplateParams p, @NonNull TemplateBindResult result)6252 private void bindLargeIcon(RemoteViews contentView, @NonNull StandardTemplateParams p, 6253 @NonNull TemplateBindResult result) { 6254 if (mN.mLargeIcon == null && mN.largeIcon != null) { 6255 mN.mLargeIcon = Icon.createWithBitmap(mN.largeIcon); 6256 } 6257 6258 // Determine the left and right icons 6259 Icon leftIcon = p.mHideLeftIcon ? null : mN.mLargeIcon; 6260 Icon rightIcon = p.mHideRightIcon ? null 6261 : (p.mPromotedPicture != null ? p.mPromotedPicture : mN.mLargeIcon); 6262 6263 // Apply the left icon (without duplicating the bitmap) 6264 if (leftIcon != rightIcon || leftIcon == null) { 6265 // If the leftIcon is explicitly hidden or different from the rightIcon, then set it 6266 // explicitly and make sure it won't take the right_icon drawable. 6267 contentView.setImageViewIcon(R.id.left_icon, leftIcon); 6268 contentView.setIntTag(R.id.left_icon, R.id.tag_uses_right_icon_drawable, 0); 6269 } else { 6270 // If the leftIcon equals the rightIcon, just set the flag to use the right_icon 6271 // drawable. This avoids the view having two copies of the same bitmap. 6272 contentView.setIntTag(R.id.left_icon, R.id.tag_uses_right_icon_drawable, 1); 6273 } 6274 6275 // Always calculate dimens to populate `result` for the GONE case 6276 boolean isPromotedPicture = p.mPromotedPicture != null; 6277 calculateRightIconDimens(rightIcon, isPromotedPicture, result); 6278 6279 // Bind the right icon 6280 if (rightIcon != null) { 6281 contentView.setViewLayoutWidth(R.id.right_icon, 6282 result.mRightIconWidthDp, TypedValue.COMPLEX_UNIT_DIP); 6283 contentView.setViewLayoutHeight(R.id.right_icon, 6284 result.mRightIconHeightDp, TypedValue.COMPLEX_UNIT_DIP); 6285 contentView.setViewVisibility(R.id.right_icon, View.VISIBLE); 6286 contentView.setImageViewIcon(R.id.right_icon, rightIcon); 6287 contentView.setIntTag(R.id.right_icon, R.id.tag_keep_when_showing_left_icon, 6288 isPromotedPicture ? 1 : 0); 6289 processLargeLegacyIcon(rightIcon, contentView, p); 6290 } else { 6291 // The "reset" doesn't clear the drawable, so we do it here. This clear is 6292 // important because the presence of a drawable in this view (regardless of the 6293 // visibility) is used by NotificationGroupingUtil to set the visibility. 6294 contentView.setImageViewIcon(R.id.right_icon, null); 6295 contentView.setIntTag(R.id.right_icon, R.id.tag_keep_when_showing_left_icon, 0); 6296 } 6297 } 6298 bindNotificationHeader(RemoteViews contentView, StandardTemplateParams p)6299 private void bindNotificationHeader(RemoteViews contentView, StandardTemplateParams p) { 6300 bindSmallIcon(contentView, p); 6301 // Populate text left-to-right so that separators are only shown between strings 6302 boolean hasTextToLeft = bindHeaderAppName(contentView, p, false /* force */); 6303 hasTextToLeft |= bindHeaderTextSecondary(contentView, p, hasTextToLeft); 6304 hasTextToLeft |= bindHeaderText(contentView, p, hasTextToLeft); 6305 if (!hasTextToLeft) { 6306 // If there's still no text, force add the app name so there is some text. 6307 hasTextToLeft |= bindHeaderAppName(contentView, p, true /* force */); 6308 } 6309 bindHeaderChronometerAndTime(contentView, p, hasTextToLeft); 6310 bindPhishingAlertIcon(contentView, p); 6311 bindProfileBadge(contentView, p); 6312 bindAlertedIcon(contentView, p); 6313 bindExpandButton(contentView, p); 6314 bindCloseButton(contentView, p); 6315 mN.mUsesStandardHeader = true; 6316 } 6317 bindExpandButton(RemoteViews contentView, StandardTemplateParams p)6318 private void bindExpandButton(RemoteViews contentView, StandardTemplateParams p) { 6319 // set default colors 6320 int bgColor = getBackgroundColor(p); 6321 int pillColor = Colors.flattenAlpha(getColors(p).getProtectionColor(), bgColor); 6322 int textColor = Colors.flattenAlpha(getPrimaryTextColor(p), pillColor); 6323 contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor); 6324 contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor); 6325 // Use different highlighted colors for conversations' unread count 6326 if (p.mHighlightExpander) { 6327 pillColor = Colors.flattenAlpha( 6328 getColors(p).getTertiaryFixedDimAccentColor(), bgColor); 6329 textColor = Colors.flattenAlpha( 6330 getColors(p).getOnTertiaryFixedAccentTextColor(), pillColor); 6331 } 6332 contentView.setInt(R.id.expand_button, "setHighlightTextColor", textColor); 6333 contentView.setInt(R.id.expand_button, "setHighlightPillColor", pillColor); 6334 } 6335 bindCloseButton(RemoteViews contentView, StandardTemplateParams p)6336 private void bindCloseButton(RemoteViews contentView, StandardTemplateParams p) { 6337 // set default colors 6338 int bgColor = getBackgroundColor(p); 6339 int backgroundColor = Colors.flattenAlpha(getColors(p).getProtectionColor(), bgColor); 6340 int foregroundColor = Colors.flattenAlpha(getPrimaryTextColor(p), backgroundColor); 6341 contentView.setInt(R.id.close_button, "setForegroundColor", foregroundColor); 6342 contentView.setInt(R.id.close_button, "setBackgroundColor", backgroundColor); 6343 } 6344 bindHeaderChronometerAndTime(RemoteViews contentView, StandardTemplateParams p, boolean hasTextToLeft)6345 private void bindHeaderChronometerAndTime(RemoteViews contentView, 6346 StandardTemplateParams p, boolean hasTextToLeft) { 6347 if (!p.mHideTime && showsTimeOrChronometer()) { 6348 if (hasTextToLeft) { 6349 contentView.setViewVisibility(R.id.time_divider, View.VISIBLE); 6350 setTextViewColorSecondary(contentView, R.id.time_divider, p); 6351 } 6352 if (mN.extras.getBoolean(EXTRA_SHOW_CHRONOMETER)) { 6353 contentView.setViewVisibility(R.id.chronometer, View.VISIBLE); 6354 contentView.setLong(R.id.chronometer, "setBase", mN.getWhen() 6355 + (SystemClock.elapsedRealtime() - System.currentTimeMillis())); 6356 contentView.setBoolean(R.id.chronometer, "setStarted", true); 6357 boolean countsDown = mN.extras.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN); 6358 contentView.setChronometerCountDown(R.id.chronometer, countsDown); 6359 setTextViewColorSecondary(contentView, R.id.chronometer, p); 6360 } else { 6361 contentView.setViewVisibility(R.id.time, View.VISIBLE); 6362 contentView.setLong(R.id.time, "setTime", mN.getWhen()); 6363 setTextViewColorSecondary(contentView, R.id.time, p); 6364 } 6365 } else { 6366 // We still want a time to be set but gone, such that we can show and hide it 6367 // on demand in case it's a child notification without anything in the header 6368 contentView.setLong(R.id.time, "setTime", mN.getWhen() != 0 ? mN.getWhen() : 6369 mN.creationTime); 6370 setTextViewColorSecondary(contentView, R.id.time, p); 6371 } 6372 } 6373 6374 /** 6375 * @return true if the header text will be visible 6376 */ bindHeaderText(RemoteViews contentView, StandardTemplateParams p, boolean hasTextToLeft)6377 private boolean bindHeaderText(RemoteViews contentView, StandardTemplateParams p, 6378 boolean hasTextToLeft) { 6379 if (p.mHideSubText) { 6380 return false; 6381 } 6382 CharSequence headerText = p.mSubText; 6383 if (headerText == null && mStyle != null && mStyle.mSummaryTextSet 6384 && mStyle.hasSummaryInHeader()) { 6385 headerText = mStyle.mSummaryText; 6386 } 6387 if (headerText == null 6388 && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N 6389 && mN.extras.getCharSequence(EXTRA_INFO_TEXT) != null) { 6390 headerText = mN.extras.getCharSequence(EXTRA_INFO_TEXT); 6391 } 6392 if (!TextUtils.isEmpty(headerText)) { 6393 contentView.setTextViewText(R.id.header_text, ensureColorSpanContrastOrStripStyling( 6394 processLegacyText(headerText), p)); 6395 setTextViewColorSecondary(contentView, R.id.header_text, p); 6396 contentView.setViewVisibility(R.id.header_text, View.VISIBLE); 6397 if (hasTextToLeft) { 6398 contentView.setViewVisibility(R.id.header_text_divider, View.VISIBLE); 6399 setTextViewColorSecondary(contentView, R.id.header_text_divider, p); 6400 } 6401 return true; 6402 } 6403 return false; 6404 } 6405 6406 /** 6407 * @return true if the secondary header text will be visible 6408 */ bindHeaderTextSecondary(RemoteViews contentView, StandardTemplateParams p, boolean hasTextToLeft)6409 private boolean bindHeaderTextSecondary(RemoteViews contentView, StandardTemplateParams p, 6410 boolean hasTextToLeft) { 6411 if (p.mHideSubText) { 6412 return false; 6413 } 6414 if (!TextUtils.isEmpty(p.mHeaderTextSecondary)) { 6415 contentView.setTextViewText(R.id.header_text_secondary, 6416 ensureColorSpanContrastOrStripStyling( 6417 processLegacyText(p.mHeaderTextSecondary), p)); 6418 setTextViewColorSecondary(contentView, R.id.header_text_secondary, p); 6419 contentView.setViewVisibility(R.id.header_text_secondary, View.VISIBLE); 6420 if (hasTextToLeft) { 6421 contentView.setViewVisibility(R.id.header_text_secondary_divider, View.VISIBLE); 6422 setTextViewColorSecondary(contentView, R.id.header_text_secondary_divider, p); 6423 } 6424 return true; 6425 } 6426 return false; 6427 } 6428 6429 /** 6430 * @hide 6431 */ 6432 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) loadHeaderAppName()6433 public String loadHeaderAppName() { 6434 return mN.loadHeaderAppName(mContext); 6435 } 6436 6437 /** 6438 * @return true if the app name will be visible 6439 */ bindHeaderAppName(RemoteViews contentView, StandardTemplateParams p, boolean force)6440 private boolean bindHeaderAppName(RemoteViews contentView, StandardTemplateParams p, 6441 boolean force) { 6442 if (p.mViewType == StandardTemplateParams.VIEW_TYPE_MINIMIZED && !force) { 6443 // unless the force flag is set, don't show the app name in the minimized state. 6444 return false; 6445 } 6446 if (p.mHeaderless && p.hasTitle()) { 6447 // the headerless template will have the TITLE in this position; return true to 6448 // keep the divider visible between that title and the next text element. 6449 return true; 6450 } 6451 if (p.mHideAppName) { 6452 // The app name is being hidden, so we definitely want to return here. 6453 // Assume that there is a title which will replace it in the header. 6454 return p.hasTitle(); 6455 } 6456 contentView.setViewVisibility(R.id.app_name_text, View.VISIBLE); 6457 contentView.setTextViewText(R.id.app_name_text, loadHeaderAppName()); 6458 contentView.setTextColor(R.id.app_name_text, getSecondaryTextColor(p)); 6459 return true; 6460 } 6461 6462 /** 6463 * Determines if the notification should be colorized *for the purposes of applying colors*. 6464 * If this is the minimized view of a colorized notification, this will return false so that 6465 * internal coloring logic can still render the notification normally. 6466 */ isBackgroundColorized(StandardTemplateParams p)6467 private boolean isBackgroundColorized(StandardTemplateParams p) { 6468 return p.allowColorization && mN.isColorized(); 6469 } 6470 isCallActionColorCustomizable()6471 private boolean isCallActionColorCustomizable() { 6472 // NOTE: this doesn't need to check StandardTemplateParams.allowColorization because 6473 // that is only used for disallowing colorization of headers for the minimized state, 6474 // and neither of those conditions applies when showing actions. 6475 // Not requiring StandardTemplateParams as an argument simplifies the creation process. 6476 return mN.isColorized() && mContext.getResources().getBoolean( 6477 R.bool.config_callNotificationActionColorsRequireColorized); 6478 } 6479 bindSmallIcon(RemoteViews contentView, StandardTemplateParams p)6480 private void bindSmallIcon(RemoteViews contentView, StandardTemplateParams p) { 6481 if (mN.mSmallIcon == null && mN.icon != 0) { 6482 mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon); 6483 } 6484 contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); 6485 contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel); 6486 processSmallIconColor(mN.mSmallIcon, contentView, p); 6487 } 6488 6489 /** 6490 * @return true if the built notification will show the time or the chronometer; false 6491 * otherwise 6492 */ showsTimeOrChronometer()6493 private boolean showsTimeOrChronometer() { 6494 return mN.showsTime() || mN.showsChronometer(); 6495 } 6496 resetStandardTemplateWithActions(RemoteViews contentView)6497 private void resetStandardTemplateWithActions(RemoteViews contentView) { 6498 // actions_container is only reset when there are no actions to avoid focus issues with 6499 // remote inputs. 6500 contentView.setViewVisibility(R.id.actions, View.GONE); 6501 contentView.removeAllViews(R.id.actions); 6502 6503 contentView.setViewVisibility(R.id.notification_material_reply_container, View.GONE); 6504 contentView.setTextViewText(R.id.notification_material_reply_text_1, null); 6505 contentView.setViewVisibility(R.id.notification_material_reply_text_1_container, 6506 View.GONE); 6507 contentView.setViewVisibility(R.id.notification_material_reply_progress, View.GONE); 6508 6509 contentView.setViewVisibility(R.id.notification_material_reply_text_2, View.GONE); 6510 contentView.setTextViewText(R.id.notification_material_reply_text_2, null); 6511 contentView.setViewVisibility(R.id.notification_material_reply_text_3, View.GONE); 6512 contentView.setTextViewText(R.id.notification_material_reply_text_3, null); 6513 6514 if (!notificationsRedesignTemplates()) { 6515 // This may get erased by bindSnoozeAction, or if we're showing the bubble icon 6516 contentView.setViewLayoutMarginDimen(R.id.notification_action_list_margin_target, 6517 RemoteViews.MARGIN_BOTTOM, R.dimen.notification_content_margin); 6518 } 6519 } 6520 bindSnoozeAction(RemoteViews contentView, StandardTemplateParams p)6521 private boolean bindSnoozeAction(RemoteViews contentView, StandardTemplateParams p) { 6522 boolean hideSnoozeButton = mN.isFgsOrUij() 6523 || mN.fullScreenIntent != null 6524 || isBackgroundColorized(p) 6525 || p.mViewType != StandardTemplateParams.VIEW_TYPE_EXPANDED; 6526 contentView.setBoolean(R.id.snooze_button, "setEnabled", !hideSnoozeButton); 6527 if (hideSnoozeButton) { 6528 // Only hide; NotificationContentView will show it when it adds the click listener 6529 contentView.setViewVisibility(R.id.snooze_button, View.GONE); 6530 } 6531 6532 final boolean snoozeEnabled = !hideSnoozeButton 6533 && mContext.getContentResolver() != null 6534 && isSnoozeSettingEnabled(); 6535 if (!notificationsRedesignTemplates() && snoozeEnabled) { 6536 contentView.setViewLayoutMarginDimen(R.id.notification_action_list_margin_target, 6537 RemoteViews.MARGIN_BOTTOM, 0); 6538 } 6539 return snoozeEnabled; 6540 } 6541 isSnoozeSettingEnabled()6542 private boolean isSnoozeSettingEnabled() { 6543 try { 6544 return Settings.Secure.getIntForUser(mContext.getContentResolver(), 6545 Settings.Secure.SHOW_NOTIFICATION_SNOOZE, 0, UserHandle.USER_CURRENT) == 1; 6546 } catch (SecurityException ex) { 6547 // Most 3p apps can't access this snooze setting, so their NotificationListeners 6548 // would be unable to create notification views if we propagated this exception. 6549 return false; 6550 } 6551 } 6552 6553 /** 6554 * Returns the actions that are not contextual. 6555 */ getNonContextualActions()6556 private @NonNull List<Notification.Action> getNonContextualActions() { 6557 if (mActions == null) return Collections.emptyList(); 6558 List<Notification.Action> standardActions = new ArrayList<>(); 6559 for (Notification.Action action : mActions) { 6560 // Actions with RemoteInput are ignored for RONs. 6561 if (mN.isPromotedOngoing() 6562 && hasValidRemoteInput(action)) { 6563 continue; 6564 } 6565 if (!action.isContextual()) { 6566 standardActions.add(action); 6567 } 6568 } 6569 return standardActions; 6570 } 6571 applyStandardTemplateWithActions(int layoutId, StandardTemplateParams p, TemplateBindResult result)6572 private RemoteViews applyStandardTemplateWithActions(int layoutId, 6573 StandardTemplateParams p, TemplateBindResult result) { 6574 RemoteViews contentView = applyStandardTemplate(layoutId, p, result); 6575 6576 resetStandardTemplateWithActions(contentView); 6577 boolean snoozeEnabled = bindSnoozeAction(contentView, p); 6578 // color the snooze and bubble actions with the theme color 6579 ColorStateList actionColor = ColorStateList.valueOf(getStandardActionColor(p)); 6580 contentView.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor); 6581 contentView.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor); 6582 6583 // In the UI, contextual actions appear separately from the standard actions, so we 6584 // filter them out here. 6585 List<Notification.Action> nonContextualActions = getNonContextualActions(); 6586 6587 int numActions = Math.min(nonContextualActions.size(), MAX_ACTION_BUTTONS); 6588 boolean emphasizedMode = mN.fullScreenIntent != null 6589 || p.mCallStyleActions 6590 || ((mN.flags & FLAG_FSI_REQUESTED_BUT_DENIED) != 0); 6591 6592 if (p.mCallStyleActions) { 6593 // Clear view padding to allow buttons to start on the left edge. 6594 // This must be done before 'setEmphasizedMode' which sets top/bottom margins. 6595 contentView.setViewPadding(R.id.actions, 0, 0, 0, 0); 6596 if (!Flags.notificationsRedesignTemplates()) { 6597 // Add an optional indent that will make buttons start at the correct column 6598 // when there is enough space to do so (and fall back to the left edge if not). 6599 // This is handled directly in NotificationActionListLayout in the new design. 6600 contentView.setInt(R.id.actions, "setCollapsibleIndentDimen", 6601 R.dimen.call_notification_collapsible_indent); 6602 } 6603 if (evenlyDividedCallStyleActionLayout()) { 6604 if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { 6605 Log.d(TAG, "setting evenly divided mode on action list"); 6606 } 6607 contentView.setBoolean(R.id.actions, "setEvenlyDividedMode", true); 6608 } 6609 } 6610 if (!notificationsRedesignTemplates()) { 6611 contentView.setBoolean(R.id.actions, "setEmphasizedMode", emphasizedMode); 6612 } 6613 6614 boolean validRemoteInput = false; 6615 // With the new design, the actions_container should always be visible to act as padding 6616 // when there are no actions. We're making its child GONE instead. 6617 int actionsContainerForVisibilityChange = notificationsRedesignTemplates() 6618 ? R.id.actions_container_layout : R.id.actions_container; 6619 if (numActions > 0 && !p.mHideActions) { 6620 contentView.setViewVisibility(actionsContainerForVisibilityChange, View.VISIBLE); 6621 contentView.setViewVisibility(R.id.actions, View.VISIBLE); 6622 updateMarginsForActions(contentView, emphasizedMode); 6623 validRemoteInput = populateActionsContainer(contentView, p, nonContextualActions, 6624 numActions, emphasizedMode); 6625 } else { 6626 contentView.setViewVisibility(actionsContainerForVisibilityChange, View.GONE); 6627 } 6628 6629 RemoteInputHistoryItem[] replyText = getParcelableArrayFromBundle( 6630 mN.extras, EXTRA_REMOTE_INPUT_HISTORY_ITEMS, RemoteInputHistoryItem.class); 6631 if (validRemoteInput && replyText != null && replyText.length > 0 6632 && !TextUtils.isEmpty(replyText[0].getText()) 6633 && p.maxRemoteInputHistory > 0) { 6634 boolean showSpinner = mN.extras.getBoolean(EXTRA_SHOW_REMOTE_INPUT_SPINNER); 6635 contentView.setViewVisibility(R.id.notification_material_reply_container, 6636 View.VISIBLE); 6637 contentView.setViewVisibility(R.id.notification_material_reply_text_1_container, 6638 View.VISIBLE); 6639 contentView.setTextViewText(R.id.notification_material_reply_text_1, 6640 ensureColorSpanContrastOrStripStyling(replyText[0].getText(), p)); 6641 setTextViewColorSecondary(contentView, R.id.notification_material_reply_text_1, p); 6642 contentView.setViewVisibility(R.id.notification_material_reply_progress, 6643 showSpinner ? View.VISIBLE : View.GONE); 6644 contentView.setProgressIndeterminateTintList( 6645 R.id.notification_material_reply_progress, 6646 ColorStateList.valueOf(getPrimaryAccentColor(p))); 6647 6648 if (replyText.length > 1 && !TextUtils.isEmpty(replyText[1].getText()) 6649 && p.maxRemoteInputHistory > 1) { 6650 contentView.setViewVisibility(R.id.notification_material_reply_text_2, 6651 View.VISIBLE); 6652 contentView.setTextViewText(R.id.notification_material_reply_text_2, 6653 ensureColorSpanContrastOrStripStyling(replyText[1].getText(), p)); 6654 setTextViewColorSecondary(contentView, R.id.notification_material_reply_text_2, 6655 p); 6656 6657 if (replyText.length > 2 && !TextUtils.isEmpty(replyText[2].getText()) 6658 && p.maxRemoteInputHistory > 2) { 6659 contentView.setViewVisibility( 6660 R.id.notification_material_reply_text_3, View.VISIBLE); 6661 contentView.setTextViewText(R.id.notification_material_reply_text_3, 6662 ensureColorSpanContrastOrStripStyling(replyText[2].getText(), p)); 6663 setTextViewColorSecondary(contentView, 6664 R.id.notification_material_reply_text_3, p); 6665 } 6666 } 6667 } 6668 6669 return contentView; 6670 } 6671 updateMarginsForActions(RemoteViews contentView, boolean emphasizedMode)6672 private void updateMarginsForActions(RemoteViews contentView, boolean emphasizedMode) { 6673 if (notificationsRedesignTemplates()) { 6674 if (emphasizedMode) { 6675 // Emphasized actions look similar to smart replies, so let's use the same 6676 // margins. 6677 contentView.setViewLayoutMarginDimen(R.id.actions_container, 6678 RemoteViews.MARGIN_TOP, 6679 R.dimen.notification_2025_smart_reply_container_margin); 6680 contentView.setViewLayoutMarginDimen(R.id.actions_container, 6681 RemoteViews.MARGIN_BOTTOM, 6682 R.dimen.notification_2025_smart_reply_container_margin); 6683 } else { 6684 contentView.setViewLayoutMarginDimen(R.id.actions_container, 6685 RemoteViews.MARGIN_TOP, 0); 6686 contentView.setViewLayoutMarginDimen(R.id.actions_container, 6687 RemoteViews.MARGIN_BOTTOM, 6688 R.dimen.notification_2025_action_list_margin_bottom); 6689 } 6690 } else { 6691 contentView.setViewLayoutMarginDimen(R.id.notification_action_list_margin_target, 6692 RemoteViews.MARGIN_BOTTOM, 0); 6693 } 6694 } 6695 populateActionsContainer(RemoteViews contentView, StandardTemplateParams p, List<Action> nonContextualActions, int numActions, boolean emphasizedMode)6696 private boolean populateActionsContainer(RemoteViews contentView, StandardTemplateParams p, 6697 List<Action> nonContextualActions, int numActions, boolean emphasizedMode) { 6698 boolean validRemoteInput = false; 6699 for (int i = 0; i < numActions; i++) { 6700 Action action = nonContextualActions.get(i); 6701 6702 boolean actionHasValidInput = hasValidRemoteInput(action); 6703 validRemoteInput |= actionHasValidInput; 6704 6705 final RemoteViews button = generateActionButton(action, emphasizedMode, p); 6706 if (actionHasValidInput && !emphasizedMode) { 6707 // Clear the drawable 6708 button.setInt(R.id.action0, "setBackgroundResource", 0); 6709 } 6710 if (emphasizedMode && i > 0) { 6711 // Clear start margin from non-first buttons to reduce the gap between them. 6712 // (8dp remaining gap is from all buttons' standard 4dp inset). 6713 button.setViewLayoutMarginDimen(R.id.action0, RemoteViews.MARGIN_START, 0); 6714 } 6715 contentView.addView(R.id.actions, button); 6716 } 6717 return validRemoteInput; 6718 } 6719 6720 /** 6721 * Calculate the top margin for the content in px, to allow enough space for the top line 6722 * above, using the given resource ID for the desired spacing. 6723 * 6724 * @hide 6725 */ getContentMarginTop(Context context, @DimenRes int spacingRes)6726 public static int getContentMarginTop(Context context, @DimenRes int spacingRes) { 6727 final Resources resources = context.getResources(); 6728 // The margin above the text, at the top of the notification (originally in dp) 6729 int notifMargin = resources.getDimensionPixelSize(R.dimen.notification_2025_margin); 6730 // Spacing between the text lines, scaling with the font size (originally in sp) 6731 int spacing = resources.getDimensionPixelSize(spacingRes); 6732 // Size of the text in the notification top line (originally in sp) 6733 int textSize = resources.getDimensionPixelSize(R.dimen.notification_subtext_size); 6734 6735 // Adding up all the values as pixels 6736 return notifMargin + spacing + textSize; 6737 } 6738 hasValidRemoteInput(Action action)6739 private boolean hasValidRemoteInput(Action action) { 6740 if (TextUtils.isEmpty(action.title) || action.actionIntent == null) { 6741 // Weird actions 6742 return false; 6743 } 6744 6745 RemoteInput[] remoteInputs = action.getRemoteInputs(); 6746 if (remoteInputs == null) { 6747 return false; 6748 } 6749 6750 for (RemoteInput r : remoteInputs) { 6751 CharSequence[] choices = r.getChoices(); 6752 if (r.getAllowFreeFormInput() || (choices != null && choices.length != 0)) { 6753 return true; 6754 } 6755 } 6756 return false; 6757 } 6758 6759 /** 6760 * Construct a RemoteViews representing the standard notification layout. 6761 * 6762 * @deprecated For performance and system health reasons, this API is no longer required to 6763 * be used directly by the System UI when rendering Notifications to the user. While the UI 6764 * returned by this method will still represent the content of the Notification being 6765 * built, it may differ from the visual style of the system. 6766 * 6767 * NOTE: this API has always had severe limitations; for example it does not support any 6768 * interactivity, it ignores the app theme, it hard-codes the colors from the system theme 6769 * at the time it is called, and it does Bitmap decoding on the main thread which can cause 6770 * UI jank. 6771 */ 6772 @Deprecated createContentView()6773 public RemoteViews createContentView() { 6774 if (useExistingRemoteView(mN.contentView)) { 6775 return fullyCustomViewRequiresDecoration(false /* fromStyle */) 6776 ? minimallyDecoratedContentView(mN.contentView) : mN.contentView; 6777 } else if (mStyle != null) { 6778 final RemoteViews styleView = mStyle.makeContentView(); 6779 if (styleView != null) { 6780 return fullyCustomViewRequiresDecoration(true /* fromStyle */) 6781 ? minimallyDecoratedContentView(styleView) : styleView; 6782 } 6783 } 6784 StandardTemplateParams p = mParams.reset() 6785 .viewType(StandardTemplateParams.VIEW_TYPE_NORMAL) 6786 .fillTextsFrom(this); 6787 return applyStandardTemplate(getCollapsedBaseLayoutResource(), p, null /* result */); 6788 } 6789 6790 // This code is executed on behalf of other apps' notifications, sometimes even by 3p apps, 6791 // a use case that is not supported by the Compat Framework library. Workarounds to resolve 6792 // the change's state in NotificationManagerService were very complex. While it's possible 6793 // apps can detect the change, it's most likely that the changes will simply result in 6794 // visual regressions. 6795 @SuppressWarnings("AndroidFrameworkCompatChange") fullyCustomViewRequiresDecoration(boolean fromStyle)6796 private boolean fullyCustomViewRequiresDecoration(boolean fromStyle) { 6797 // Custom views which come from a platform style class are safe, and thus do not need to 6798 // be wrapped. Any subclass of those styles has the opportunity to make arbitrary 6799 // changes to the RemoteViews, and thus can't be trusted as a fully vetted view. 6800 if (fromStyle && isPlatformStyle(mStyle)) { 6801 return false; 6802 } 6803 return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S; 6804 } 6805 minimallyDecoratedContentView(@onNull RemoteViews customContent)6806 private RemoteViews minimallyDecoratedContentView(@NonNull RemoteViews customContent) { 6807 StandardTemplateParams p = mParams.reset() 6808 .viewType(StandardTemplateParams.VIEW_TYPE_NORMAL) 6809 .decorationType(StandardTemplateParams.DECORATION_MINIMAL) 6810 .fillTextsFrom(this); 6811 TemplateBindResult result = new TemplateBindResult(); 6812 RemoteViews standard = applyStandardTemplate(getCollapsedBaseLayoutResource(), 6813 p, result); 6814 buildCustomContentIntoTemplate(mContext, standard, customContent, 6815 p, result); 6816 return standard; 6817 } 6818 minimallyDecoratedExpandedContentView( @onNull RemoteViews customContent)6819 private RemoteViews minimallyDecoratedExpandedContentView( 6820 @NonNull RemoteViews customContent) { 6821 StandardTemplateParams p = mParams.reset() 6822 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED) 6823 .decorationType(StandardTemplateParams.DECORATION_MINIMAL) 6824 .fillTextsFrom(this); 6825 TemplateBindResult result = new TemplateBindResult(); 6826 RemoteViews standard = applyStandardTemplateWithActions(getExpandedBaseLayoutResource(), 6827 p, result); 6828 buildCustomContentIntoTemplate(mContext, standard, customContent, 6829 p, result); 6830 makeHeaderExpanded(standard); 6831 return standard; 6832 } 6833 minimallyDecoratedHeadsUpContentView( @onNull RemoteViews customContent)6834 private RemoteViews minimallyDecoratedHeadsUpContentView( 6835 @NonNull RemoteViews customContent) { 6836 StandardTemplateParams p = mParams.reset() 6837 .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) 6838 .decorationType(StandardTemplateParams.DECORATION_MINIMAL) 6839 .fillTextsFrom(this); 6840 TemplateBindResult result = new TemplateBindResult(); 6841 RemoteViews standard = applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(), 6842 p, result); 6843 buildCustomContentIntoTemplate(mContext, standard, customContent, 6844 p, result); 6845 return standard; 6846 } 6847 useExistingRemoteView(RemoteViews customContent)6848 private boolean useExistingRemoteView(RemoteViews customContent) { 6849 if (customContent == null) { 6850 return false; 6851 } 6852 if (styleDisplaysCustomViewInline()) { 6853 // the provided custom view is intended to be wrapped by the style. 6854 return false; 6855 } 6856 if (fullyCustomViewRequiresDecoration(false) 6857 && isStandardLayout(customContent.getLayoutId())) { 6858 // If the app's custom views are objects returned from Builder.create*ContentView() 6859 // then the app is most likely attempting to spoof the user. Even if they are not, 6860 // the result would be broken (b/189189308) so we will ignore it. 6861 Log.w(TAG, "For apps targeting S, a custom content view that is a modified " 6862 + "version of any standard layout is disallowed."); 6863 return false; 6864 } 6865 return true; 6866 } 6867 6868 /** 6869 * Construct a RemoteViews representing the expanded notification layout. 6870 * 6871 * @deprecated For performance and system health reasons, this API is no longer required to 6872 * be used directly by the System UI when rendering Notifications to the user. While the UI 6873 * returned by this method will still represent the content of the Notification being 6874 * built, it may differ from the visual style of the system. 6875 * 6876 * NOTE: this API has always had severe limitations; for example it does not support any 6877 * interactivity, it ignores the app theme, it hard-codes the colors from the system theme 6878 * at the time it is called, and it does Bitmap decoding on the main thread which can cause 6879 * UI jank. 6880 */ 6881 @Deprecated createBigContentView()6882 public RemoteViews createBigContentView() { 6883 return createExpandedContentView(); 6884 } 6885 createExpandedContentView()6886 private RemoteViews createExpandedContentView() { 6887 RemoteViews result = null; 6888 if (useExistingRemoteView(mN.bigContentView)) { 6889 return fullyCustomViewRequiresDecoration(false /* fromStyle */) 6890 ? minimallyDecoratedExpandedContentView(mN.bigContentView) 6891 : mN.bigContentView; 6892 } 6893 if (mStyle != null) { 6894 result = mStyle.makeExpandedContentView(); 6895 if (fullyCustomViewRequiresDecoration(true /* fromStyle */)) { 6896 result = minimallyDecoratedExpandedContentView(result); 6897 } 6898 } 6899 if (result == null) { 6900 if (expandedContentViewRequired()) { 6901 StandardTemplateParams p = mParams.reset() 6902 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED) 6903 .allowTextWithProgress(true) 6904 .fillTextsFrom(this); 6905 result = applyStandardTemplateWithActions(getExpandedBaseLayoutResource(), p, 6906 null /* result */); 6907 } 6908 } 6909 makeHeaderExpanded(result); 6910 return result; 6911 } 6912 6913 // This code is executed on behalf of other apps' notifications, sometimes even by 3p apps, 6914 // a use case that is not supported by the Compat Framework library. Workarounds to resolve 6915 // the change's state in NotificationManagerService were very complex. While it's possible 6916 // apps can detect the change, it's most likely that the changes will simply result in 6917 // visual regressions. 6918 @SuppressWarnings("AndroidFrameworkCompatChange") expandedContentViewRequired()6919 private boolean expandedContentViewRequired() { 6920 if (Flags.notificationExpansionOptional()) { 6921 // Notifications without a bigContentView, style, or actions do not need to expand 6922 boolean exempt = mN.bigContentView == null 6923 && mStyle == null && mActions.size() == 0; 6924 return !exempt; 6925 } 6926 if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) { 6927 return true; 6928 } 6929 // Notifications with contentView and without a bigContentView, style, or actions would 6930 // not have an expanded state before S, so showing the standard template expanded state 6931 // usually looks wrong, so we keep it simple and don't show the expanded state. 6932 boolean exempt = mN.contentView != null && mN.bigContentView == null 6933 && mStyle == null && mActions.size() == 0; 6934 return !exempt; 6935 } 6936 6937 /** 6938 * Construct a RemoteViews for the final notification header only. This will not be 6939 * colorized. 6940 * 6941 * @hide 6942 */ makeNotificationGroupHeader()6943 public RemoteViews makeNotificationGroupHeader() { 6944 return makeNotificationHeader(mParams.reset().disallowColorization() 6945 .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER) 6946 .fillTextsFrom(this)); 6947 } 6948 6949 /** 6950 * Construct a RemoteViews for the final notification header only. This will not be 6951 * colorized. 6952 * 6953 * @param p the template params to inflate this with 6954 */ makeNotificationHeader(StandardTemplateParams p)6955 private RemoteViews makeNotificationHeader(StandardTemplateParams p) { 6956 RemoteViews header = new BuilderRemoteViews(mContext.getApplicationInfo(), 6957 getHeaderLayoutResource()); 6958 resetNotificationHeader(header); 6959 bindNotificationHeader(header, p); 6960 updateHeaderBackgroundColor(header, p); 6961 if (Flags.notificationsRedesignTemplates() 6962 && (p.mViewType == StandardTemplateParams.VIEW_TYPE_MINIMIZED 6963 || p.mViewType == StandardTemplateParams.VIEW_TYPE_PUBLIC)) { 6964 // Center top line vertically in minimized and public header-only views 6965 header.setBoolean(R.id.notification_header, "centerTopLine", true); 6966 } 6967 return header; 6968 } 6969 6970 /** 6971 * Adapt the Notification header if this view is used as an expanded view. 6972 * 6973 * @hide 6974 */ makeHeaderExpanded(RemoteViews result)6975 public static void makeHeaderExpanded(RemoteViews result) { 6976 if (result != null) { 6977 result.setBoolean(R.id.expand_button, "setExpanded", true); 6978 } 6979 } 6980 6981 /** 6982 * Construct a RemoteViews for the final compact heads-up notification layout. 6983 * @hide 6984 */ createCompactHeadsUpContentView()6985 public RemoteViews createCompactHeadsUpContentView() { 6986 // Don't show compact heads up for FSI notifications. 6987 if (mN.fullScreenIntent != null) { 6988 return createHeadsUpContentView(); 6989 } 6990 6991 if (mStyle != null) { 6992 final RemoteViews styleView = mStyle.makeCompactHeadsUpContentView(); 6993 if (styleView != null) { 6994 return styleView; 6995 } 6996 } 6997 6998 final StandardTemplateParams p = mParams.reset() 6999 .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) 7000 .fillTextsFrom(this); 7001 // Notification text is shown as secondary header text 7002 // for the minimal hun when it is provided. 7003 // Time(when and chronometer) is not shown for the minimal hun. 7004 p.headerTextSecondary(p.mText).text(null).hideTime(true).summaryText(""); 7005 7006 return applyStandardTemplate( 7007 getCompactHeadsUpBaseLayoutResource(), p, 7008 null /* result */); 7009 } 7010 7011 /** 7012 * Construct a RemoteViews representing the heads up notification layout. 7013 * 7014 * @deprecated For performance and system health reasons, this API is no longer required to 7015 * be used directly by the System UI when rendering Notifications to the user. While the UI 7016 * returned by this method will still represent the content of the Notification being 7017 * built, it may differ from the visual style of the system. 7018 * 7019 * NOTE: this API has always had severe limitations; for example it does not support any 7020 * interactivity, it ignores the app theme, it hard-codes the colors from the system theme 7021 * at the time it is called, and it does Bitmap decoding on the main thread which can cause 7022 * UI jank. 7023 */ 7024 @Deprecated createHeadsUpContentView()7025 public RemoteViews createHeadsUpContentView() { 7026 if (useExistingRemoteView(mN.headsUpContentView)) { 7027 return fullyCustomViewRequiresDecoration(false /* fromStyle */) 7028 ? minimallyDecoratedHeadsUpContentView(mN.headsUpContentView) 7029 : mN.headsUpContentView; 7030 } else if (mStyle != null) { 7031 final RemoteViews styleView = mStyle.makeHeadsUpContentView(); 7032 if (styleView != null) { 7033 return fullyCustomViewRequiresDecoration(true /* fromStyle */) 7034 ? minimallyDecoratedHeadsUpContentView(styleView) : styleView; 7035 } 7036 } else if (mActions.size() == 0) { 7037 return null; 7038 } 7039 7040 // We only want at most a single remote input history to be shown here, otherwise 7041 // the content would become squished. 7042 StandardTemplateParams p = mParams.reset() 7043 .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) 7044 .fillTextsFrom(this) 7045 .setMaxRemoteInputHistory(1); 7046 return applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(), p, 7047 null /* result */); 7048 } 7049 7050 /** 7051 * Construct a RemoteViews for the display in public contexts like on the lockscreen. 7052 * 7053 * @param isLowPriority is this notification low priority 7054 * @hide 7055 */ 7056 @UnsupportedAppUsage makePublicContentView(boolean isLowPriority)7057 public RemoteViews makePublicContentView(boolean isLowPriority) { 7058 if (mN.publicVersion != null) { 7059 final Builder builder = recoverBuilder(mContext, mN.publicVersion); 7060 // copy non-sensitive style fields to the public style 7061 if (mStyle instanceof Notification.MessagingStyle privateStyle) { 7062 if (builder.mStyle instanceof Notification.MessagingStyle publicStyle) { 7063 publicStyle.mConversationType = privateStyle.mConversationType; 7064 } 7065 } 7066 return builder.createContentView(); 7067 } 7068 Bundle savedBundle = mN.extras; 7069 Style style = mStyle; 7070 mStyle = null; 7071 Icon largeIcon = mN.mLargeIcon; 7072 mN.mLargeIcon = null; 7073 Bitmap largeIconLegacy = mN.largeIcon; 7074 mN.largeIcon = null; 7075 ArrayList<Action> actions = mActions; 7076 mActions = new ArrayList<>(); 7077 Bundle publicExtras = new Bundle(); 7078 publicExtras.putBoolean(EXTRA_SHOW_WHEN, 7079 savedBundle.getBoolean(EXTRA_SHOW_WHEN)); 7080 publicExtras.putBoolean(EXTRA_SHOW_CHRONOMETER, 7081 savedBundle.getBoolean(EXTRA_SHOW_CHRONOMETER)); 7082 publicExtras.putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, 7083 savedBundle.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN)); 7084 if (mN.isPromotedOngoing()) { 7085 publicExtras.putBoolean(EXTRA_COLORIZED, 7086 savedBundle.getBoolean(EXTRA_COLORIZED)); 7087 } 7088 String appName = savedBundle.getString(EXTRA_SUBSTITUTE_APP_NAME); 7089 if (appName != null) { 7090 publicExtras.putString(EXTRA_SUBSTITUTE_APP_NAME, appName); 7091 } 7092 mN.extras = publicExtras; 7093 RemoteViews view; 7094 StandardTemplateParams params = mParams.reset() 7095 .viewType(StandardTemplateParams.VIEW_TYPE_PUBLIC) 7096 .fillTextsFrom(this); 7097 if (isLowPriority) { 7098 params.highlightExpander(false); 7099 } 7100 if (!mN.isPromotedOngoing()) { 7101 params.disallowColorization(); 7102 } 7103 view = makeNotificationHeader(params); 7104 view.setBoolean(R.id.notification_header, "setExpandOnlyOnButton", true); 7105 mN.extras = savedBundle; 7106 mN.mLargeIcon = largeIcon; 7107 mN.largeIcon = largeIconLegacy; 7108 mActions = actions; 7109 mStyle = style; 7110 return view; 7111 } 7112 7113 /** 7114 * Construct a content view for the display when low - priority 7115 * 7116 * @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise 7117 * a new subtext is created consisting of the content of the 7118 * notification. 7119 * @hide 7120 */ makeLowPriorityContentView(boolean useRegularSubtext)7121 public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) { 7122 StandardTemplateParams p = mParams.reset().disallowColorization() 7123 .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED) 7124 .highlightExpander(false) 7125 .fillTextsFrom(this); 7126 if (!useRegularSubtext || TextUtils.isEmpty(p.mSubText)) { 7127 p.summaryText(createSummaryText()); 7128 } 7129 RemoteViews header = makeNotificationHeader(p); 7130 header.setBoolean(R.id.notification_header, "setAcceptAllTouches", true); 7131 // The low priority header has no app name and shows the text 7132 header.setBoolean(R.id.notification_header, "styleTextAsTitle", true); 7133 return header; 7134 } 7135 createSummaryText()7136 private CharSequence createSummaryText() { 7137 CharSequence titleText = mN.extras.getCharSequence(Notification.EXTRA_TITLE); 7138 if (USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY) { 7139 return titleText; 7140 } 7141 SpannableStringBuilder summary = new SpannableStringBuilder(); 7142 if (titleText == null) { 7143 titleText = mN.extras.getCharSequence(Notification.EXTRA_TITLE_BIG); 7144 } 7145 BidiFormatter bidi = BidiFormatter.getInstance(); 7146 if (titleText != null) { 7147 summary.append(bidi.unicodeWrap(titleText)); 7148 } 7149 CharSequence contentText = mN.extras.getCharSequence(Notification.EXTRA_TEXT); 7150 if (titleText != null && contentText != null) { 7151 summary.append(bidi.unicodeWrap(mContext.getText( 7152 R.string.notification_header_divider_symbol_with_spaces))); 7153 } 7154 if (contentText != null) { 7155 summary.append(bidi.unicodeWrap(contentText)); 7156 } 7157 return summary; 7158 } 7159 generateActionButton(Action action, boolean emphasizedMode, StandardTemplateParams p)7160 private RemoteViews generateActionButton(Action action, boolean emphasizedMode, 7161 StandardTemplateParams p) { 7162 final boolean tombstone = (action.actionIntent == null); 7163 final RemoteViews button = new BuilderRemoteViews(mContext.getApplicationInfo(), 7164 getActionButtonLayoutResource(emphasizedMode, tombstone)); 7165 if (!tombstone) { 7166 button.setOnClickPendingIntent(R.id.action0, action.actionIntent); 7167 } 7168 button.setContentDescription(R.id.action0, action.title); 7169 if (action.mRemoteInputs != null) { 7170 button.setRemoteInputs(R.id.action0, action.mRemoteInputs); 7171 } 7172 if (emphasizedMode) { 7173 // change the background bgColor 7174 CharSequence title = action.title; 7175 int buttonFillColor = getColors(p).getSecondaryAccentColor(); 7176 if (tombstone) { 7177 buttonFillColor = setAlphaComponentByFloatDimen(mContext, 7178 ContrastColorUtil.resolveSecondaryColor( 7179 mContext, getColors(p).getBackgroundColor(), mInNightMode), 7180 R.dimen.notification_action_disabled_container_alpha); 7181 } 7182 if (Flags.cleanUpSpansAndNewLines()) { 7183 if (!isLegacy()) { 7184 // Check for a full-length span color to use as the button fill color. 7185 Integer fullLengthColor = getFullLengthSpanColor(title); 7186 if (fullLengthColor != null) { 7187 // Ensure the custom button fill has 1.3:1 contrast w/ notification bg. 7188 int notifBackgroundColor = getColors(p).getBackgroundColor(); 7189 buttonFillColor = ensureButtonFillContrast( 7190 fullLengthColor, notifBackgroundColor); 7191 } 7192 } 7193 } else { 7194 if (isLegacy()) { 7195 title = ContrastColorUtil.clearColorSpans(title); 7196 } else { 7197 // Check for a full-length span color to use as the button fill color. 7198 Integer fullLengthColor = getFullLengthSpanColor(title); 7199 if (fullLengthColor != null) { 7200 // Ensure the custom button fill has 1.3:1 contrast w/ notification bg. 7201 int notifBackgroundColor = getColors(p).getBackgroundColor(); 7202 buttonFillColor = ensureButtonFillContrast( 7203 fullLengthColor, notifBackgroundColor); 7204 } 7205 // Remove full-length color spans 7206 // and ensure text contrast with the button fill. 7207 title = ContrastColorUtil.ensureColorSpanContrast(title, buttonFillColor); 7208 } 7209 } 7210 7211 7212 final CharSequence label = ensureColorSpanContrastOrStripStyling(title, p); 7213 if (p.mCallStyleActions && evenlyDividedCallStyleActionLayout()) { 7214 if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { 7215 Log.d(TAG, "new action layout enabled, gluing instead of setting text"); 7216 } 7217 button.setCharSequence(R.id.action0, "glueLabel", label); 7218 } else { 7219 button.setTextViewText(R.id.action0, label); 7220 } 7221 int textColor = ContrastColorUtil.resolvePrimaryColor(mContext, 7222 buttonFillColor, mInNightMode); 7223 if (tombstone) { 7224 textColor = setAlphaComponentByFloatDimen(mContext, 7225 ContrastColorUtil.resolveSecondaryColor( 7226 mContext, getColors(p).getBackgroundColor(), mInNightMode), 7227 R.dimen.notification_action_disabled_content_alpha); 7228 } 7229 button.setTextColor(R.id.action0, textColor); 7230 // We only want about 20% alpha for the ripple 7231 final int rippleColor = (textColor & 0x00ffffff) | 0x33000000; 7232 button.setColorStateList(R.id.action0, "setRippleColor", 7233 ColorStateList.valueOf(rippleColor)); 7234 button.setColorStateList(R.id.action0, "setButtonBackground", 7235 ColorStateList.valueOf(buttonFillColor)); 7236 if (p.mCallStyleActions) { 7237 if (evenlyDividedCallStyleActionLayout()) { 7238 if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { 7239 Log.d(TAG, "new action layout enabled, gluing instead of setting icon"); 7240 } 7241 button.setIcon(R.id.action0, "glueIcon", action.getIcon()); 7242 } else { 7243 button.setImageViewIcon(R.id.action0, action.getIcon()); 7244 } 7245 boolean priority = action.getExtras().getBoolean(CallStyle.KEY_ACTION_PRIORITY); 7246 button.setBoolean(R.id.action0, "setIsPriority", priority); 7247 int minWidthDimen = 7248 priority ? R.dimen.call_notification_system_action_min_width : 0; 7249 button.setIntDimen(R.id.action0, "setMinimumWidth", minWidthDimen); 7250 } 7251 } else { 7252 button.setTextViewText(R.id.action0, ensureColorSpanContrastOrStripStyling( 7253 action.title, p)); 7254 button.setTextColor(R.id.action0, getStandardActionColor(p)); 7255 } 7256 // CallStyle notifications add action buttons which don't actually exist in mActions, 7257 // so we have to omit the index in that case. 7258 int actionIndex = mActions.indexOf(action); 7259 if (actionIndex != -1) { 7260 button.setIntTag(R.id.action0, R.id.notification_action_index_tag, actionIndex); 7261 } 7262 return button; 7263 } 7264 getActionButtonLayoutResource(boolean emphasizedMode, boolean tombstone)7265 private int getActionButtonLayoutResource(boolean emphasizedMode, boolean tombstone) { 7266 if (emphasizedMode) { 7267 return tombstone ? getEmphasizedTombstoneActionLayoutResource() 7268 : getEmphasizedActionLayoutResource(); 7269 } else { 7270 return tombstone ? getActionTombstoneLayoutResource() 7271 : getActionLayoutResource(); 7272 } 7273 } 7274 7275 /** 7276 * Set the alpha component of {@code color} to be {@code alphaDimenResId}. 7277 */ setAlphaComponentByFloatDimen(Context context, @ColorInt int color, @DimenRes int alphaDimenResId)7278 private static int setAlphaComponentByFloatDimen(Context context, @ColorInt int color, 7279 @DimenRes int alphaDimenResId) { 7280 final TypedValue alphaValue = new TypedValue(); 7281 context.getResources().getValue(alphaDimenResId, alphaValue, true); 7282 return ColorUtils.setAlphaComponent(color, Math.round(alphaValue.getFloat() * 255)); 7283 } 7284 7285 /** 7286 * Extract the color from a full-length span from the text. 7287 * 7288 * @param charSequence the charSequence containing spans 7289 * @return the raw color of the text's last full-length span containing a color, or null if 7290 * no full-length span sets the text color. 7291 * @hide 7292 */ 7293 @VisibleForTesting 7294 @Nullable getFullLengthSpanColor(CharSequence charSequence)7295 public static Integer getFullLengthSpanColor(CharSequence charSequence) { 7296 // NOTE: this method preserves the functionality that for a CharSequence with multiple 7297 // full-length spans, the color of the last one is used. 7298 Integer result = null; 7299 if (charSequence instanceof Spanned) { 7300 Spanned ss = (Spanned) charSequence; 7301 Object[] spans = ss.getSpans(0, ss.length(), Object.class); 7302 // First read through all full-length spans to get the button fill color, which will 7303 // be used as the background color for ensuring contrast of non-full-length spans. 7304 for (Object span : spans) { 7305 int spanStart = ss.getSpanStart(span); 7306 int spanEnd = ss.getSpanEnd(span); 7307 boolean fullLength = (spanEnd - spanStart) == charSequence.length(); 7308 if (!fullLength) { 7309 continue; 7310 } 7311 if (span instanceof TextAppearanceSpan) { 7312 TextAppearanceSpan originalSpan = (TextAppearanceSpan) span; 7313 ColorStateList textColor = originalSpan.getTextColor(); 7314 if (textColor != null) { 7315 result = textColor.getDefaultColor(); 7316 } 7317 } else if (span instanceof ForegroundColorSpan) { 7318 ForegroundColorSpan originalSpan = (ForegroundColorSpan) span; 7319 result = originalSpan.getForegroundColor(); 7320 } 7321 } 7322 } 7323 return result; 7324 } 7325 7326 /** 7327 * @hide 7328 */ ensureColorSpanContrastOrStripStyling(CharSequence cs, StandardTemplateParams p)7329 public CharSequence ensureColorSpanContrastOrStripStyling(CharSequence cs, 7330 StandardTemplateParams p) { 7331 return ensureColorSpanContrastOrStripStyling(cs, getBackgroundColor(p)); 7332 } 7333 7334 /** 7335 * @hide 7336 */ ensureColorSpanContrastOrStripStyling(CharSequence cs, int buttonFillColor)7337 public CharSequence ensureColorSpanContrastOrStripStyling(CharSequence cs, 7338 int buttonFillColor) { 7339 // Ongoing promoted notifications are allowed to have styling. 7340 final boolean isPromotedOngoing = mN.isPromotedOngoing(); 7341 if (!isPromotedOngoing && Flags.cleanUpSpansAndNewLines()) { 7342 return stripStyling(cs); 7343 } 7344 7345 return ContrastColorUtil.ensureColorSpanContrast(cs, buttonFillColor); 7346 } 7347 7348 /** 7349 * Ensures contrast on color spans against a background color. 7350 * Note that any full-length color spans will be removed instead of being contrasted. 7351 * 7352 * @hide 7353 */ 7354 @VisibleForTesting ensureColorSpanContrast(CharSequence charSequence, StandardTemplateParams p)7355 public CharSequence ensureColorSpanContrast(CharSequence charSequence, 7356 StandardTemplateParams p) { 7357 return ContrastColorUtil.ensureColorSpanContrast(charSequence, getBackgroundColor(p)); 7358 } 7359 7360 /** 7361 * Determines if the color is light or dark. Specifically, this is using the same metric as 7362 * {@link ContrastColorUtil#resolvePrimaryColor(Context, int, boolean)} and peers so that 7363 * the direction of color shift is consistent. 7364 * 7365 * @param color the color to check 7366 * @return true if the color has higher contrast with white than black 7367 * @hide 7368 */ isColorDark(int color)7369 public static boolean isColorDark(int color) { 7370 // as per ContrastColorUtil.shouldUseDark, this uses the color contrast midpoint. 7371 return ContrastColorUtil.calculateLuminance(color) <= 0.17912878474; 7372 } 7373 7374 /** 7375 * Finds a button fill color with sufficient contrast over bg (1.3:1) that has the same hue 7376 * as the original color, but is lightened or darkened depending on whether the background 7377 * is dark or light. 7378 * 7379 * @hide 7380 */ 7381 @VisibleForTesting ensureButtonFillContrast(int color, int bg)7382 public static int ensureButtonFillContrast(int color, int bg) { 7383 return ensureColorContrast(color, bg, 1.3); 7384 } 7385 7386 ensureColorContrast(int color, int bg, double contrastRatio)7387 private static int ensureColorContrast(int color, int bg, double contrastRatio) { 7388 return isColorDark(bg) 7389 ? ContrastColorUtil.findContrastColorAgainstDark(color, bg, true, contrastRatio) 7390 : ContrastColorUtil.findContrastColor(color, bg, true, contrastRatio); 7391 } 7392 7393 /** 7394 * @return Whether we are currently building a notification from a legacy (an app that 7395 * doesn't create material notifications by itself) app. 7396 */ isLegacy()7397 private boolean isLegacy() { 7398 if (!mIsLegacyInitialized) { 7399 mIsLegacy = mContext.getApplicationInfo().targetSdkVersion 7400 < Build.VERSION_CODES.LOLLIPOP; 7401 mIsLegacyInitialized = true; 7402 } 7403 return mIsLegacy; 7404 } 7405 7406 private CharSequence processLegacyText(CharSequence charSequence) { 7407 boolean isAlreadyLightText = isLegacy() || textColorsNeedInversion(); 7408 if (isAlreadyLightText) { 7409 return getColorUtil().invertCharSequenceColors(charSequence); 7410 } else { 7411 return charSequence; 7412 } 7413 } 7414 7415 /** 7416 * Apply any necessary colors to the small icon 7417 */ 7418 private void processSmallIconColor(Icon smallIcon, RemoteViews contentView, 7419 StandardTemplateParams p) { 7420 boolean colorable = !isLegacy() || getColorUtil().isGrayscaleIcon(mContext, 7421 smallIcon); 7422 int color = getSmallIconColor(p); 7423 contentView.setInt(R.id.icon, "setBackgroundColor", 7424 getBackgroundColor(p)); 7425 contentView.setInt(R.id.icon, "setOriginalIconColor", 7426 colorable ? color : COLOR_INVALID); 7427 } 7428 7429 /** 7430 * Make the largeIcon dark if it's a fake smallIcon (that is, 7431 * if it's grayscale). 7432 */ 7433 // TODO: also check bounds, transparency, that sort of thing. 7434 private void processLargeLegacyIcon(Icon largeIcon, RemoteViews contentView, 7435 StandardTemplateParams p) { 7436 if (largeIcon != null && isLegacy() 7437 && getColorUtil().isGrayscaleIcon(mContext, largeIcon)) { 7438 // resolve color will fall back to the default when legacy 7439 int color = getSmallIconColor(p); 7440 contentView.setInt(R.id.icon, "setOriginalIconColor", color); 7441 } 7442 } 7443 7444 private void sanitizeColor() { 7445 if (mN.color != COLOR_DEFAULT) { 7446 mN.color |= 0xFF000000; // no alpha for custom colors 7447 } 7448 } 7449 7450 /** 7451 * Gets the standard action button color 7452 */ 7453 private @ColorInt int getStandardActionColor(Notification.StandardTemplateParams p) { 7454 return mTintActionButtons || isBackgroundColorized(p) 7455 ? getPrimaryAccentColor(p) : getSecondaryTextColor(p); 7456 } 7457 7458 /** 7459 * Gets the foreground color of the small icon. If the notification is colorized, this 7460 * is the primary text color, otherwise it's the contrast-adjusted app-provided color. 7461 */ 7462 private @ColorInt int getSmallIconColor(StandardTemplateParams p) { 7463 return getColors(p).getContrastColor(); 7464 } 7465 7466 /** 7467 * Gets the foreground color of the small icon. If the notification is colorized, this 7468 * is the primary text color, otherwise it's the contrast-adjusted app-provided color. 7469 * @hide 7470 */ 7471 public @ColorInt int getSmallIconColor(boolean isHeader) { 7472 return getColors(/* isHeader = */ isHeader).getContrastColor(); 7473 } 7474 7475 /** 7476 * Gets the background color of the notification. 7477 * @hide 7478 */ 7479 public @ColorInt int getBackgroundColor(boolean isHeader) { 7480 return getColors(/* isHeader = */ isHeader).getBackgroundColor(); 7481 } 7482 7483 /** @return the theme's accent color for colored UI elements. */ 7484 private @ColorInt int getPrimaryAccentColor(StandardTemplateParams p) { 7485 return getColors(p).getPrimaryAccentColor(); 7486 } 7487 7488 /** 7489 * Apply the unstyled operations and return a new {@link Notification} object. 7490 * @hide 7491 */ 7492 @NonNull 7493 public Notification buildUnstyled() { 7494 if (mActions.size() > 0) { 7495 mN.actions = new Action[mActions.size()]; 7496 mActions.toArray(mN.actions); 7497 } 7498 if (!mPersonList.isEmpty()) { 7499 mN.extras.putParcelableArrayList(EXTRA_PEOPLE_LIST, mPersonList); 7500 } 7501 if (mN.bigContentView != null || mN.contentView != null 7502 || mN.headsUpContentView != null) { 7503 mN.extras.putBoolean(EXTRA_CONTAINS_CUSTOM_VIEW, true); 7504 } 7505 return mN; 7506 } 7507 7508 /** 7509 * Creates a Builder from an existing notification so further changes can be made. 7510 * @param context The context for your application / activity. 7511 * @param n The notification to create a Builder from. 7512 */ 7513 @NonNull recoverBuilder(Context context, Notification n)7514 public static Notification.Builder recoverBuilder(Context context, Notification n) { 7515 Trace.beginSection("Notification.Builder#recoverBuilder"); 7516 7517 try { 7518 // Re-create notification context so we can access app resources. 7519 ApplicationInfo applicationInfo = n.extras.getParcelable( 7520 EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class); 7521 Context builderContext; 7522 if (applicationInfo != null) { 7523 try { 7524 builderContext = context.createApplicationContext(applicationInfo, 7525 Context.CONTEXT_RESTRICTED); 7526 } catch (NameNotFoundException e) { 7527 Log.e(TAG, "ApplicationInfo " + applicationInfo + " not found"); 7528 builderContext = context; // try with our context 7529 } 7530 } else { 7531 builderContext = context; // try with given context 7532 } 7533 7534 return new Builder(builderContext, n); 7535 } finally { 7536 Trace.endSection(); 7537 } 7538 } 7539 7540 /** 7541 * Determines whether the platform can generate contextual actions for a notification. 7542 * By default this is true. 7543 */ 7544 @NonNull setAllowSystemGeneratedContextualActions(boolean allowed)7545 public Builder setAllowSystemGeneratedContextualActions(boolean allowed) { 7546 mN.mAllowSystemGeneratedContextualActions = allowed; 7547 return this; 7548 } 7549 7550 /** 7551 * @deprecated Use {@link #build()} instead. 7552 */ 7553 @Deprecated getNotification()7554 public Notification getNotification() { 7555 return build(); 7556 } 7557 7558 /** 7559 * Combine all of the options that have been set and return a new {@link Notification} 7560 * object. 7561 * 7562 * If this notification has {@link BubbleMetadata} attached that was created with 7563 * a shortcutId a check will be performed to ensure the shortcutId supplied to bubble 7564 * metadata matches the shortcutId set on the notification builder, if one was set. 7565 * If the shortcutId's were specified but do not match, an exception is thrown here. 7566 * 7567 * @see BubbleMetadata.Builder#Builder(String) 7568 * @see #setShortcutId(String) 7569 */ 7570 @NonNull build()7571 public Notification build() { 7572 // Check shortcut id matches 7573 if (mN.mShortcutId != null 7574 && mN.mBubbleMetadata != null 7575 && mN.mBubbleMetadata.getShortcutId() != null 7576 && !mN.mShortcutId.equals(mN.mBubbleMetadata.getShortcutId())) { 7577 throw new IllegalArgumentException( 7578 "Notification and BubbleMetadata shortcut id's don't match," 7579 + " notification: " + mN.mShortcutId 7580 + " vs bubble: " + mN.mBubbleMetadata.getShortcutId()); 7581 } 7582 7583 // Adds any new extras provided by the user. 7584 if (mUserExtras != null) { 7585 final Bundle saveExtras = (Bundle) mUserExtras.clone(); 7586 mN.extras.putAll(saveExtras); 7587 } 7588 7589 if (!Flags.sortSectionByTime()) { 7590 mN.creationTime = System.currentTimeMillis(); 7591 } 7592 7593 // lazy stuff from mContext; see comment in Builder(Context, Notification) 7594 Notification.addFieldsFromContext(mContext, mN); 7595 7596 buildUnstyled(); 7597 7598 if (mStyle != null) { 7599 mStyle.reduceImageSizes(mContext); 7600 mStyle.purgeResources(); 7601 mStyle.validate(mContext); 7602 mStyle.buildStyled(mN); 7603 } 7604 mN.reduceImageSizes(mContext); 7605 7606 if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N 7607 && !styleDisplaysCustomViewInline()) { 7608 RemoteViews newContentView = mN.contentView; 7609 RemoteViews newBigContentView = mN.bigContentView; 7610 RemoteViews newHeadsUpContentView = mN.headsUpContentView; 7611 if (newContentView == null) { 7612 newContentView = createContentView(); 7613 mN.extras.putInt(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT, 7614 newContentView.getSequenceNumber()); 7615 } 7616 if (newBigContentView == null) { 7617 newBigContentView = createBigContentView(); 7618 if (newBigContentView != null) { 7619 mN.extras.putInt(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT, 7620 newBigContentView.getSequenceNumber()); 7621 } 7622 } 7623 if (newHeadsUpContentView == null) { 7624 newHeadsUpContentView = createHeadsUpContentView(); 7625 if (newHeadsUpContentView != null) { 7626 mN.extras.putInt(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT, 7627 newHeadsUpContentView.getSequenceNumber()); 7628 } 7629 } 7630 // Don't set any of the content views until after they have all been generated, 7631 // to avoid the generated .contentView triggering the logic which skips generating 7632 // the .bigContentView. 7633 mN.contentView = newContentView; 7634 mN.bigContentView = newBigContentView; 7635 mN.headsUpContentView = newHeadsUpContentView; 7636 } 7637 7638 if ((mN.defaults & DEFAULT_LIGHTS) != 0) { 7639 mN.flags |= FLAG_SHOW_LIGHTS; 7640 } 7641 7642 mN.allPendingIntents = null; 7643 7644 return mN; 7645 } 7646 styleDisplaysCustomViewInline()7647 private boolean styleDisplaysCustomViewInline() { 7648 return mStyle != null && mStyle.displayCustomViewInline(); 7649 } 7650 7651 /** 7652 * Apply this Builder to an existing {@link Notification} object. 7653 * 7654 * @hide 7655 */ 7656 @NonNull buildInto(@onNull Notification n)7657 public Notification buildInto(@NonNull Notification n) { 7658 build().cloneInto(n, true); 7659 return n; 7660 } 7661 7662 /** 7663 * Removes RemoteViews that were created for compatibility from {@param n}, if they did not 7664 * change. 7665 * 7666 * @return {@param n}, if no stripping is needed, otherwise a stripped clone of {@param n}. 7667 * 7668 * @hide 7669 */ maybeCloneStrippedForDelivery(Notification n)7670 public static Notification maybeCloneStrippedForDelivery(Notification n) { 7671 String templateClass = n.extras.getString(EXTRA_TEMPLATE); 7672 7673 // Only strip views for known Styles because we won't know how to 7674 // re-create them otherwise. 7675 if (!TextUtils.isEmpty(templateClass) 7676 && getNotificationStyleClass(templateClass) == null) { 7677 return n; 7678 } 7679 7680 // Only strip unmodified BuilderRemoteViews. 7681 boolean stripContentView = n.contentView instanceof BuilderRemoteViews && 7682 n.extras.getInt(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT, -1) == 7683 n.contentView.getSequenceNumber(); 7684 boolean stripBigContentView = n.bigContentView instanceof BuilderRemoteViews && 7685 n.extras.getInt(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT, -1) == 7686 n.bigContentView.getSequenceNumber(); 7687 boolean stripHeadsUpContentView = n.headsUpContentView instanceof BuilderRemoteViews && 7688 n.extras.getInt(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT, -1) == 7689 n.headsUpContentView.getSequenceNumber(); 7690 7691 // Nothing to do here, no need to clone. 7692 if (!stripContentView && !stripBigContentView && !stripHeadsUpContentView) { 7693 return n; 7694 } 7695 7696 Notification clone = n.clone(); 7697 if (stripContentView) { 7698 clone.contentView = null; 7699 clone.extras.remove(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT); 7700 } 7701 if (stripBigContentView) { 7702 clone.bigContentView = null; 7703 clone.extras.remove(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT); 7704 } 7705 if (stripHeadsUpContentView) { 7706 clone.headsUpContentView = null; 7707 clone.extras.remove(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT); 7708 } 7709 return clone; 7710 } 7711 getHeaderLayoutResource()7712 private int getHeaderLayoutResource() { 7713 if (Flags.notificationsRedesignTemplates()) { 7714 return R.layout.notification_2025_template_header; 7715 } else { 7716 return R.layout.notification_template_header; 7717 } 7718 } 7719 7720 @UnsupportedAppUsage getCollapsedBaseLayoutResource()7721 private int getCollapsedBaseLayoutResource() { 7722 if (Flags.notificationsRedesignTemplates()) { 7723 return R.layout.notification_2025_template_collapsed_base; 7724 } else { 7725 return R.layout.notification_template_material_base; 7726 } 7727 } 7728 getHeadsUpBaseLayoutResource()7729 private int getHeadsUpBaseLayoutResource() { 7730 if (Flags.notificationsRedesignTemplates()) { 7731 return R.layout.notification_2025_template_heads_up_base; 7732 } else { 7733 return R.layout.notification_template_material_heads_up_base; 7734 } 7735 } 7736 getCompactHeadsUpBaseLayoutResource()7737 private int getCompactHeadsUpBaseLayoutResource() { 7738 if (Flags.notificationsRedesignTemplates()) { 7739 return R.layout.notification_2025_template_compact_heads_up_base; 7740 } else { 7741 return R.layout.notification_template_material_compact_heads_up_base; 7742 } 7743 } 7744 getMessagingCompactHeadsUpLayoutResource()7745 private int getMessagingCompactHeadsUpLayoutResource() { 7746 if (Flags.notificationsRedesignTemplates()) { 7747 return R.layout.notification_2025_template_compact_heads_up_messaging; 7748 } else { 7749 return R.layout.notification_template_material_messaging_compact_heads_up; 7750 } 7751 } 7752 getExpandedBaseLayoutResource()7753 private int getExpandedBaseLayoutResource() { 7754 if (Flags.notificationsRedesignTemplates()) { 7755 return R.layout.notification_2025_template_expanded_base; 7756 } else { 7757 return R.layout.notification_template_material_big_base; 7758 } 7759 } 7760 getBigPictureLayoutResource()7761 private int getBigPictureLayoutResource() { 7762 if (Flags.notificationsRedesignTemplates()) { 7763 return R.layout.notification_2025_template_expanded_big_picture; 7764 } else { 7765 return R.layout.notification_template_material_big_picture; 7766 } 7767 } 7768 getBigTextLayoutResource()7769 private int getBigTextLayoutResource() { 7770 if (Flags.notificationsRedesignTemplates()) { 7771 return R.layout.notification_2025_template_expanded_big_text; 7772 } else { 7773 return R.layout.notification_template_material_big_text; 7774 } 7775 } 7776 getInboxLayoutResource()7777 private int getInboxLayoutResource() { 7778 if (Flags.notificationsRedesignTemplates()) { 7779 return R.layout.notification_2025_template_expanded_inbox; 7780 } else { 7781 return R.layout.notification_template_material_inbox; 7782 } 7783 } 7784 getCollapsedMessagingLayoutResource()7785 private int getCollapsedMessagingLayoutResource() { 7786 if (Flags.notificationsRedesignTemplates()) { 7787 return R.layout.notification_2025_template_collapsed_messaging; 7788 } else { 7789 return R.layout.notification_template_material_messaging; 7790 } 7791 } 7792 getExpandedMessagingLayoutResource()7793 private int getExpandedMessagingLayoutResource() { 7794 if (Flags.notificationsRedesignTemplates()) { 7795 return R.layout.notification_2025_template_expanded_messaging; 7796 } else { 7797 return R.layout.notification_template_material_big_messaging; 7798 } 7799 } 7800 getCollapsedMediaLayoutResource()7801 private int getCollapsedMediaLayoutResource() { 7802 if (Flags.notificationsRedesignTemplates()) { 7803 return R.layout.notification_2025_template_collapsed_media; 7804 } else { 7805 return R.layout.notification_template_material_media; 7806 } 7807 } 7808 getExpandedMediaLayoutResource()7809 private int getExpandedMediaLayoutResource() { 7810 if (Flags.notificationsRedesignTemplates()) { 7811 return R.layout.notification_2025_template_expanded_media; 7812 } else { 7813 return R.layout.notification_template_material_big_media; 7814 } 7815 } 7816 7817 // Note: In the 2025 redesign, we use two separate layouts for the collapsed and expanded 7818 // version of conversations. See below. getConversationLayoutResource()7819 private int getConversationLayoutResource() { 7820 return R.layout.notification_template_material_conversation; 7821 } 7822 getCollapsedConversationLayoutResource()7823 private int getCollapsedConversationLayoutResource() { 7824 return R.layout.notification_2025_template_collapsed_conversation; 7825 } 7826 getExpandedConversationLayoutResource()7827 private int getExpandedConversationLayoutResource() { 7828 return R.layout.notification_2025_template_expanded_conversation; 7829 } 7830 getCollapsedCallLayoutResource()7831 private int getCollapsedCallLayoutResource() { 7832 if (Flags.notificationsRedesignTemplates()) { 7833 return R.layout.notification_2025_template_collapsed_call; 7834 } else { 7835 return R.layout.notification_template_material_call; 7836 } 7837 } 7838 getExpandedCallLayoutResource()7839 private int getExpandedCallLayoutResource() { 7840 if (Flags.notificationsRedesignTemplates()) { 7841 return R.layout.notification_2025_template_expanded_call; 7842 } else { 7843 return R.layout.notification_template_material_big_call; 7844 } 7845 } 7846 getProgressLayoutResource()7847 private int getProgressLayoutResource() { 7848 if (Flags.notificationsRedesignTemplates()) { 7849 return R.layout.notification_2025_template_expanded_progress; 7850 } else { 7851 return R.layout.notification_template_material_progress; 7852 } 7853 } 7854 getActionLayoutResource()7855 private int getActionLayoutResource() { 7856 return R.layout.notification_material_action; 7857 } 7858 getEmphasizedActionLayoutResource()7859 private int getEmphasizedActionLayoutResource() { 7860 return R.layout.notification_material_action_emphasized; 7861 } 7862 getEmphasizedTombstoneActionLayoutResource()7863 private int getEmphasizedTombstoneActionLayoutResource() { 7864 return R.layout.notification_material_action_emphasized_tombstone; 7865 } 7866 getActionTombstoneLayoutResource()7867 private int getActionTombstoneLayoutResource() { 7868 return R.layout.notification_material_action_tombstone; 7869 } 7870 getBackgroundColor(StandardTemplateParams p)7871 private @ColorInt int getBackgroundColor(StandardTemplateParams p) { 7872 return getColors(p).getBackgroundColor(); 7873 } 7874 textColorsNeedInversion()7875 private boolean textColorsNeedInversion() { 7876 if (mStyle == null || !MediaStyle.class.equals(mStyle.getClass())) { 7877 return false; 7878 } 7879 int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion; 7880 return targetSdkVersion > Build.VERSION_CODES.M 7881 && targetSdkVersion < Build.VERSION_CODES.O; 7882 } 7883 7884 /** 7885 * Get the text that should be displayed in the statusBar when heads upped. This is 7886 * usually just the app name, but may be different depending on the style. 7887 * 7888 * @param publicMode If true, return a text that is safe to display in public. 7889 * 7890 * @hide 7891 */ getHeadsUpStatusBarText(boolean publicMode)7892 public CharSequence getHeadsUpStatusBarText(boolean publicMode) { 7893 if (mStyle != null && !publicMode) { 7894 CharSequence text = mStyle.getHeadsUpStatusBarText(); 7895 if (!TextUtils.isEmpty(text)) { 7896 return text; 7897 } 7898 } 7899 return loadHeaderAppName(); 7900 } 7901 7902 /** 7903 * @return if this builder uses a template 7904 * 7905 * @hide 7906 */ usesTemplate()7907 public boolean usesTemplate() { 7908 return (mN.contentView == null && mN.headsUpContentView == null 7909 && mN.bigContentView == null) 7910 || styleDisplaysCustomViewInline(); 7911 } 7912 } 7913 7914 /** 7915 * Reduces the image sizes to conform to a maximum allowed size. This also processes all custom 7916 * remote views. 7917 * 7918 * @hide 7919 */ reduceImageSizes(Context context)7920 void reduceImageSizes(Context context) { 7921 if (extras.getBoolean(EXTRA_REDUCED_IMAGES)) { 7922 return; 7923 } 7924 boolean isLowRam = ActivityManager.isLowRamDeviceStatic(); 7925 7926 if (mSmallIcon != null 7927 // Only bitmap icons can be downscaled. 7928 && (mSmallIcon.getType() == Icon.TYPE_BITMAP 7929 || mSmallIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) { 7930 Resources resources = context.getResources(); 7931 int maxSize = resources.getDimensionPixelSize( 7932 isLowRam ? R.dimen.notification_small_icon_size_low_ram 7933 : R.dimen.notification_small_icon_size); 7934 mSmallIcon.scaleDownIfNecessary(maxSize, maxSize); 7935 } 7936 7937 if (mLargeIcon != null || largeIcon != null) { 7938 Resources resources = context.getResources(); 7939 int maxSize = resources.getDimensionPixelSize(isLowRam 7940 ? R.dimen.notification_right_icon_size_low_ram 7941 : R.dimen.notification_right_icon_size); 7942 if (mLargeIcon != null) { 7943 mLargeIcon.scaleDownIfNecessary(maxSize, maxSize); 7944 } 7945 if (largeIcon != null) { 7946 largeIcon = Icon.scaleDownIfNecessary(largeIcon, maxSize, maxSize); 7947 } 7948 } 7949 reduceImageSizesForRemoteView(contentView, context, isLowRam); 7950 reduceImageSizesForRemoteView(headsUpContentView, context, isLowRam); 7951 reduceImageSizesForRemoteView(bigContentView, context, isLowRam); 7952 extras.putBoolean(EXTRA_REDUCED_IMAGES, true); 7953 } 7954 reduceImageSizesForRemoteView(RemoteViews remoteView, Context context, boolean isLowRam)7955 private void reduceImageSizesForRemoteView(RemoteViews remoteView, Context context, 7956 boolean isLowRam) { 7957 if (remoteView != null) { 7958 Resources resources = context.getResources(); 7959 int maxWidth = resources.getDimensionPixelSize(isLowRam 7960 ? R.dimen.notification_custom_view_max_image_width_low_ram 7961 : R.dimen.notification_custom_view_max_image_width); 7962 int maxHeight = resources.getDimensionPixelSize(isLowRam 7963 ? R.dimen.notification_custom_view_max_image_height_low_ram 7964 : R.dimen.notification_custom_view_max_image_height); 7965 remoteView.reduceImageSizes(maxWidth, maxHeight); 7966 } 7967 } 7968 7969 /** 7970 * @return whether this notification is a foreground service notification 7971 * @hide 7972 */ isForegroundService()7973 public boolean isForegroundService() { 7974 return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; 7975 } 7976 7977 /** 7978 * @return whether this notification is associated with a user initiated job 7979 * @hide 7980 */ 7981 @TestApi isUserInitiatedJob()7982 public boolean isUserInitiatedJob() { 7983 return (flags & Notification.FLAG_USER_INITIATED_JOB) != 0; 7984 } 7985 7986 /** 7987 * @return whether this notification is associated with either a foreground service or 7988 * a user initiated job 7989 * @hide 7990 */ isFgsOrUij()7991 public boolean isFgsOrUij() { 7992 return isForegroundService() || isUserInitiatedJob(); 7993 } 7994 7995 /** 7996 * Describe whether this notification's content such that it should always display 7997 * immediately when tied to a foreground service, even if the system might generally 7998 * avoid showing the notifications for short-lived foreground service lifetimes. 7999 * 8000 * Immediate visibility of the Notification is indicated when: 8001 * <ul> 8002 * <li>The app specifically indicated it with 8003 * {@link Notification.Builder#setForegroundServiceBehavior(int) 8004 * setForegroundServiceBehavior(BEHAVIOR_IMMEDIATE_DISPLAY)}</li> 8005 * <li>It is a media notification or has an associated media session</li> 8006 * <li>It is a call or navigation notification</li> 8007 * <li>It provides additional action affordances</li> 8008 * </ul> 8009 * 8010 * If the app has specified 8011 * {@code NotificationBuilder.setForegroundServiceBehavior(BEHAVIOR_DEFERRED_DISPLAY)} 8012 * then this method will return {@code false} and notification visibility will be 8013 * deferred following the service's transition to the foreground state even in the 8014 * circumstances described above. 8015 * 8016 * @return whether this notification should be displayed immediately when 8017 * its associated service transitions to the foreground state 8018 * @hide 8019 */ 8020 @TestApi shouldShowForegroundImmediately()8021 public boolean shouldShowForegroundImmediately() { 8022 // Has the app demanded immediate display? 8023 if (mFgsDeferBehavior == FOREGROUND_SERVICE_IMMEDIATE) { 8024 return true; 8025 } 8026 8027 // Has the app demanded deferred display? 8028 if (mFgsDeferBehavior == FOREGROUND_SERVICE_DEFERRED) { 8029 return false; 8030 } 8031 8032 // We show these sorts of notifications immediately in the absence of 8033 // any explicit app declaration 8034 if (isMediaNotification() 8035 || CATEGORY_CALL.equals(category) 8036 || CATEGORY_NAVIGATION.equals(category) 8037 || (actions != null && actions.length > 0)) { 8038 return true; 8039 } 8040 8041 // No extenuating circumstances: defer visibility 8042 return false; 8043 } 8044 8045 /** 8046 * Has forced deferral for FGS purposes been specified? 8047 * @hide 8048 */ isForegroundDisplayForceDeferred()8049 public boolean isForegroundDisplayForceDeferred() { 8050 return FOREGROUND_SERVICE_DEFERRED == mFgsDeferBehavior; 8051 } 8052 8053 /** 8054 * @return the style class of this notification 8055 * @hide 8056 */ getNotificationStyle()8057 public Class<? extends Notification.Style> getNotificationStyle() { 8058 String templateClass = extras.getString(Notification.EXTRA_TEMPLATE); 8059 8060 if (!TextUtils.isEmpty(templateClass)) { 8061 return Notification.getNotificationStyleClass(templateClass); 8062 } 8063 return null; 8064 } 8065 8066 /** 8067 * @return whether the style of this notification is the one provided 8068 * @hide 8069 */ isStyle(@onNull Class<? extends Style> styleClass)8070 public boolean isStyle(@NonNull Class<? extends Style> styleClass) { 8071 String templateClass = extras.getString(Notification.EXTRA_TEMPLATE); 8072 return Objects.equals(templateClass, styleClass.getName()); 8073 } 8074 8075 /** 8076 * @return true if this notification is colorized *for the purposes of ranking*. If the 8077 * {@link #color} is {@link #COLOR_DEFAULT} this will be true, even though the actual 8078 * appearance of the notification may not be "colorized". 8079 * 8080 * @hide 8081 */ isColorized()8082 public boolean isColorized() { 8083 return isColorizedRequested() 8084 && (hasColorizedPermission() || isFgsOrUij() || isPromotedOngoing()); 8085 } 8086 8087 /** 8088 * @return true if this notification has requested to be colorized, regardless of whether it 8089 * meets the requirements to be displayed that way. 8090 */ isColorizedRequested()8091 private boolean isColorizedRequested() { 8092 return extras.getBoolean(EXTRA_COLORIZED); 8093 } 8094 8095 /** 8096 * Returns whether an app can colorize due to the android.permission.USE_COLORIZED_NOTIFICATIONS 8097 * permission. The permission is checked when a notification is enqueued. 8098 * 8099 * @hide 8100 */ hasColorizedPermission()8101 public boolean hasColorizedPermission() { 8102 return (flags & Notification.FLAG_CAN_COLORIZE) != 0; 8103 } 8104 8105 /** 8106 * Returns whether this notification is a promoted ongoing notification. 8107 * 8108 * This requires the Notification.FLAG_PROMOTED_ONGOING flag to be set 8109 * (which may be true once the api_rich_ongoing feature flag is enabled), 8110 * and requires that the ui_rich_ongoing feature flag is enabled. 8111 * 8112 * @hide 8113 */ isPromotedOngoing()8114 public boolean isPromotedOngoing() { 8115 return Flags.uiRichOngoing() && (flags & Notification.FLAG_PROMOTED_ONGOING) != 0; 8116 } 8117 8118 /** 8119 * @return true if this is a media style notification with a media session 8120 * 8121 * @hide 8122 */ isMediaNotification()8123 public boolean isMediaNotification() { 8124 Class<? extends Style> style = getNotificationStyle(); 8125 boolean isMediaStyle = (MediaStyle.class.equals(style) 8126 || DecoratedMediaCustomViewStyle.class.equals(style)); 8127 8128 boolean hasMediaSession = extras.getParcelable(Notification.EXTRA_MEDIA_SESSION, 8129 MediaSession.Token.class) != null; 8130 8131 return isMediaStyle && hasMediaSession; 8132 } 8133 8134 /** 8135 * @return true for custom notifications, including notifications 8136 * with DecoratedCustomViewStyle or DecoratedMediaCustomViewStyle, 8137 * and other notifications with user-provided custom views. 8138 * 8139 * @hide 8140 */ isCustomNotification()8141 public Boolean isCustomNotification() { 8142 if (contentView == null 8143 && bigContentView == null 8144 && headsUpContentView == null) { 8145 return false; 8146 } 8147 return true; 8148 } 8149 8150 /** 8151 * @return true if this notification is showing as a bubble 8152 * 8153 * @hide 8154 */ isBubbleNotification()8155 public boolean isBubbleNotification() { 8156 return (flags & Notification.FLAG_BUBBLE) != 0; 8157 } 8158 hasLargeIcon()8159 private boolean hasLargeIcon() { 8160 return mLargeIcon != null || largeIcon != null; 8161 } 8162 8163 /** 8164 * Returns #when, unless it's set to 0, which should be shown as/treated as a 'current' 8165 * notification. 0 is treated as a special value because it was special in an old version of 8166 * android, and some apps are still (incorrectly) using it. 8167 * 8168 * @hide 8169 */ getWhen()8170 public long getWhen() { 8171 if (Flags.sortSectionByTime()) { 8172 if (when == 0) { 8173 return creationTime; 8174 } 8175 } 8176 return when; 8177 } 8178 8179 /** 8180 * @return true if the notification will show the time; false otherwise 8181 * @hide 8182 */ showsTime()8183 public boolean showsTime() { 8184 if (Flags.sortSectionByTime()) { 8185 return extras.getBoolean(EXTRA_SHOW_WHEN); 8186 } 8187 return when != 0 && extras.getBoolean(EXTRA_SHOW_WHEN); 8188 } 8189 8190 /** 8191 * @return true if the notification will show a chronometer; false otherwise 8192 * @hide 8193 */ showsChronometer()8194 public boolean showsChronometer() { 8195 if (Flags.sortSectionByTime()) { 8196 return extras.getBoolean(EXTRA_SHOW_CHRONOMETER); 8197 } 8198 return when != 0 && extras.getBoolean(EXTRA_SHOW_CHRONOMETER); 8199 } 8200 8201 /** 8202 * @return true if the notification has image 8203 */ hasImage()8204 public boolean hasImage() { 8205 if (isStyle(MessagingStyle.class) && extras != null) { 8206 final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, 8207 Parcelable.class); 8208 if (!ArrayUtils.isEmpty(messages)) { 8209 for (MessagingStyle.Message m : MessagingStyle.Message 8210 .getMessagesFromBundleArray(messages)) { 8211 if (m.getDataUri() != null 8212 && m.getDataMimeType() != null 8213 && m.getDataMimeType().startsWith("image/")) { 8214 return true; 8215 } 8216 } 8217 } 8218 } else if (hasLargeIcon()) { 8219 return true; 8220 } else if (extras.containsKey(EXTRA_BACKGROUND_IMAGE_URI)) { 8221 return true; 8222 } 8223 return false; 8224 } 8225 8226 8227 /** 8228 * @removed 8229 */ 8230 @SystemApi getNotificationStyleClass(String templateClass)8231 public static Class<? extends Style> getNotificationStyleClass(String templateClass) { 8232 for (Class<? extends Style> innerClass : PLATFORM_STYLE_CLASSES) { 8233 if (templateClass.equals(innerClass.getName())) { 8234 return innerClass; 8235 } 8236 } 8237 8238 if (Flags.apiRichOngoing()) { 8239 if (templateClass.equals(ProgressStyle.class.getName())) { 8240 return ProgressStyle.class; 8241 } 8242 } 8243 return null; 8244 } 8245 buildCustomContentIntoTemplate(@onNull Context context, @NonNull RemoteViews template, @Nullable RemoteViews customContent, @NonNull StandardTemplateParams p, @NonNull TemplateBindResult result)8246 private static void buildCustomContentIntoTemplate(@NonNull Context context, 8247 @NonNull RemoteViews template, @Nullable RemoteViews customContent, 8248 @NonNull StandardTemplateParams p, @NonNull TemplateBindResult result) { 8249 int childIndex = -1; 8250 if (customContent != null) { 8251 // Need to clone customContent before adding, because otherwise it can no longer be 8252 // parceled independently of remoteViews. 8253 customContent = customContent.clone(); 8254 if (p.mHeaderless) { 8255 template.removeFromParent(R.id.notification_top_line); 8256 // We do not know how many lines ar emote view has, so we presume it has 2; this 8257 // ensures that we don't under-pad the content, which could lead to abuse, at the 8258 // cost of making single-line custom content over-padded. 8259 Builder.setHeaderlessVerticalMargins(template, p, true /* hasSecondLine */); 8260 } else { 8261 // also update the end margin to account for the large icon or expander 8262 Resources resources = context.getResources(); 8263 result.mTitleMarginSet.applyToView(template, R.id.notification_main_column, 8264 resources.getDimension(R.dimen.notification_content_margin_end) 8265 / resources.getDisplayMetrics().density); 8266 } 8267 template.removeAllViewsExceptId(R.id.notification_main_column, R.id.progress); 8268 template.addView(R.id.notification_main_column, customContent, 0 /* index */); 8269 template.addFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED); 8270 childIndex = 0; 8271 } 8272 template.setIntTag(R.id.notification_main_column, 8273 com.android.internal.R.id.notification_custom_view_index_tag, 8274 childIndex); 8275 } 8276 8277 /** 8278 * An object that can apply a rich notification style to a {@link Notification.Builder} 8279 * object. 8280 */ 8281 public static abstract class Style { 8282 8283 /** 8284 * @deprecated public access to the constructor of Style() is only useful for creating 8285 * custom subclasses, but that has actually been impossible due to hidden abstract 8286 * methods, so this constructor is now officially deprecated to clarify that this is 8287 * intended to be disallowed. 8288 */ 8289 @Deprecated Style()8290 public Style() {} 8291 8292 /** 8293 * The number of items allowed simulatanously in the remote input history. 8294 * @hide 8295 */ 8296 static final int MAX_REMOTE_INPUT_HISTORY_LINES = 3; 8297 private CharSequence mBigContentTitle; 8298 8299 /** 8300 * @hide 8301 */ 8302 protected CharSequence mSummaryText = null; 8303 8304 /** 8305 * @hide 8306 */ 8307 protected boolean mSummaryTextSet = false; 8308 8309 protected Builder mBuilder; 8310 8311 /** 8312 * Overrides ContentTitle in the expanded form of the template. 8313 * This defaults to the value passed to setContentTitle(). 8314 */ internalSetBigContentTitle(CharSequence title)8315 protected void internalSetBigContentTitle(CharSequence title) { 8316 mBigContentTitle = title; 8317 } 8318 8319 /** 8320 * Set the first line of text after the detail section in the expanded form of the template. 8321 */ internalSetSummaryText(CharSequence cs)8322 protected void internalSetSummaryText(CharSequence cs) { 8323 mSummaryText = cs; 8324 mSummaryTextSet = true; 8325 } 8326 setBuilder(Builder builder)8327 public void setBuilder(Builder builder) { 8328 if (mBuilder != builder) { 8329 mBuilder = builder; 8330 if (mBuilder != null) { 8331 mBuilder.setStyle(this); 8332 } 8333 } 8334 } 8335 checkBuilder()8336 protected void checkBuilder() { 8337 if (mBuilder == null) { 8338 throw new IllegalArgumentException("Style requires a valid Builder object"); 8339 } 8340 } 8341 getStandardView(int layoutId)8342 protected RemoteViews getStandardView(int layoutId) { 8343 // TODO(jeffdq): set the view type based on the layout resource? 8344 StandardTemplateParams p = mBuilder.mParams.reset() 8345 .viewType(StandardTemplateParams.VIEW_TYPE_UNSPECIFIED) 8346 .fillTextsFrom(mBuilder); 8347 return getStandardView(layoutId, p, null); 8348 } 8349 8350 8351 /** 8352 * Get the standard view for this style. 8353 * 8354 * @param layoutId The layout id to use. 8355 * @param p the params for this inflation. 8356 * @param result The result where template bind information is saved. 8357 * @return A remoteView for this style. 8358 * @hide 8359 */ getStandardView(int layoutId, StandardTemplateParams p, TemplateBindResult result)8360 protected RemoteViews getStandardView(int layoutId, StandardTemplateParams p, 8361 TemplateBindResult result) { 8362 checkBuilder(); 8363 8364 if (mBigContentTitle != null) { 8365 p.mTitle = mBigContentTitle; 8366 } 8367 8368 return mBuilder.applyStandardTemplateWithActions(layoutId, p, result); 8369 } 8370 8371 /** 8372 * Construct a Style-specific RemoteViews for the collapsed notification layout. 8373 * The default implementation has nothing additional to add. 8374 * 8375 * @hide 8376 */ makeContentView()8377 public RemoteViews makeContentView() { 8378 return null; 8379 } 8380 8381 /** 8382 * Construct a Style-specific RemoteViews for the final expanded notification layout. 8383 * @hide 8384 */ makeExpandedContentView()8385 public RemoteViews makeExpandedContentView() { 8386 return null; 8387 } 8388 8389 /** 8390 * Construct a Style-specific RemoteViews for the final HUN layout. 8391 * 8392 * @hide 8393 */ makeHeadsUpContentView()8394 public RemoteViews makeHeadsUpContentView() { 8395 return null; 8396 } 8397 8398 /** 8399 * Construct a Style-specific RemoteViews for the final compact HUN layout. 8400 * return null to use the standard compact heads up view. 8401 * @hide 8402 */ 8403 @Nullable makeCompactHeadsUpContentView()8404 public RemoteViews makeCompactHeadsUpContentView() { 8405 return null; 8406 } 8407 8408 /** 8409 * Apply any style-specific extras to this notification before shipping it out. 8410 * @hide 8411 */ addExtras(Bundle extras)8412 public void addExtras(Bundle extras) { 8413 if (mSummaryTextSet) { 8414 extras.putCharSequence(EXTRA_SUMMARY_TEXT, mSummaryText); 8415 } 8416 if (mBigContentTitle != null) { 8417 extras.putCharSequence(EXTRA_TITLE_BIG, mBigContentTitle); 8418 } 8419 extras.putString(EXTRA_TEMPLATE, this.getClass().getName()); 8420 } 8421 8422 /** 8423 * Reconstruct the internal state of this Style object from extras. 8424 * @hide 8425 */ restoreFromExtras(Bundle extras)8426 protected void restoreFromExtras(Bundle extras) { 8427 if (extras.containsKey(EXTRA_SUMMARY_TEXT)) { 8428 mSummaryText = extras.getCharSequence(EXTRA_SUMMARY_TEXT); 8429 mSummaryTextSet = true; 8430 } 8431 if (extras.containsKey(EXTRA_TITLE_BIG)) { 8432 mBigContentTitle = extras.getCharSequence(EXTRA_TITLE_BIG); 8433 } 8434 } 8435 8436 8437 /** 8438 * @hide 8439 */ buildStyled(Notification wip)8440 public Notification buildStyled(Notification wip) { 8441 addExtras(wip.extras); 8442 return wip; 8443 } 8444 8445 /** 8446 * @hide 8447 */ purgeResources()8448 public void purgeResources() {} 8449 8450 /** 8451 * Calls {@link android.app.Notification.Builder#build()} on the Builder this Style is 8452 * attached to. 8453 * <p> 8454 * Note: Calling build() multiple times returns the same Notification instance, 8455 * so reusing a builder to create multiple Notifications is discouraged. 8456 * 8457 * @return the fully constructed Notification. 8458 */ build()8459 public Notification build() { 8460 checkBuilder(); 8461 return mBuilder.build(); 8462 } 8463 8464 /** 8465 * @hide 8466 * @return Whether we should put the summary be put into the notification header 8467 */ hasSummaryInHeader()8468 public boolean hasSummaryInHeader() { 8469 return true; 8470 } 8471 8472 /** 8473 * @hide 8474 * @return Whether custom content views are displayed inline in the style 8475 */ displayCustomViewInline()8476 public boolean displayCustomViewInline() { 8477 return false; 8478 } 8479 8480 /** 8481 * Reduces the image sizes contained in this style. 8482 * 8483 * @hide 8484 */ reduceImageSizes(Context context)8485 public void reduceImageSizes(Context context) { 8486 } 8487 8488 /** 8489 * Validate that this style was properly composed. This is called at build time. 8490 * @hide 8491 */ validate(Context context)8492 public void validate(Context context) { 8493 } 8494 8495 /** 8496 * @hide 8497 */ 8498 @SuppressWarnings("HiddenAbstractMethod") areNotificationsVisiblyDifferent(Style other)8499 public abstract boolean areNotificationsVisiblyDifferent(Style other); 8500 8501 /** 8502 * @return the text that should be displayed in the statusBar when heads-upped. 8503 * If {@code null} is returned, the default implementation will be used. 8504 * 8505 * @hide 8506 */ getHeadsUpStatusBarText()8507 public CharSequence getHeadsUpStatusBarText() { 8508 return null; 8509 } 8510 } 8511 8512 /** 8513 * Helper class for generating large-format notifications that include a large image attachment. 8514 * 8515 * Here's how you'd set the <code>BigPictureStyle</code> on a notification: 8516 * <pre class="prettyprint"> 8517 * Notification notif = new Notification.Builder(mContext) 8518 * .setContentTitle("New photo from " + sender.toString()) 8519 * .setContentText(subject) 8520 * .setSmallIcon(R.drawable.new_post) 8521 * .setLargeIcon(aBitmap) 8522 * .setStyle(new Notification.BigPictureStyle() 8523 * .bigPicture(aBigBitmap)) 8524 * .build(); 8525 * </pre> 8526 * 8527 * @see Notification#bigContentView 8528 */ 8529 public static class BigPictureStyle extends Style { 8530 private Icon mPictureIcon; 8531 private Icon mBigLargeIcon; 8532 private boolean mBigLargeIconSet = false; 8533 private CharSequence mPictureContentDescription; 8534 private boolean mShowBigPictureWhenCollapsed; 8535 BigPictureStyle()8536 public BigPictureStyle() { 8537 } 8538 8539 /** 8540 * @deprecated use {@code BigPictureStyle()}. 8541 */ 8542 @Deprecated BigPictureStyle(Builder builder)8543 public BigPictureStyle(Builder builder) { 8544 setBuilder(builder); 8545 } 8546 8547 /** 8548 * Overrides ContentTitle in the expanded form of the template. 8549 * This defaults to the value passed to setContentTitle(). 8550 */ 8551 @NonNull setBigContentTitle(@ullable CharSequence title)8552 public BigPictureStyle setBigContentTitle(@Nullable CharSequence title) { 8553 internalSetBigContentTitle(safeCharSequence(title)); 8554 return this; 8555 } 8556 8557 /** 8558 * Set the first line of text after the detail section in the expanded form of the template. 8559 */ 8560 @NonNull setSummaryText(@ullable CharSequence cs)8561 public BigPictureStyle setSummaryText(@Nullable CharSequence cs) { 8562 internalSetSummaryText(safeCharSequence(cs)); 8563 return this; 8564 } 8565 8566 /** 8567 * Set the content description of the big picture. 8568 */ 8569 @NonNull setContentDescription( @ullable CharSequence contentDescription)8570 public BigPictureStyle setContentDescription( 8571 @Nullable CharSequence contentDescription) { 8572 mPictureContentDescription = contentDescription; 8573 return this; 8574 } 8575 8576 /** 8577 * @hide 8578 */ 8579 @Nullable getBigPicture()8580 public Icon getBigPicture() { 8581 if (mPictureIcon != null) { 8582 return mPictureIcon; 8583 } 8584 return null; 8585 } 8586 8587 /** 8588 * Provide the bitmap to be used as the payload for the BigPicture notification. 8589 */ 8590 @NonNull bigPicture(@ullable Bitmap b)8591 public BigPictureStyle bigPicture(@Nullable Bitmap b) { 8592 mPictureIcon = b == null ? null : Icon.createWithBitmap(b); 8593 return this; 8594 } 8595 8596 /** 8597 * Provide the content Uri to be used as the payload for the BigPicture notification. 8598 */ 8599 @NonNull bigPicture(@ullable Icon icon)8600 public BigPictureStyle bigPicture(@Nullable Icon icon) { 8601 mPictureIcon = icon; 8602 return this; 8603 } 8604 8605 /** 8606 * When set, the {@link #bigPicture(Bitmap) big picture} of this style will be promoted and 8607 * shown in place of the {@link Builder#setLargeIcon(Icon) large icon} in the collapsed 8608 * state of this notification. 8609 */ 8610 @NonNull showBigPictureWhenCollapsed(boolean show)8611 public BigPictureStyle showBigPictureWhenCollapsed(boolean show) { 8612 mShowBigPictureWhenCollapsed = show; 8613 return this; 8614 } 8615 8616 /** 8617 * Override the large icon when the expanded notification is shown. 8618 */ 8619 @NonNull bigLargeIcon(@ullable Bitmap b)8620 public BigPictureStyle bigLargeIcon(@Nullable Bitmap b) { 8621 return bigLargeIcon(b != null ? Icon.createWithBitmap(b) : null); 8622 } 8623 8624 /** 8625 * Override the large icon when the expanded notification is shown. 8626 */ 8627 @NonNull bigLargeIcon(@ullable Icon icon)8628 public BigPictureStyle bigLargeIcon(@Nullable Icon icon) { 8629 mBigLargeIconSet = true; 8630 mBigLargeIcon = icon; 8631 return this; 8632 } 8633 8634 /** @hide */ 8635 public static final int MIN_ASHMEM_BITMAP_SIZE = 128 * (1 << 10); 8636 8637 /** 8638 * @hide 8639 */ 8640 @Override purgeResources()8641 public void purgeResources() { 8642 super.purgeResources(); 8643 if (mPictureIcon != null) { 8644 mPictureIcon.convertToAshmem(); 8645 } 8646 if (mBigLargeIcon != null) { 8647 mBigLargeIcon.convertToAshmem(); 8648 } 8649 } 8650 8651 /** 8652 * @hide 8653 */ 8654 @Override reduceImageSizes(Context context)8655 public void reduceImageSizes(Context context) { 8656 super.reduceImageSizes(context); 8657 Resources resources = context.getResources(); 8658 boolean isLowRam = ActivityManager.isLowRamDeviceStatic(); 8659 if (mPictureIcon != null) { 8660 int maxPictureHeight = resources.getDimensionPixelSize(isLowRam 8661 ? R.dimen.notification_big_picture_max_height_low_ram 8662 : R.dimen.notification_big_picture_max_height); 8663 int maxPictureWidth = resources.getDimensionPixelSize(isLowRam 8664 ? R.dimen.notification_big_picture_max_width_low_ram 8665 : R.dimen.notification_big_picture_max_width); 8666 mPictureIcon.scaleDownIfNecessary(maxPictureWidth, maxPictureHeight); 8667 } 8668 if (mBigLargeIcon != null) { 8669 int rightIconSize = resources.getDimensionPixelSize(isLowRam 8670 ? R.dimen.notification_right_icon_size_low_ram 8671 : R.dimen.notification_right_icon_size); 8672 mBigLargeIcon.scaleDownIfNecessary(rightIconSize, rightIconSize); 8673 } 8674 } 8675 8676 /** 8677 * @hide 8678 */ 8679 @Override makeContentView()8680 public RemoteViews makeContentView() { 8681 if (mPictureIcon == null || !mShowBigPictureWhenCollapsed) { 8682 return super.makeContentView(); 8683 } 8684 8685 StandardTemplateParams p = mBuilder.mParams.reset() 8686 .viewType(StandardTemplateParams.VIEW_TYPE_NORMAL) 8687 .fillTextsFrom(mBuilder) 8688 .promotedPicture(mPictureIcon); 8689 return getStandardView(mBuilder.getCollapsedBaseLayoutResource(), p, null /* result */); 8690 } 8691 8692 /** 8693 * @hide 8694 */ 8695 @Override makeHeadsUpContentView()8696 public RemoteViews makeHeadsUpContentView() { 8697 if (mPictureIcon == null || !mShowBigPictureWhenCollapsed) { 8698 return super.makeHeadsUpContentView(); 8699 } 8700 8701 StandardTemplateParams p = mBuilder.mParams.reset() 8702 .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) 8703 .fillTextsFrom(mBuilder) 8704 .promotedPicture(mPictureIcon); 8705 return getStandardView(mBuilder.getHeadsUpBaseLayoutResource(), p, null /* result */); 8706 } 8707 8708 /** 8709 * @hide 8710 */ makeExpandedContentView()8711 public RemoteViews makeExpandedContentView() { 8712 // Replace mN.mLargeIcon with mBigLargeIcon if mBigLargeIconSet 8713 // This covers the following cases: 8714 // 1. mBigLargeIconSet -> mBigLargeIcon (null or non-null) applies, overrides 8715 // mN.mLargeIcon 8716 // 2. !mBigLargeIconSet -> mN.mLargeIcon applies 8717 Icon oldLargeIcon = null; 8718 Bitmap largeIconLegacy = null; 8719 if (mBigLargeIconSet) { 8720 oldLargeIcon = mBuilder.mN.mLargeIcon; 8721 mBuilder.mN.mLargeIcon = mBigLargeIcon; 8722 // The legacy largeIcon might not allow us to clear the image, as it's taken in 8723 // replacement if the other one is null. Because we're restoring these legacy icons 8724 // for old listeners, this is in general non-null. 8725 largeIconLegacy = mBuilder.mN.largeIcon; 8726 mBuilder.mN.largeIcon = null; 8727 } 8728 8729 StandardTemplateParams p = mBuilder.mParams.reset() 8730 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED).fillTextsFrom(mBuilder); 8731 RemoteViews contentView = getStandardView(mBuilder.getBigPictureLayoutResource(), 8732 p, null /* result */); 8733 if (mSummaryTextSet) { 8734 contentView.setTextViewText(R.id.text, 8735 mBuilder.ensureColorSpanContrastOrStripStyling( 8736 mBuilder.processLegacyText(mSummaryText), p)); 8737 mBuilder.setTextViewColorSecondary(contentView, R.id.text, p); 8738 contentView.setViewVisibility(R.id.text, View.VISIBLE); 8739 } 8740 8741 if (mBigLargeIconSet) { 8742 mBuilder.mN.mLargeIcon = oldLargeIcon; 8743 mBuilder.mN.largeIcon = largeIconLegacy; 8744 } 8745 8746 contentView.setImageViewIcon(R.id.big_picture, mPictureIcon); 8747 8748 if (mPictureContentDescription != null) { 8749 contentView.setContentDescription(R.id.big_picture, mPictureContentDescription); 8750 } 8751 8752 return contentView; 8753 } 8754 8755 /** 8756 * @hide 8757 */ addExtras(Bundle extras)8758 public void addExtras(Bundle extras) { 8759 super.addExtras(extras); 8760 8761 if (mBigLargeIconSet) { 8762 extras.putParcelable(EXTRA_LARGE_ICON_BIG, mBigLargeIcon); 8763 } 8764 if (mPictureContentDescription != null) { 8765 extras.putCharSequence(EXTRA_PICTURE_CONTENT_DESCRIPTION, 8766 mPictureContentDescription); 8767 } 8768 extras.putBoolean(EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED, mShowBigPictureWhenCollapsed); 8769 8770 if (mPictureIcon == null) { 8771 extras.remove(EXTRA_PICTURE_ICON); 8772 extras.remove(EXTRA_PICTURE); 8773 } else if (mPictureIcon.getType() == Icon.TYPE_BITMAP) { 8774 // If the icon contains a bitmap, use the old extra so that listeners which look 8775 // for that extra can still find the picture. Don't include the new extra in 8776 // that case, to avoid duplicating data. Leave the unused extra set to null to avoid 8777 // crashing apps that came to expect it to be present but null. 8778 extras.putParcelable(EXTRA_PICTURE, mPictureIcon.getBitmap()); 8779 extras.putParcelable(EXTRA_PICTURE_ICON, null); 8780 } else { 8781 extras.putParcelable(EXTRA_PICTURE, null); 8782 extras.putParcelable(EXTRA_PICTURE_ICON, mPictureIcon); 8783 } 8784 } 8785 8786 /** 8787 * @hide 8788 */ 8789 @Override restoreFromExtras(Bundle extras)8790 protected void restoreFromExtras(Bundle extras) { 8791 super.restoreFromExtras(extras); 8792 8793 if (extras.containsKey(EXTRA_LARGE_ICON_BIG)) { 8794 mBigLargeIconSet = true; 8795 mBigLargeIcon = extras.getParcelable(EXTRA_LARGE_ICON_BIG, Icon.class); 8796 } 8797 8798 if (extras.containsKey(EXTRA_PICTURE_CONTENT_DESCRIPTION)) { 8799 mPictureContentDescription = 8800 extras.getCharSequence(EXTRA_PICTURE_CONTENT_DESCRIPTION); 8801 } 8802 8803 mShowBigPictureWhenCollapsed = extras.getBoolean(EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED); 8804 8805 mPictureIcon = getPictureIcon(extras); 8806 } 8807 8808 /** @hide */ 8809 @Nullable getPictureIcon(@ullable Bundle extras)8810 public static Icon getPictureIcon(@Nullable Bundle extras) { 8811 if (extras == null) return null; 8812 // When this style adds a picture, we only add one of the keys. If both were added, 8813 // it would most likely be a legacy app trying to override the picture in some way. 8814 // Because of that case it's better to give precedence to the legacy field. 8815 Bitmap bitmapPicture = extras.getParcelable(EXTRA_PICTURE, Bitmap.class); 8816 if (bitmapPicture != null) { 8817 return Icon.createWithBitmap(bitmapPicture); 8818 } else { 8819 return extras.getParcelable(EXTRA_PICTURE_ICON, Icon.class); 8820 } 8821 } 8822 8823 /** 8824 * @hide 8825 */ 8826 @Override hasSummaryInHeader()8827 public boolean hasSummaryInHeader() { 8828 return false; 8829 } 8830 8831 /** 8832 * @hide 8833 */ 8834 @Override areNotificationsVisiblyDifferent(Style other)8835 public boolean areNotificationsVisiblyDifferent(Style other) { 8836 if (other == null || getClass() != other.getClass()) { 8837 return true; 8838 } 8839 BigPictureStyle otherS = (BigPictureStyle) other; 8840 return areIconsMaybeDifferent(getBigPicture(), otherS.getBigPicture()); 8841 } 8842 } 8843 8844 /** 8845 * Helper class for generating large-format notifications that include a lot of text. 8846 * 8847 * Here's how you'd set the <code>BigTextStyle</code> on a notification: 8848 * <pre class="prettyprint"> 8849 * Notification notif = new Notification.Builder(mContext) 8850 * .setContentTitle("New mail from " + sender.toString()) 8851 * .setContentText(subject) 8852 * .setSmallIcon(R.drawable.new_mail) 8853 * .setLargeIcon(aBitmap) 8854 * .setStyle(new Notification.BigTextStyle() 8855 * .bigText(aVeryLongString)) 8856 * .build(); 8857 * </pre> 8858 * 8859 * @see Notification#bigContentView 8860 */ 8861 public static class BigTextStyle extends Style { 8862 8863 private CharSequence mBigText; 8864 BigTextStyle()8865 public BigTextStyle() { 8866 } 8867 8868 /** 8869 * @deprecated use {@code BigTextStyle()}. 8870 */ 8871 @Deprecated BigTextStyle(Builder builder)8872 public BigTextStyle(Builder builder) { 8873 setBuilder(builder); 8874 } 8875 8876 /** 8877 * Overrides ContentTitle in the expanded form of the template. 8878 * This defaults to the value passed to setContentTitle(). 8879 */ setBigContentTitle(CharSequence title)8880 public BigTextStyle setBigContentTitle(CharSequence title) { 8881 internalSetBigContentTitle(safeCharSequence(title)); 8882 return this; 8883 } 8884 8885 /** 8886 * Set the first line of text after the detail section in the expanded form of the template. 8887 */ setSummaryText(CharSequence cs)8888 public BigTextStyle setSummaryText(CharSequence cs) { 8889 internalSetSummaryText(safeCharSequence(cs)); 8890 return this; 8891 } 8892 8893 /** 8894 * Provide the longer text to be displayed in the expanded form of the 8895 * template in place of the content text. 8896 */ bigText(CharSequence cs)8897 public BigTextStyle bigText(CharSequence cs) { 8898 mBigText = safeCharSequence(cs); 8899 return this; 8900 } 8901 8902 /** 8903 * @hide 8904 */ getBigText()8905 public CharSequence getBigText() { 8906 return mBigText; 8907 } 8908 8909 /** 8910 * @hide 8911 */ addExtras(Bundle extras)8912 public void addExtras(Bundle extras) { 8913 super.addExtras(extras); 8914 8915 extras.putCharSequence(EXTRA_BIG_TEXT, mBigText); 8916 } 8917 8918 /** 8919 * @hide 8920 */ 8921 @Override restoreFromExtras(Bundle extras)8922 protected void restoreFromExtras(Bundle extras) { 8923 super.restoreFromExtras(extras); 8924 8925 mBigText = extras.getCharSequence(EXTRA_BIG_TEXT); 8926 } 8927 8928 /** 8929 * @hide 8930 */ makeExpandedContentView()8931 public RemoteViews makeExpandedContentView() { 8932 StandardTemplateParams p = mBuilder.mParams.reset() 8933 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED) 8934 .allowTextWithProgress(true) 8935 .textViewId(R.id.big_text) 8936 .fillTextsFrom(mBuilder); 8937 8938 // Replace the text with the big text, but only if the big text is not empty. 8939 CharSequence bigTextText = mBuilder.processLegacyText(mBigText); 8940 // Ongoing promoted notifications are allowed to have styling. 8941 final boolean isPromotedOngoing = mBuilder.mN.isPromotedOngoing(); 8942 if (!isPromotedOngoing && Flags.cleanUpSpansAndNewLines()) { 8943 bigTextText = normalizeBigText(stripStyling(bigTextText)); 8944 } 8945 if (!TextUtils.isEmpty(bigTextText)) { 8946 p.text(bigTextText); 8947 } 8948 8949 return getStandardView(mBuilder.getBigTextLayoutResource(), p, null /* result */); 8950 } 8951 8952 /** 8953 * @hide 8954 * Spans are ignored when comparing text for visual difference. 8955 */ 8956 @Override areNotificationsVisiblyDifferent(Style other)8957 public boolean areNotificationsVisiblyDifferent(Style other) { 8958 if (other == null || getClass() != other.getClass()) { 8959 return true; 8960 } 8961 BigTextStyle newS = (BigTextStyle) other; 8962 return !Objects.equals(String.valueOf(getBigText()), String.valueOf(newS.getBigText())); 8963 } 8964 8965 } 8966 8967 /** 8968 * Helper class for generating large-format notifications that include multiple back-and-forth 8969 * messages of varying types between any number of people. 8970 * 8971 * <p> 8972 * If the platform does not provide large-format notifications, this method has no effect. The 8973 * user will always see the normal notification view. 8974 * 8975 * <p> 8976 * If the app is targeting Android {@link android.os.Build.VERSION_CODES#P} and above, it is 8977 * required to use the {@link Person} class in order to get an optimal rendering of the 8978 * notification and its avatars. For conversations involving multiple people, the app should 8979 * also make sure that it marks the conversation as a group with 8980 * {@link #setGroupConversation(boolean)}. 8981 * 8982 * <p> 8983 * From Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, messaging style 8984 * notifications that are associated with a valid conversation shortcut 8985 * (via {@link Notification.Builder#setShortcutId(String)}) are displayed in a dedicated 8986 * conversation section in the shade above non-conversation alerting and silence notifications. 8987 * To be a valid conversation shortcut, the shortcut must be a 8988 * {@link ShortcutInfo.Builder#setLongLived(boolean)} dynamic or cached sharing shortcut. 8989 * 8990 * <p> 8991 * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior. 8992 * Here's an example of how this may be used: 8993 * <pre class="prettyprint"> 8994 * 8995 * Person user = new Person.Builder().setIcon(userIcon).setName(userName).build(); 8996 * MessagingStyle style = new MessagingStyle(user) 8997 * .addMessage(messages[1].getText(), messages[1].getTime(), messages[1].getPerson()) 8998 * .addMessage(messages[2].getText(), messages[2].getTime(), messages[2].getPerson()) 8999 * .setGroupConversation(hasMultiplePeople()); 9000 * 9001 * Notification noti = new Notification.Builder() 9002 * .setContentTitle("2 new messages with " + sender.toString()) 9003 * .setContentText(subject) 9004 * .setSmallIcon(R.drawable.new_message) 9005 * .setLargeIcon(aBitmap) 9006 * .setStyle(style) 9007 * .build(); 9008 * </pre> 9009 */ 9010 public static class MessagingStyle extends Style { 9011 9012 /** 9013 * The maximum number of messages that will be retained in the Notification itself (the 9014 * number displayed is up to the platform). 9015 */ 9016 public static final int MAXIMUM_RETAINED_MESSAGES = 25; 9017 9018 9019 /** @hide */ 9020 public static final int CONVERSATION_TYPE_LEGACY = 0; 9021 /** @hide */ 9022 public static final int CONVERSATION_TYPE_NORMAL = 1; 9023 /** @hide */ 9024 public static final int CONVERSATION_TYPE_IMPORTANT = 2; 9025 9026 /** @hide */ 9027 @IntDef(prefix = {"CONVERSATION_TYPE_"}, value = { 9028 CONVERSATION_TYPE_LEGACY, CONVERSATION_TYPE_NORMAL, CONVERSATION_TYPE_IMPORTANT 9029 }) 9030 @Retention(RetentionPolicy.SOURCE) 9031 public @interface ConversationType {} 9032 9033 @NonNull Person mUser; 9034 @Nullable CharSequence mConversationTitle; 9035 @Nullable Icon mShortcutIcon; 9036 List<Message> mMessages = new ArrayList<>(); 9037 List<Message> mHistoricMessages = new ArrayList<>(); 9038 boolean mIsGroupConversation; 9039 @ConversationType int mConversationType = CONVERSATION_TYPE_LEGACY; 9040 int mUnreadMessageCount; 9041 MessagingStyle()9042 MessagingStyle() { 9043 } 9044 9045 /** 9046 * @param userDisplayName Required - the name to be displayed for any replies sent by the 9047 * user before the posting app reposts the notification with those messages after they've 9048 * been actually sent and in previous messages sent by the user added in 9049 * {@link #addMessage(Notification.MessagingStyle.Message)} 9050 * 9051 * @deprecated use {@code MessagingStyle(Person)} 9052 */ MessagingStyle(@onNull CharSequence userDisplayName)9053 public MessagingStyle(@NonNull CharSequence userDisplayName) { 9054 this(new Person.Builder().setName(userDisplayName).build()); 9055 } 9056 9057 /** 9058 * @param user Required - The person displayed for any messages that are sent by the 9059 * user. Any messages added with {@link #addMessage(Notification.MessagingStyle.Message)} 9060 * who don't have a Person associated with it will be displayed as if they were sent 9061 * by this user. The user also needs to have a valid name associated with it, which is 9062 * enforced starting in Android {@link android.os.Build.VERSION_CODES#P}. 9063 */ MessagingStyle(@onNull Person user)9064 public MessagingStyle(@NonNull Person user) { 9065 mUser = user; 9066 } 9067 9068 /** 9069 * Validate that this style was properly composed. This is called at build time. 9070 * @hide 9071 */ 9072 @Override validate(Context context)9073 public void validate(Context context) { 9074 super.validate(context); 9075 if (context.getApplicationInfo().targetSdkVersion 9076 >= Build.VERSION_CODES.P && (mUser == null || mUser.getName() == null)) { 9077 throw new RuntimeException("User must be valid and have a name."); 9078 } 9079 } 9080 9081 /** 9082 * @return the text that should be displayed in the statusBar when heads upped. 9083 * If {@code null} is returned, the default implementation will be used. 9084 * 9085 * @hide 9086 */ 9087 @Override getHeadsUpStatusBarText()9088 public CharSequence getHeadsUpStatusBarText() { 9089 CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle) 9090 ? super.mBigContentTitle 9091 : mConversationTitle; 9092 if (mConversationType == CONVERSATION_TYPE_LEGACY 9093 && !TextUtils.isEmpty(conversationTitle) && !hasOnlyWhiteSpaceSenders()) { 9094 return conversationTitle; 9095 } 9096 return null; 9097 } 9098 9099 /** 9100 * @return the user to be displayed for any replies sent by the user 9101 */ 9102 @NonNull getUser()9103 public Person getUser() { 9104 return mUser; 9105 } 9106 9107 /** 9108 * Returns the name to be displayed for any replies sent by the user 9109 * 9110 * @deprecated use {@link #getUser()} instead 9111 */ getUserDisplayName()9112 public CharSequence getUserDisplayName() { 9113 return mUser.getName(); 9114 } 9115 9116 /** 9117 * Sets the title to be displayed on this conversation. May be set to {@code null}. 9118 * 9119 * <p>Starting in {@link Build.VERSION_CODES#R}, this conversation title will be ignored 9120 * if a valid shortcutId is added via {@link Notification.Builder#setShortcutId(String)}. 9121 * In this case, {@link ShortcutInfo#getLongLabel()} (or, if missing, 9122 * {@link ShortcutInfo#getShortLabel()}) will be shown as the conversation title 9123 * instead. 9124 * 9125 * <p>This API's behavior was changed in SDK version {@link Build.VERSION_CODES#P}. If your 9126 * application's target version is less than {@link Build.VERSION_CODES#P}, setting a 9127 * conversation title to a non-null value will make {@link #isGroupConversation()} return 9128 * {@code true} and passing {@code null} will make it return {@code false}. In 9129 * {@link Build.VERSION_CODES#P} and beyond, use {@link #setGroupConversation(boolean)} 9130 * to set group conversation status. 9131 * 9132 * @param conversationTitle Title displayed for this conversation 9133 * @return this object for method chaining 9134 */ setConversationTitle(@ullable CharSequence conversationTitle)9135 public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) { 9136 mConversationTitle = conversationTitle; 9137 return this; 9138 } 9139 9140 /** 9141 * Return the title to be displayed on this conversation. May return {@code null}. 9142 */ 9143 @Nullable getConversationTitle()9144 public CharSequence getConversationTitle() { 9145 return mConversationTitle; 9146 } 9147 9148 /** 9149 * Sets the icon to be displayed on the conversation, derived from the shortcutId. 9150 * 9151 * @hide 9152 */ setShortcutIcon(@ullable Icon conversationIcon)9153 public MessagingStyle setShortcutIcon(@Nullable Icon conversationIcon) { 9154 mShortcutIcon = conversationIcon; 9155 return this; 9156 } 9157 9158 /** 9159 * Return the icon to be displayed on this conversation, derived from the shortcutId. May 9160 * return {@code null}. 9161 * 9162 * @hide 9163 */ 9164 @Nullable getShortcutIcon()9165 public Icon getShortcutIcon() { 9166 return mShortcutIcon; 9167 } 9168 9169 /** 9170 * Sets the conversation type of this MessageStyle notification. 9171 * {@link #CONVERSATION_TYPE_LEGACY} will use the "older" layout from pre-R, 9172 * {@link #CONVERSATION_TYPE_NORMAL} will use the new "conversation" layout, and 9173 * {@link #CONVERSATION_TYPE_IMPORTANT} will add additional "important" treatments. 9174 * 9175 * @hide 9176 */ setConversationType(@onversationType int conversationType)9177 public MessagingStyle setConversationType(@ConversationType int conversationType) { 9178 mConversationType = conversationType; 9179 return this; 9180 } 9181 9182 /** @hide */ 9183 @ConversationType getConversationType()9184 public int getConversationType() { 9185 return mConversationType; 9186 } 9187 9188 /** @hide */ getUnreadMessageCount()9189 public int getUnreadMessageCount() { 9190 return mUnreadMessageCount; 9191 } 9192 9193 /** @hide */ setUnreadMessageCount(int unreadMessageCount)9194 public MessagingStyle setUnreadMessageCount(int unreadMessageCount) { 9195 mUnreadMessageCount = unreadMessageCount; 9196 return this; 9197 } 9198 9199 /** 9200 * Adds a message for display by this notification. Convenience call for a simple 9201 * {@link Message} in {@link #addMessage(Notification.MessagingStyle.Message)}. 9202 * @param text A {@link CharSequence} to be displayed as the message content 9203 * @param timestamp Time in milliseconds at which the message arrived 9204 * @param sender A {@link CharSequence} to be used for displaying the name of the 9205 * sender. Should be <code>null</code> for messages by the current user, in which case 9206 * the platform will insert {@link #getUserDisplayName()}. 9207 * Should be unique amongst all individuals in the conversation, and should be 9208 * consistent during re-posts of the notification. 9209 * 9210 * @see Message#Message(CharSequence, long, CharSequence) 9211 * 9212 * @return this object for method chaining 9213 * 9214 * @deprecated use {@link #addMessage(CharSequence, long, Person)} 9215 */ addMessage(CharSequence text, long timestamp, CharSequence sender)9216 public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) { 9217 return addMessage(text, timestamp, 9218 sender == null ? null : new Person.Builder().setName(sender).build()); 9219 } 9220 9221 /** 9222 * Adds a message for display by this notification. Convenience call for a simple 9223 * {@link Message} in {@link #addMessage(Notification.MessagingStyle.Message)}. 9224 * @param text A {@link CharSequence} to be displayed as the message content 9225 * @param timestamp Time in milliseconds at which the message arrived 9226 * @param sender The {@link Person} who sent the message. 9227 * Should be <code>null</code> for messages by the current user, in which case 9228 * the platform will insert the user set in {@code MessagingStyle(Person)}. 9229 * 9230 * @see Message#Message(CharSequence, long, CharSequence) 9231 * 9232 * @return this object for method chaining 9233 */ addMessage(@onNull CharSequence text, long timestamp, @Nullable Person sender)9234 public MessagingStyle addMessage(@NonNull CharSequence text, long timestamp, 9235 @Nullable Person sender) { 9236 return addMessage(new Message(text, timestamp, sender)); 9237 } 9238 9239 /** 9240 * Adds a {@link Message} for display in this notification. 9241 * 9242 * <p>The messages should be added in chronologic order, i.e. the oldest first, 9243 * the newest last. 9244 * 9245 * <p>Multiple Messages in a row with the same timestamp and sender may be grouped as a 9246 * single message. This means an app should represent a message that has both an image and 9247 * text as two Message objects, one with the image (and fallback text), and the other with 9248 * the message text. For consistency, a text message (if any) should be provided after Uri 9249 * content. 9250 * 9251 * @param message The {@link Message} to be displayed 9252 * @return this object for method chaining 9253 */ addMessage(Message message)9254 public MessagingStyle addMessage(Message message) { 9255 mMessages.add(message); 9256 if (mMessages.size() > MAXIMUM_RETAINED_MESSAGES) { 9257 mMessages.remove(0); 9258 } 9259 return this; 9260 } 9261 9262 /** 9263 * Adds a {@link Message} for historic context in this notification. 9264 * 9265 * <p>Messages should be added as historic if they are not the main subject of the 9266 * notification but may give context to a conversation. The system may choose to present 9267 * them only when relevant, e.g. when replying to a message through a {@link RemoteInput}. 9268 * 9269 * <p>The messages should be added in chronologic order, i.e. the oldest first, 9270 * the newest last. 9271 * 9272 * @param message The historic {@link Message} to be added 9273 * @return this object for method chaining 9274 */ addHistoricMessage(Message message)9275 public MessagingStyle addHistoricMessage(Message message) { 9276 mHistoricMessages.add(message); 9277 if (mHistoricMessages.size() > MAXIMUM_RETAINED_MESSAGES) { 9278 mHistoricMessages.remove(0); 9279 } 9280 return this; 9281 } 9282 9283 /** 9284 * Gets the list of {@code Message} objects that represent the notification. 9285 */ getMessages()9286 public List<Message> getMessages() { 9287 return mMessages; 9288 } 9289 9290 /** 9291 * Gets the list of historic {@code Message}s in the notification. 9292 */ getHistoricMessages()9293 public List<Message> getHistoricMessages() { 9294 return mHistoricMessages; 9295 } 9296 9297 /** 9298 * Sets whether this conversation notification represents a group. If the app is targeting 9299 * Android P, this is required if the app wants to display the largeIcon set with 9300 * {@link Notification.Builder#setLargeIcon(Bitmap)}, otherwise it will be hidden. 9301 * 9302 * @param isGroupConversation {@code true} if the conversation represents a group, 9303 * {@code false} otherwise. 9304 * @return this object for method chaining 9305 */ setGroupConversation(boolean isGroupConversation)9306 public MessagingStyle setGroupConversation(boolean isGroupConversation) { 9307 mIsGroupConversation = isGroupConversation; 9308 return this; 9309 } 9310 9311 /** 9312 * Returns {@code true} if this notification represents a group conversation, otherwise 9313 * {@code false}. 9314 * 9315 * <p> If the application that generated this {@link MessagingStyle} targets an SDK version 9316 * less than {@link Build.VERSION_CODES#P}, this method becomes dependent on whether or 9317 * not the conversation title is set; returning {@code true} if the conversation title is 9318 * a non-null value, or {@code false} otherwise. From {@link Build.VERSION_CODES#P} forward, 9319 * this method returns what's set by {@link #setGroupConversation(boolean)} allowing for 9320 * named, non-group conversations. 9321 * 9322 * @see #setConversationTitle(CharSequence) 9323 */ isGroupConversation()9324 public boolean isGroupConversation() { 9325 // When target SDK version is < P, a non-null conversation title dictates if this is 9326 // as group conversation. 9327 if (mBuilder != null 9328 && mBuilder.mContext.getApplicationInfo().targetSdkVersion 9329 < Build.VERSION_CODES.P) { 9330 return mConversationTitle != null; 9331 } 9332 9333 return mIsGroupConversation; 9334 } 9335 9336 /** 9337 * @hide 9338 */ 9339 @Override addExtras(Bundle extras)9340 public void addExtras(Bundle extras) { 9341 super.addExtras(extras); 9342 addExtras(extras, false, 0); 9343 } 9344 9345 /** 9346 * @hide 9347 */ addExtras(Bundle extras, boolean ensureContrast, int backgroundColor)9348 public void addExtras(Bundle extras, boolean ensureContrast, int backgroundColor) { 9349 if (mUser != null) { 9350 // For legacy usages 9351 extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUser.getName()); 9352 extras.putParcelable(EXTRA_MESSAGING_PERSON, mUser); 9353 } 9354 if (mConversationTitle != null) { 9355 extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle); 9356 } 9357 if (!mMessages.isEmpty()) { 9358 extras.putParcelableArray(EXTRA_MESSAGES, 9359 getBundleArrayForMessages(mMessages, ensureContrast, backgroundColor)); 9360 } 9361 if (!mHistoricMessages.isEmpty()) { 9362 extras.putParcelableArray(EXTRA_HISTORIC_MESSAGES, getBundleArrayForMessages( 9363 mHistoricMessages, ensureContrast, backgroundColor)); 9364 } 9365 if (mShortcutIcon != null) { 9366 extras.putParcelable(EXTRA_CONVERSATION_ICON, mShortcutIcon); 9367 } 9368 extras.putInt(EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT, mUnreadMessageCount); 9369 9370 fixTitleAndTextExtras(extras); 9371 extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation); 9372 } 9373 getBundleArrayForMessages(List<Message> messages, boolean ensureContrast, int backgroundColor)9374 private static Bundle[] getBundleArrayForMessages(List<Message> messages, 9375 boolean ensureContrast, int backgroundColor) { 9376 Bundle[] bundles = new Bundle[messages.size()]; 9377 final int N = messages.size(); 9378 for (int i = 0; i < N; i++) { 9379 final Message m = messages.get(i); 9380 if (ensureContrast) { 9381 m.ensureColorContrastOrStripStyling(backgroundColor); 9382 } 9383 bundles[i] = m.toBundle(); 9384 } 9385 return bundles; 9386 } 9387 fixTitleAndTextExtras(Bundle extras)9388 private void fixTitleAndTextExtras(Bundle extras) { 9389 Message m = findLatestIncomingMessage(); 9390 CharSequence text = (m == null) ? null : m.mText; 9391 CharSequence sender = m == null ? null 9392 : m.mSender == null || TextUtils.isEmpty(m.mSender.getName()) 9393 ? mUser.getName() : m.mSender.getName(); 9394 CharSequence title; 9395 if (!TextUtils.isEmpty(mConversationTitle)) { 9396 if (!TextUtils.isEmpty(sender)) { 9397 BidiFormatter bidi = BidiFormatter.getInstance(); 9398 title = mBuilder.mContext.getString( 9399 com.android.internal.R.string.notification_messaging_title_template, 9400 bidi.unicodeWrap(mConversationTitle), bidi.unicodeWrap(sender)); 9401 } else { 9402 title = mConversationTitle; 9403 } 9404 } else { 9405 title = sender; 9406 } 9407 if (Flags.cleanUpSpansAndNewLines()) { 9408 title = stripStyling(title); 9409 } 9410 if (title != null) { 9411 extras.putCharSequence(EXTRA_TITLE, title); 9412 } 9413 if (text != null) { 9414 extras.putCharSequence(EXTRA_TEXT, text); 9415 } 9416 } 9417 fixTitleAndTextForCompactMessaging(StandardTemplateParams p)9418 private void fixTitleAndTextForCompactMessaging(StandardTemplateParams p) { 9419 Message m = findLatestIncomingMessage(); 9420 final CharSequence text = (m == null) ? null : m.mText; 9421 CharSequence sender = m == null ? null 9422 : m.mSender == null || TextUtils.isEmpty(m.mSender.getName()) 9423 ? mUser.getName() : m.mSender.getName(); 9424 9425 CharSequence conversationTitle = mIsGroupConversation ? mConversationTitle : null; 9426 9427 // we want to have colon for possible title for conversation. 9428 final BidiFormatter bidi = BidiFormatter.getInstance(); 9429 if (sender != null) { 9430 sender = mBuilder.mContext.getString( 9431 com.android.internal.R.string.notification_messaging_title_template, 9432 bidi.unicodeWrap(sender), ""); 9433 } else if (conversationTitle != null) { 9434 conversationTitle = mBuilder.mContext.getString( 9435 com.android.internal.R.string.notification_messaging_title_template, 9436 bidi.unicodeWrap(conversationTitle), ""); 9437 } 9438 9439 if (Flags.cleanUpSpansAndNewLines()) { 9440 conversationTitle = stripStyling(conversationTitle); 9441 sender = stripStyling(sender); 9442 } 9443 9444 final CharSequence title; 9445 final boolean isConversationTitleAvailable = showConversationTitle() 9446 && conversationTitle != null; 9447 if (isConversationTitleAvailable) { 9448 title = conversationTitle; 9449 } else { 9450 title = sender; 9451 } 9452 9453 p.title(title); 9454 // when the conversation title is available, use headerTextSecondary for sender and 9455 // summaryText for text 9456 if (isConversationTitleAvailable) { 9457 p.headerTextSecondary(sender); 9458 p.summaryText(text); 9459 } else { 9460 // when it is not, use headerTextSecondary for text and don't use summaryText 9461 p.headerTextSecondary(text); 9462 p.summaryText(null); 9463 } 9464 } 9465 9466 /** (b/342370742) Developer settings to show conversation title. */ showConversationTitle()9467 private boolean showConversationTitle() { 9468 return SystemProperties.getBoolean( 9469 "persist.compact_heads_up_notification.show_conversation_title_for_group", 9470 false); 9471 } 9472 9473 /** 9474 * @hide 9475 */ 9476 @Override restoreFromExtras(Bundle extras)9477 protected void restoreFromExtras(Bundle extras) { 9478 super.restoreFromExtras(extras); 9479 9480 Person user = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class); 9481 if (user == null) { 9482 CharSequence displayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME); 9483 mUser = new Person.Builder().setName(displayName).build(); 9484 } else { 9485 mUser = user; 9486 } 9487 mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE); 9488 Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, Parcelable.class); 9489 mMessages = Message.getMessagesFromBundleArray(messages); 9490 Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES, 9491 Parcelable.class); 9492 mHistoricMessages = Message.getMessagesFromBundleArray(histMessages); 9493 mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION); 9494 mUnreadMessageCount = extras.getInt(EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); 9495 mShortcutIcon = extras.getParcelable(EXTRA_CONVERSATION_ICON, Icon.class); 9496 } 9497 9498 /** 9499 * @hide 9500 */ 9501 @Override makeContentView()9502 public RemoteViews makeContentView() { 9503 // All messaging templates contain the actions 9504 ArrayList<Action> originalActions = mBuilder.mActions; 9505 try { 9506 mBuilder.mActions = new ArrayList<>(); 9507 return makeMessagingView(StandardTemplateParams.VIEW_TYPE_NORMAL); 9508 } finally { 9509 mBuilder.mActions = originalActions; 9510 } 9511 } 9512 9513 /** 9514 * @hide 9515 * Spans are ignored when comparing text for visual difference. 9516 */ 9517 @Override areNotificationsVisiblyDifferent(Style other)9518 public boolean areNotificationsVisiblyDifferent(Style other) { 9519 if (other == null || getClass() != other.getClass()) { 9520 return true; 9521 } 9522 MessagingStyle newS = (MessagingStyle) other; 9523 List<MessagingStyle.Message> oldMs = getMessages(); 9524 List<MessagingStyle.Message> newMs = newS.getMessages(); 9525 9526 if (oldMs == null || newMs == null) { 9527 newMs = new ArrayList<>(); 9528 } 9529 9530 int n = oldMs.size(); 9531 if (n != newMs.size()) { 9532 return true; 9533 } 9534 for (int i = 0; i < n; i++) { 9535 MessagingStyle.Message oldM = oldMs.get(i); 9536 MessagingStyle.Message newM = newMs.get(i); 9537 if (!Objects.equals( 9538 String.valueOf(oldM.getText()), 9539 String.valueOf(newM.getText()))) { 9540 return true; 9541 } 9542 if (!Objects.equals(oldM.getDataUri(), newM.getDataUri())) { 9543 return true; 9544 } 9545 String oldSender = String.valueOf(oldM.getSenderPerson() == null 9546 ? oldM.getSender() 9547 : oldM.getSenderPerson().getName()); 9548 String newSender = String.valueOf(newM.getSenderPerson() == null 9549 ? newM.getSender() 9550 : newM.getSenderPerson().getName()); 9551 if (!Objects.equals(oldSender, newSender)) { 9552 return true; 9553 } 9554 9555 String oldKey = oldM.getSenderPerson() == null 9556 ? null : oldM.getSenderPerson().getKey(); 9557 String newKey = newM.getSenderPerson() == null 9558 ? null : newM.getSenderPerson().getKey(); 9559 if (!Objects.equals(oldKey, newKey)) { 9560 return true; 9561 } 9562 // Other fields (like timestamp) intentionally excluded 9563 } 9564 return false; 9565 } 9566 findLatestIncomingMessage()9567 private Message findLatestIncomingMessage() { 9568 return findLatestIncomingMessage(mMessages); 9569 } 9570 9571 /** 9572 * @hide 9573 */ 9574 @Nullable findLatestIncomingMessage( List<Message> messages)9575 public static Message findLatestIncomingMessage( 9576 List<Message> messages) { 9577 for (int i = messages.size() - 1; i >= 0; i--) { 9578 Message m = messages.get(i); 9579 // Incoming messages have a non-empty sender. 9580 if (m.mSender != null && !TextUtils.isEmpty(m.mSender.getName())) { 9581 return m; 9582 } 9583 } 9584 if (!messages.isEmpty()) { 9585 // No incoming messages, fall back to outgoing message 9586 return messages.get(messages.size() - 1); 9587 } 9588 return null; 9589 } 9590 9591 /** 9592 * @hide 9593 */ 9594 @Override makeExpandedContentView()9595 public RemoteViews makeExpandedContentView() { 9596 return makeMessagingView(StandardTemplateParams.VIEW_TYPE_EXPANDED); 9597 } 9598 9599 /** 9600 * Create a messaging layout. 9601 * 9602 * @param viewType one of StandardTemplateParams.VIEW_TYPE_NORMAL, VIEW_TYPE_EXPANDEDIG, 9603 * VIEW_TYPE_HEADS_UP 9604 * @return the created remoteView. 9605 */ 9606 @NonNull makeMessagingView(int viewType)9607 private RemoteViews makeMessagingView(int viewType) { 9608 boolean isCollapsed = viewType != StandardTemplateParams.VIEW_TYPE_EXPANDED; 9609 boolean hideRightIcons = viewType != StandardTemplateParams.VIEW_TYPE_NORMAL; 9610 boolean isConversationLayout = mConversationType != CONVERSATION_TYPE_LEGACY; 9611 boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT; 9612 boolean isLegacyHeaderless = !isConversationLayout && isCollapsed; 9613 9614 //TODO (b/217799515): ensure mConversationTitle always returns the correct 9615 // conversationTitle, probably set mConversationTitle = conversationTitle after this 9616 CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle) 9617 ? super.mBigContentTitle 9618 : mConversationTitle; 9619 boolean atLeastP = mBuilder.mContext.getApplicationInfo().targetSdkVersion 9620 >= Build.VERSION_CODES.P; 9621 boolean isOneToOne; 9622 CharSequence nameReplacement = null; 9623 if (!atLeastP) { 9624 isOneToOne = TextUtils.isEmpty(conversationTitle); 9625 if (hasOnlyWhiteSpaceSenders()) { 9626 isOneToOne = true; 9627 nameReplacement = conversationTitle; 9628 conversationTitle = null; 9629 } 9630 } else { 9631 isOneToOne = !isGroupConversation(); 9632 } 9633 if ((isLegacyHeaderless || notificationsRedesignTemplates()) 9634 && isOneToOne && TextUtils.isEmpty(conversationTitle)) { 9635 conversationTitle = getOtherPersonName(); 9636 } 9637 9638 Icon largeIcon = mBuilder.mN.mLargeIcon; 9639 TemplateBindResult bindResult = new TemplateBindResult(); 9640 StandardTemplateParams p = mBuilder.mParams.reset() 9641 .viewType(viewType) 9642 .highlightExpander(isConversationLayout) 9643 .hideProgress(true) 9644 .hideLeftIcon(isOneToOne) 9645 .hideRightIcon(hideRightIcons || isOneToOne); 9646 if (notificationsRedesignTemplates()) { 9647 String lastMessage = !mMessages.isEmpty() 9648 ? mMessages.getLast().mText.toString() : null; 9649 9650 p.title(conversationTitle) 9651 // The text is not actually displayed like this (since we're using a 9652 // MessagingLinearLayout instead of the regular text), but we're using it to 9653 // know whether the notification will have a second line in practice. 9654 .text(lastMessage) 9655 .hideAppName(isCollapsed); 9656 } else { 9657 p.title(isLegacyHeaderless ? conversationTitle : null) 9658 .text(null) 9659 .headerTextSecondary(isLegacyHeaderless ? null : conversationTitle); 9660 } 9661 RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( 9662 getMessagingLayoutResource(isConversationLayout, isCollapsed), 9663 p, 9664 bindResult); 9665 if (isConversationLayout && !notificationsRedesignTemplates()) { 9666 // Redesign note: This view is replaced by the `title`, which is handled normally. 9667 mBuilder.setTextViewColorPrimary(contentView, R.id.conversation_text, p); 9668 // Redesign note: This special divider is no longer needed. 9669 mBuilder.setTextViewColorSecondary(contentView, R.id.app_name_divider, p); 9670 } 9671 9672 addExtras(mBuilder.mN.extras, true, mBuilder.getBackgroundColor(p)); 9673 contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", 9674 mBuilder.getSmallIconColor(p)); 9675 contentView.setInt(R.id.status_bar_latest_event_content, "setSenderTextColor", 9676 mBuilder.getPrimaryTextColor(p)); 9677 contentView.setInt(R.id.status_bar_latest_event_content, "setMessageTextColor", 9678 mBuilder.getSecondaryTextColor(p)); 9679 contentView.setInt(R.id.status_bar_latest_event_content, 9680 "setNotificationBackgroundColor", 9681 mBuilder.getBackgroundColor(p)); 9682 contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsCollapsed", 9683 isCollapsed); 9684 contentView.setIcon(R.id.status_bar_latest_event_content, "setAvatarReplacement", 9685 mBuilder.mN.mLargeIcon); 9686 contentView.setCharSequence(R.id.status_bar_latest_event_content, "setNameReplacement", 9687 nameReplacement); 9688 contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsOneToOne", 9689 isOneToOne); 9690 contentView.setCharSequence(R.id.status_bar_latest_event_content, 9691 "setConversationTitle", conversationTitle); 9692 if (isConversationLayout) { 9693 contentView.setIcon(R.id.status_bar_latest_event_content, 9694 "setShortcutIcon", mShortcutIcon); 9695 contentView.setBoolean(R.id.status_bar_latest_event_content, 9696 "setIsImportantConversation", isImportantConversation); 9697 } 9698 if (notificationsRedesignTemplates() && !isCollapsed) { 9699 // Align the title to the app/small icon in the expanded form. In other layouts, 9700 // this margin is added directly to the notification_main_column parent, but for 9701 // messages we don't want the margin to be applied to the actual messaging 9702 // content since it can contain icons that are displayed below the app icon. 9703 Resources res = mBuilder.mContext.getResources(); 9704 int marginStart = res.getDimensionPixelSize( 9705 R.dimen.notification_2025_content_margin_start); 9706 contentView.setViewLayoutMargin(R.id.title, 9707 RemoteViews.MARGIN_START, marginStart, COMPLEX_UNIT_PX); 9708 } 9709 if (isLegacyHeaderless) { 9710 // Collapsed legacy messaging style has a 1-line limit. 9711 contentView.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1); 9712 } 9713 contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon", 9714 largeIcon); 9715 contentView.setBundle(R.id.status_bar_latest_event_content, "setData", 9716 mBuilder.mN.extras); 9717 return contentView; 9718 } 9719 getMessagingLayoutResource(boolean isConversationLayout, boolean isCollapsed)9720 private int getMessagingLayoutResource(boolean isConversationLayout, boolean isCollapsed) { 9721 if (notificationsRedesignTemplates()) { 9722 // Note: We eventually would like to use the same layouts for both conversations and 9723 // regular messaging notifications. 9724 if (isConversationLayout) { 9725 if (isCollapsed) { 9726 return mBuilder.getCollapsedConversationLayoutResource(); 9727 } else { 9728 return mBuilder.getExpandedConversationLayoutResource(); 9729 } 9730 } else { 9731 if (isCollapsed) { 9732 return mBuilder.getCollapsedMessagingLayoutResource(); 9733 } else { 9734 return mBuilder.getExpandedMessagingLayoutResource(); 9735 } 9736 } 9737 9738 } else { 9739 return isConversationLayout 9740 ? mBuilder.getConversationLayoutResource() 9741 : isCollapsed 9742 ? mBuilder.getCollapsedMessagingLayoutResource() 9743 : mBuilder.getExpandedMessagingLayoutResource(); 9744 } 9745 } 9746 getKey(Person person)9747 private CharSequence getKey(Person person) { 9748 return person == null ? null 9749 : person.getKey() == null ? person.getName() : person.getKey(); 9750 } 9751 getOtherPersonName()9752 private CharSequence getOtherPersonName() { 9753 CharSequence userKey = getKey(mUser); 9754 for (int i = mMessages.size() - 1; i >= 0; i--) { 9755 Person sender = mMessages.get(i).getSenderPerson(); 9756 if (sender != null && !TextUtils.equals(userKey, getKey(sender))) { 9757 return sender.getName(); 9758 } 9759 } 9760 // If we've reached this point without finding a sender that doesn't match the user, it 9761 // likely points to an incorrect use of our API, where the user isn't being set 9762 // correctly. It's either that, or perhaps the user actually is having a conversation 9763 // with themselves ¯\_(ツ)_/¯ so let's not leave the name empty. 9764 return notificationsRedesignTemplates() ? mUser.getName() : null; 9765 } 9766 hasOnlyWhiteSpaceSenders()9767 private boolean hasOnlyWhiteSpaceSenders() { 9768 for (int i = 0; i < mMessages.size(); i++) { 9769 Message m = mMessages.get(i); 9770 Person sender = m.getSenderPerson(); 9771 if (sender != null && !isWhiteSpace(sender.getName())) { 9772 return false; 9773 } 9774 } 9775 return true; 9776 } 9777 isWhiteSpace(CharSequence sender)9778 private boolean isWhiteSpace(CharSequence sender) { 9779 if (TextUtils.isEmpty(sender)) { 9780 return true; 9781 } 9782 if (sender.toString().matches("^\\s*$")) { 9783 return true; 9784 } 9785 // Let's check if we only have 0 whitespace chars. Some apps did this as a workaround 9786 // For the presentation that we had. 9787 for (int i = 0; i < sender.length(); i++) { 9788 char c = sender.charAt(i); 9789 if (c != '\u200B') { 9790 return false; 9791 } 9792 } 9793 return true; 9794 } 9795 9796 /** 9797 * @hide 9798 */ 9799 @Override makeHeadsUpContentView()9800 public RemoteViews makeHeadsUpContentView() { 9801 return makeMessagingView(StandardTemplateParams.VIEW_TYPE_HEADS_UP); 9802 } 9803 9804 /** 9805 * @hide 9806 */ 9807 @Nullable 9808 @Override makeCompactHeadsUpContentView()9809 public RemoteViews makeCompactHeadsUpContentView() { 9810 final boolean isConversationLayout = mConversationType != CONVERSATION_TYPE_LEGACY; 9811 Icon conversationIcon = null; 9812 Notification.Action remoteInputAction = null; 9813 if (isConversationLayout) { 9814 9815 conversationIcon = mShortcutIcon; 9816 9817 // conversation icon is m 9818 // Extract the conversation icon for one to one conversations from 9819 // the latest incoming message since 9820 // fixTitleAndTextExtras also uses it as data source for title and text 9821 if (conversationIcon == null && !mIsGroupConversation) { 9822 final Message message = findLatestIncomingMessage(); 9823 if (message != null) { 9824 final Person sender = message.mSender; 9825 if (sender != null) { 9826 conversationIcon = sender.getIcon(); 9827 } 9828 } 9829 } 9830 9831 if (Flags.compactHeadsUpNotificationReply()) { 9832 // Get the first non-contextual inline reply action. 9833 final List<Notification.Action> nonContextualActions = 9834 mBuilder.getNonContextualActions(); 9835 for (int i = 0; i < nonContextualActions.size(); i++) { 9836 final Notification.Action action = nonContextualActions.get(i); 9837 if (mBuilder.hasValidRemoteInput(action)) { 9838 remoteInputAction = action; 9839 break; 9840 } 9841 } 9842 } 9843 } 9844 9845 final StandardTemplateParams p = mBuilder.mParams.reset() 9846 .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) 9847 .highlightExpander(isConversationLayout) 9848 .fillTextsFrom(mBuilder) 9849 .hideTime(true); 9850 9851 fixTitleAndTextForCompactMessaging(p); 9852 TemplateBindResult bindResult = new TemplateBindResult(); 9853 9854 RemoteViews contentView = mBuilder.applyStandardTemplate( 9855 mBuilder.getMessagingCompactHeadsUpLayoutResource(), p, bindResult); 9856 contentView.setViewVisibility(R.id.header_text_secondary_divider, View.GONE); 9857 contentView.setViewVisibility(R.id.header_text_divider, View.GONE); 9858 if (conversationIcon != null) { 9859 contentView.setViewVisibility(R.id.icon, View.GONE); 9860 contentView.setViewVisibility(R.id.conversation_face_pile, View.GONE); 9861 contentView.setViewVisibility(R.id.conversation_icon, View.VISIBLE); 9862 contentView.setImageViewIcon(R.id.conversation_icon, conversationIcon); 9863 } else if (mIsGroupConversation) { 9864 contentView.setViewVisibility(R.id.icon, View.GONE); 9865 contentView.setViewVisibility(R.id.conversation_icon, View.GONE); 9866 contentView.setInt(R.id.status_bar_latest_event_content, 9867 "setNotificationBackgroundColor", mBuilder.getBackgroundColor(p)); 9868 contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", 9869 mBuilder.getSmallIconColor(p)); 9870 contentView.setBundle(R.id.status_bar_latest_event_content, "setGroupFacePile", 9871 mBuilder.mN.extras); 9872 } 9873 9874 if (remoteInputAction != null) { 9875 contentView.setViewVisibility(R.id.reply_action_container, View.VISIBLE); 9876 9877 final RemoteViews inlineReplyButton = 9878 mBuilder.generateActionButton(remoteInputAction, false, p); 9879 // Clear the drawable 9880 inlineReplyButton.setInt(R.id.action0, "setBackgroundResource", 0); 9881 inlineReplyButton.setTextViewText(R.id.action0, 9882 mBuilder.mContext.getString(R.string.notification_compact_heads_up_reply)); 9883 contentView.addView(R.id.reply_action_container, inlineReplyButton); 9884 } else { 9885 contentView.setViewVisibility(R.id.reply_action_container, View.GONE); 9886 } 9887 return contentView; 9888 } 9889 9890 9891 /** 9892 * @hide 9893 */ 9894 @Override reduceImageSizes(Context context)9895 public void reduceImageSizes(Context context) { 9896 super.reduceImageSizes(context); 9897 Resources resources = context.getResources(); 9898 boolean isLowRam = ActivityManager.isLowRamDeviceStatic(); 9899 if (mShortcutIcon != null) { 9900 int maxSize = resources.getDimensionPixelSize( 9901 isLowRam ? R.dimen.notification_small_icon_size_low_ram 9902 : R.dimen.notification_small_icon_size); 9903 mShortcutIcon.scaleDownIfNecessary(maxSize, maxSize); 9904 } 9905 9906 int maxAvatarSize = resources.getDimensionPixelSize( 9907 isLowRam ? R.dimen.notification_person_icon_max_size_low_ram 9908 : R.dimen.notification_person_icon_max_size); 9909 if (mUser != null && mUser.getIcon() != null) { 9910 mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize); 9911 } 9912 9913 reduceMessagesIconSizes(mMessages, maxAvatarSize); 9914 reduceMessagesIconSizes(mHistoricMessages, maxAvatarSize); 9915 } 9916 9917 /** 9918 * @hide 9919 */ reduceMessagesIconSizes(@ullable List<Message> messages, int maxSize)9920 private static void reduceMessagesIconSizes(@Nullable List<Message> messages, int maxSize) { 9921 if (messages == null) { 9922 return; 9923 } 9924 9925 for (Message message : messages) { 9926 Person sender = message.mSender; 9927 if (sender != null) { 9928 Icon icon = sender.getIcon(); 9929 if (icon != null) { 9930 icon.scaleDownIfNecessary(maxSize, maxSize); 9931 } 9932 } 9933 } 9934 } 9935 9936 /* 9937 * An object representing a simple message or piece of media within a mixed-media message. 9938 * 9939 * This object can only represent text or a single binary piece of media. For apps which 9940 * support mixed-media messages (e.g. text + image), multiple Messages should be used, one 9941 * to represent each piece of the message, and they should all be given the same timestamp. 9942 * For consistency, a text message should be added last of all Messages with the same 9943 * timestamp. 9944 */ 9945 public static final class Message { 9946 /** @hide */ 9947 public static final String KEY_TEXT = "text"; 9948 static final String KEY_TIMESTAMP = "time"; 9949 static final String KEY_SENDER = "sender"; 9950 static final String KEY_SENDER_PERSON = "sender_person"; 9951 static final String KEY_DATA_MIME_TYPE = "type"; 9952 static final String KEY_DATA_URI= "uri"; 9953 static final String KEY_EXTRAS_BUNDLE = "extras"; 9954 static final String KEY_REMOTE_INPUT_HISTORY = "remote_input_history"; 9955 9956 private CharSequence mText; 9957 private final long mTimestamp; 9958 @Nullable 9959 private final Person mSender; 9960 /** True if this message was generated from the extra 9961 * {@link Notification#EXTRA_REMOTE_INPUT_HISTORY_ITEMS} 9962 */ 9963 private final boolean mRemoteInputHistory; 9964 9965 private Bundle mExtras = new Bundle(); 9966 private String mDataMimeType; 9967 private Uri mDataUri; 9968 9969 /** 9970 * Constructor 9971 * @param text A {@link CharSequence} to be displayed as the message content 9972 * @param timestamp Time at which the message arrived 9973 * @param sender A {@link CharSequence} to be used for displaying the name of the 9974 * sender. Should be <code>null</code> for messages by the current user, in which case 9975 * the platform will insert {@link MessagingStyle#getUserDisplayName()}. 9976 * Should be unique amongst all individuals in the conversation, and should be 9977 * consistent during re-posts of the notification. 9978 * 9979 * @deprecated use {@code Message(CharSequence, long, Person)} 9980 */ Message(CharSequence text, long timestamp, CharSequence sender)9981 public Message(CharSequence text, long timestamp, CharSequence sender){ 9982 this(text, timestamp, sender == null ? null 9983 : new Person.Builder().setName(sender).build()); 9984 } 9985 9986 /** 9987 * Constructor 9988 * @param text A {@link CharSequence} to be displayed as the message content 9989 * @param timestamp Time at which the message arrived 9990 * @param sender The {@link Person} who sent the message. 9991 * Should be <code>null</code> for messages by the current user, in which case 9992 * the platform will insert the user set in {@code MessagingStyle(Person)}. 9993 * <p> 9994 * The person provided should contain an Icon, set with 9995 * {@link Person.Builder#setIcon(Icon)} and also have a name provided 9996 * with {@link Person.Builder#setName(CharSequence)}. If multiple users have the same 9997 * name, consider providing a key with {@link Person.Builder#setKey(String)} in order 9998 * to differentiate between the different users. 9999 * </p> 10000 */ Message(@onNull CharSequence text, long timestamp, @Nullable Person sender)10001 public Message(@NonNull CharSequence text, long timestamp, @Nullable Person sender) { 10002 this(text, timestamp, sender, false /* remoteHistory */); 10003 } 10004 10005 /** 10006 * Constructor 10007 * @param text A {@link CharSequence} to be displayed as the message content 10008 * @param timestamp Time at which the message arrived 10009 * @param sender The {@link Person} who sent the message. 10010 * Should be <code>null</code> for messages by the current user, in which case 10011 * the platform will insert the user set in {@code MessagingStyle(Person)}. 10012 * @param remoteInputHistory True if the messages was generated from the extra 10013 * {@link Notification#EXTRA_REMOTE_INPUT_HISTORY_ITEMS}. 10014 * <p> 10015 * The person provided should contain an Icon, set with 10016 * {@link Person.Builder#setIcon(Icon)} and also have a name provided 10017 * with {@link Person.Builder#setName(CharSequence)}. If multiple users have the same 10018 * name, consider providing a key with {@link Person.Builder#setKey(String)} in order 10019 * to differentiate between the different users. 10020 * </p> 10021 * @hide 10022 */ Message(@onNull CharSequence text, long timestamp, @Nullable Person sender, boolean remoteInputHistory)10023 public Message(@NonNull CharSequence text, long timestamp, @Nullable Person sender, 10024 boolean remoteInputHistory) { 10025 mText = safeCharSequence(text); 10026 mTimestamp = timestamp; 10027 mSender = sender; 10028 mRemoteInputHistory = remoteInputHistory; 10029 } 10030 10031 /** 10032 * Sets a binary blob of data and an associated MIME type for a message. In the case 10033 * where the platform or the UI state doesn't support the MIME type, the original text 10034 * provided in the constructor will be used. When this data can be presented to the 10035 * user, the original text will only be used as accessibility text. 10036 * @param dataMimeType The MIME type of the content. See 10037 * {@link android.graphics.ImageDecoder#isMimeTypeSupported(String)} for a list of 10038 * supported image MIME types. 10039 * @param dataUri The uri containing the content whose type is given by the MIME type. 10040 * <p class="note"> 10041 * Notification Listeners including the System UI need permission to access the 10042 * data the Uri points to. The recommended ways to do this are: 10043 * <ol> 10044 * <li>Store the data in your own ContentProvider, making sure that other apps have 10045 * the correct permission to access your provider. The preferred mechanism for 10046 * providing access is to use per-URI permissions which are temporary and only 10047 * grant access to the receiving application. An easy way to create a 10048 * ContentProvider like this is to use the FileProvider helper class.</li> 10049 * <li>Use the system MediaStore. The MediaStore is primarily aimed at video, audio 10050 * and image MIME types, however beginning with Android 3.0 (API level 11) it can 10051 * also store non-media types (see MediaStore.Files for more info). Files can be 10052 * inserted into the MediaStore using scanFile() after which a content:// style 10053 * Uri suitable for sharing is passed to the provided onScanCompleted() callback. 10054 * Note that once added to the system MediaStore the content is accessible to any 10055 * app on the device.</li> 10056 * </ol> 10057 * @return this object for method chaining 10058 */ setData(String dataMimeType, Uri dataUri)10059 public Message setData(String dataMimeType, Uri dataUri) { 10060 mDataMimeType = dataMimeType; 10061 mDataUri = dataUri; 10062 return this; 10063 } 10064 10065 /** 10066 * Strip styling or updates TextAppearance spans in message text. 10067 * @hide 10068 */ ensureColorContrastOrStripStyling(int backgroundColor)10069 public void ensureColorContrastOrStripStyling(int backgroundColor) { 10070 if (Flags.cleanUpSpansAndNewLines()) { 10071 mText = stripNonStyleSpans(mText); 10072 } else { 10073 ensureColorContrast(backgroundColor); 10074 } 10075 } 10076 stripNonStyleSpans(CharSequence text)10077 private CharSequence stripNonStyleSpans(CharSequence text) { 10078 10079 if (text instanceof Spanned) { 10080 Spanned ss = (Spanned) text; 10081 Object[] spans = ss.getSpans(0, ss.length(), Object.class); 10082 SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); 10083 for (Object span : spans) { 10084 final Object resultSpan; 10085 if (span instanceof StyleSpan 10086 || span instanceof StrikethroughSpan 10087 || span instanceof UnderlineSpan) { 10088 resultSpan = span; 10089 } else if (span instanceof TextAppearanceSpan) { 10090 final TextAppearanceSpan originalSpan = (TextAppearanceSpan) span; 10091 resultSpan = new TextAppearanceSpan( 10092 null, 10093 originalSpan.getTextStyle(), 10094 -1, 10095 null, 10096 null); 10097 } else { 10098 continue; 10099 } 10100 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), 10101 ss.getSpanFlags(span)); 10102 } 10103 return builder; 10104 } 10105 return text; 10106 } 10107 10108 /** 10109 * Updates TextAppearance spans in the message text so it has sufficient contrast 10110 * against its background. 10111 * @hide 10112 */ ensureColorContrast(int backgroundColor)10113 public void ensureColorContrast(int backgroundColor) { 10114 mText = ContrastColorUtil.ensureColorSpanContrast(mText, backgroundColor); 10115 } 10116 10117 /** 10118 * Get the text to be used for this message, or the fallback text if a type and content 10119 * Uri have been set 10120 */ getText()10121 public CharSequence getText() { 10122 return mText; 10123 } 10124 10125 /** 10126 * Get the time at which this message arrived 10127 */ getTimestamp()10128 public long getTimestamp() { 10129 return mTimestamp; 10130 } 10131 10132 /** 10133 * Get the extras Bundle for this message. 10134 */ getExtras()10135 public Bundle getExtras() { 10136 return mExtras; 10137 } 10138 10139 /** 10140 * Get the text used to display the contact's name in the messaging experience 10141 * 10142 * @deprecated use {@link #getSenderPerson()} 10143 */ getSender()10144 public CharSequence getSender() { 10145 return mSender == null ? null : mSender.getName(); 10146 } 10147 10148 /** 10149 * Get the sender associated with this message. 10150 */ 10151 @Nullable getSenderPerson()10152 public Person getSenderPerson() { 10153 return mSender; 10154 } 10155 10156 /** 10157 * Get the MIME type of the data pointed to by the Uri 10158 */ getDataMimeType()10159 public String getDataMimeType() { 10160 return mDataMimeType; 10161 } 10162 10163 /** 10164 * Get the Uri pointing to the content of the message. Can be null, in which case 10165 * {@see #getText()} is used. 10166 */ getDataUri()10167 public Uri getDataUri() { 10168 return mDataUri; 10169 } 10170 10171 /** 10172 * @return True if the message was generated from 10173 * {@link Notification#EXTRA_REMOTE_INPUT_HISTORY_ITEMS}. 10174 * @hide 10175 */ isRemoteInputHistory()10176 public boolean isRemoteInputHistory() { 10177 return mRemoteInputHistory; 10178 } 10179 10180 /** 10181 * Converts the message into a {@link Bundle}. To extract the message back, 10182 * check {@link #getMessageFromBundle()} 10183 * @hide 10184 */ 10185 @NonNull toBundle()10186 public Bundle toBundle() { 10187 Bundle bundle = new Bundle(); 10188 if (mText != null) { 10189 bundle.putCharSequence(KEY_TEXT, mText); 10190 } 10191 bundle.putLong(KEY_TIMESTAMP, mTimestamp); 10192 if (mSender != null) { 10193 // Legacy listeners need this 10194 bundle.putCharSequence(KEY_SENDER, safeCharSequence(mSender.getName())); 10195 bundle.putParcelable(KEY_SENDER_PERSON, mSender); 10196 } 10197 if (mDataMimeType != null) { 10198 bundle.putString(KEY_DATA_MIME_TYPE, mDataMimeType); 10199 } 10200 if (mDataUri != null) { 10201 bundle.putParcelable(KEY_DATA_URI, mDataUri); 10202 } 10203 if (mExtras != null) { 10204 bundle.putBundle(KEY_EXTRAS_BUNDLE, mExtras); 10205 } 10206 if (mRemoteInputHistory) { 10207 bundle.putBoolean(KEY_REMOTE_INPUT_HISTORY, mRemoteInputHistory); 10208 } 10209 return bundle; 10210 } 10211 10212 /** 10213 * See {@link Notification#visitUris(Consumer)}. 10214 * 10215 * @hide 10216 */ visitUris(@onNull Consumer<Uri> visitor)10217 public void visitUris(@NonNull Consumer<Uri> visitor) { 10218 visitor.accept(getDataUri()); 10219 if (mSender != null) { 10220 mSender.visitUris(visitor); 10221 } 10222 } 10223 10224 /** 10225 * Returns a list of messages read from the given bundle list, e.g. 10226 * {@link #EXTRA_MESSAGES} or {@link #EXTRA_HISTORIC_MESSAGES}. 10227 */ 10228 @NonNull getMessagesFromBundleArray(@ullable Parcelable[] bundles)10229 public static List<Message> getMessagesFromBundleArray(@Nullable Parcelable[] bundles) { 10230 if (bundles == null) { 10231 return new ArrayList<>(); 10232 } 10233 List<Message> messages = new ArrayList<>(bundles.length); 10234 for (int i = 0; i < bundles.length; i++) { 10235 if (bundles[i] instanceof Bundle) { 10236 Message message = getMessageFromBundle((Bundle)bundles[i]); 10237 if (message != null) { 10238 messages.add(message); 10239 } 10240 } 10241 } 10242 return messages; 10243 } 10244 10245 /** 10246 * Returns the message that is stored in the bundle (e.g. one of the values in the lists 10247 * in {@link #EXTRA_MESSAGES} or {@link #EXTRA_HISTORIC_MESSAGES}) or null if the 10248 * message couldn't be resolved. 10249 * @hide 10250 */ 10251 @Nullable getMessageFromBundle(@onNull Bundle bundle)10252 public static Message getMessageFromBundle(@NonNull Bundle bundle) { 10253 try { 10254 if (!bundle.containsKey(KEY_TEXT) || !bundle.containsKey(KEY_TIMESTAMP)) { 10255 return null; 10256 } else { 10257 10258 Person senderPerson = bundle.getParcelable(KEY_SENDER_PERSON, Person.class); 10259 if (senderPerson == null) { 10260 // Legacy apps that use compat don't actually provide the sender objects 10261 // We need to fix the compat version to provide people / use 10262 // the native api instead 10263 CharSequence senderName = bundle.getCharSequence(KEY_SENDER); 10264 if (senderName != null) { 10265 senderPerson = new Person.Builder().setName(senderName).build(); 10266 } 10267 } 10268 Message message = new Message(bundle.getCharSequence(KEY_TEXT), 10269 bundle.getLong(KEY_TIMESTAMP), 10270 senderPerson, 10271 bundle.getBoolean(KEY_REMOTE_INPUT_HISTORY, false)); 10272 if (bundle.containsKey(KEY_DATA_MIME_TYPE) && 10273 bundle.containsKey(KEY_DATA_URI)) { 10274 message.setData(bundle.getString(KEY_DATA_MIME_TYPE), 10275 bundle.getParcelable(KEY_DATA_URI, Uri.class)); 10276 } 10277 if (bundle.containsKey(KEY_EXTRAS_BUNDLE)) { 10278 message.getExtras().putAll(bundle.getBundle(KEY_EXTRAS_BUNDLE)); 10279 } 10280 return message; 10281 } 10282 } catch (ClassCastException e) { 10283 return null; 10284 } 10285 } 10286 } 10287 } 10288 10289 /** 10290 * Helper class for generating large-format notifications that include a list of (up to 5) strings. 10291 * 10292 * Here's how you'd set the <code>InboxStyle</code> on a notification: 10293 * <pre class="prettyprint"> 10294 * Notification notif = new Notification.Builder(mContext) 10295 * .setContentTitle("5 New mails from " + sender.toString()) 10296 * .setContentText(subject) 10297 * .setSmallIcon(R.drawable.new_mail) 10298 * .setLargeIcon(aBitmap) 10299 * .setStyle(new Notification.InboxStyle() 10300 * .addLine(str1) 10301 * .addLine(str2) 10302 * .setContentTitle("") 10303 * .setSummaryText("+3 more")) 10304 * .build(); 10305 * </pre> 10306 * 10307 * @see Notification#bigContentView 10308 */ 10309 public static class InboxStyle extends Style { 10310 10311 /** 10312 * The number of lines of remote input history allowed until we start reducing lines. 10313 */ 10314 private static final int NUMBER_OF_HISTORY_ALLOWED_UNTIL_REDUCTION = 1; 10315 private ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>(5); 10316 InboxStyle()10317 public InboxStyle() { 10318 } 10319 10320 /** 10321 * @deprecated use {@code InboxStyle()}. 10322 */ 10323 @Deprecated InboxStyle(Builder builder)10324 public InboxStyle(Builder builder) { 10325 setBuilder(builder); 10326 } 10327 10328 /** 10329 * Overrides ContentTitle in the expanded form of the template. 10330 * This defaults to the value passed to setContentTitle(). 10331 */ setBigContentTitle(CharSequence title)10332 public InboxStyle setBigContentTitle(CharSequence title) { 10333 internalSetBigContentTitle(safeCharSequence(title)); 10334 return this; 10335 } 10336 10337 /** 10338 * Set the first line of text after the detail section in the expanded form of the template. 10339 */ setSummaryText(CharSequence cs)10340 public InboxStyle setSummaryText(CharSequence cs) { 10341 internalSetSummaryText(safeCharSequence(cs)); 10342 return this; 10343 } 10344 10345 /** 10346 * Append a line to the digest section of the Inbox notification. 10347 */ addLine(CharSequence cs)10348 public InboxStyle addLine(CharSequence cs) { 10349 mTexts.add(safeCharSequence(cs)); 10350 return this; 10351 } 10352 10353 /** 10354 * @hide 10355 */ getLines()10356 public ArrayList<CharSequence> getLines() { 10357 return mTexts; 10358 } 10359 10360 /** 10361 * @hide 10362 */ addExtras(Bundle extras)10363 public void addExtras(Bundle extras) { 10364 super.addExtras(extras); 10365 10366 CharSequence[] a = new CharSequence[mTexts.size()]; 10367 extras.putCharSequenceArray(EXTRA_TEXT_LINES, mTexts.toArray(a)); 10368 } 10369 10370 /** 10371 * @hide 10372 */ 10373 @Override restoreFromExtras(Bundle extras)10374 protected void restoreFromExtras(Bundle extras) { 10375 super.restoreFromExtras(extras); 10376 10377 mTexts.clear(); 10378 if (extras.containsKey(EXTRA_TEXT_LINES)) { 10379 Collections.addAll(mTexts, extras.getCharSequenceArray(EXTRA_TEXT_LINES)); 10380 } 10381 } 10382 10383 /** 10384 * @hide 10385 */ makeExpandedContentView()10386 public RemoteViews makeExpandedContentView() { 10387 StandardTemplateParams p = mBuilder.mParams.reset() 10388 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED) 10389 .fillTextsFrom(mBuilder).text(null); 10390 TemplateBindResult result = new TemplateBindResult(); 10391 RemoteViews contentView = getStandardView(mBuilder.getInboxLayoutResource(), p, result); 10392 10393 int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3, 10394 R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6}; 10395 10396 // Make sure all rows are gone in case we reuse a view. 10397 for (int rowId : rowIds) { 10398 contentView.setViewVisibility(rowId, View.GONE); 10399 } 10400 10401 int i=0; 10402 int topPadding = mBuilder.mContext.getResources().getDimensionPixelSize( 10403 R.dimen.notification_inbox_item_top_padding); 10404 boolean first = true; 10405 int onlyViewId = 0; 10406 int maxRows = rowIds.length; 10407 if (mBuilder.mActions.size() > 0) { 10408 maxRows--; 10409 } 10410 RemoteInputHistoryItem[] remoteInputHistory = getParcelableArrayFromBundle( 10411 mBuilder.mN.extras, EXTRA_REMOTE_INPUT_HISTORY_ITEMS, 10412 RemoteInputHistoryItem.class); 10413 if (remoteInputHistory != null 10414 && remoteInputHistory.length > NUMBER_OF_HISTORY_ALLOWED_UNTIL_REDUCTION) { 10415 // Let's remove some messages to make room for the remote input history. 10416 // 1 is always able to fit, but let's remove them if they are 2 or 3 10417 int numRemoteInputs = Math.min(remoteInputHistory.length, 10418 MAX_REMOTE_INPUT_HISTORY_LINES); 10419 int totalNumRows = mTexts.size() + numRemoteInputs 10420 - NUMBER_OF_HISTORY_ALLOWED_UNTIL_REDUCTION; 10421 if (totalNumRows > maxRows) { 10422 int overflow = totalNumRows - maxRows; 10423 if (mTexts.size() > maxRows) { 10424 // Heuristic: if the Texts don't fit anyway, we'll rather drop the last 10425 // few messages, even with the remote input 10426 maxRows -= overflow; 10427 } else { 10428 // otherwise we drop the first messages 10429 i = overflow; 10430 } 10431 } 10432 } 10433 while (i < mTexts.size() && i < maxRows) { 10434 CharSequence str = mTexts.get(i); 10435 if (!TextUtils.isEmpty(str)) { 10436 contentView.setViewVisibility(rowIds[i], View.VISIBLE); 10437 contentView.setTextViewText(rowIds[i], 10438 mBuilder.ensureColorSpanContrastOrStripStyling( 10439 mBuilder.processLegacyText(str), p)); 10440 mBuilder.setTextViewColorSecondary(contentView, rowIds[i], p); 10441 contentView.setViewPadding(rowIds[i], 0, topPadding, 0, 0); 10442 if (first) { 10443 onlyViewId = rowIds[i]; 10444 } else { 10445 onlyViewId = 0; 10446 } 10447 first = false; 10448 } 10449 i++; 10450 } 10451 if (onlyViewId != 0) { 10452 // We only have 1 entry, lets make it look like the normal Text of a Bigtext 10453 topPadding = mBuilder.mContext.getResources().getDimensionPixelSize( 10454 R.dimen.notification_text_margin_top); 10455 contentView.setViewPadding(onlyViewId, 0, topPadding, 0, 0); 10456 } 10457 10458 return contentView; 10459 } 10460 10461 /** 10462 * @hide 10463 */ 10464 @Override areNotificationsVisiblyDifferent(Style other)10465 public boolean areNotificationsVisiblyDifferent(Style other) { 10466 if (other == null || getClass() != other.getClass()) { 10467 return true; 10468 } 10469 InboxStyle newS = (InboxStyle) other; 10470 10471 final ArrayList<CharSequence> myLines = getLines(); 10472 final ArrayList<CharSequence> newLines = newS.getLines(); 10473 final int n = myLines.size(); 10474 if (n != newLines.size()) { 10475 return true; 10476 } 10477 10478 for (int i = 0; i < n; i++) { 10479 if (!Objects.equals( 10480 String.valueOf(myLines.get(i)), 10481 String.valueOf(newLines.get(i)))) { 10482 return true; 10483 } 10484 } 10485 return false; 10486 } 10487 } 10488 10489 /** 10490 * Notification style for media playback notifications. 10491 * 10492 * In the expanded form, {@link Notification#bigContentView}, up to 5 10493 * {@link Notification.Action}s specified with 10494 * {@link Notification.Builder#addAction(Action) addAction} will be 10495 * shown as icon-only pushbuttons, suitable for transport controls. The Bitmap given to 10496 * {@link Notification.Builder#setLargeIcon(android.graphics.Bitmap) setLargeIcon()} will be 10497 * treated as album artwork. 10498 * <p> 10499 * Unlike the other styles provided here, MediaStyle can also modify the standard-size 10500 * {@link Notification#contentView}; by providing action indices to 10501 * {@link #setShowActionsInCompactView(int...)} you can promote up to 3 actions to be displayed 10502 * in the standard view alongside the usual content. 10503 * <p> 10504 * Notifications created with MediaStyle will have their category set to 10505 * {@link Notification#CATEGORY_TRANSPORT CATEGORY_TRANSPORT} unless you set a different 10506 * category using {@link Notification.Builder#setCategory(String) setCategory()}. 10507 * <p> 10508 * Finally, if you attach a {@link android.media.session.MediaSession.Token} using 10509 * {@link android.app.Notification.MediaStyle#setMediaSession(MediaSession.Token)}, 10510 * the System UI can identify this as a notification representing an active media session 10511 * and respond accordingly (by showing album artwork in the lockscreen, for example). 10512 * 10513 * <p> 10514 * Starting at {@link android.os.Build.VERSION_CODES#O Android O} any notification that has a 10515 * media session attached with {@link #setMediaSession(MediaSession.Token)} will be colorized. 10516 * You can opt-out of this behavior by using {@link Notification.Builder#setColorized(boolean)}. 10517 * <p> 10518 * 10519 * <p> 10520 * Starting at {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM Android V} the 10521 * {@link Notification#FLAG_NO_CLEAR NO_CLEAR flag} will be set for valid MediaStyle 10522 * notifications. 10523 * <p> 10524 * 10525 * To use this style with your Notification, feed it to 10526 * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: 10527 * <pre class="prettyprint"> 10528 * Notification noti = new Notification.Builder() 10529 * .setSmallIcon(R.drawable.ic_stat_player) 10530 * .setContentTitle("Track title") 10531 * .setContentText("Artist - Album") 10532 * .setLargeIcon(albumArtBitmap)) 10533 * .setStyle(<b>new Notification.MediaStyle()</b> 10534 * .setMediaSession(mySession)) 10535 * .build(); 10536 * </pre> 10537 * 10538 * @see Notification#bigContentView 10539 * @see Notification.Builder#setColorized(boolean) 10540 */ 10541 public static class MediaStyle extends Style { 10542 // Changing max media buttons requires also changing templates 10543 // (notification_template_material_media and notification_template_material_big_media). 10544 static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3; 10545 static final int MAX_MEDIA_BUTTONS = 5; 10546 @IdRes private static final int[] MEDIA_BUTTON_IDS = { 10547 R.id.action0, 10548 R.id.action1, 10549 R.id.action2, 10550 R.id.action3, 10551 R.id.action4, 10552 }; 10553 10554 private int[] mActionsToShowInCompact = null; 10555 private MediaSession.Token mToken; 10556 private CharSequence mDeviceName; 10557 private int mDeviceIcon; 10558 private PendingIntent mDeviceIntent; 10559 MediaStyle()10560 public MediaStyle() { 10561 } 10562 10563 /** 10564 * @deprecated use {@code MediaStyle()}. 10565 */ 10566 @Deprecated MediaStyle(Builder builder)10567 public MediaStyle(Builder builder) { 10568 setBuilder(builder); 10569 } 10570 10571 /** 10572 * Request up to 3 actions (by index in the order of addition) to be shown in the compact 10573 * notification view. 10574 * 10575 * @param actions the indices of the actions to show in the compact notification view 10576 */ setShowActionsInCompactView(int...actions)10577 public MediaStyle setShowActionsInCompactView(int...actions) { 10578 mActionsToShowInCompact = actions; 10579 return this; 10580 } 10581 10582 /** 10583 * Attach a {@link android.media.session.MediaSession.Token} to this Notification 10584 * to provide additional playback information and control to the SystemUI. 10585 */ setMediaSession(MediaSession.Token token)10586 public MediaStyle setMediaSession(MediaSession.Token token) { 10587 mToken = token; 10588 return this; 10589 } 10590 10591 /** 10592 * For media notifications associated with playback on a remote device, provide device 10593 * information that will replace the default values for the output switcher chip on the 10594 * media control, as well as an intent to use when the output switcher chip is tapped, 10595 * on devices where this is supported. 10596 * <p> 10597 * This method is intended for system applications to provide information and/or 10598 * functionality that would otherwise be unavailable to the default output switcher because 10599 * the media originated on a remote device. 10600 * 10601 * @param deviceName The name of the remote device to display 10602 * @param iconResource Icon resource representing the device 10603 * @param chipIntent PendingIntent to send when the output switcher is tapped. May be 10604 * {@code null}, in which case the output switcher will be disabled. 10605 * This intent should open an Activity or it will be ignored. 10606 * @return MediaStyle 10607 */ 10608 @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) 10609 @NonNull setRemotePlaybackInfo(@onNull CharSequence deviceName, @DrawableRes int iconResource, @Nullable PendingIntent chipIntent)10610 public MediaStyle setRemotePlaybackInfo(@NonNull CharSequence deviceName, 10611 @DrawableRes int iconResource, @Nullable PendingIntent chipIntent) { 10612 mDeviceName = deviceName; 10613 mDeviceIcon = iconResource; 10614 mDeviceIntent = chipIntent; 10615 return this; 10616 } 10617 10618 /** 10619 * @hide 10620 */ 10621 @Override 10622 @UnsupportedAppUsage buildStyled(Notification wip)10623 public Notification buildStyled(Notification wip) { 10624 super.buildStyled(wip); 10625 if (wip.category == null) { 10626 wip.category = Notification.CATEGORY_TRANSPORT; 10627 } 10628 return wip; 10629 } 10630 10631 /** 10632 * @hide 10633 */ 10634 @Override makeContentView()10635 public RemoteViews makeContentView() { 10636 return makeMediaContentView(null /* customContent */); 10637 } 10638 10639 /** 10640 * @hide 10641 */ 10642 @Override makeExpandedContentView()10643 public RemoteViews makeExpandedContentView() { 10644 return makeMediaExpandedContentView(null /* customContent */); 10645 } 10646 10647 /** 10648 * @hide 10649 */ 10650 @Override makeHeadsUpContentView()10651 public RemoteViews makeHeadsUpContentView() { 10652 return makeMediaContentView(null /* customContent */); 10653 } 10654 10655 /** @hide */ 10656 @Override addExtras(Bundle extras)10657 public void addExtras(Bundle extras) { 10658 super.addExtras(extras); 10659 10660 if (mToken != null) { 10661 extras.putParcelable(EXTRA_MEDIA_SESSION, mToken); 10662 } 10663 if (mActionsToShowInCompact != null) { 10664 extras.putIntArray(EXTRA_COMPACT_ACTIONS, mActionsToShowInCompact); 10665 } 10666 if (mDeviceName != null) { 10667 extras.putCharSequence(EXTRA_MEDIA_REMOTE_DEVICE, mDeviceName); 10668 } 10669 if (mDeviceIcon > 0) { 10670 extras.putInt(EXTRA_MEDIA_REMOTE_ICON, mDeviceIcon); 10671 } 10672 if (mDeviceIntent != null) { 10673 extras.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, mDeviceIntent); 10674 } 10675 } 10676 10677 /** 10678 * @hide 10679 */ 10680 @Override restoreFromExtras(Bundle extras)10681 protected void restoreFromExtras(Bundle extras) { 10682 super.restoreFromExtras(extras); 10683 10684 if (extras.containsKey(EXTRA_MEDIA_SESSION)) { 10685 mToken = extras.getParcelable(EXTRA_MEDIA_SESSION, MediaSession.Token.class); 10686 } 10687 if (extras.containsKey(EXTRA_COMPACT_ACTIONS)) { 10688 mActionsToShowInCompact = extras.getIntArray(EXTRA_COMPACT_ACTIONS); 10689 } 10690 if (extras.containsKey(EXTRA_MEDIA_REMOTE_DEVICE)) { 10691 mDeviceName = extras.getCharSequence(EXTRA_MEDIA_REMOTE_DEVICE); 10692 } 10693 if (extras.containsKey(EXTRA_MEDIA_REMOTE_ICON)) { 10694 mDeviceIcon = extras.getInt(EXTRA_MEDIA_REMOTE_ICON); 10695 } 10696 if (extras.containsKey(EXTRA_MEDIA_REMOTE_INTENT)) { 10697 mDeviceIntent = extras.getParcelable( 10698 EXTRA_MEDIA_REMOTE_INTENT, PendingIntent.class); 10699 } 10700 } 10701 10702 /** 10703 * @hide 10704 */ 10705 @Override areNotificationsVisiblyDifferent(Style other)10706 public boolean areNotificationsVisiblyDifferent(Style other) { 10707 if (other == null || getClass() != other.getClass()) { 10708 return true; 10709 } 10710 // All fields to compare are on the Notification object 10711 return false; 10712 } 10713 bindMediaActionButton(RemoteViews container, @IdRes int buttonId, Action action, StandardTemplateParams p)10714 private void bindMediaActionButton(RemoteViews container, @IdRes int buttonId, 10715 Action action, StandardTemplateParams p) { 10716 final boolean tombstone = (action.actionIntent == null); 10717 container.setViewVisibility(buttonId, View.VISIBLE); 10718 container.setImageViewIcon(buttonId, action.getIcon()); 10719 10720 // If the action buttons should not be tinted, then just use the default 10721 // notification color. Otherwise, just use the passed-in color. 10722 int tintColor = mBuilder.getStandardActionColor(p); 10723 10724 container.setDrawableTint(buttonId, false, tintColor, 10725 PorterDuff.Mode.SRC_ATOP); 10726 10727 int rippleAlpha = mBuilder.getColors(p).getRippleAlpha(); 10728 int rippleColor = Color.argb(rippleAlpha, Color.red(tintColor), Color.green(tintColor), 10729 Color.blue(tintColor)); 10730 container.setRippleDrawableColor(buttonId, ColorStateList.valueOf(rippleColor)); 10731 10732 if (!tombstone) { 10733 container.setOnClickPendingIntent(buttonId, action.actionIntent); 10734 } 10735 container.setContentDescription(buttonId, action.title); 10736 } 10737 10738 /** @hide */ makeMediaContentView(@ullable RemoteViews customContent)10739 protected RemoteViews makeMediaContentView(@Nullable RemoteViews customContent) { 10740 final int numActions = mBuilder.mActions.size(); 10741 final int numActionsToShow = Math.min(mActionsToShowInCompact == null 10742 ? 0 : mActionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT); 10743 if (numActionsToShow > numActions) { 10744 throw new IllegalArgumentException(String.format( 10745 "setShowActionsInCompactView: action %d out of bounds (max %d)", 10746 numActions, numActions - 1)); 10747 } 10748 10749 StandardTemplateParams p = mBuilder.mParams.reset() 10750 .viewType(StandardTemplateParams.VIEW_TYPE_NORMAL) 10751 .hideTime(numActionsToShow > 1) // hide if actions wider than a right icon 10752 .hideSubText(numActionsToShow > 1) // hide if actions wider than a right icon 10753 .hideLeftIcon(false) // allow large icon on left when grouped 10754 .hideRightIcon(numActionsToShow > 0) // right icon or actions; not both 10755 .hideProgress(true) 10756 .fillTextsFrom(mBuilder); 10757 TemplateBindResult result = new TemplateBindResult(); 10758 RemoteViews template = mBuilder.applyStandardTemplate( 10759 mBuilder.getCollapsedMediaLayoutResource(), p, 10760 null /* result */); 10761 10762 for (int i = 0; i < MAX_MEDIA_BUTTONS_IN_COMPACT; i++) { 10763 if (i < numActionsToShow) { 10764 final Action action = mBuilder.mActions.get(mActionsToShowInCompact[i]); 10765 bindMediaActionButton(template, MEDIA_BUTTON_IDS[i], action, p); 10766 } else { 10767 template.setViewVisibility(MEDIA_BUTTON_IDS[i], View.GONE); 10768 } 10769 } 10770 // Prevent a swooping expand animation when there are no actions 10771 boolean hasActions = numActionsToShow != 0; 10772 template.setViewVisibility(R.id.media_actions, hasActions ? View.VISIBLE : View.GONE); 10773 10774 // Add custom view if provided by subclass. 10775 buildCustomContentIntoTemplate(mBuilder.mContext, template, customContent, p, result); 10776 return template; 10777 } 10778 10779 /** @hide */ makeMediaExpandedContentView(@ullable RemoteViews customContent)10780 protected RemoteViews makeMediaExpandedContentView(@Nullable RemoteViews customContent) { 10781 final int actionCount = Math.min(mBuilder.mActions.size(), MAX_MEDIA_BUTTONS); 10782 StandardTemplateParams p = mBuilder.mParams.reset() 10783 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED) 10784 .hideProgress(true) 10785 .fillTextsFrom(mBuilder); 10786 TemplateBindResult result = new TemplateBindResult(); 10787 RemoteViews template = mBuilder.applyStandardTemplate( 10788 mBuilder.getExpandedMediaLayoutResource(), p , result); 10789 10790 for (int i = 0; i < MAX_MEDIA_BUTTONS; i++) { 10791 if (i < actionCount) { 10792 bindMediaActionButton(template, 10793 MEDIA_BUTTON_IDS[i], mBuilder.mActions.get(i), p); 10794 } else { 10795 template.setViewVisibility(MEDIA_BUTTON_IDS[i], View.GONE); 10796 } 10797 } 10798 buildCustomContentIntoTemplate(mBuilder.mContext, template, customContent, p, result); 10799 return template; 10800 } 10801 } 10802 10803 /** 10804 * Helper class for generating large-format notifications that include a large image attachment. 10805 * 10806 * Here's how you'd set the <code>CallStyle</code> on a notification: 10807 * <pre class="prettyprint"> 10808 * Notification notif = new Notification.Builder(mContext) 10809 * .setSmallIcon(R.drawable.new_post) 10810 * .setStyle(Notification.CallStyle.forIncomingCall(caller, declineIntent, answerIntent)) 10811 * .build(); 10812 * </pre> 10813 */ 10814 public static class CallStyle extends Style { 10815 /** 10816 * @hide 10817 */ 10818 public static final boolean DEBUG_NEW_ACTION_LAYOUT = true; 10819 10820 /** 10821 * @hide 10822 */ 10823 @Retention(RetentionPolicy.SOURCE) 10824 @IntDef({ 10825 CALL_TYPE_UNKNOWN, 10826 CALL_TYPE_INCOMING, 10827 CALL_TYPE_ONGOING, 10828 CALL_TYPE_SCREENING 10829 }) 10830 public @interface CallType {}; 10831 10832 /** 10833 * Unknown call type. 10834 * 10835 * See {@link #EXTRA_CALL_TYPE}. 10836 */ 10837 public static final int CALL_TYPE_UNKNOWN = 0; 10838 10839 /** 10840 * Call type for incoming calls. 10841 * 10842 * See {@link #EXTRA_CALL_TYPE}. 10843 */ 10844 public static final int CALL_TYPE_INCOMING = 1; 10845 /** 10846 * Call type for ongoing calls. 10847 * 10848 * See {@link #EXTRA_CALL_TYPE}. 10849 */ 10850 public static final int CALL_TYPE_ONGOING = 2; 10851 /** 10852 * Call type for calls that are being screened. 10853 * 10854 * See {@link #EXTRA_CALL_TYPE}. 10855 */ 10856 public static final int CALL_TYPE_SCREENING = 3; 10857 10858 /** 10859 * This is a key used privately on the action.extras to give spacing priority 10860 * to the required call actions 10861 */ 10862 private static final String KEY_ACTION_PRIORITY = "key_action_priority"; 10863 10864 private int mCallType; 10865 private Person mPerson; 10866 private PendingIntent mAnswerIntent; 10867 private PendingIntent mDeclineIntent; 10868 private PendingIntent mHangUpIntent; 10869 private boolean mIsVideo; 10870 private Integer mAnswerButtonColor; 10871 private Integer mDeclineButtonColor; 10872 private Icon mVerificationIcon; 10873 private CharSequence mVerificationText; 10874 CallStyle()10875 CallStyle() { 10876 } 10877 10878 /** 10879 * Create a CallStyle for an incoming call. 10880 * This notification will have a decline and an answer action, will allow a single 10881 * custom {@link Builder#addAction(Action) action}, and will have a default 10882 * {@link Builder#setContentText(CharSequence) content text} for an incoming call. 10883 * 10884 * @param person The person displayed as the caller. 10885 * The person also needs to have a non-empty name associated with it. 10886 * @param declineIntent The intent to be sent when the user taps the decline action 10887 * @param answerIntent The intent to be sent when the user taps the answer action 10888 */ 10889 @NonNull forIncomingCall(@onNull Person person, @NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent)10890 public static CallStyle forIncomingCall(@NonNull Person person, 10891 @NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent) { 10892 return new CallStyle(CALL_TYPE_INCOMING, person, 10893 null /* hangUpIntent */, 10894 requireNonNull(declineIntent, "declineIntent is required"), 10895 requireNonNull(answerIntent, "answerIntent is required") 10896 ); 10897 } 10898 10899 /** 10900 * Create a CallStyle for an ongoing call. 10901 * This notification will have a hang up action, will allow up to two 10902 * custom {@link Builder#addAction(Action) actions}, and will have a default 10903 * {@link Builder#setContentText(CharSequence) content text} for an ongoing call. 10904 * 10905 * @param person The person displayed as being on the other end of the call. 10906 * The person also needs to have a non-empty name associated with it. 10907 * @param hangUpIntent The intent to be sent when the user taps the hang up action 10908 */ 10909 @NonNull forOngoingCall(@onNull Person person, @NonNull PendingIntent hangUpIntent)10910 public static CallStyle forOngoingCall(@NonNull Person person, 10911 @NonNull PendingIntent hangUpIntent) { 10912 return new CallStyle(CALL_TYPE_ONGOING, person, 10913 requireNonNull(hangUpIntent, "hangUpIntent is required"), 10914 null /* declineIntent */, 10915 null /* answerIntent */ 10916 ); 10917 } 10918 10919 /** 10920 * Create a CallStyle for a call that is being screened. 10921 * This notification will have a hang up and an answer action, will allow a single 10922 * custom {@link Builder#addAction(Action) action}, and will have a default 10923 * {@link Builder#setContentText(CharSequence) content text} for a call that is being 10924 * screened. 10925 * 10926 * @param person The person displayed as the caller. 10927 * The person also needs to have a non-empty name associated with it. 10928 * @param hangUpIntent The intent to be sent when the user taps the hang up action 10929 * @param answerIntent The intent to be sent when the user taps the answer action 10930 */ 10931 @NonNull forScreeningCall(@onNull Person person, @NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent)10932 public static CallStyle forScreeningCall(@NonNull Person person, 10933 @NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent) { 10934 return new CallStyle(CALL_TYPE_SCREENING, person, 10935 requireNonNull(hangUpIntent, "hangUpIntent is required"), 10936 null /* declineIntent */, 10937 requireNonNull(answerIntent, "answerIntent is required") 10938 ); 10939 } 10940 10941 /** 10942 * @param callType The type of the call 10943 * @param person The person displayed for the incoming call. 10944 * The user also needs to have a non-empty name associated with it. 10945 * @param hangUpIntent The intent to be sent when the user taps the hang up action 10946 * @param declineIntent The intent to be sent when the user taps the decline action 10947 * @param answerIntent The intent to be sent when the user taps the answer action 10948 */ CallStyle(@allType int callType, @NonNull Person person, @Nullable PendingIntent hangUpIntent, @Nullable PendingIntent declineIntent, @Nullable PendingIntent answerIntent)10949 private CallStyle(@CallType int callType, @NonNull Person person, 10950 @Nullable PendingIntent hangUpIntent, @Nullable PendingIntent declineIntent, 10951 @Nullable PendingIntent answerIntent) { 10952 if (person == null || TextUtils.isEmpty(person.getName())) { 10953 throw new IllegalArgumentException("person must have a non-empty a name"); 10954 } 10955 mCallType = callType; 10956 mPerson = person; 10957 mAnswerIntent = answerIntent; 10958 mDeclineIntent = declineIntent; 10959 mHangUpIntent = hangUpIntent; 10960 } 10961 10962 /** 10963 * Sets whether the call is a video call, which may affect the icons or text used on the 10964 * required action buttons. 10965 */ 10966 @NonNull setIsVideo(boolean isVideo)10967 public CallStyle setIsVideo(boolean isVideo) { 10968 mIsVideo = isVideo; 10969 return this; 10970 } 10971 10972 /** 10973 * Optional icon to be displayed with {@link #setVerificationText(CharSequence) text} 10974 * as a verification status of the caller. 10975 */ 10976 @NonNull setVerificationIcon(@ullable Icon verificationIcon)10977 public CallStyle setVerificationIcon(@Nullable Icon verificationIcon) { 10978 mVerificationIcon = verificationIcon; 10979 return this; 10980 } 10981 10982 /** 10983 * Optional text to be displayed with an {@link #setVerificationIcon(Icon) icon} 10984 * as a verification status of the caller. 10985 */ 10986 @NonNull setVerificationText(@ullable CharSequence verificationText)10987 public CallStyle setVerificationText(@Nullable CharSequence verificationText) { 10988 mVerificationText = safeCharSequence(verificationText); 10989 return this; 10990 } 10991 10992 /** 10993 * Optional color to be used as a hint for the Answer action button's color. 10994 * The system may change this color to ensure sufficient contrast with the background. 10995 * The system may choose to disregard this hint if the notification is not colorized. 10996 */ 10997 @NonNull setAnswerButtonColorHint(@olorInt int color)10998 public CallStyle setAnswerButtonColorHint(@ColorInt int color) { 10999 mAnswerButtonColor = color; 11000 return this; 11001 } 11002 11003 /** 11004 * Optional color to be used as a hint for the Decline or Hang Up action button's color. 11005 * The system may change this color to ensure sufficient contrast with the background. 11006 * The system may choose to disregard this hint if the notification is not colorized. 11007 */ 11008 @NonNull setDeclineButtonColorHint(@olorInt int color)11009 public CallStyle setDeclineButtonColorHint(@ColorInt int color) { 11010 mDeclineButtonColor = color; 11011 return this; 11012 } 11013 11014 /** @hide */ 11015 @Override buildStyled(Notification wip)11016 public Notification buildStyled(Notification wip) { 11017 wip = super.buildStyled(wip); 11018 // ensure that the actions in the builder and notification are corrected. 11019 mBuilder.mActions = getActionsListWithSystemActions(); 11020 wip.actions = new Action[mBuilder.mActions.size()]; 11021 mBuilder.mActions.toArray(wip.actions); 11022 return wip; 11023 } 11024 11025 /** 11026 * @hide 11027 */ displayCustomViewInline()11028 public boolean displayCustomViewInline() { 11029 // This is a lie; True is returned to make sure that the custom view is not used 11030 // instead of the template, but it will not actually be included. 11031 return true; 11032 } 11033 11034 /** 11035 * @hide 11036 */ 11037 @Override purgeResources()11038 public void purgeResources() { 11039 super.purgeResources(); 11040 if (mVerificationIcon != null) { 11041 mVerificationIcon.convertToAshmem(); 11042 } 11043 } 11044 11045 /** 11046 * @hide 11047 */ 11048 @Override reduceImageSizes(Context context)11049 public void reduceImageSizes(Context context) { 11050 super.reduceImageSizes(context); 11051 if (mVerificationIcon != null) { 11052 int rightIconSize = context.getResources().getDimensionPixelSize( 11053 ActivityManager.isLowRamDeviceStatic() 11054 ? R.dimen.notification_right_icon_size_low_ram 11055 : R.dimen.notification_right_icon_size); 11056 mVerificationIcon.scaleDownIfNecessary(rightIconSize, rightIconSize); 11057 } 11058 } 11059 11060 /** 11061 * @hide 11062 */ 11063 @Override makeContentView()11064 public RemoteViews makeContentView() { 11065 return makeCallLayout(StandardTemplateParams.VIEW_TYPE_NORMAL); 11066 } 11067 11068 /** 11069 * @hide 11070 */ 11071 @Override makeHeadsUpContentView()11072 public RemoteViews makeHeadsUpContentView() { 11073 return makeCallLayout(StandardTemplateParams.VIEW_TYPE_HEADS_UP); 11074 } 11075 11076 /** 11077 * @hide 11078 */ 11079 @Nullable 11080 @Override makeCompactHeadsUpContentView()11081 public RemoteViews makeCompactHeadsUpContentView() { 11082 // Use existing heads up for call style. 11083 return makeHeadsUpContentView(); 11084 } 11085 11086 /** 11087 * @hide 11088 */ makeExpandedContentView()11089 public RemoteViews makeExpandedContentView() { 11090 return makeCallLayout(StandardTemplateParams.VIEW_TYPE_EXPANDED); 11091 } 11092 11093 @NonNull makeNegativeAction()11094 private Action makeNegativeAction() { 11095 if (mDeclineIntent == null) { 11096 return makeAction(R.drawable.ic_call_decline, 11097 R.string.call_notification_hang_up_action, 11098 mDeclineButtonColor, R.color.call_notification_decline_color, 11099 mHangUpIntent); 11100 } else { 11101 return makeAction(R.drawable.ic_call_decline, 11102 R.string.call_notification_decline_action, 11103 mDeclineButtonColor, R.color.call_notification_decline_color, 11104 mDeclineIntent); 11105 } 11106 } 11107 11108 @Nullable makeAnswerAction()11109 private Action makeAnswerAction() { 11110 return mAnswerIntent == null ? null : makeAction( 11111 mIsVideo ? R.drawable.ic_call_answer_video : R.drawable.ic_call_answer, 11112 mIsVideo ? R.string.call_notification_answer_video_action 11113 : R.string.call_notification_answer_action, 11114 mAnswerButtonColor, R.color.call_notification_answer_color, 11115 mAnswerIntent); 11116 } 11117 11118 @NonNull makeAction(@rawableRes int icon, @StringRes int title, @ColorInt Integer colorInt, @ColorRes int defaultColorRes, PendingIntent intent)11119 private Action makeAction(@DrawableRes int icon, @StringRes int title, 11120 @ColorInt Integer colorInt, @ColorRes int defaultColorRes, PendingIntent intent) { 11121 if (colorInt == null || !mBuilder.isCallActionColorCustomizable()) { 11122 colorInt = mBuilder.mContext.getColor(defaultColorRes); 11123 } 11124 Action action = new Action.Builder(Icon.createWithResource("", icon), 11125 new SpannableStringBuilder().append(mBuilder.mContext.getString(title), 11126 new ForegroundColorSpan(colorInt), 11127 SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE), 11128 intent).build(); 11129 action.getExtras().putBoolean(KEY_ACTION_PRIORITY, true); 11130 return action; 11131 } 11132 isActionAddedByCallStyle(Action action)11133 private boolean isActionAddedByCallStyle(Action action) { 11134 // This is an internal extra added by the style to these actions. If an app were to add 11135 // this extra to the action themselves, the action would be dropped. :shrug: 11136 return action != null && action.getExtras().getBoolean(KEY_ACTION_PRIORITY); 11137 } 11138 11139 /** 11140 * Gets the actions list for the call with the answer/decline/hangUp actions inserted in 11141 * the correct place. This returns the correct result even if the system actions have 11142 * already been added, and even if more actions were added since then. 11143 * @hide 11144 */ 11145 @NonNull getActionsListWithSystemActions()11146 public ArrayList<Action> getActionsListWithSystemActions() { 11147 // Define the system actions we expect to see 11148 final Action firstAction = makeNegativeAction(); 11149 final Action lastAction = makeAnswerAction(); 11150 11151 // Start creating the result list. 11152 int nonContextualActionSlotsRemaining = MAX_ACTION_BUTTONS; 11153 ArrayList<Action> resultActions = new ArrayList<>(MAX_ACTION_BUTTONS); 11154 11155 // Always have a first action. 11156 resultActions.add(firstAction); 11157 --nonContextualActionSlotsRemaining; 11158 11159 // Copy actions into the new list, correcting system actions. 11160 if (mBuilder.mActions != null) { 11161 for (Notification.Action action : mBuilder.mActions) { 11162 if (action.isContextual()) { 11163 // Always include all contextual actions 11164 resultActions.add(action); 11165 } else if (isActionAddedByCallStyle(action)) { 11166 // Drop any old versions of system actions 11167 } else { 11168 // Copy non-contextual actions; decrement the remaining action slots. 11169 resultActions.add(action); 11170 --nonContextualActionSlotsRemaining; 11171 } 11172 // If there's exactly one action slot left, fill it with the lastAction. 11173 if (lastAction != null && nonContextualActionSlotsRemaining == 1) { 11174 resultActions.add(lastAction); 11175 --nonContextualActionSlotsRemaining; 11176 } 11177 } 11178 } 11179 // If there are any action slots left, the lastAction still needs to be added. 11180 if (lastAction != null && nonContextualActionSlotsRemaining >= 1) { 11181 resultActions.add(lastAction); 11182 } 11183 return resultActions; 11184 } 11185 makeCallLayout(int viewType)11186 private RemoteViews makeCallLayout(int viewType) { 11187 final boolean isCollapsed = viewType == StandardTemplateParams.VIEW_TYPE_NORMAL; 11188 final boolean isHeadsUp = viewType == StandardTemplateParams.VIEW_TYPE_HEADS_UP; 11189 Bundle extras = mBuilder.mN.extras; 11190 CharSequence title = mPerson != null ? mPerson.getName() : null; 11191 CharSequence text = mBuilder.processLegacyText(extras.getCharSequence(EXTRA_TEXT)); 11192 if (text == null) { 11193 text = getDefaultText(); 11194 } 11195 11196 // Bind standard template 11197 StandardTemplateParams p = mBuilder.mParams.reset() 11198 .viewType(viewType) 11199 .callStyleActions(true) 11200 .allowTextWithProgress(true) 11201 .hideLeftIcon(true) 11202 .hideRightIcon(true) 11203 .hideAppName(isCollapsed) 11204 .title(title) 11205 .text(text); 11206 if (!notificationsRedesignTemplates()) { 11207 // We're using the normal title in the redesign, not a special text. 11208 p.titleViewId(R.id.conversation_text) 11209 // The verification text is now part of the top line views, so this is no 11210 // longer necessary. 11211 .summaryText(mBuilder.processLegacyText(mVerificationText)); 11212 } 11213 mBuilder.mActions = getActionsListWithSystemActions(); 11214 final RemoteViews contentView; 11215 if (isCollapsed) { 11216 contentView = mBuilder.applyStandardTemplate( 11217 mBuilder.getCollapsedCallLayoutResource(), p, null /* result */); 11218 } else if (notificationsRedesignTemplates() && isHeadsUp) { 11219 contentView = mBuilder.applyStandardTemplateWithActions( 11220 mBuilder.getCollapsedCallLayoutResource(), p, null /* result */); 11221 } else { 11222 contentView = mBuilder.applyStandardTemplateWithActions( 11223 mBuilder.getExpandedCallLayoutResource(), p, null /* result */); 11224 } 11225 11226 // Bind some extra conversation-specific header fields. 11227 if (!notificationsRedesignTemplates() && !p.mHideAppName) { 11228 // Redesign note: This special divider is no longer needed. 11229 mBuilder.setTextViewColorSecondary(contentView, R.id.app_name_divider, p); 11230 contentView.setViewVisibility(R.id.app_name_divider, View.VISIBLE); 11231 } 11232 bindCallerVerification(contentView, p); 11233 11234 // Bind some custom CallLayout properties 11235 contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", 11236 mBuilder.getSmallIconColor(p)); 11237 contentView.setInt(R.id.status_bar_latest_event_content, 11238 "setNotificationBackgroundColor", mBuilder.getBackgroundColor(p)); 11239 contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon", 11240 mBuilder.mN.mLargeIcon); 11241 contentView.setBundle(R.id.status_bar_latest_event_content, "setData", 11242 mBuilder.mN.extras); 11243 11244 return contentView; 11245 } 11246 bindCallerVerification(RemoteViews contentView, StandardTemplateParams p)11247 private void bindCallerVerification(RemoteViews contentView, StandardTemplateParams p) { 11248 String iconContentDescription = null; 11249 boolean showDivider = true; 11250 if (mVerificationIcon != null) { 11251 contentView.setImageViewIcon(R.id.verification_icon, mVerificationIcon); 11252 contentView.setDrawableTint(R.id.verification_icon, false /* targetBackground */, 11253 mBuilder.getSecondaryTextColor(p), PorterDuff.Mode.SRC_ATOP); 11254 contentView.setViewVisibility(R.id.verification_icon, View.VISIBLE); 11255 iconContentDescription = mBuilder.mContext.getString( 11256 R.string.notification_verified_content_description); 11257 showDivider = false; // the icon replaces the divider 11258 } else { 11259 contentView.setViewVisibility(R.id.verification_icon, View.GONE); 11260 } 11261 if (!TextUtils.isEmpty(mVerificationText)) { 11262 contentView.setTextViewText(R.id.verification_text, mVerificationText); 11263 mBuilder.setTextViewColorSecondary(contentView, R.id.verification_text, p); 11264 contentView.setViewVisibility(R.id.verification_text, View.VISIBLE); 11265 iconContentDescription = null; // let the app's text take precedence 11266 } else { 11267 contentView.setViewVisibility(R.id.verification_text, View.GONE); 11268 showDivider = false; // no divider if no text 11269 } 11270 contentView.setContentDescription(R.id.verification_icon, iconContentDescription); 11271 if (showDivider) { 11272 contentView.setViewVisibility(R.id.verification_divider, View.VISIBLE); 11273 mBuilder.setTextViewColorSecondary(contentView, R.id.verification_divider, p); 11274 } else { 11275 contentView.setViewVisibility(R.id.verification_divider, View.GONE); 11276 } 11277 } 11278 11279 @Nullable getDefaultText()11280 private String getDefaultText() { 11281 switch (mCallType) { 11282 case CALL_TYPE_INCOMING: 11283 return mBuilder.mContext.getString(R.string.call_notification_incoming_text); 11284 case CALL_TYPE_ONGOING: 11285 return mBuilder.mContext.getString(R.string.call_notification_ongoing_text); 11286 case CALL_TYPE_SCREENING: 11287 return mBuilder.mContext.getString(R.string.call_notification_screening_text); 11288 } 11289 return null; 11290 } 11291 11292 /** 11293 * @hide 11294 */ addExtras(Bundle extras)11295 public void addExtras(Bundle extras) { 11296 super.addExtras(extras); 11297 extras.putInt(EXTRA_CALL_TYPE, mCallType); 11298 extras.putBoolean(EXTRA_CALL_IS_VIDEO, mIsVideo); 11299 extras.putParcelable(EXTRA_CALL_PERSON, mPerson); 11300 if (mVerificationIcon != null) { 11301 extras.putParcelable(EXTRA_VERIFICATION_ICON, mVerificationIcon); 11302 } 11303 if (mVerificationText != null) { 11304 extras.putCharSequence(EXTRA_VERIFICATION_TEXT, mVerificationText); 11305 } 11306 if (mAnswerIntent != null) { 11307 extras.putParcelable(EXTRA_ANSWER_INTENT, mAnswerIntent); 11308 } 11309 if (mDeclineIntent != null) { 11310 extras.putParcelable(EXTRA_DECLINE_INTENT, mDeclineIntent); 11311 } 11312 if (mHangUpIntent != null) { 11313 extras.putParcelable(EXTRA_HANG_UP_INTENT, mHangUpIntent); 11314 } 11315 if (mAnswerButtonColor != null) { 11316 extras.putInt(EXTRA_ANSWER_COLOR, mAnswerButtonColor); 11317 } 11318 if (mDeclineButtonColor != null) { 11319 extras.putInt(EXTRA_DECLINE_COLOR, mDeclineButtonColor); 11320 } 11321 fixTitleAndTextExtras(extras); 11322 } 11323 fixTitleAndTextExtras(Bundle extras)11324 private void fixTitleAndTextExtras(Bundle extras) { 11325 CharSequence sender = mPerson != null ? mPerson.getName() : null; 11326 if (sender != null) { 11327 extras.putCharSequence(EXTRA_TITLE, sender); 11328 } 11329 if (extras.getCharSequence(EXTRA_TEXT) == null) { 11330 extras.putCharSequence(EXTRA_TEXT, getDefaultText()); 11331 } 11332 } 11333 11334 /** 11335 * @hide 11336 */ 11337 @Override restoreFromExtras(Bundle extras)11338 protected void restoreFromExtras(Bundle extras) { 11339 super.restoreFromExtras(extras); 11340 mCallType = extras.getInt(EXTRA_CALL_TYPE); 11341 mIsVideo = extras.getBoolean(EXTRA_CALL_IS_VIDEO); 11342 mPerson = extras.getParcelable(EXTRA_CALL_PERSON, Person.class); 11343 mVerificationIcon = extras.getParcelable(EXTRA_VERIFICATION_ICON, android.graphics.drawable.Icon.class); 11344 mVerificationText = extras.getCharSequence(EXTRA_VERIFICATION_TEXT); 11345 mAnswerIntent = extras.getParcelable(EXTRA_ANSWER_INTENT, PendingIntent.class); 11346 mDeclineIntent = extras.getParcelable(EXTRA_DECLINE_INTENT, PendingIntent.class); 11347 mHangUpIntent = extras.getParcelable(EXTRA_HANG_UP_INTENT, PendingIntent.class); 11348 mAnswerButtonColor = extras.containsKey(EXTRA_ANSWER_COLOR) 11349 ? extras.getInt(EXTRA_ANSWER_COLOR) : null; 11350 mDeclineButtonColor = extras.containsKey(EXTRA_DECLINE_COLOR) 11351 ? extras.getInt(EXTRA_DECLINE_COLOR) : null; 11352 } 11353 11354 /** 11355 * @hide 11356 */ 11357 @Override hasSummaryInHeader()11358 public boolean hasSummaryInHeader() { 11359 return false; 11360 } 11361 11362 /** 11363 * @hide 11364 */ 11365 @Override areNotificationsVisiblyDifferent(Style other)11366 public boolean areNotificationsVisiblyDifferent(Style other) { 11367 if (other == null || getClass() != other.getClass()) { 11368 return true; 11369 } 11370 CallStyle otherS = (CallStyle) other; 11371 return !Objects.equals(mCallType, otherS.mCallType) 11372 || !Objects.equals(mPerson, otherS.mPerson) 11373 || !Objects.equals(mVerificationText, otherS.mVerificationText); 11374 } 11375 } 11376 11377 /** 11378 * A Notification Style used to define a notification whose expanded state includes 11379 * a highly customizable progress bar with segments, points, a custom tracker icon, 11380 * and custom icons at the start and end of the progress bar. 11381 * 11382 * This style is suggested for use cases where the app is showing a tracker to the 11383 * user of a thing they are interested in: the location of a car on its way 11384 * to pick them up, food being delivered, or their own progress in a navigation 11385 * journey. 11386 * 11387 * To use this style with your Notification, feed it to 11388 * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: 11389 * <pre class="prettyprint"> 11390 * new Notification.Builder(context) 11391 * .setSmallIcon(R.drawable.ic_notification) 11392 * .setColor(Color.GREEN) 11393 * .setColorized(true) 11394 * .setContentTitle("Arrive 10:08 AM"). 11395 * .setContentText("Dominique Ansel Bakery Soho") 11396 * .addAction(new Notification.Action("Exit navigation",...)) 11397 * .setStyle(new Notification.ProgressStyle() 11398 * .setStyledByProgress(false) 11399 * .setProgress(456) 11400 * .setProgressTrackerIcon(Icon.createWithResource(R.drawable.ic_driving_tracker)) 11401 * .addProgressSegment(new Segment(41).setColor(Color.BLACK)) 11402 * .addProgressSegment(new Segment(552).setColor(Color.YELLOW)) 11403 * .addProgressSegment(new Segment(253).setColor(Color.YELLOW)) 11404 * .addProgressSegment(new Segment(94).setColor(Color.BLUE)) 11405 * .addProgressPoint(new Point(60).setColor(Color.RED)) 11406 * .addProgressPoint(new Point(560).setColor(Color.YELLOW)) 11407 * ) 11408 * </pre> 11409 * 11410 * 11411 * <p> 11412 * NOTE: The progress bar layout will be mirrored for RTL layout. 11413 * </p> 11414 * 11415 * <p> 11416 * NOTE: The extras set by {@link Notification.Builder#setProgress} will be overridden by 11417 * the values set on this style object when the notification is built. Therefore, that method 11418 * is not used with this style. 11419 * </p> 11420 * 11421 */ 11422 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) 11423 public static class ProgressStyle extends Notification.Style { 11424 private static final String KEY_ELEMENT_ID = "id"; 11425 private static final String KEY_ELEMENT_COLOR = "colorInt"; 11426 private static final String KEY_SEGMENT_LENGTH = "length"; 11427 private static final String KEY_POINT_POSITION = "position"; 11428 11429 private static final int MAX_PROGRESS_SEGMENT_LIMIT = 10; 11430 private static final int MAX_PROGRESS_POINT_LIMIT = 4; 11431 private static final int DEFAULT_PROGRESS_MAX = 100; 11432 11433 private List<Segment> mProgressSegments = new ArrayList<>(); 11434 private List<Point> mProgressPoints = new ArrayList<>(); 11435 11436 private int mProgress = 0; 11437 11438 private boolean mIndeterminate; 11439 11440 private boolean mIsStyledByProgress = true; 11441 11442 @Nullable 11443 private Icon mTrackerIcon; 11444 @Nullable 11445 private Icon mStartIcon; 11446 @Nullable 11447 private Icon mEndIcon; 11448 11449 /** 11450 * @hide 11451 */ 11452 @Override areNotificationsVisiblyDifferent(Style other)11453 public boolean areNotificationsVisiblyDifferent(Style other) { 11454 if (other == null || getClass() != other.getClass()) { 11455 return true; 11456 } 11457 11458 final ProgressStyle progressStyle = (ProgressStyle) other; 11459 11460 /** 11461 * @see #setProgressIndeterminate 11462 */ 11463 if (!Objects.equals(mIndeterminate, progressStyle.mIndeterminate)) { 11464 return true; 11465 } 11466 boolean nonIndeterminateCheckResult = false; 11467 if (!mIndeterminate) { 11468 nonIndeterminateCheckResult = !Objects.equals(mProgress, progressStyle.mProgress) 11469 || !Objects.equals(mIsStyledByProgress, progressStyle.mIsStyledByProgress) 11470 || !Objects.equals(mProgressSegments, progressStyle.mProgressSegments) 11471 || !Objects.equals(mProgressPoints, progressStyle.mProgressPoints) 11472 || !Objects.equals(mTrackerIcon, progressStyle.mTrackerIcon); 11473 } 11474 11475 return !Objects.equals(mStartIcon, progressStyle.mStartIcon) 11476 || !Objects.equals(mEndIcon, progressStyle.mEndIcon) 11477 || nonIndeterminateCheckResult; 11478 } 11479 11480 /** 11481 * Gets the segments that define the background layer of the progress bar. 11482 * 11483 * If no segments are provided, the progress bar will be rendered with a single segment 11484 * with length 100 and default color. 11485 * 11486 * @see #setProgressSegments 11487 * @see #addProgressSegment 11488 * @see Segment 11489 */ getProgressSegments()11490 public @NonNull List<Segment> getProgressSegments() { 11491 return mProgressSegments; 11492 } 11493 11494 /** 11495 * Sets or replaces the segments of the progress bar. 11496 * 11497 * Segments allow for creating progress bars with multiple colors or sections 11498 * to represent different stages or categories of progress. 11499 * For example, Traffic conditions along a navigation journey. 11500 * @see Segment 11501 */ setProgressSegments(@onNull List<Segment> progressSegments)11502 public @NonNull ProgressStyle setProgressSegments(@NonNull List<Segment> progressSegments) { 11503 if (mProgressSegments == null) { 11504 mProgressSegments = new ArrayList<>(); 11505 } 11506 mProgressSegments.clear(); 11507 for (Segment segment : progressSegments) { 11508 addProgressSegment(segment); 11509 } 11510 return this; 11511 } 11512 11513 /** 11514 * Appends a segment to the end of the progress bar. 11515 * 11516 * Segments allow for creating progress bars with multiple colors or sections 11517 * to represent different stages or categories of progress. 11518 * For example, Traffic conditions along a navigation journey. 11519 * @see Segment 11520 */ addProgressSegment(@onNull Segment segment)11521 public @NonNull ProgressStyle addProgressSegment(@NonNull Segment segment) { 11522 if (mProgressSegments == null) { 11523 mProgressSegments = new ArrayList<>(); 11524 } 11525 if (segment.getLength() > 0) { 11526 mProgressSegments.add(segment); 11527 } else { 11528 Log.w(TAG, "Dropped the segment. The length is not a positive integer."); 11529 } 11530 11531 return this; 11532 } 11533 11534 /** 11535 * Gets the points that are displayed on the progress bar. 11536 *. 11537 * @see #setProgressPoints 11538 * @see #addProgressPoint 11539 * @see Point 11540 */ getProgressPoints()11541 public @NonNull List<Point> getProgressPoints() { 11542 return mProgressPoints; 11543 } 11544 11545 /** 11546 * Replaces all the progress points. 11547 * 11548 * Points within a progress bar are used to visualize distinct stages or milestones. 11549 * For example, you might use points to mark stops in a multi-stop 11550 * navigation journey, where each point represents a destination. 11551 * @see Point 11552 */ setProgressPoints(@onNull List<Point> points)11553 public @NonNull ProgressStyle setProgressPoints(@NonNull List<Point> points) { 11554 if (mProgressPoints == null) { 11555 mProgressPoints = new ArrayList<>(); 11556 } 11557 mProgressPoints.clear(); 11558 11559 for (Point point: points) { 11560 addProgressPoint(point); 11561 } 11562 return this; 11563 } 11564 11565 /** 11566 * Adds another point. 11567 * 11568 * Points within a progress bar are used to visualize distinct stages or milestones. 11569 * 11570 * For example, you might use points to mark stops in a multi-stop 11571 * navigation journey, where each point represents a destination. 11572 * 11573 * Points can be added in any order, as their 11574 * position within the progress bar is determined by their individual 11575 * {@link Point#getPosition()}. 11576 * @see Point 11577 */ addProgressPoint(@onNull Point point)11578 public @NonNull ProgressStyle addProgressPoint(@NonNull Point point) { 11579 if (mProgressPoints == null) { 11580 mProgressPoints = new ArrayList<>(); 11581 } 11582 if (point.getPosition() > 0) { 11583 mProgressPoints.add(point); 11584 11585 if (mProgressPoints.size() > MAX_PROGRESS_POINT_LIMIT) { 11586 Log.w(TAG, "Progress points limit is reached. First" 11587 + MAX_PROGRESS_POINT_LIMIT + " points will be rendered."); 11588 } 11589 11590 } else { 11591 Log.w(TAG, "Dropped the point. The position is a negative or zero integer."); 11592 } 11593 11594 return this; 11595 } 11596 11597 /** 11598 * Gets the progress value of the progress bar. 11599 * @see #setProgress 11600 */ getProgress()11601 public int getProgress() { 11602 return mProgress; 11603 } 11604 11605 /** 11606 * Specifies the progress (in the same units as {@link Segment#getLength()}) 11607 * of the tracker along the length of the bar. 11608 * 11609 * The max progress value is the sum of all Segment lengths. 11610 * The default value is 0. 11611 */ setProgress(int progress)11612 public @NonNull ProgressStyle setProgress(int progress) { 11613 mProgress = progress; 11614 return this; 11615 } 11616 11617 /** 11618 * Gets the sum of the lengths of all Segments in the style, which 11619 * defines the maximum progress. Defaults to 100 when segments are omitted. 11620 */ getProgressMax()11621 public int getProgressMax() { 11622 final List<Segment> progressSegment = mProgressSegments; 11623 if (progressSegment == null || progressSegment.isEmpty()) { 11624 return DEFAULT_PROGRESS_MAX; 11625 } else { 11626 int progressMax = 0; 11627 int validSegmentCount = 0; 11628 for (int i = 0; i < progressSegment.size(); i++) { 11629 int segmentLength = progressSegment.get(i).getLength(); 11630 if (segmentLength > 0) { 11631 try { 11632 progressMax = Math.addExact(progressMax, segmentLength); 11633 validSegmentCount++; 11634 } catch (ArithmeticException e) { 11635 Log.e(TAG, 11636 "Notification.ProgressStyle segment total overflowed.", e); 11637 return DEFAULT_PROGRESS_MAX; 11638 } 11639 } 11640 } 11641 11642 if (validSegmentCount == 0) { 11643 return DEFAULT_PROGRESS_MAX; 11644 } 11645 11646 return progressMax; 11647 } 11648 11649 } 11650 11651 /** 11652 * Get indeterminate value of the progress bar. 11653 * @see #setProgressIndeterminate 11654 */ isProgressIndeterminate()11655 public boolean isProgressIndeterminate() { 11656 return mIndeterminate; 11657 } 11658 11659 /** 11660 * Used to indicate an initialization state without a known progress amount. 11661 * When specified, the following fields are ignored: 11662 * @see #setProgress 11663 * @see #setProgressSegments 11664 * @see #setProgressPoints 11665 * @see #setProgressTrackerIcon 11666 * @see #setStyledByProgress 11667 * 11668 * If the app provides exactly one Segment, that segment's color will be 11669 * used to style the indeterminate bar. 11670 */ setProgressIndeterminate(boolean indeterminate)11671 public @NonNull ProgressStyle setProgressIndeterminate(boolean indeterminate) { 11672 mIndeterminate = indeterminate; 11673 return this; 11674 } 11675 11676 /** 11677 * Gets whether the progress bar's style is based on its progress. 11678 * @see #setStyledByProgress 11679 */ isStyledByProgress()11680 public boolean isStyledByProgress() { 11681 return mIsStyledByProgress; 11682 } 11683 11684 /** 11685 * Indicates whether the segments and points will be styled differently 11686 * based on whether they are behind or ahead of the current progress. 11687 * When true, segments appearing ahead of the current progress will be given a 11688 * slightly different appearance to indicate that it is part of the progress bar 11689 * that is not "filled". 11690 * When false, all segments will be given the filled appearance, and it will be 11691 * the app's responsibility to use #setProgressTrackerIcon or segment colors 11692 * to make the current progress clear to the user. 11693 * the default value is true. 11694 */ setStyledByProgress(boolean enabled)11695 public @NonNull ProgressStyle setStyledByProgress(boolean enabled) { 11696 mIsStyledByProgress = enabled; 11697 return this; 11698 } 11699 11700 11701 /** 11702 * Gets the progress tracker icon for the progress bar. 11703 * @see #setProgressTrackerIcon 11704 */ getProgressTrackerIcon()11705 public @Nullable Icon getProgressTrackerIcon() { 11706 return mTrackerIcon; 11707 } 11708 11709 /** 11710 * An optional icon that can appear as an overlay on the bar at the point of 11711 * current progress. 11712 * Aspect ratio may be anywhere from 2:1 to 1:2; content outside that 11713 * aspect ratio range will be cropped. 11714 * This icon will be mirrored in RTL. 11715 */ setProgressTrackerIcon(@ullable Icon trackerIcon)11716 public @NonNull ProgressStyle setProgressTrackerIcon(@Nullable Icon trackerIcon) { 11717 mTrackerIcon = trackerIcon; 11718 return this; 11719 } 11720 11721 /** 11722 * Gets the progress bar start icon. 11723 * @see #setProgressStartIcon 11724 */ getProgressStartIcon()11725 public @Nullable Icon getProgressStartIcon() { 11726 return mStartIcon; 11727 } 11728 11729 /** 11730 * An optional square icon that appears at the start of the progress bar. 11731 * This icon will be cropped to its central square. 11732 * This icon will NOT be mirrored in RTL layouts. 11733 */ setProgressStartIcon(@ullable Icon startIcon)11734 public @NonNull ProgressStyle setProgressStartIcon(@Nullable Icon startIcon) { 11735 mStartIcon = startIcon; 11736 return this; 11737 } 11738 11739 /** 11740 * Gets the progress bar end icon. 11741 * @see #setProgressEndIcon(Icon) 11742 */ getProgressEndIcon()11743 public @Nullable Icon getProgressEndIcon() { 11744 return mEndIcon; 11745 } 11746 11747 /** 11748 * An optional square icon that appears at the end of the progress bar. 11749 * This icon will be cropped to its central square. 11750 * This icon will NOT be mirrored in RTL layouts. 11751 */ setProgressEndIcon(@ullable Icon endIcon)11752 public @NonNull ProgressStyle setProgressEndIcon(@Nullable Icon endIcon) { 11753 mEndIcon = endIcon; 11754 return this; 11755 } 11756 11757 /** 11758 * @hide 11759 */ 11760 @Override purgeResources()11761 public void purgeResources() { 11762 super.purgeResources(); 11763 if (mTrackerIcon != null) { 11764 mTrackerIcon.convertToAshmem(); 11765 } 11766 if (mStartIcon != null) { 11767 mStartIcon.convertToAshmem(); 11768 } 11769 if (mEndIcon != null) { 11770 mEndIcon.convertToAshmem(); 11771 } 11772 } 11773 11774 /** 11775 * @hide 11776 */ 11777 @Override reduceImageSizes(Context context)11778 public void reduceImageSizes(Context context) { 11779 super.reduceImageSizes(context); 11780 11781 final Resources resources = context.getResources(); 11782 11783 int progressIconSize = 11784 resources.getDimensionPixelSize(R.dimen.notification_progress_icon_size); 11785 if (mStartIcon != null) { 11786 mStartIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); 11787 } 11788 if (mEndIcon != null) { 11789 mEndIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); 11790 } 11791 if (mTrackerIcon != null) { 11792 int progressTrackerWidth = resources.getDimensionPixelSize( 11793 R.dimen.notification_progress_tracker_width); 11794 int progressTrackerHeight = resources.getDimensionPixelSize( 11795 R.dimen.notification_progress_tracker_height); 11796 mTrackerIcon.scaleDownIfNecessary(progressTrackerWidth, progressTrackerHeight); 11797 } 11798 } 11799 11800 /** 11801 * @hide 11802 */ 11803 @Override addExtras(Bundle extras)11804 public void addExtras(Bundle extras) { 11805 super.addExtras(extras); 11806 extras.putParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, 11807 getProgressSegmentsAsBundleList(mProgressSegments)); 11808 extras.putParcelableArrayList(EXTRA_PROGRESS_POINTS, 11809 getProgressPointsAsBundleList(mProgressPoints)); 11810 11811 extras.putInt(EXTRA_PROGRESS, mProgress); 11812 extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mIndeterminate); 11813 extras.putInt(EXTRA_PROGRESS_MAX, getProgressMax()); 11814 extras.putBoolean(EXTRA_STYLED_BY_PROGRESS, mIsStyledByProgress); 11815 11816 if (mTrackerIcon != null) { 11817 extras.putParcelable(EXTRA_PROGRESS_TRACKER_ICON, mTrackerIcon); 11818 } else { 11819 extras.remove(EXTRA_PROGRESS_TRACKER_ICON); 11820 } 11821 11822 if (mStartIcon != null) { 11823 extras.putParcelable(EXTRA_PROGRESS_START_ICON, mStartIcon); 11824 } else { 11825 extras.remove(EXTRA_PROGRESS_START_ICON); 11826 } 11827 11828 if (mEndIcon != null) { 11829 extras.putParcelable(EXTRA_PROGRESS_END_ICON, mEndIcon); 11830 } else { 11831 extras.remove(EXTRA_PROGRESS_END_ICON); 11832 } 11833 } 11834 11835 /** 11836 * @hide 11837 */ 11838 @Override restoreFromExtras(Bundle extras)11839 protected void restoreFromExtras(Bundle extras) { 11840 super.restoreFromExtras(extras); 11841 mProgressSegments = getProgressSegmentsFromBundleList( 11842 extras.getParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, Bundle.class)); 11843 mProgress = extras.getInt(EXTRA_PROGRESS, 0); 11844 mIndeterminate = extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE, false); 11845 mIsStyledByProgress = extras.getBoolean(EXTRA_STYLED_BY_PROGRESS, true); 11846 mTrackerIcon = extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class); 11847 mStartIcon = extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class); 11848 mEndIcon = extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class); 11849 mProgressPoints = getProgressPointsFromBundleList( 11850 extras.getParcelableArrayList(EXTRA_PROGRESS_POINTS, Bundle.class)); 11851 } 11852 11853 /** 11854 * @hide 11855 */ 11856 @Override displayCustomViewInline()11857 public boolean displayCustomViewInline() { 11858 // This is a lie; True is returned for progress notifications to make sure 11859 // that the custom view is not used instead of the template, but it will not 11860 // actually be included. 11861 return true; 11862 } 11863 /** 11864 * @hide 11865 */ 11866 @Override makeContentView()11867 public RemoteViews makeContentView() { 11868 final StandardTemplateParams p = mBuilder.mParams.reset() 11869 .viewType(StandardTemplateParams.VIEW_TYPE_NORMAL) 11870 .hideProgress(true) 11871 .fillTextsFrom(mBuilder); 11872 11873 return getStandardView(mBuilder.getCollapsedBaseLayoutResource(), p, null /* result */); 11874 } 11875 /** 11876 * @hide 11877 */ 11878 @Override makeHeadsUpContentView()11879 public RemoteViews makeHeadsUpContentView() { 11880 final StandardTemplateParams p = mBuilder.mParams.reset() 11881 .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) 11882 .hideProgress(true) 11883 .fillTextsFrom(mBuilder); 11884 11885 return getStandardView(mBuilder.getHeadsUpBaseLayoutResource(), p, null /* result */); 11886 } 11887 /** 11888 * @hide 11889 */ 11890 @Override makeExpandedContentView()11891 public RemoteViews makeExpandedContentView() { 11892 StandardTemplateParams p = mBuilder.mParams.reset() 11893 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED) 11894 .allowTextWithProgress(true) 11895 .hideProgress(true) 11896 .fillTextsFrom(mBuilder); 11897 11898 // Replace the text with the big text, but only if the big text is not empty. 11899 RemoteViews contentView = getStandardView(mBuilder.getProgressLayoutResource(), p, 11900 null /* result */); 11901 11902 // Bind progress start and end icons. 11903 if (mStartIcon != null) { 11904 contentView.setViewVisibility(R.id.notification_progress_start_icon, View.VISIBLE); 11905 contentView.setImageViewIcon(R.id.notification_progress_start_icon, mStartIcon); 11906 } else { 11907 contentView.setViewVisibility(R.id.notification_progress_start_icon, View.GONE); 11908 } 11909 11910 if (mEndIcon != null) { 11911 contentView.setViewVisibility(R.id.notification_progress_end_icon, View.VISIBLE); 11912 contentView.setImageViewIcon(R.id.notification_progress_end_icon, mEndIcon); 11913 } else { 11914 contentView.setViewVisibility(R.id.notification_progress_end_icon, View.GONE); 11915 } 11916 11917 contentView.setViewVisibility(R.id.progress, View.VISIBLE); 11918 11919 final int backgroundColor = mBuilder.getColors(p).getBackgroundColor(); 11920 final int defaultProgressColor = mBuilder.getPrimaryAccentColor(p); 11921 final NotificationProgressModel model = createProgressModel( 11922 defaultProgressColor, backgroundColor); 11923 contentView.setBundle(R.id.progress, 11924 "setProgressModel", model.toBundle()); 11925 11926 contentView.setIcon(R.id.progress, 11927 "setProgressTrackerIcon", 11928 mTrackerIcon); 11929 11930 return contentView; 11931 } 11932 11933 /** 11934 * @hide 11935 */ getProgressSegmentsAsBundleList( @ullable List<Segment> progressSegments)11936 public static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList( 11937 @Nullable List<Segment> progressSegments) { 11938 final ArrayList<Bundle> segments = new ArrayList<>(); 11939 if (progressSegments != null && !progressSegments.isEmpty()) { 11940 for (int i = 0; i < progressSegments.size(); i++) { 11941 final Segment segment = progressSegments.get(i); 11942 if (segment.getLength() <= 0) { 11943 continue; 11944 } 11945 11946 final Bundle bundle = new Bundle(); 11947 bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength()); 11948 bundle.putInt(KEY_ELEMENT_ID, segment.getId()); 11949 bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor()); 11950 11951 segments.add(bundle); 11952 } 11953 } 11954 11955 return segments; 11956 } 11957 11958 /** 11959 * @hide 11960 */ getProgressSegmentsFromBundleList( @ullable List<Bundle> segmentBundleList)11961 public static @NonNull List<Segment> getProgressSegmentsFromBundleList( 11962 @Nullable List<Bundle> segmentBundleList) { 11963 final ArrayList<Segment> segments = new ArrayList<>(); 11964 if (segmentBundleList != null && !segmentBundleList.isEmpty()) { 11965 for (int i = 0; i < segmentBundleList.size(); i++) { 11966 final Bundle segmentBundle = segmentBundleList.get(i); 11967 final int length = segmentBundle.getInt(KEY_SEGMENT_LENGTH); 11968 if (length <= 0) { 11969 continue; 11970 } 11971 11972 final int id = segmentBundle.getInt(KEY_ELEMENT_ID); 11973 final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR, 11974 Notification.COLOR_DEFAULT); 11975 final Segment segment = new Segment(length) 11976 .setId(id).setColor(color); 11977 11978 segments.add(segment); 11979 } 11980 } 11981 11982 return segments; 11983 } 11984 /** 11985 * @hide 11986 */ getProgressPointsAsBundleList( @ullable List<Point> progressPoints)11987 public static @NonNull ArrayList<Bundle> getProgressPointsAsBundleList( 11988 @Nullable List<Point> progressPoints) { 11989 final ArrayList<Bundle> points = new ArrayList<>(); 11990 if (progressPoints != null && !progressPoints.isEmpty()) { 11991 for (int i = 0; i < progressPoints.size(); i++) { 11992 final Point point = progressPoints.get(i); 11993 if (point.getPosition() < 0) { 11994 continue; 11995 } 11996 11997 final Bundle bundle = new Bundle(); 11998 bundle.putInt(KEY_POINT_POSITION, point.getPosition()); 11999 bundle.putInt(KEY_ELEMENT_ID, point.getId()); 12000 bundle.putInt(KEY_ELEMENT_COLOR, point.getColor()); 12001 12002 points.add(bundle); 12003 } 12004 } 12005 12006 return points; 12007 } 12008 12009 /** 12010 * @hide 12011 */ getProgressPointsFromBundleList( @ullable List<Bundle> pointBundleList)12012 public static @NonNull List<Point> getProgressPointsFromBundleList( 12013 @Nullable List<Bundle> pointBundleList) { 12014 final ArrayList<Point> points = new ArrayList<>(); 12015 12016 if (pointBundleList != null && !pointBundleList.isEmpty()) { 12017 for (int i = 0; i < pointBundleList.size(); i++) { 12018 final Bundle pointBundle = pointBundleList.get(i); 12019 final int position = pointBundle.getInt(KEY_POINT_POSITION); 12020 if (position < 0) { 12021 continue; 12022 } 12023 final int id = pointBundle.getInt(KEY_ELEMENT_ID); 12024 final int color = pointBundle.getInt(KEY_ELEMENT_COLOR, 12025 Notification.COLOR_DEFAULT); 12026 final Point point = new Point(position).setId(id).setColor(color); 12027 points.add(point); 12028 } 12029 } 12030 12031 return points; 12032 } 12033 12034 /** 12035 * @hide 12036 */ createProgressModel(int defaultProgressColor, int backgroundColor)12037 public @NonNull NotificationProgressModel createProgressModel(int defaultProgressColor, 12038 int backgroundColor) { 12039 final NotificationProgressModel model; 12040 if (mIndeterminate) { 12041 final int indeterminateColor; 12042 if (!mProgressSegments.isEmpty()) { 12043 indeterminateColor = mProgressSegments.get(0).mColor; 12044 } else { 12045 indeterminateColor = defaultProgressColor; 12046 } 12047 12048 model = new NotificationProgressModel( 12049 sanitizeProgressColor(indeterminateColor, 12050 backgroundColor, defaultProgressColor)); 12051 } else { 12052 // Ensure segment color contrasts. 12053 final List<Segment> segments = new ArrayList<>(); 12054 int totalLength = 0; 12055 for (Segment segment : mProgressSegments) { 12056 final int length = segment.getLength(); 12057 if (length <= 0) continue; 12058 12059 try { 12060 totalLength = Math.addExact(totalLength, length); 12061 segments.add(sanitizeSegment(segment, backgroundColor, 12062 defaultProgressColor)); 12063 } catch (ArithmeticException e) { 12064 totalLength = DEFAULT_PROGRESS_MAX; 12065 segments.clear(); 12066 break; 12067 } 12068 } 12069 12070 // Create default segment when no segments are provided. 12071 if (segments.isEmpty()) { 12072 totalLength = DEFAULT_PROGRESS_MAX; 12073 segments.add(sanitizeSegment(new Segment(totalLength), backgroundColor, 12074 defaultProgressColor)); 12075 } else if (segments.size() > MAX_PROGRESS_SEGMENT_LIMIT) { 12076 // If segment limit is exceeded. All segments will be replaced 12077 // with a single segment 12078 boolean allSameColor = true; 12079 int firstSegmentColor = segments.getFirst().getColor(); 12080 12081 for (int i = 1; i < segments.size(); i++) { 12082 if (segments.get(i).getColor() != firstSegmentColor) { 12083 allSameColor = false; 12084 break; 12085 } 12086 } 12087 12088 // This single segment length has same max as total. 12089 final Segment singleSegment = new Segment(totalLength); 12090 // Single segment color: if all segments have the same color, 12091 // use that color. Otherwise, use 0 / default. 12092 singleSegment.setColor(allSameColor ? firstSegmentColor 12093 : Notification.COLOR_DEFAULT); 12094 12095 segments.clear(); 12096 segments.add(sanitizeSegment(singleSegment, 12097 backgroundColor, 12098 defaultProgressColor)); 12099 } 12100 12101 // Ensure point color contrasts. 12102 final List<Point> points = new ArrayList<>(); 12103 for (Point point : mProgressPoints) { 12104 final int position = point.getPosition(); 12105 // The points at start/end aren't supposed to show in the progress bar. 12106 // Therefore those are also dropped here. 12107 if (position <= 0 || position >= totalLength) continue; 12108 points.add(sanitizePoint(point, backgroundColor, defaultProgressColor)); 12109 if (points.size() == MAX_PROGRESS_POINT_LIMIT) { 12110 break; 12111 } 12112 } 12113 12114 // If the segments and points can't all fit inside the progress drawable, the 12115 // view will replace all segments with a single segment. 12116 final int segmentsFallbackColor; 12117 if (segments.size() <= 1) { 12118 segmentsFallbackColor = NotificationProgressModel.INVALID_COLOR; 12119 } else { 12120 12121 boolean allSameColor = true; 12122 int firstSegmentColor = segments.getFirst().getColor(); 12123 for (int i = 1; i < segments.size(); i++) { 12124 if (segments.get(i).getColor() != firstSegmentColor) { 12125 allSameColor = false; 12126 break; 12127 } 12128 } 12129 // If the segments are of the same color, the view can just use that color. 12130 // In that case there is no need to send the fallback color. 12131 segmentsFallbackColor = allSameColor ? NotificationProgressModel.INVALID_COLOR 12132 : sanitizeProgressColor(Notification.COLOR_DEFAULT, backgroundColor, 12133 defaultProgressColor); 12134 } 12135 12136 model = new NotificationProgressModel(segments, points, 12137 Math.clamp(mProgress, 0, totalLength), mIsStyledByProgress, 12138 segmentsFallbackColor); 12139 } 12140 return model; 12141 } 12142 sanitizeSegment(@onNull Segment segment, @ColorInt int bg, @ColorInt int defaultColor)12143 private Segment sanitizeSegment(@NonNull Segment segment, 12144 @ColorInt int bg, 12145 @ColorInt int defaultColor) { 12146 return new Segment(segment.getLength()) 12147 .setId(segment.getId()) 12148 .setColor(sanitizeProgressColor(segment.getColor(), bg, defaultColor)); 12149 } 12150 sanitizePoint(@onNull Point point, @ColorInt int bg, @ColorInt int defaultColor)12151 private Point sanitizePoint(@NonNull Point point, 12152 @ColorInt int bg, 12153 @ColorInt int defaultColor) { 12154 return new Point(point.getPosition()).setId(point.getId()) 12155 .setColor(sanitizeProgressColor(point.getColor(), bg, defaultColor)); 12156 } 12157 12158 /** 12159 * Finds steps and points fill color with sufficient contrast over bg (3:1) that 12160 * has the same hue as the original color, but is lightened or darkened depending on 12161 * whether the background is dark or light. 12162 * 12163 * @hide 12164 */ 12165 @VisibleForTesting sanitizeProgressColor(@olorInt int color, @ColorInt int bg, @ColorInt int defaultColor)12166 public static int sanitizeProgressColor(@ColorInt int color, 12167 @ColorInt int bg, 12168 @ColorInt int defaultColor) { 12169 return Builder.ensureColorContrast( 12170 Color.alpha(color) == 0 ? defaultColor : color, 12171 bg, 12172 3); 12173 } 12174 12175 /** 12176 * A segment of the progress bar, which defines its length and color. 12177 * Segments allow for creating progress bars with multiple colors or sections 12178 * to represent different stages or categories of progress. 12179 * For example, Traffic conditions along a navigation journey. 12180 */ 12181 public static final class Segment { 12182 private int mLength; 12183 private int mId = 0; 12184 @ColorInt 12185 private int mColor = Notification.COLOR_DEFAULT; 12186 12187 /** 12188 * Create a segment with a non-zero length. 12189 * @param length 12190 * See {@link #getLength} 12191 */ Segment(int length)12192 public Segment(int length) { 12193 mLength = length; 12194 } 12195 12196 /** 12197 * The length of this Segment within the progress bar. 12198 * This value has no units, it is just relative to the length of other segments, 12199 * and the value provided to {@link ProgressStyle#setProgress}. 12200 */ getLength()12201 public int getLength() { 12202 return mLength; 12203 } 12204 12205 /** 12206 * Gets the id of this Segment. 12207 * 12208 * @see #setId 12209 */ getId()12210 public int getId() { 12211 return mId; 12212 } 12213 12214 /** 12215 * Optional ID used to uniquely identify the element across updates. 12216 */ setId(int id)12217 public @NonNull Segment setId(int id) { 12218 mId = id; 12219 return this; 12220 } 12221 12222 /** 12223 * Returns the color of this Segment. 12224 * 12225 * @see #setColor 12226 */ 12227 @ColorInt getColor()12228 public int getColor() { 12229 return mColor; 12230 } 12231 12232 /** 12233 * Optional color of this Segment 12234 */ setColor(@olorInt int color)12235 public @NonNull Segment setColor(@ColorInt int color) { 12236 mColor = color; 12237 return this; 12238 } 12239 12240 /** 12241 * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} 12242 */ 12243 @Override equals(Object o)12244 public boolean equals(Object o) { 12245 if (this == o) return true; 12246 if (o == null || getClass() != o.getClass()) return false; 12247 final Segment segment = (Segment) o; 12248 return mLength == segment.mLength && mId == segment.mId 12249 && mColor == segment.mColor; 12250 } 12251 12252 @Override hashCode()12253 public int hashCode() { 12254 return Objects.hash(mLength, mId, mColor); 12255 } 12256 } 12257 12258 /** 12259 * A point within the progress bar, defining its position and color. 12260 * Points within a progress bar are used to visualize distinct stages or milestones. 12261 * For example, you might use points to mark stops in a multi-stop 12262 * navigation journey, where each point represents a destination. 12263 */ 12264 public static final class Point { 12265 12266 private int mPosition; 12267 private int mId; 12268 @ColorInt 12269 private int mColor = Notification.COLOR_DEFAULT; 12270 12271 /** 12272 * Create a point element. 12273 * The position of this point on the progress bar 12274 * relative to {@link ProgressStyle#getProgressMax} 12275 * @param position 12276 * See {@link #getPosition} 12277 */ Point(int position)12278 public Point(int position) { 12279 mPosition = position; 12280 } 12281 12282 /** 12283 * Gets the position of this Point. 12284 * The position of this point on the progress bar 12285 * relative to {@link ProgressStyle#getProgressMax}. 12286 */ getPosition()12287 public int getPosition() { 12288 return mPosition; 12289 } 12290 12291 12292 /** 12293 * Optional ID used to uniquely identify the element across updates. 12294 */ getId()12295 public int getId() { 12296 return mId; 12297 } 12298 12299 /** 12300 * Optional ID used to uniquely identify the element across updates. 12301 */ setId(int id)12302 public @NonNull Point setId(int id) { 12303 mId = id; 12304 return this; 12305 } 12306 12307 /** 12308 * Returns the color of this Segment. 12309 * 12310 * @see #setColor 12311 */ 12312 @ColorInt getColor()12313 public int getColor() { 12314 return mColor; 12315 } 12316 12317 /** 12318 * Optional color of this Segment 12319 */ setColor(@olorInt int color)12320 public @NonNull Point setColor(@ColorInt int color) { 12321 mColor = color; 12322 return this; 12323 } 12324 12325 /** 12326 * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} 12327 */ 12328 @Override equals(Object o)12329 public boolean equals(Object o) { 12330 if (this == o) return true; 12331 if (o == null || getClass() != o.getClass()) return false; 12332 final Point point = (Point) o; 12333 return mPosition == point.mPosition && mId == point.mId 12334 && mColor == point.mColor; 12335 } 12336 12337 @Override hashCode()12338 public int hashCode() { 12339 return Objects.hash(mPosition, mId, mColor); 12340 } 12341 } 12342 } 12343 12344 /** 12345 * Notification style for custom views that are decorated by the system 12346 * 12347 * <p>Instead of providing a notification that is completely custom, a developer can set this 12348 * style and still obtain system decorations like the notification header with the expand 12349 * affordance and actions. 12350 * 12351 * <p>Use {@link android.app.Notification.Builder#setCustomContentView(RemoteViews)}, 12352 * {@link android.app.Notification.Builder#setCustomBigContentView(RemoteViews)} and 12353 * {@link android.app.Notification.Builder#setCustomHeadsUpContentView(RemoteViews)} to set the 12354 * corresponding custom views to display. 12355 * 12356 * To use this style with your Notification, feed it to 12357 * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: 12358 * <pre class="prettyprint"> 12359 * Notification noti = new Notification.Builder() 12360 * .setSmallIcon(R.drawable.ic_stat_player) 12361 * .setLargeIcon(albumArtBitmap)) 12362 * .setCustomContentView(contentView); 12363 * .setStyle(<b>new Notification.DecoratedCustomViewStyle()</b>) 12364 * .build(); 12365 * </pre> 12366 */ 12367 public static class DecoratedCustomViewStyle extends Style { 12368 DecoratedCustomViewStyle()12369 public DecoratedCustomViewStyle() { 12370 } 12371 12372 /** 12373 * @hide 12374 */ displayCustomViewInline()12375 public boolean displayCustomViewInline() { 12376 return true; 12377 } 12378 12379 /** 12380 * @hide 12381 */ 12382 @Override makeContentView()12383 public RemoteViews makeContentView() { 12384 return makeStandardTemplateWithCustomContent(mBuilder.mN.contentView); 12385 } 12386 12387 /** 12388 * @hide 12389 */ 12390 @Override makeExpandedContentView()12391 public RemoteViews makeExpandedContentView() { 12392 return makeDecoratedExpandedContentView(); 12393 } 12394 12395 /** 12396 * @hide 12397 */ 12398 @Override makeHeadsUpContentView()12399 public RemoteViews makeHeadsUpContentView() { 12400 return makeDecoratedHeadsUpContentView(); 12401 } 12402 makeDecoratedHeadsUpContentView()12403 private RemoteViews makeDecoratedHeadsUpContentView() { 12404 RemoteViews headsUpContentView = mBuilder.mN.headsUpContentView == null 12405 ? mBuilder.mN.contentView 12406 : mBuilder.mN.headsUpContentView; 12407 if (headsUpContentView == null) { 12408 return null; // no custom view; use the default behavior 12409 } 12410 if (mBuilder.mActions.size() == 0) { 12411 return makeStandardTemplateWithCustomContent(headsUpContentView); 12412 } 12413 TemplateBindResult result = new TemplateBindResult(); 12414 StandardTemplateParams p = mBuilder.mParams.reset() 12415 .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) 12416 .decorationType(StandardTemplateParams.DECORATION_PARTIAL) 12417 .fillTextsFrom(mBuilder); 12418 RemoteViews remoteViews = mBuilder.applyStandardTemplateWithActions( 12419 mBuilder.getHeadsUpBaseLayoutResource(), p, result); 12420 buildCustomContentIntoTemplate(mBuilder.mContext, remoteViews, headsUpContentView, 12421 p, result); 12422 return remoteViews; 12423 } 12424 makeStandardTemplateWithCustomContent(RemoteViews customContent)12425 private RemoteViews makeStandardTemplateWithCustomContent(RemoteViews customContent) { 12426 if (customContent == null) { 12427 return null; // no custom view; use the default behavior 12428 } 12429 TemplateBindResult result = new TemplateBindResult(); 12430 StandardTemplateParams p = mBuilder.mParams.reset() 12431 .viewType(StandardTemplateParams.VIEW_TYPE_NORMAL) 12432 .decorationType(StandardTemplateParams.DECORATION_PARTIAL) 12433 .fillTextsFrom(mBuilder); 12434 RemoteViews remoteViews = mBuilder.applyStandardTemplate( 12435 mBuilder.getCollapsedBaseLayoutResource(), p, result); 12436 buildCustomContentIntoTemplate(mBuilder.mContext, remoteViews, customContent, 12437 p, result); 12438 return remoteViews; 12439 } 12440 makeDecoratedExpandedContentView()12441 private RemoteViews makeDecoratedExpandedContentView() { 12442 RemoteViews bigContentView = mBuilder.mN.bigContentView == null 12443 ? mBuilder.mN.contentView 12444 : mBuilder.mN.bigContentView; 12445 if (bigContentView == null) { 12446 return null; // no custom view; use the default behavior 12447 } 12448 TemplateBindResult result = new TemplateBindResult(); 12449 StandardTemplateParams p = mBuilder.mParams.reset() 12450 .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED) 12451 .decorationType(StandardTemplateParams.DECORATION_PARTIAL) 12452 .fillTextsFrom(mBuilder); 12453 RemoteViews remoteViews = mBuilder.applyStandardTemplateWithActions( 12454 mBuilder.getExpandedBaseLayoutResource(), p, result); 12455 buildCustomContentIntoTemplate(mBuilder.mContext, remoteViews, bigContentView, 12456 p, result); 12457 return remoteViews; 12458 } 12459 12460 /** 12461 * @hide 12462 */ 12463 @Override areNotificationsVisiblyDifferent(Style other)12464 public boolean areNotificationsVisiblyDifferent(Style other) { 12465 if (other == null || getClass() != other.getClass()) { 12466 return true; 12467 } 12468 // Comparison done for all custom RemoteViews, independent of style 12469 return false; 12470 } 12471 } 12472 12473 /** 12474 * Notification style for media custom views that are decorated by the system 12475 * 12476 * <p>Instead of providing a media notification that is completely custom, a developer can set 12477 * this style and still obtain system decorations like the notification header with the expand 12478 * affordance and actions. 12479 * 12480 * <p>Use {@link android.app.Notification.Builder#setCustomContentView(RemoteViews)}, 12481 * {@link android.app.Notification.Builder#setCustomBigContentView(RemoteViews)} and 12482 * {@link android.app.Notification.Builder#setCustomHeadsUpContentView(RemoteViews)} to set the 12483 * corresponding custom views to display. 12484 * <p> 12485 * Contrary to {@link MediaStyle} a developer has to opt-in to the colorizing of the 12486 * notification by using {@link Notification.Builder#setColorized(boolean)}. 12487 * <p> 12488 * To use this style with your Notification, feed it to 12489 * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: 12490 * <pre class="prettyprint"> 12491 * Notification noti = new Notification.Builder() 12492 * .setSmallIcon(R.drawable.ic_stat_player) 12493 * .setLargeIcon(albumArtBitmap)) 12494 * .setCustomContentView(contentView); 12495 * .setStyle(<b>new Notification.DecoratedMediaCustomViewStyle()</b> 12496 * .setMediaSession(mySession)) 12497 * .build(); 12498 * </pre> 12499 * 12500 * @see android.app.Notification.DecoratedCustomViewStyle 12501 * @see android.app.Notification.MediaStyle 12502 */ 12503 public static class DecoratedMediaCustomViewStyle extends MediaStyle { 12504 DecoratedMediaCustomViewStyle()12505 public DecoratedMediaCustomViewStyle() { 12506 } 12507 12508 /** 12509 * @hide 12510 */ displayCustomViewInline()12511 public boolean displayCustomViewInline() { 12512 return true; 12513 } 12514 12515 /** 12516 * @hide 12517 */ 12518 @Override makeContentView()12519 public RemoteViews makeContentView() { 12520 return makeMediaContentView(mBuilder.mN.contentView); 12521 } 12522 12523 /** 12524 * @hide 12525 */ 12526 @Override makeExpandedContentView()12527 public RemoteViews makeExpandedContentView() { 12528 RemoteViews customContent = mBuilder.mN.bigContentView != null 12529 ? mBuilder.mN.bigContentView 12530 : mBuilder.mN.contentView; 12531 return makeMediaExpandedContentView(customContent); 12532 } 12533 12534 /** 12535 * @hide 12536 */ 12537 @Override makeHeadsUpContentView()12538 public RemoteViews makeHeadsUpContentView() { 12539 RemoteViews customContent = mBuilder.mN.headsUpContentView != null 12540 ? mBuilder.mN.headsUpContentView 12541 : mBuilder.mN.contentView; 12542 return makeMediaExpandedContentView(customContent); 12543 } 12544 12545 /** 12546 * @hide 12547 */ 12548 @Override areNotificationsVisiblyDifferent(Style other)12549 public boolean areNotificationsVisiblyDifferent(Style other) { 12550 if (other == null || getClass() != other.getClass()) { 12551 return true; 12552 } 12553 // Comparison done for all custom RemoteViews, independent of style 12554 return false; 12555 } 12556 } 12557 12558 /** 12559 * Encapsulates the information needed to display a notification as a bubble. 12560 * 12561 * <p>A bubble is used to display app content in a floating window over the existing 12562 * foreground activity. A bubble has a collapsed state represented by an icon and an 12563 * expanded state that displays an activity. These may be defined via 12564 * {@link Builder#Builder(PendingIntent, Icon)} or they may 12565 * be defined via an existing shortcut using {@link Builder#Builder(String)}. 12566 * </p> 12567 * 12568 * <b>Notifications with a valid and allowed bubble will display in collapsed state 12569 * outside of the notification shade on unlocked devices. When a user interacts with the 12570 * collapsed bubble, the bubble activity will be invoked and displayed.</b> 12571 * 12572 * @see Notification.Builder#setBubbleMetadata(BubbleMetadata) 12573 */ 12574 public static final class BubbleMetadata implements Parcelable { 12575 12576 private PendingIntent mPendingIntent; 12577 private PendingIntent mDeleteIntent; 12578 private Icon mIcon; 12579 private int mDesiredHeight; 12580 @DimenRes private int mDesiredHeightResId; 12581 private int mFlags; 12582 private String mShortcutId; 12583 12584 /** 12585 * If set and the app creating the bubble is in the foreground, the bubble will be posted 12586 * in its expanded state. 12587 * 12588 * <p>This flag has no effect if the app posting the bubble is not in the foreground. 12589 * The app is considered foreground if it is visible and on the screen, note that 12590 * a foreground service does not qualify. 12591 * </p> 12592 * 12593 * <p>Generally this flag should only be set if the user has performed an action to request 12594 * or create a bubble.</p> 12595 * 12596 * @hide 12597 */ 12598 public static final int FLAG_AUTO_EXPAND_BUBBLE = 0x00000001; 12599 12600 /** 12601 * Indicates whether the notification associated with the bubble is being visually 12602 * suppressed from the notification shade. When <code>true</code> the notification is 12603 * hidden, when <code>false</code> the notification shows as normal. 12604 * 12605 * <p>Apps sending bubbles may set this flag so that the bubble is posted <b>without</b> 12606 * the associated notification in the notification shade.</p> 12607 * 12608 * <p>Generally this flag should only be set by the app if the user has performed an 12609 * action to request or create a bubble, or if the user has seen the content in the 12610 * notification and the notification is no longer relevant. </p> 12611 * 12612 * <p>The system will also update this flag with <code>true</code> to hide the notification 12613 * from the user once the bubble has been expanded. </p> 12614 * 12615 * @hide 12616 */ 12617 public static final int FLAG_SUPPRESS_NOTIFICATION = 0x00000002; 12618 12619 /** 12620 * Indicates whether the bubble should be visually suppressed from the bubble stack if the 12621 * user is viewing the same content outside of the bubble. For example, the user has a 12622 * bubble with Alice and then opens up the main app and navigates to Alice's page. 12623 * 12624 * @hide 12625 */ 12626 public static final int FLAG_SUPPRESSABLE_BUBBLE = 0x00000004; 12627 12628 /** 12629 * Indicates whether the bubble is visually suppressed from the bubble stack. 12630 * 12631 * @hide 12632 */ 12633 public static final int FLAG_SUPPRESS_BUBBLE = 0x00000008; 12634 BubbleMetadata(PendingIntent expandIntent, PendingIntent deleteIntent, Icon icon, int height, @DimenRes int heightResId, String shortcutId)12635 private BubbleMetadata(PendingIntent expandIntent, PendingIntent deleteIntent, 12636 Icon icon, int height, @DimenRes int heightResId, String shortcutId) { 12637 mPendingIntent = expandIntent; 12638 mIcon = icon; 12639 mDesiredHeight = height; 12640 mDesiredHeightResId = heightResId; 12641 mDeleteIntent = deleteIntent; 12642 mShortcutId = shortcutId; 12643 } 12644 BubbleMetadata(Parcel in)12645 private BubbleMetadata(Parcel in) { 12646 if (in.readInt() != 0) { 12647 mPendingIntent = PendingIntent.CREATOR.createFromParcel(in); 12648 } 12649 if (in.readInt() != 0) { 12650 mIcon = Icon.CREATOR.createFromParcel(in); 12651 } 12652 mDesiredHeight = in.readInt(); 12653 mFlags = in.readInt(); 12654 if (in.readInt() != 0) { 12655 mDeleteIntent = PendingIntent.CREATOR.createFromParcel(in); 12656 } 12657 mDesiredHeightResId = in.readInt(); 12658 if (in.readInt() != 0) { 12659 mShortcutId = in.readString8(); 12660 } 12661 } 12662 12663 /** 12664 * @return the shortcut id used for this bubble if created via 12665 * {@link Builder#Builder(String)} or null if created 12666 * via {@link Builder#Builder(PendingIntent, Icon)}. 12667 */ 12668 @Nullable getShortcutId()12669 public String getShortcutId() { 12670 return mShortcutId; 12671 } 12672 12673 /** 12674 * @return the pending intent used to populate the floating window for this bubble, or 12675 * null if this bubble is created via {@link Builder#Builder(String)}. 12676 */ 12677 @SuppressLint("InvalidNullConversion") 12678 @Nullable getIntent()12679 public PendingIntent getIntent() { 12680 return mPendingIntent; 12681 } 12682 12683 /** 12684 * @return the pending intent to send when the bubble is dismissed by a user, if one exists. 12685 */ 12686 @Nullable getDeleteIntent()12687 public PendingIntent getDeleteIntent() { 12688 return mDeleteIntent; 12689 } 12690 12691 /** 12692 * @return the icon that will be displayed for this bubble when it is collapsed, or null 12693 * if the bubble is created via {@link Builder#Builder(String)}. 12694 */ 12695 @SuppressLint("InvalidNullConversion") 12696 @Nullable getIcon()12697 public Icon getIcon() { 12698 return mIcon; 12699 } 12700 12701 /** 12702 * @return the ideal height, in DPs, for the floating window that app content defined by 12703 * {@link #getIntent()} for this bubble. A value of 0 indicates a desired height has 12704 * not been set. 12705 */ 12706 @Dimension(unit = DP) getDesiredHeight()12707 public int getDesiredHeight() { 12708 return mDesiredHeight; 12709 } 12710 12711 /** 12712 * @return the resId of ideal height for the floating window that app content defined by 12713 * {@link #getIntent()} for this bubble. A value of 0 indicates a res value has not 12714 * been provided for the desired height. 12715 */ 12716 @DimenRes getDesiredHeightResId()12717 public int getDesiredHeightResId() { 12718 return mDesiredHeightResId; 12719 } 12720 12721 /** 12722 * @return whether this bubble should auto expand when it is posted. 12723 * 12724 * @see BubbleMetadata.Builder#setAutoExpandBubble(boolean) 12725 */ getAutoExpandBubble()12726 public boolean getAutoExpandBubble() { 12727 return (mFlags & FLAG_AUTO_EXPAND_BUBBLE) != 0; 12728 } 12729 12730 /** 12731 * Indicates whether the notification associated with the bubble is being visually 12732 * suppressed from the notification shade. When <code>true</code> the notification is 12733 * hidden, when <code>false</code> the notification shows as normal. 12734 * 12735 * <p>Apps sending bubbles may set this flag so that the bubble is posted <b>without</b> 12736 * the associated notification in the notification shade.</p> 12737 * 12738 * <p>Generally the app should only set this flag if the user has performed an 12739 * action to request or create a bubble, or if the user has seen the content in the 12740 * notification and the notification is no longer relevant. </p> 12741 * 12742 * <p>The system will update this flag with <code>true</code> to hide the notification 12743 * from the user once the bubble has been expanded.</p> 12744 * 12745 * @return whether this bubble should suppress the notification when it is posted. 12746 * 12747 * @see BubbleMetadata.Builder#setSuppressNotification(boolean) 12748 */ isNotificationSuppressed()12749 public boolean isNotificationSuppressed() { 12750 return (mFlags & FLAG_SUPPRESS_NOTIFICATION) != 0; 12751 } 12752 12753 /** 12754 * Indicates whether the bubble should be visually suppressed from the bubble stack if the 12755 * user is viewing the same content outside of the bubble. For example, the user has a 12756 * bubble with Alice and then opens up the main app and navigates to Alice's page. 12757 * 12758 * To match the activity and the bubble notification, the bubble notification should 12759 * have a locus id set that matches a locus id set on the activity. 12760 * 12761 * @return whether this bubble should be suppressed when the same content is visible 12762 * outside of the bubble. 12763 * 12764 * @see BubbleMetadata.Builder#setSuppressableBubble(boolean) 12765 */ isBubbleSuppressable()12766 public boolean isBubbleSuppressable() { 12767 return (mFlags & FLAG_SUPPRESSABLE_BUBBLE) != 0; 12768 } 12769 12770 /** 12771 * Indicates whether the bubble is currently visually suppressed from the bubble stack. 12772 * 12773 * @see BubbleMetadata.Builder#setSuppressableBubble(boolean) 12774 */ isBubbleSuppressed()12775 public boolean isBubbleSuppressed() { 12776 return (mFlags & FLAG_SUPPRESS_BUBBLE) != 0; 12777 } 12778 12779 /** 12780 * Sets whether the notification associated with the bubble is being visually 12781 * suppressed from the notification shade. When <code>true</code> the notification is 12782 * hidden, when <code>false</code> the notification shows as normal. 12783 * 12784 * @hide 12785 */ setSuppressNotification(boolean suppressed)12786 public void setSuppressNotification(boolean suppressed) { 12787 if (suppressed) { 12788 mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 12789 } else { 12790 mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 12791 } 12792 } 12793 12794 /** 12795 * Sets whether the bubble should be visually suppressed from the bubble stack if the 12796 * user is viewing the same content outside of the bubble. For example, the user has a 12797 * bubble with Alice and then opens up the main app and navigates to Alice's page. 12798 * 12799 * @hide 12800 */ setSuppressBubble(boolean suppressed)12801 public void setSuppressBubble(boolean suppressed) { 12802 if (suppressed) { 12803 mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; 12804 } else { 12805 mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; 12806 } 12807 } 12808 12809 /** 12810 * @hide 12811 */ setFlags(int flags)12812 public void setFlags(int flags) { 12813 mFlags = flags; 12814 } 12815 12816 /** 12817 * @hide 12818 */ getFlags()12819 public int getFlags() { 12820 return mFlags; 12821 } 12822 12823 public static final @android.annotation.NonNull Parcelable.Creator<BubbleMetadata> CREATOR = 12824 new Parcelable.Creator<BubbleMetadata>() { 12825 12826 @Override 12827 public BubbleMetadata createFromParcel(Parcel source) { 12828 return new BubbleMetadata(source); 12829 } 12830 12831 @Override 12832 public BubbleMetadata[] newArray(int size) { 12833 return new BubbleMetadata[size]; 12834 } 12835 }; 12836 12837 @Override describeContents()12838 public int describeContents() { 12839 return 0; 12840 } 12841 12842 @Override writeToParcel(Parcel out, int flags)12843 public void writeToParcel(Parcel out, int flags) { 12844 out.writeInt(mPendingIntent != null ? 1 : 0); 12845 if (mPendingIntent != null) { 12846 mPendingIntent.writeToParcel(out, 0); 12847 } 12848 out.writeInt(mIcon != null ? 1 : 0); 12849 if (mIcon != null) { 12850 mIcon.writeToParcel(out, 0); 12851 } 12852 out.writeInt(mDesiredHeight); 12853 out.writeInt(mFlags); 12854 out.writeInt(mDeleteIntent != null ? 1 : 0); 12855 if (mDeleteIntent != null) { 12856 mDeleteIntent.writeToParcel(out, 0); 12857 } 12858 out.writeInt(mDesiredHeightResId); 12859 out.writeInt(TextUtils.isEmpty(mShortcutId) ? 0 : 1); 12860 if (!TextUtils.isEmpty(mShortcutId)) { 12861 out.writeString8(mShortcutId); 12862 } 12863 } 12864 12865 /** 12866 * Builder to construct a {@link BubbleMetadata} object. 12867 */ 12868 public static final class Builder { 12869 12870 private PendingIntent mPendingIntent; 12871 private Icon mIcon; 12872 private int mDesiredHeight; 12873 @DimenRes private int mDesiredHeightResId; 12874 private int mFlags; 12875 private PendingIntent mDeleteIntent; 12876 private String mShortcutId; 12877 12878 /** 12879 * @deprecated use {@link Builder#Builder(String)} for a bubble created via a 12880 * {@link ShortcutInfo} or {@link Builder#Builder(PendingIntent, Icon)} for a bubble 12881 * created via a {@link PendingIntent}. 12882 */ 12883 @Deprecated Builder()12884 public Builder() { 12885 } 12886 12887 /** 12888 * Creates a {@link BubbleMetadata.Builder} based on a {@link ShortcutInfo}. To create 12889 * a shortcut bubble, ensure that the shortcut associated with the provided 12890 * {@param shortcutId} is published as a dynamic shortcut that was built with 12891 * {@link ShortcutInfo.Builder#setLongLived(boolean)} being true, otherwise your 12892 * notification will not be able to bubble. 12893 * 12894 * <p>The shortcut icon will be used to represent the bubble when it is collapsed.</p> 12895 * 12896 * <p>The shortcut activity will be used when the bubble is expanded. This will display 12897 * the shortcut activity in a floating window over the existing foreground activity.</p> 12898 * 12899 * <p>When the activity is launched from a bubble, 12900 * {@link Activity#isLaunchedFromBubble()} will return with {@code true}. 12901 * </p> 12902 * 12903 * <p>If the shortcut has not been published when the bubble notification is sent, 12904 * no bubble will be produced. If the shortcut is deleted while the bubble is active, 12905 * the bubble will be removed.</p> 12906 * 12907 * @throws NullPointerException if shortcutId is null. 12908 * 12909 * @see ShortcutInfo 12910 * @see ShortcutInfo.Builder#setLongLived(boolean) 12911 * @see android.content.pm.ShortcutManager#addDynamicShortcuts(List) 12912 */ Builder(@onNull String shortcutId)12913 public Builder(@NonNull String shortcutId) { 12914 if (TextUtils.isEmpty(shortcutId)) { 12915 throw new NullPointerException("Bubble requires a non-null shortcut id"); 12916 } 12917 mShortcutId = shortcutId; 12918 } 12919 12920 /** 12921 * Creates a {@link BubbleMetadata.Builder} based on the provided intent and icon. 12922 * 12923 * <p>The icon will be used to represent the bubble when it is collapsed. An icon 12924 * should be representative of the content within the bubble. If your app produces 12925 * multiple bubbles, the icon should be unique for each of them.</p> 12926 * 12927 * <p>The intent that will be used when the bubble is expanded. This will display the 12928 * app content in a floating window over the existing foreground activity. The intent 12929 * should point to a resizable activity. </p> 12930 * 12931 * <p>When the activity is launched from a bubble, 12932 * {@link Activity#isLaunchedFromBubble()} will return with {@code true}. 12933 * </p> 12934 * 12935 * Note that the pending intent used here requires PendingIntent.FLAG_MUTABLE. 12936 * 12937 * @throws NullPointerException if intent is null. 12938 * @throws NullPointerException if icon is null. 12939 */ Builder(@onNull PendingIntent intent, @NonNull Icon icon)12940 public Builder(@NonNull PendingIntent intent, @NonNull Icon icon) { 12941 if (intent == null) { 12942 throw new NullPointerException("Bubble requires non-null pending intent"); 12943 } 12944 if (icon == null) { 12945 throw new NullPointerException("Bubbles require non-null icon"); 12946 } 12947 if (icon.getType() != TYPE_URI_ADAPTIVE_BITMAP 12948 && icon.getType() != TYPE_URI) { 12949 Log.w(TAG, "Bubbles work best with icons of TYPE_URI or " 12950 + "TYPE_URI_ADAPTIVE_BITMAP. " 12951 + "In the future, using an icon of this type will be required."); 12952 } 12953 mPendingIntent = intent; 12954 mIcon = icon; 12955 } 12956 12957 /** 12958 * Sets the intent for the bubble. 12959 * 12960 * <p>The intent that will be used when the bubble is expanded. This will display the 12961 * app content in a floating window over the existing foreground activity. The intent 12962 * should point to a resizable activity. </p> 12963 * 12964 * @throws NullPointerException if intent is null. 12965 * @throws IllegalStateException if this builder was created via 12966 * {@link Builder#Builder(String)}. 12967 */ 12968 @NonNull setIntent(@onNull PendingIntent intent)12969 public BubbleMetadata.Builder setIntent(@NonNull PendingIntent intent) { 12970 if (mShortcutId != null) { 12971 throw new IllegalStateException("Created as a shortcut bubble, cannot set a " 12972 + "PendingIntent. Consider using " 12973 + "BubbleMetadata.Builder(PendingIntent,Icon) instead."); 12974 } 12975 if (intent == null) { 12976 throw new NullPointerException("Bubble requires non-null pending intent"); 12977 } 12978 mPendingIntent = intent; 12979 return this; 12980 } 12981 12982 /** 12983 * Sets the icon for the bubble. Can only be used if the bubble was created 12984 * via {@link Builder#Builder(PendingIntent, Icon)}. 12985 * 12986 * <p>The icon will be used to represent the bubble when it is collapsed. An icon 12987 * should be representative of the content within the bubble. If your app produces 12988 * multiple bubbles, the icon should be unique for each of them.</p> 12989 * 12990 * <p>It is recommended to use an {@link Icon} of type {@link Icon#TYPE_URI} 12991 * or {@link Icon#TYPE_URI_ADAPTIVE_BITMAP}</p> 12992 * 12993 * @throws NullPointerException if icon is null. 12994 * @throws IllegalStateException if this builder was created via 12995 * {@link Builder#Builder(String)}. 12996 */ 12997 @NonNull setIcon(@onNull Icon icon)12998 public BubbleMetadata.Builder setIcon(@NonNull Icon icon) { 12999 if (mShortcutId != null) { 13000 throw new IllegalStateException("Created as a shortcut bubble, cannot set an " 13001 + "Icon. Consider using " 13002 + "BubbleMetadata.Builder(PendingIntent,Icon) instead."); 13003 } 13004 if (icon == null) { 13005 throw new NullPointerException("Bubbles require non-null icon"); 13006 } 13007 if (icon.getType() != TYPE_URI_ADAPTIVE_BITMAP 13008 && icon.getType() != TYPE_URI) { 13009 Log.w(TAG, "Bubbles work best with icons of TYPE_URI or " 13010 + "TYPE_URI_ADAPTIVE_BITMAP. " 13011 + "In the future, using an icon of this type will be required."); 13012 } 13013 mIcon = icon; 13014 return this; 13015 } 13016 13017 /** 13018 * Sets the desired height in DPs for the expanded content of the bubble. 13019 * 13020 * <p>This height may not be respected if there is not enough space on the screen or if 13021 * the provided height is too small to be useful.</p> 13022 * 13023 * <p>If {@link #setDesiredHeightResId(int)} was previously called on this builder, the 13024 * previous value set will be cleared after calling this method, and this value will 13025 * be used instead.</p> 13026 * 13027 * <p>A desired height (in DPs or via resID) is optional.</p> 13028 * 13029 * @see #setDesiredHeightResId(int) 13030 */ 13031 @NonNull setDesiredHeight(@imensionunit = DP) int height)13032 public BubbleMetadata.Builder setDesiredHeight(@Dimension(unit = DP) int height) { 13033 mDesiredHeight = Math.max(height, 0); 13034 mDesiredHeightResId = 0; 13035 return this; 13036 } 13037 13038 13039 /** 13040 * Sets the desired height via resId for the expanded content of the bubble. 13041 * 13042 * <p>This height may not be respected if there is not enough space on the screen or if 13043 * the provided height is too small to be useful.</p> 13044 * 13045 * <p>If {@link #setDesiredHeight(int)} was previously called on this builder, the 13046 * previous value set will be cleared after calling this method, and this value will 13047 * be used instead.</p> 13048 * 13049 * <p>A desired height (in DPs or via resID) is optional.</p> 13050 * 13051 * @see #setDesiredHeight(int) 13052 */ 13053 @NonNull setDesiredHeightResId(@imenRes int heightResId)13054 public BubbleMetadata.Builder setDesiredHeightResId(@DimenRes int heightResId) { 13055 mDesiredHeightResId = heightResId; 13056 mDesiredHeight = 0; 13057 return this; 13058 } 13059 13060 /** 13061 * Sets whether the bubble will be posted in its expanded state. 13062 * 13063 * <p>This flag has no effect if the app posting the bubble is not in the foreground. 13064 * The app is considered foreground if it is visible and on the screen, note that 13065 * a foreground service does not qualify. 13066 * </p> 13067 * 13068 * <p>Generally, this flag should only be set if the user has performed an action to 13069 * request or create a bubble.</p> 13070 * 13071 * <p>Setting this flag is optional; it defaults to false.</p> 13072 */ 13073 @NonNull setAutoExpandBubble(boolean shouldExpand)13074 public BubbleMetadata.Builder setAutoExpandBubble(boolean shouldExpand) { 13075 setFlag(FLAG_AUTO_EXPAND_BUBBLE, shouldExpand); 13076 return this; 13077 } 13078 13079 /** 13080 * Sets whether the bubble will be posted <b>without</b> the associated notification in 13081 * the notification shade. 13082 * 13083 * <p>Generally, this flag should only be set if the user has performed an action to 13084 * request or create a bubble, or if the user has seen the content in the notification 13085 * and the notification is no longer relevant.</p> 13086 * 13087 * <p>Setting this flag is optional; it defaults to false.</p> 13088 */ 13089 @NonNull setSuppressNotification(boolean shouldSuppressNotif)13090 public BubbleMetadata.Builder setSuppressNotification(boolean shouldSuppressNotif) { 13091 setFlag(FLAG_SUPPRESS_NOTIFICATION, shouldSuppressNotif); 13092 return this; 13093 } 13094 13095 /** 13096 * Indicates whether the bubble should be visually suppressed from the bubble stack if 13097 * the user is viewing the same content outside of the bubble. For example, the user has 13098 * a bubble with Alice and then opens up the main app and navigates to Alice's page. 13099 * 13100 * To match the activity and the bubble notification, the bubble notification should 13101 * have a locus id set that matches a locus id set on the activity. 13102 * 13103 * {@link Notification.Builder#setLocusId(LocusId)} 13104 * {@link Activity#setLocusContext(LocusId, Bundle)} 13105 */ 13106 @NonNull setSuppressableBubble(boolean suppressBubble)13107 public BubbleMetadata.Builder setSuppressableBubble(boolean suppressBubble) { 13108 setFlag(FLAG_SUPPRESSABLE_BUBBLE, suppressBubble); 13109 return this; 13110 } 13111 13112 /** 13113 * Sets an intent to send when this bubble is explicitly removed by the user. 13114 * 13115 * <p>Setting a delete intent is optional.</p> 13116 */ 13117 @NonNull setDeleteIntent(@ullable PendingIntent deleteIntent)13118 public BubbleMetadata.Builder setDeleteIntent(@Nullable PendingIntent deleteIntent) { 13119 mDeleteIntent = deleteIntent; 13120 return this; 13121 } 13122 13123 /** 13124 * Creates the {@link BubbleMetadata} defined by this builder. 13125 * 13126 * @throws NullPointerException if required elements have not been set. 13127 */ 13128 @NonNull build()13129 public BubbleMetadata build() { 13130 if (mShortcutId == null && mPendingIntent == null) { 13131 throw new NullPointerException( 13132 "Must supply pending intent or shortcut to bubble"); 13133 } 13134 if (mShortcutId == null && mIcon == null) { 13135 throw new NullPointerException( 13136 "Must supply an icon or shortcut for the bubble"); 13137 } 13138 BubbleMetadata data = new BubbleMetadata(mPendingIntent, mDeleteIntent, 13139 mIcon, mDesiredHeight, mDesiredHeightResId, mShortcutId); 13140 data.setFlags(mFlags); 13141 return data; 13142 } 13143 13144 /** 13145 * @hide 13146 */ setFlag(int mask, boolean value)13147 public BubbleMetadata.Builder setFlag(int mask, boolean value) { 13148 if (value) { 13149 mFlags |= mask; 13150 } else { 13151 mFlags &= ~mask; 13152 } 13153 return this; 13154 } 13155 } 13156 } 13157 13158 13159 // When adding a new Style subclass here, don't forget to update 13160 // Builder.getNotificationStyleClass. 13161 13162 /** 13163 * Extender interface for use with {@link Builder#extend}. Extenders may be used to add 13164 * metadata or change options on a notification builder. 13165 */ 13166 public interface Extender { 13167 /** 13168 * Apply this extender to a notification builder. 13169 * @param builder the builder to be modified. 13170 * @return the build object for chaining. 13171 */ extend(Builder builder)13172 public Builder extend(Builder builder); 13173 } 13174 13175 /** 13176 * Helper class to add wearable extensions to notifications. 13177 * <p class="note"> See 13178 * <a href="{@docRoot}wear/notifications/creating.html">Creating Notifications 13179 * for Android Wear</a> for more information on how to use this class. 13180 * <p> 13181 * To create a notification with wearable extensions: 13182 * <ol> 13183 * <li>Create a {@link android.app.Notification.Builder}, setting any desired 13184 * properties. 13185 * <li>Create a {@link android.app.Notification.WearableExtender}. 13186 * <li>Set wearable-specific properties using the 13187 * {@code add} and {@code set} methods of {@link android.app.Notification.WearableExtender}. 13188 * <li>Call {@link android.app.Notification.Builder#extend} to apply the extensions to a 13189 * notification. 13190 * <li>Post the notification to the notification system with the 13191 * {@code NotificationManager.notify(...)} methods. 13192 * </ol> 13193 * 13194 * <pre class="prettyprint"> 13195 * Notification notif = new Notification.Builder(mContext) 13196 * .setContentTitle("New mail from " + sender.toString()) 13197 * .setContentText(subject) 13198 * .setSmallIcon(R.drawable.new_mail) 13199 * .extend(new Notification.WearableExtender() 13200 * .setContentIcon(R.drawable.new_mail)) 13201 * .build(); 13202 * NotificationManager notificationManger = 13203 * (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 13204 * notificationManger.notify(0, notif);</pre> 13205 * 13206 * <p>Wearable extensions can be accessed on an existing notification by using the 13207 * {@code WearableExtender(Notification)} constructor, 13208 * and then using the {@code get} methods to access values. 13209 * 13210 * <pre class="prettyprint"> 13211 * Notification.WearableExtender wearableExtender = new Notification.WearableExtender( 13212 * notification); 13213 * List<Notification> pages = wearableExtender.getPages();</pre> 13214 */ 13215 public static final class WearableExtender implements Extender { 13216 /** 13217 * Sentinel value for an action index that is unset. 13218 */ 13219 public static final int UNSET_ACTION_INDEX = -1; 13220 13221 /** 13222 * Size value for use with {@link #setCustomSizePreset} to show this notification with 13223 * default sizing. 13224 * <p>For custom display notifications created using {@link #setDisplayIntent}, 13225 * the default is {@link #SIZE_MEDIUM}. All other notifications size automatically based 13226 * on their content. 13227 * 13228 * @deprecated Display intents are no longer supported. 13229 */ 13230 @Deprecated 13231 public static final int SIZE_DEFAULT = 0; 13232 13233 /** 13234 * Size value for use with {@link #setCustomSizePreset} to show this notification 13235 * with an extra small size. 13236 * <p>This value is only applicable for custom display notifications created using 13237 * {@link #setDisplayIntent}. 13238 * 13239 * @deprecated Display intents are no longer supported. 13240 */ 13241 @Deprecated 13242 public static final int SIZE_XSMALL = 1; 13243 13244 /** 13245 * Size value for use with {@link #setCustomSizePreset} to show this notification 13246 * with a small size. 13247 * <p>This value is only applicable for custom display notifications created using 13248 * {@link #setDisplayIntent}. 13249 * 13250 * @deprecated Display intents are no longer supported. 13251 */ 13252 @Deprecated 13253 public static final int SIZE_SMALL = 2; 13254 13255 /** 13256 * Size value for use with {@link #setCustomSizePreset} to show this notification 13257 * with a medium size. 13258 * <p>This value is only applicable for custom display notifications created using 13259 * {@link #setDisplayIntent}. 13260 * 13261 * @deprecated Display intents are no longer supported. 13262 */ 13263 @Deprecated 13264 public static final int SIZE_MEDIUM = 3; 13265 13266 /** 13267 * Size value for use with {@link #setCustomSizePreset} to show this notification 13268 * with a large size. 13269 * <p>This value is only applicable for custom display notifications created using 13270 * {@link #setDisplayIntent}. 13271 * 13272 * @deprecated Display intents are no longer supported. 13273 */ 13274 @Deprecated 13275 public static final int SIZE_LARGE = 4; 13276 13277 /** 13278 * Size value for use with {@link #setCustomSizePreset} to show this notification 13279 * full screen. 13280 * <p>This value is only applicable for custom display notifications created using 13281 * {@link #setDisplayIntent}. 13282 * 13283 * @deprecated Display intents are no longer supported. 13284 */ 13285 @Deprecated 13286 public static final int SIZE_FULL_SCREEN = 5; 13287 13288 /** 13289 * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on for a 13290 * short amount of time when this notification is displayed on the screen. This 13291 * is the default value. 13292 * 13293 * @deprecated This feature is no longer supported. 13294 */ 13295 @Deprecated 13296 public static final int SCREEN_TIMEOUT_SHORT = 0; 13297 13298 /** 13299 * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on 13300 * for a longer amount of time when this notification is displayed on the screen. 13301 * 13302 * @deprecated This feature is no longer supported. 13303 */ 13304 @Deprecated 13305 public static final int SCREEN_TIMEOUT_LONG = -1; 13306 13307 /** Notification extra which contains wearable extensions */ 13308 private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; 13309 13310 // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. 13311 private static final String KEY_ACTIONS = "actions"; 13312 private static final String KEY_FLAGS = "flags"; 13313 static final String KEY_DISPLAY_INTENT = "displayIntent"; 13314 private static final String KEY_PAGES = "pages"; 13315 static final String KEY_BACKGROUND = "background"; 13316 private static final String KEY_CONTENT_ICON = "contentIcon"; 13317 private static final String KEY_CONTENT_ICON_GRAVITY = "contentIconGravity"; 13318 private static final String KEY_CONTENT_ACTION_INDEX = "contentActionIndex"; 13319 private static final String KEY_CUSTOM_SIZE_PRESET = "customSizePreset"; 13320 private static final String KEY_CUSTOM_CONTENT_HEIGHT = "customContentHeight"; 13321 private static final String KEY_GRAVITY = "gravity"; 13322 private static final String KEY_HINT_SCREEN_TIMEOUT = "hintScreenTimeout"; 13323 private static final String KEY_DISMISSAL_ID = "dismissalId"; 13324 private static final String KEY_BRIDGE_TAG = "bridgeTag"; 13325 13326 // Flags bitwise-ored to mFlags 13327 private static final int FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE = 0x1; 13328 private static final int FLAG_HINT_HIDE_ICON = 1 << 1; 13329 private static final int FLAG_HINT_SHOW_BACKGROUND_ONLY = 1 << 2; 13330 private static final int FLAG_START_SCROLL_BOTTOM = 1 << 3; 13331 private static final int FLAG_HINT_AVOID_BACKGROUND_CLIPPING = 1 << 4; 13332 private static final int FLAG_BIG_PICTURE_AMBIENT = 1 << 5; 13333 private static final int FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY = 1 << 6; 13334 13335 // Default value for flags integer 13336 private static final int DEFAULT_FLAGS = FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE; 13337 13338 private static final int DEFAULT_CONTENT_ICON_GRAVITY = Gravity.END; 13339 private static final int DEFAULT_GRAVITY = Gravity.BOTTOM; 13340 13341 private ArrayList<Action> mActions = new ArrayList<Action>(); 13342 private int mFlags = DEFAULT_FLAGS; 13343 private PendingIntent mDisplayIntent; 13344 private ArrayList<Notification> mPages = new ArrayList<Notification>(); 13345 private Bitmap mBackground; 13346 private int mContentIcon; 13347 private int mContentIconGravity = DEFAULT_CONTENT_ICON_GRAVITY; 13348 private int mContentActionIndex = UNSET_ACTION_INDEX; 13349 private int mCustomSizePreset = SIZE_DEFAULT; 13350 private int mCustomContentHeight; 13351 private int mGravity = DEFAULT_GRAVITY; 13352 private int mHintScreenTimeout; 13353 private String mDismissalId; 13354 private String mBridgeTag; 13355 13356 /** 13357 * Create a {@link android.app.Notification.WearableExtender} with default 13358 * options. 13359 */ WearableExtender()13360 public WearableExtender() { 13361 } 13362 WearableExtender(Notification notif)13363 public WearableExtender(Notification notif) { 13364 Bundle wearableBundle = notif.extras.getBundle(EXTRA_WEARABLE_EXTENSIONS); 13365 if (wearableBundle != null) { 13366 List<Action> actions = wearableBundle.getParcelableArrayList(KEY_ACTIONS, android.app.Notification.Action.class); 13367 if (actions != null) { 13368 mActions.addAll(actions); 13369 } 13370 13371 mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); 13372 mDisplayIntent = wearableBundle.getParcelable( 13373 KEY_DISPLAY_INTENT, PendingIntent.class); 13374 13375 Notification[] pages = getParcelableArrayFromBundle( 13376 wearableBundle, KEY_PAGES, Notification.class); 13377 if (pages != null) { 13378 Collections.addAll(mPages, pages); 13379 } 13380 13381 mBackground = wearableBundle.getParcelable(KEY_BACKGROUND, Bitmap.class); 13382 mContentIcon = wearableBundle.getInt(KEY_CONTENT_ICON); 13383 mContentIconGravity = wearableBundle.getInt(KEY_CONTENT_ICON_GRAVITY, 13384 DEFAULT_CONTENT_ICON_GRAVITY); 13385 mContentActionIndex = wearableBundle.getInt(KEY_CONTENT_ACTION_INDEX, 13386 UNSET_ACTION_INDEX); 13387 mCustomSizePreset = wearableBundle.getInt(KEY_CUSTOM_SIZE_PRESET, 13388 SIZE_DEFAULT); 13389 mCustomContentHeight = wearableBundle.getInt(KEY_CUSTOM_CONTENT_HEIGHT); 13390 mGravity = wearableBundle.getInt(KEY_GRAVITY, DEFAULT_GRAVITY); 13391 mHintScreenTimeout = wearableBundle.getInt(KEY_HINT_SCREEN_TIMEOUT); 13392 mDismissalId = wearableBundle.getString(KEY_DISMISSAL_ID); 13393 mBridgeTag = wearableBundle.getString(KEY_BRIDGE_TAG); 13394 } 13395 } 13396 13397 /** 13398 * Apply wearable extensions to a notification that is being built. This is typically 13399 * called by the {@link android.app.Notification.Builder#extend} method of 13400 * {@link android.app.Notification.Builder}. 13401 */ 13402 @Override extend(Notification.Builder builder)13403 public Notification.Builder extend(Notification.Builder builder) { 13404 Bundle wearableBundle = new Bundle(); 13405 13406 if (!mActions.isEmpty()) { 13407 wearableBundle.putParcelableArrayList(KEY_ACTIONS, mActions); 13408 } 13409 if (mFlags != DEFAULT_FLAGS) { 13410 wearableBundle.putInt(KEY_FLAGS, mFlags); 13411 } 13412 if (mDisplayIntent != null) { 13413 wearableBundle.putParcelable(KEY_DISPLAY_INTENT, mDisplayIntent); 13414 } 13415 if (!mPages.isEmpty()) { 13416 wearableBundle.putParcelableArray(KEY_PAGES, mPages.toArray( 13417 new Notification[mPages.size()])); 13418 } 13419 13420 if (mBackground != null) { 13421 // Keeping WearableExtender backgrounds in memory despite them being deprecated has 13422 // added noticeable increase in system server and system ui memory usage. After 13423 // target VERSION_CODE#VANILLA_ICE_CREAM the background will not be populated 13424 // anymore. 13425 if (CompatChanges.isChangeEnabled(WEARABLE_EXTENDER_BACKGROUND_BLOCKED)) { 13426 Log.d(TAG, "Use of background in WearableExtenders has been deprecated and " 13427 + "will not be populated anymore."); 13428 } else { 13429 wearableBundle.putParcelable(KEY_BACKGROUND, mBackground); 13430 } 13431 } 13432 13433 if (mContentIcon != 0) { 13434 wearableBundle.putInt(KEY_CONTENT_ICON, mContentIcon); 13435 } 13436 if (mContentIconGravity != DEFAULT_CONTENT_ICON_GRAVITY) { 13437 wearableBundle.putInt(KEY_CONTENT_ICON_GRAVITY, mContentIconGravity); 13438 } 13439 if (mContentActionIndex != UNSET_ACTION_INDEX) { 13440 wearableBundle.putInt(KEY_CONTENT_ACTION_INDEX, 13441 mContentActionIndex); 13442 } 13443 if (mCustomSizePreset != SIZE_DEFAULT) { 13444 wearableBundle.putInt(KEY_CUSTOM_SIZE_PRESET, mCustomSizePreset); 13445 } 13446 if (mCustomContentHeight != 0) { 13447 wearableBundle.putInt(KEY_CUSTOM_CONTENT_HEIGHT, mCustomContentHeight); 13448 } 13449 if (mGravity != DEFAULT_GRAVITY) { 13450 wearableBundle.putInt(KEY_GRAVITY, mGravity); 13451 } 13452 if (mHintScreenTimeout != 0) { 13453 wearableBundle.putInt(KEY_HINT_SCREEN_TIMEOUT, mHintScreenTimeout); 13454 } 13455 if (mDismissalId != null) { 13456 wearableBundle.putString(KEY_DISMISSAL_ID, mDismissalId); 13457 } 13458 if (mBridgeTag != null) { 13459 wearableBundle.putString(KEY_BRIDGE_TAG, mBridgeTag); 13460 } 13461 13462 builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); 13463 return builder; 13464 } 13465 13466 @Override clone()13467 public WearableExtender clone() { 13468 WearableExtender that = new WearableExtender(); 13469 that.mActions = new ArrayList<Action>(this.mActions); 13470 that.mFlags = this.mFlags; 13471 that.mDisplayIntent = this.mDisplayIntent; 13472 that.mPages = new ArrayList<Notification>(this.mPages); 13473 that.mBackground = this.mBackground; 13474 that.mContentIcon = this.mContentIcon; 13475 that.mContentIconGravity = this.mContentIconGravity; 13476 that.mContentActionIndex = this.mContentActionIndex; 13477 that.mCustomSizePreset = this.mCustomSizePreset; 13478 that.mCustomContentHeight = this.mCustomContentHeight; 13479 that.mGravity = this.mGravity; 13480 that.mHintScreenTimeout = this.mHintScreenTimeout; 13481 that.mDismissalId = this.mDismissalId; 13482 that.mBridgeTag = this.mBridgeTag; 13483 return that; 13484 } 13485 13486 /** 13487 * Add a wearable action to this notification. 13488 * 13489 * <p>When wearable actions are added using this method, the set of actions that 13490 * show on a wearable device splits from devices that only show actions added 13491 * using {@link android.app.Notification.Builder#addAction}. This allows for customization 13492 * of which actions display on different devices. 13493 * 13494 * @param action the action to add to this notification 13495 * @return this object for method chaining 13496 * @see android.app.Notification.Action 13497 */ addAction(Action action)13498 public WearableExtender addAction(Action action) { 13499 mActions.add(action); 13500 return this; 13501 } 13502 13503 /** 13504 * Adds wearable actions to this notification. 13505 * 13506 * <p>When wearable actions are added using this method, the set of actions that 13507 * show on a wearable device splits from devices that only show actions added 13508 * using {@link android.app.Notification.Builder#addAction}. This allows for customization 13509 * of which actions display on different devices. 13510 * 13511 * @param actions the actions to add to this notification 13512 * @return this object for method chaining 13513 * @see android.app.Notification.Action 13514 */ addActions(List<Action> actions)13515 public WearableExtender addActions(List<Action> actions) { 13516 mActions.addAll(actions); 13517 return this; 13518 } 13519 13520 /** 13521 * Clear all wearable actions present on this builder. 13522 * @return this object for method chaining. 13523 * @see #addAction 13524 */ clearActions()13525 public WearableExtender clearActions() { 13526 mActions.clear(); 13527 return this; 13528 } 13529 13530 /** 13531 * Get the wearable actions present on this notification. 13532 */ getActions()13533 public List<Action> getActions() { 13534 return mActions; 13535 } 13536 13537 /** 13538 * Set an intent to launch inside of an activity view when displaying 13539 * this notification. The {@link PendingIntent} provided should be for an activity. 13540 * 13541 * <pre class="prettyprint"> 13542 * Intent displayIntent = new Intent(context, MyDisplayActivity.class); 13543 * PendingIntent displayPendingIntent = PendingIntent.getActivity(context, 13544 * 0, displayIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED); 13545 * Notification notif = new Notification.Builder(context) 13546 * .extend(new Notification.WearableExtender() 13547 * .setDisplayIntent(displayPendingIntent) 13548 * .setCustomSizePreset(Notification.WearableExtender.SIZE_MEDIUM)) 13549 * .build();</pre> 13550 * 13551 * <p>The activity to launch needs to allow embedding, must be exported, and 13552 * should have an empty task affinity. It is also recommended to use the device 13553 * default light theme. 13554 * 13555 * <p>Example AndroidManifest.xml entry: 13556 * <pre class="prettyprint"> 13557 * <activity android:name="com.example.MyDisplayActivity" 13558 * android:exported="true" 13559 * android:allowEmbedded="true" 13560 * android:taskAffinity="" 13561 * android:theme="@android:style/Theme.DeviceDefault.Light" /></pre> 13562 * 13563 * @param intent the {@link PendingIntent} for an activity 13564 * @return this object for method chaining 13565 * @see android.app.Notification.WearableExtender#getDisplayIntent 13566 * @deprecated Display intents are no longer supported. 13567 */ 13568 @Deprecated setDisplayIntent(PendingIntent intent)13569 public WearableExtender setDisplayIntent(PendingIntent intent) { 13570 mDisplayIntent = intent; 13571 return this; 13572 } 13573 13574 /** 13575 * Get the intent to launch inside of an activity view when displaying this 13576 * notification. This {@code PendingIntent} should be for an activity. 13577 * 13578 * @deprecated Display intents are no longer supported. 13579 */ 13580 @Deprecated getDisplayIntent()13581 public PendingIntent getDisplayIntent() { 13582 return mDisplayIntent; 13583 } 13584 13585 /** 13586 * Add an additional page of content to display with this notification. The current 13587 * notification forms the first page, and pages added using this function form 13588 * subsequent pages. This field can be used to separate a notification into multiple 13589 * sections. 13590 * 13591 * @param page the notification to add as another page 13592 * @return this object for method chaining 13593 * @see android.app.Notification.WearableExtender#getPages 13594 * @deprecated Multiple content pages are no longer supported. 13595 */ 13596 @Deprecated addPage(Notification page)13597 public WearableExtender addPage(Notification page) { 13598 mPages.add(page); 13599 return this; 13600 } 13601 13602 /** 13603 * Add additional pages of content to display with this notification. The current 13604 * notification forms the first page, and pages added using this function form 13605 * subsequent pages. This field can be used to separate a notification into multiple 13606 * sections. 13607 * 13608 * @param pages a list of notifications 13609 * @return this object for method chaining 13610 * @see android.app.Notification.WearableExtender#getPages 13611 * @deprecated Multiple content pages are no longer supported. 13612 */ 13613 @Deprecated addPages(List<Notification> pages)13614 public WearableExtender addPages(List<Notification> pages) { 13615 mPages.addAll(pages); 13616 return this; 13617 } 13618 13619 /** 13620 * Clear all additional pages present on this builder. 13621 * @return this object for method chaining. 13622 * @see #addPage 13623 * @deprecated Multiple content pages are no longer supported. 13624 */ 13625 @Deprecated clearPages()13626 public WearableExtender clearPages() { 13627 mPages.clear(); 13628 return this; 13629 } 13630 13631 /** 13632 * Get the array of additional pages of content for displaying this notification. The 13633 * current notification forms the first page, and elements within this array form 13634 * subsequent pages. This field can be used to separate a notification into multiple 13635 * sections. 13636 * @return the pages for this notification 13637 * @deprecated Multiple content pages are no longer supported. 13638 */ 13639 @Deprecated getPages()13640 public List<Notification> getPages() { 13641 return mPages; 13642 } 13643 13644 /** 13645 * Set a background image to be displayed behind the notification content. 13646 * Contrary to the {@link android.app.Notification.BigPictureStyle}, this background 13647 * will work with any notification style. 13648 * 13649 * @param background the background bitmap 13650 * @return this object for method chaining 13651 * @removed Not functional since {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}. 13652 * The wearable background is not used by wearables anymore and uses up 13653 * unnecessary memory. 13654 */ 13655 @Deprecated setBackground(Bitmap background)13656 public WearableExtender setBackground(Bitmap background) { 13657 // Keeping WearableExtender backgrounds in memory despite them being deprecated has 13658 // added noticeable increase in system server and system ui memory usage. After 13659 // target VERSION_CODE#VANILLA_ICE_CREAM the background will not be populated anymore. 13660 if (CompatChanges.isChangeEnabled(WEARABLE_EXTENDER_BACKGROUND_BLOCKED)) { 13661 Log.d(TAG, "Use of background in WearableExtenders has been deprecated and " 13662 + "will not be populated anymore."); 13663 } else { 13664 mBackground = background; 13665 } 13666 return this; 13667 } 13668 13669 /** 13670 * Get a background image to be displayed behind the notification content. 13671 * Contrary to the {@link android.app.Notification.BigPictureStyle}, this background 13672 * will work with any notification style. 13673 * 13674 * @return the background image 13675 * @removed Not functional since {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}. The 13676 * wearable background is not used by wearables anymore and uses up 13677 * unnecessary memory. 13678 */ 13679 @Deprecated getBackground()13680 public Bitmap getBackground() { 13681 Log.w(TAG, "Use of background in WearableExtender has been removed, returning null."); 13682 return mBackground; 13683 } 13684 13685 /** 13686 * Set an icon that goes with the content of this notification. 13687 */ 13688 @Deprecated setContentIcon(int icon)13689 public WearableExtender setContentIcon(int icon) { 13690 mContentIcon = icon; 13691 return this; 13692 } 13693 13694 /** 13695 * Get an icon that goes with the content of this notification. 13696 */ 13697 @Deprecated getContentIcon()13698 public int getContentIcon() { 13699 return mContentIcon; 13700 } 13701 13702 /** 13703 * Set the gravity that the content icon should have within the notification display. 13704 * Supported values include {@link android.view.Gravity#START} and 13705 * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. 13706 * @see #setContentIcon 13707 */ 13708 @Deprecated setContentIconGravity(int contentIconGravity)13709 public WearableExtender setContentIconGravity(int contentIconGravity) { 13710 mContentIconGravity = contentIconGravity; 13711 return this; 13712 } 13713 13714 /** 13715 * Get the gravity that the content icon should have within the notification display. 13716 * Supported values include {@link android.view.Gravity#START} and 13717 * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. 13718 * @see #getContentIcon 13719 */ 13720 @Deprecated getContentIconGravity()13721 public int getContentIconGravity() { 13722 return mContentIconGravity; 13723 } 13724 13725 /** 13726 * Set an action from this notification's actions as the primary action. If the action has a 13727 * {@link RemoteInput} associated with it, shortcuts to the options for that input are shown 13728 * directly on the notification. 13729 * 13730 * @param actionIndex The index of the primary action. 13731 * If wearable actions were added to the main notification, this index 13732 * will apply to that list, otherwise it will apply to the regular 13733 * actions list. 13734 */ setContentAction(int actionIndex)13735 public WearableExtender setContentAction(int actionIndex) { 13736 mContentActionIndex = actionIndex; 13737 return this; 13738 } 13739 13740 /** 13741 * Get the index of the notification action, if any, that was specified as the primary 13742 * action. 13743 * 13744 * <p>If wearable specific actions were added to the main notification, this index will 13745 * apply to that list, otherwise it will apply to the regular actions list. 13746 * 13747 * @return the action index or {@link #UNSET_ACTION_INDEX} if no action was selected. 13748 */ getContentAction()13749 public int getContentAction() { 13750 return mContentActionIndex; 13751 } 13752 13753 /** 13754 * Set the gravity that this notification should have within the available viewport space. 13755 * Supported values include {@link android.view.Gravity#TOP}, 13756 * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. 13757 * The default value is {@link android.view.Gravity#BOTTOM}. 13758 */ 13759 @Deprecated setGravity(int gravity)13760 public WearableExtender setGravity(int gravity) { 13761 mGravity = gravity; 13762 return this; 13763 } 13764 13765 /** 13766 * Get the gravity that this notification should have within the available viewport space. 13767 * Supported values include {@link android.view.Gravity#TOP}, 13768 * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. 13769 * The default value is {@link android.view.Gravity#BOTTOM}. 13770 */ 13771 @Deprecated getGravity()13772 public int getGravity() { 13773 return mGravity; 13774 } 13775 13776 /** 13777 * Set the custom size preset for the display of this notification out of the available 13778 * presets found in {@link android.app.Notification.WearableExtender}, e.g. 13779 * {@link #SIZE_LARGE}. 13780 * <p>Some custom size presets are only applicable for custom display notifications created 13781 * using {@link android.app.Notification.WearableExtender#setDisplayIntent}. Check the 13782 * documentation for the preset in question. See also 13783 * {@link #setCustomContentHeight} and {@link #getCustomSizePreset}. 13784 */ 13785 @Deprecated setCustomSizePreset(int sizePreset)13786 public WearableExtender setCustomSizePreset(int sizePreset) { 13787 mCustomSizePreset = sizePreset; 13788 return this; 13789 } 13790 13791 /** 13792 * Get the custom size preset for the display of this notification out of the available 13793 * presets found in {@link android.app.Notification.WearableExtender}, e.g. 13794 * {@link #SIZE_LARGE}. 13795 * <p>Some custom size presets are only applicable for custom display notifications created 13796 * using {@link #setDisplayIntent}. Check the documentation for the preset in question. 13797 * See also {@link #setCustomContentHeight} and {@link #setCustomSizePreset}. 13798 */ 13799 @Deprecated getCustomSizePreset()13800 public int getCustomSizePreset() { 13801 return mCustomSizePreset; 13802 } 13803 13804 /** 13805 * Set the custom height in pixels for the display of this notification's content. 13806 * <p>This option is only available for custom display notifications created 13807 * using {@link android.app.Notification.WearableExtender#setDisplayIntent}. See also 13808 * {@link android.app.Notification.WearableExtender#setCustomSizePreset} and 13809 * {@link #getCustomContentHeight}. 13810 */ 13811 @Deprecated setCustomContentHeight(int height)13812 public WearableExtender setCustomContentHeight(int height) { 13813 mCustomContentHeight = height; 13814 return this; 13815 } 13816 13817 /** 13818 * Get the custom height in pixels for the display of this notification's content. 13819 * <p>This option is only available for custom display notifications created 13820 * using {@link #setDisplayIntent}. See also {@link #setCustomSizePreset} and 13821 * {@link #setCustomContentHeight}. 13822 */ 13823 @Deprecated getCustomContentHeight()13824 public int getCustomContentHeight() { 13825 return mCustomContentHeight; 13826 } 13827 13828 /** 13829 * Set whether the scrolling position for the contents of this notification should start 13830 * at the bottom of the contents instead of the top when the contents are too long to 13831 * display within the screen. Default is false (start scroll at the top). 13832 */ setStartScrollBottom(boolean startScrollBottom)13833 public WearableExtender setStartScrollBottom(boolean startScrollBottom) { 13834 setFlag(FLAG_START_SCROLL_BOTTOM, startScrollBottom); 13835 return this; 13836 } 13837 13838 /** 13839 * Get whether the scrolling position for the contents of this notification should start 13840 * at the bottom of the contents instead of the top when the contents are too long to 13841 * display within the screen. Default is false (start scroll at the top). 13842 */ getStartScrollBottom()13843 public boolean getStartScrollBottom() { 13844 return (mFlags & FLAG_START_SCROLL_BOTTOM) != 0; 13845 } 13846 13847 /** 13848 * Set whether the content intent is available when the wearable device is not connected 13849 * to a companion device. The user can still trigger this intent when the wearable device 13850 * is offline, but a visual hint will indicate that the content intent may not be available. 13851 * Defaults to true. 13852 */ setContentIntentAvailableOffline( boolean contentIntentAvailableOffline)13853 public WearableExtender setContentIntentAvailableOffline( 13854 boolean contentIntentAvailableOffline) { 13855 setFlag(FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE, contentIntentAvailableOffline); 13856 return this; 13857 } 13858 13859 /** 13860 * Get whether the content intent is available when the wearable device is not connected 13861 * to a companion device. The user can still trigger this intent when the wearable device 13862 * is offline, but a visual hint will indicate that the content intent may not be available. 13863 * Defaults to true. 13864 */ getContentIntentAvailableOffline()13865 public boolean getContentIntentAvailableOffline() { 13866 return (mFlags & FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE) != 0; 13867 } 13868 13869 /** 13870 * Set a hint that this notification's icon should not be displayed. 13871 * @param hintHideIcon {@code true} to hide the icon, {@code false} otherwise. 13872 * @return this object for method chaining 13873 */ 13874 @Deprecated setHintHideIcon(boolean hintHideIcon)13875 public WearableExtender setHintHideIcon(boolean hintHideIcon) { 13876 setFlag(FLAG_HINT_HIDE_ICON, hintHideIcon); 13877 return this; 13878 } 13879 13880 /** 13881 * Get a hint that this notification's icon should not be displayed. 13882 * @return {@code true} if this icon should not be displayed, false otherwise. 13883 * The default value is {@code false} if this was never set. 13884 */ 13885 @Deprecated getHintHideIcon()13886 public boolean getHintHideIcon() { 13887 return (mFlags & FLAG_HINT_HIDE_ICON) != 0; 13888 } 13889 13890 /** 13891 * Set a visual hint that only the background image of this notification should be 13892 * displayed, and other semantic content should be hidden. This hint is only applicable 13893 * to sub-pages added using {@link #addPage}. 13894 */ 13895 @Deprecated setHintShowBackgroundOnly(boolean hintShowBackgroundOnly)13896 public WearableExtender setHintShowBackgroundOnly(boolean hintShowBackgroundOnly) { 13897 setFlag(FLAG_HINT_SHOW_BACKGROUND_ONLY, hintShowBackgroundOnly); 13898 return this; 13899 } 13900 13901 /** 13902 * Get a visual hint that only the background image of this notification should be 13903 * displayed, and other semantic content should be hidden. This hint is only applicable 13904 * to sub-pages added using {@link android.app.Notification.WearableExtender#addPage}. 13905 */ 13906 @Deprecated getHintShowBackgroundOnly()13907 public boolean getHintShowBackgroundOnly() { 13908 return (mFlags & FLAG_HINT_SHOW_BACKGROUND_ONLY) != 0; 13909 } 13910 13911 /** 13912 * Set a hint that this notification's background should not be clipped if possible, 13913 * and should instead be resized to fully display on the screen, retaining the aspect 13914 * ratio of the image. This can be useful for images like barcodes or qr codes. 13915 * @param hintAvoidBackgroundClipping {@code true} to avoid clipping if possible. 13916 * @return this object for method chaining 13917 */ 13918 @Deprecated setHintAvoidBackgroundClipping( boolean hintAvoidBackgroundClipping)13919 public WearableExtender setHintAvoidBackgroundClipping( 13920 boolean hintAvoidBackgroundClipping) { 13921 setFlag(FLAG_HINT_AVOID_BACKGROUND_CLIPPING, hintAvoidBackgroundClipping); 13922 return this; 13923 } 13924 13925 /** 13926 * Get a hint that this notification's background should not be clipped if possible, 13927 * and should instead be resized to fully display on the screen, retaining the aspect 13928 * ratio of the image. This can be useful for images like barcodes or qr codes. 13929 * @return {@code true} if it's ok if the background is clipped on the screen, false 13930 * otherwise. The default value is {@code false} if this was never set. 13931 */ 13932 @Deprecated getHintAvoidBackgroundClipping()13933 public boolean getHintAvoidBackgroundClipping() { 13934 return (mFlags & FLAG_HINT_AVOID_BACKGROUND_CLIPPING) != 0; 13935 } 13936 13937 /** 13938 * Set a hint that the screen should remain on for at least this duration when 13939 * this notification is displayed on the screen. 13940 * @param timeout The requested screen timeout in milliseconds. Can also be either 13941 * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. 13942 * @return this object for method chaining 13943 */ 13944 @Deprecated setHintScreenTimeout(int timeout)13945 public WearableExtender setHintScreenTimeout(int timeout) { 13946 mHintScreenTimeout = timeout; 13947 return this; 13948 } 13949 13950 /** 13951 * Get the duration, in milliseconds, that the screen should remain on for 13952 * when this notification is displayed. 13953 * @return the duration in milliseconds if > 0, or either one of the sentinel values 13954 * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. 13955 */ 13956 @Deprecated getHintScreenTimeout()13957 public int getHintScreenTimeout() { 13958 return mHintScreenTimeout; 13959 } 13960 13961 /** 13962 * Set a hint that this notification's {@link BigPictureStyle} (if present) should be 13963 * converted to low-bit and displayed in ambient mode, especially useful for barcodes and 13964 * qr codes, as well as other simple black-and-white tickets. 13965 * @param hintAmbientBigPicture {@code true} to enable converstion and ambient. 13966 * @return this object for method chaining 13967 * @deprecated This feature is no longer supported. 13968 */ 13969 @Deprecated setHintAmbientBigPicture(boolean hintAmbientBigPicture)13970 public WearableExtender setHintAmbientBigPicture(boolean hintAmbientBigPicture) { 13971 setFlag(FLAG_BIG_PICTURE_AMBIENT, hintAmbientBigPicture); 13972 return this; 13973 } 13974 13975 /** 13976 * Get a hint that this notification's {@link BigPictureStyle} (if present) should be 13977 * converted to low-bit and displayed in ambient mode, especially useful for barcodes and 13978 * qr codes, as well as other simple black-and-white tickets. 13979 * @return {@code true} if it should be displayed in ambient, false otherwise 13980 * otherwise. The default value is {@code false} if this was never set. 13981 * @deprecated This feature is no longer supported. 13982 */ 13983 @Deprecated getHintAmbientBigPicture()13984 public boolean getHintAmbientBigPicture() { 13985 return (mFlags & FLAG_BIG_PICTURE_AMBIENT) != 0; 13986 } 13987 13988 /** 13989 * Set a hint that this notification's content intent will launch an {@link Activity} 13990 * directly, telling the platform that it can generate the appropriate transitions. 13991 * @param hintContentIntentLaunchesActivity {@code true} if the content intent will launch 13992 * an activity and transitions should be generated, false otherwise. 13993 * @return this object for method chaining 13994 */ setHintContentIntentLaunchesActivity( boolean hintContentIntentLaunchesActivity)13995 public WearableExtender setHintContentIntentLaunchesActivity( 13996 boolean hintContentIntentLaunchesActivity) { 13997 setFlag(FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY, hintContentIntentLaunchesActivity); 13998 return this; 13999 } 14000 14001 /** 14002 * Get a hint that this notification's content intent will launch an {@link Activity} 14003 * directly, telling the platform that it can generate the appropriate transitions 14004 * @return {@code true} if the content intent will launch an activity and transitions should 14005 * be generated, false otherwise. The default value is {@code false} if this was never set. 14006 */ getHintContentIntentLaunchesActivity()14007 public boolean getHintContentIntentLaunchesActivity() { 14008 return (mFlags & FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY) != 0; 14009 } 14010 14011 /** 14012 * Sets the dismissal id for this notification. If a notification is posted with a 14013 * dismissal id, then when that notification is canceled, notifications on other wearables 14014 * and the paired Android phone having that same dismissal id will also be canceled. See 14015 * <a href="{@docRoot}wear/notifications/index.html">Adding Wearable Features to 14016 * Notifications</a> for more information. 14017 * @param dismissalId the dismissal id of the notification. 14018 * @return this object for method chaining 14019 */ setDismissalId(String dismissalId)14020 public WearableExtender setDismissalId(String dismissalId) { 14021 mDismissalId = dismissalId; 14022 return this; 14023 } 14024 14025 /** 14026 * Returns the dismissal id of the notification. 14027 * @return the dismissal id of the notification or null if it has not been set. 14028 */ getDismissalId()14029 public String getDismissalId() { 14030 return mDismissalId; 14031 } 14032 14033 /** 14034 * Sets a bridge tag for this notification. A bridge tag can be set for notifications 14035 * posted from a phone to provide finer-grained control on what notifications are bridged 14036 * to wearables. See <a href="{@docRoot}wear/notifications/index.html">Adding Wearable 14037 * Features to Notifications</a> for more information. 14038 * @param bridgeTag the bridge tag of the notification. 14039 * @return this object for method chaining 14040 */ setBridgeTag(String bridgeTag)14041 public WearableExtender setBridgeTag(String bridgeTag) { 14042 mBridgeTag = bridgeTag; 14043 return this; 14044 } 14045 14046 /** 14047 * Returns the bridge tag of the notification. 14048 * @return the bridge tag or null if not present. 14049 */ getBridgeTag()14050 public String getBridgeTag() { 14051 return mBridgeTag; 14052 } 14053 setFlag(int mask, boolean value)14054 private void setFlag(int mask, boolean value) { 14055 if (value) { 14056 mFlags |= mask; 14057 } else { 14058 mFlags &= ~mask; 14059 } 14060 } 14061 visitUris(@onNull Consumer<Uri> visitor)14062 private void visitUris(@NonNull Consumer<Uri> visitor) { 14063 for (Action action : mActions) { 14064 action.visitUris(visitor); 14065 } 14066 } 14067 } 14068 14069 /** 14070 * <p>Helper class to add Android Auto extensions to notifications. To create a notification 14071 * with car extensions: 14072 * 14073 * <ol> 14074 * <li>Create an {@link Notification.Builder}, setting any desired 14075 * properties. 14076 * <li>Create a {@link CarExtender}. 14077 * <li>Set car-specific properties using the {@code add} and {@code set} methods of 14078 * {@link CarExtender}. 14079 * <li>Call {@link Notification.Builder#extend(Notification.Extender)} 14080 * to apply the extensions to a notification. 14081 * </ol> 14082 * 14083 * <pre class="prettyprint"> 14084 * Notification notification = new Notification.Builder(context) 14085 * ... 14086 * .extend(new CarExtender() 14087 * .set*(...)) 14088 * .build(); 14089 * </pre> 14090 * 14091 * <p>Car extensions can be accessed on an existing notification by using the 14092 * {@code CarExtender(Notification)} constructor, and then using the {@code get} methods 14093 * to access values. 14094 */ 14095 public static final class CarExtender implements Extender { 14096 private static final String TAG = "CarExtender"; 14097 14098 private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS"; 14099 private static final String EXTRA_LARGE_ICON = "large_icon"; 14100 private static final String EXTRA_CONVERSATION = "car_conversation"; 14101 private static final String EXTRA_COLOR = "app_color"; 14102 14103 private Bitmap mLargeIcon; 14104 private UnreadConversation mUnreadConversation; 14105 private int mColor = Notification.COLOR_DEFAULT; 14106 14107 /** 14108 * Create a {@link CarExtender} with default options. 14109 */ CarExtender()14110 public CarExtender() { 14111 } 14112 14113 /** 14114 * Create a {@link CarExtender} from the CarExtender options of an existing Notification. 14115 * 14116 * @param notif The notification from which to copy options. 14117 */ CarExtender(Notification notif)14118 public CarExtender(Notification notif) { 14119 Bundle carBundle = notif.extras == null ? 14120 null : notif.extras.getBundle(EXTRA_CAR_EXTENDER); 14121 if (carBundle != null) { 14122 mLargeIcon = carBundle.getParcelable(EXTRA_LARGE_ICON, Bitmap.class); 14123 mColor = carBundle.getInt(EXTRA_COLOR, Notification.COLOR_DEFAULT); 14124 14125 Bundle b = carBundle.getBundle(EXTRA_CONVERSATION); 14126 mUnreadConversation = UnreadConversation.getUnreadConversationFromBundle(b); 14127 } 14128 } 14129 14130 /** 14131 * Apply car extensions to a notification that is being built. This is typically called by 14132 * the {@link Notification.Builder#extend(Notification.Extender)} 14133 * method of {@link Notification.Builder}. 14134 */ 14135 @Override extend(Notification.Builder builder)14136 public Notification.Builder extend(Notification.Builder builder) { 14137 Bundle carExtensions = new Bundle(); 14138 14139 if (mLargeIcon != null) { 14140 carExtensions.putParcelable(EXTRA_LARGE_ICON, mLargeIcon); 14141 } 14142 if (mColor != Notification.COLOR_DEFAULT) { 14143 carExtensions.putInt(EXTRA_COLOR, mColor); 14144 } 14145 14146 if (mUnreadConversation != null) { 14147 Bundle b = mUnreadConversation.getBundleForUnreadConversation(); 14148 carExtensions.putBundle(EXTRA_CONVERSATION, b); 14149 } 14150 14151 builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions); 14152 return builder; 14153 } 14154 14155 /** 14156 * Sets the accent color to use when Android Auto presents the notification. 14157 * 14158 * Android Auto uses the color set with {@link Notification.Builder#setColor(int)} 14159 * to accent the displayed notification. However, not all colors are acceptable in an 14160 * automotive setting. This method can be used to override the color provided in the 14161 * notification in such a situation. 14162 */ setColor(@olorInt int color)14163 public CarExtender setColor(@ColorInt int color) { 14164 mColor = color; 14165 return this; 14166 } 14167 14168 /** 14169 * Gets the accent color. 14170 * 14171 * @see #setColor 14172 */ 14173 @ColorInt getColor()14174 public int getColor() { 14175 return mColor; 14176 } 14177 14178 /** 14179 * Sets the large icon of the car notification. 14180 * 14181 * If no large icon is set in the extender, Android Auto will display the icon 14182 * specified by {@link Notification.Builder#setLargeIcon(android.graphics.Bitmap)} 14183 * 14184 * @param largeIcon The large icon to use in the car notification. 14185 * @return This object for method chaining. 14186 */ setLargeIcon(Bitmap largeIcon)14187 public CarExtender setLargeIcon(Bitmap largeIcon) { 14188 mLargeIcon = largeIcon; 14189 return this; 14190 } 14191 14192 /** 14193 * Gets the large icon used in this car notification, or null if no icon has been set. 14194 * 14195 * @return The large icon for the car notification. 14196 * @see CarExtender#setLargeIcon 14197 */ getLargeIcon()14198 public Bitmap getLargeIcon() { 14199 return mLargeIcon; 14200 } 14201 14202 /** 14203 * Sets the unread conversation in a message notification. 14204 * 14205 * @param unreadConversation The unread part of the conversation this notification conveys. 14206 * @return This object for method chaining. 14207 */ setUnreadConversation(UnreadConversation unreadConversation)14208 public CarExtender setUnreadConversation(UnreadConversation unreadConversation) { 14209 mUnreadConversation = unreadConversation; 14210 return this; 14211 } 14212 14213 /** 14214 * Returns the unread conversation conveyed by this notification. 14215 * 14216 * @see #setUnreadConversation(UnreadConversation) 14217 */ getUnreadConversation()14218 public UnreadConversation getUnreadConversation() { 14219 return mUnreadConversation; 14220 } 14221 14222 /** 14223 * A class which holds the unread messages from a conversation. 14224 */ 14225 public static class UnreadConversation { 14226 private static final String KEY_AUTHOR = "author"; 14227 private static final String KEY_TEXT = "text"; 14228 private static final String KEY_MESSAGES = "messages"; 14229 static final String KEY_REMOTE_INPUT = "remote_input"; 14230 static final String KEY_ON_REPLY = "on_reply"; 14231 static final String KEY_ON_READ = "on_read"; 14232 private static final String KEY_PARTICIPANTS = "participants"; 14233 private static final String KEY_TIMESTAMP = "timestamp"; 14234 14235 private final String[] mMessages; 14236 private final RemoteInput mRemoteInput; 14237 private final PendingIntent mReplyPendingIntent; 14238 private final PendingIntent mReadPendingIntent; 14239 private final String[] mParticipants; 14240 private final long mLatestTimestamp; 14241 UnreadConversation(String[] messages, RemoteInput remoteInput, PendingIntent replyPendingIntent, PendingIntent readPendingIntent, String[] participants, long latestTimestamp)14242 UnreadConversation(String[] messages, RemoteInput remoteInput, 14243 PendingIntent replyPendingIntent, PendingIntent readPendingIntent, 14244 String[] participants, long latestTimestamp) { 14245 mMessages = messages; 14246 mRemoteInput = remoteInput; 14247 mReadPendingIntent = readPendingIntent; 14248 mReplyPendingIntent = replyPendingIntent; 14249 mParticipants = participants; 14250 mLatestTimestamp = latestTimestamp; 14251 } 14252 14253 /** 14254 * Gets the list of messages conveyed by this notification. 14255 */ getMessages()14256 public String[] getMessages() { 14257 return mMessages; 14258 } 14259 14260 /** 14261 * Gets the remote input that will be used to convey the response to a message list, or 14262 * null if no such remote input exists. 14263 */ getRemoteInput()14264 public RemoteInput getRemoteInput() { 14265 return mRemoteInput; 14266 } 14267 14268 /** 14269 * Gets the pending intent that will be triggered when the user replies to this 14270 * notification. 14271 */ getReplyPendingIntent()14272 public PendingIntent getReplyPendingIntent() { 14273 return mReplyPendingIntent; 14274 } 14275 14276 /** 14277 * Gets the pending intent that Android Auto will send after it reads aloud all messages 14278 * in this object's message list. 14279 */ getReadPendingIntent()14280 public PendingIntent getReadPendingIntent() { 14281 return mReadPendingIntent; 14282 } 14283 14284 /** 14285 * Gets the participants in the conversation. 14286 */ getParticipants()14287 public String[] getParticipants() { 14288 return mParticipants; 14289 } 14290 14291 /** 14292 * Gets the firs participant in the conversation. 14293 */ getParticipant()14294 public String getParticipant() { 14295 return mParticipants.length > 0 ? mParticipants[0] : null; 14296 } 14297 14298 /** 14299 * Gets the timestamp of the conversation. 14300 */ getLatestTimestamp()14301 public long getLatestTimestamp() { 14302 return mLatestTimestamp; 14303 } 14304 getBundleForUnreadConversation()14305 Bundle getBundleForUnreadConversation() { 14306 Bundle b = new Bundle(); 14307 String author = null; 14308 if (mParticipants != null && mParticipants.length > 1) { 14309 author = mParticipants[0]; 14310 } 14311 Parcelable[] messages = new Parcelable[mMessages.length]; 14312 for (int i = 0; i < messages.length; i++) { 14313 Bundle m = new Bundle(); 14314 m.putString(KEY_TEXT, mMessages[i]); 14315 m.putString(KEY_AUTHOR, author); 14316 messages[i] = m; 14317 } 14318 b.putParcelableArray(KEY_MESSAGES, messages); 14319 if (mRemoteInput != null) { 14320 b.putParcelable(KEY_REMOTE_INPUT, mRemoteInput); 14321 } 14322 b.putParcelable(KEY_ON_REPLY, mReplyPendingIntent); 14323 b.putParcelable(KEY_ON_READ, mReadPendingIntent); 14324 b.putStringArray(KEY_PARTICIPANTS, mParticipants); 14325 b.putLong(KEY_TIMESTAMP, mLatestTimestamp); 14326 return b; 14327 } 14328 getUnreadConversationFromBundle(Bundle b)14329 static UnreadConversation getUnreadConversationFromBundle(Bundle b) { 14330 if (b == null) { 14331 return null; 14332 } 14333 Parcelable[] parcelableMessages = b.getParcelableArray(KEY_MESSAGES, 14334 Parcelable.class); 14335 String[] messages = null; 14336 if (parcelableMessages != null) { 14337 String[] tmp = new String[parcelableMessages.length]; 14338 boolean success = true; 14339 for (int i = 0; i < tmp.length; i++) { 14340 if (!(parcelableMessages[i] instanceof Bundle)) { 14341 success = false; 14342 break; 14343 } 14344 tmp[i] = ((Bundle) parcelableMessages[i]).getString(KEY_TEXT); 14345 if (tmp[i] == null) { 14346 success = false; 14347 break; 14348 } 14349 } 14350 if (success) { 14351 messages = tmp; 14352 } else { 14353 return null; 14354 } 14355 } 14356 14357 PendingIntent onRead = b.getParcelable(KEY_ON_READ, PendingIntent.class); 14358 PendingIntent onReply = b.getParcelable(KEY_ON_REPLY, PendingIntent.class); 14359 14360 RemoteInput remoteInput = b.getParcelable(KEY_REMOTE_INPUT, RemoteInput.class); 14361 14362 String[] participants = b.getStringArray(KEY_PARTICIPANTS); 14363 if (participants == null || participants.length != 1) { 14364 return null; 14365 } 14366 14367 return new UnreadConversation(messages, 14368 remoteInput, 14369 onReply, 14370 onRead, 14371 participants, b.getLong(KEY_TIMESTAMP)); 14372 } 14373 }; 14374 14375 /** 14376 * Builder class for {@link CarExtender.UnreadConversation} objects. 14377 */ 14378 public static class Builder { 14379 private final List<String> mMessages = new ArrayList<String>(); 14380 private final String mParticipant; 14381 private RemoteInput mRemoteInput; 14382 private PendingIntent mReadPendingIntent; 14383 private PendingIntent mReplyPendingIntent; 14384 private long mLatestTimestamp; 14385 14386 /** 14387 * Constructs a new builder for {@link CarExtender.UnreadConversation}. 14388 * 14389 * @param name The name of the other participant in the conversation. 14390 */ Builder(String name)14391 public Builder(String name) { 14392 mParticipant = name; 14393 } 14394 14395 /** 14396 * Appends a new unread message to the list of messages for this conversation. 14397 * 14398 * The messages should be added from oldest to newest. 14399 * 14400 * @param message The text of the new unread message. 14401 * @return This object for method chaining. 14402 */ addMessage(String message)14403 public Builder addMessage(String message) { 14404 mMessages.add(message); 14405 return this; 14406 } 14407 14408 /** 14409 * Sets the pending intent and remote input which will convey the reply to this 14410 * notification. 14411 * 14412 * @param pendingIntent The pending intent which will be triggered on a reply. 14413 * @param remoteInput The remote input parcelable which will carry the reply. 14414 * @return This object for method chaining. 14415 * 14416 * @see CarExtender.UnreadConversation#getRemoteInput 14417 * @see CarExtender.UnreadConversation#getReplyPendingIntent 14418 */ setReplyAction( PendingIntent pendingIntent, RemoteInput remoteInput)14419 public Builder setReplyAction( 14420 PendingIntent pendingIntent, RemoteInput remoteInput) { 14421 mRemoteInput = remoteInput; 14422 mReplyPendingIntent = pendingIntent; 14423 14424 return this; 14425 } 14426 14427 /** 14428 * Sets the pending intent that will be sent once the messages in this notification 14429 * are read. 14430 * 14431 * @param pendingIntent The pending intent to use. 14432 * @return This object for method chaining. 14433 */ setReadPendingIntent(PendingIntent pendingIntent)14434 public Builder setReadPendingIntent(PendingIntent pendingIntent) { 14435 mReadPendingIntent = pendingIntent; 14436 return this; 14437 } 14438 14439 /** 14440 * Sets the timestamp of the most recent message in an unread conversation. 14441 * 14442 * If a messaging notification has been posted by your application and has not 14443 * yet been cancelled, posting a later notification with the same id and tag 14444 * but without a newer timestamp may result in Android Auto not displaying a 14445 * heads up notification for the later notification. 14446 * 14447 * @param timestamp The timestamp of the most recent message in the conversation. 14448 * @return This object for method chaining. 14449 */ setLatestTimestamp(long timestamp)14450 public Builder setLatestTimestamp(long timestamp) { 14451 mLatestTimestamp = timestamp; 14452 return this; 14453 } 14454 14455 /** 14456 * Builds a new unread conversation object. 14457 * 14458 * @return The new unread conversation object. 14459 */ build()14460 public UnreadConversation build() { 14461 String[] messages = mMessages.toArray(new String[mMessages.size()]); 14462 String[] participants = { mParticipant }; 14463 return new UnreadConversation(messages, mRemoteInput, mReplyPendingIntent, 14464 mReadPendingIntent, participants, mLatestTimestamp); 14465 } 14466 } 14467 } 14468 14469 /** 14470 * <p>Helper class to add Android TV extensions to notifications. To create a notification 14471 * with a TV extension: 14472 * 14473 * <ol> 14474 * <li>Create an {@link Notification.Builder}, setting any desired properties. 14475 * <li>Create a {@link TvExtender}. 14476 * <li>Set TV-specific properties using the {@code set} methods of 14477 * {@link TvExtender}. 14478 * <li>Call {@link Notification.Builder#extend(Notification.Extender)} 14479 * to apply the extension to a notification. 14480 * </ol> 14481 * 14482 * <pre class="prettyprint"> 14483 * Notification notification = new Notification.Builder(context) 14484 * ... 14485 * .extend(new TvExtender() 14486 * .set*(...)) 14487 * .build(); 14488 * </pre> 14489 * 14490 * <p>TV extensions can be accessed on an existing notification by using the 14491 * {@code TvExtender(Notification)} constructor, and then using the {@code get} methods 14492 * to access values. 14493 */ 14494 @FlaggedApi(Flags.FLAG_API_TVEXTENDER) 14495 public static final class TvExtender implements Extender { 14496 private static final String TAG = "TvExtender"; 14497 14498 private static final String EXTRA_TV_EXTENDER = "android.tv.EXTENSIONS"; 14499 private static final String EXTRA_FLAGS = "flags"; 14500 static final String EXTRA_CONTENT_INTENT = "content_intent"; 14501 static final String EXTRA_DELETE_INTENT = "delete_intent"; 14502 private static final String EXTRA_CHANNEL_ID = "channel_id"; 14503 private static final String EXTRA_SUPPRESS_SHOW_OVER_APPS = "suppressShowOverApps"; 14504 14505 // Flags bitwise-ored to mFlags 14506 private static final int FLAG_AVAILABLE_ON_TV = 0x1; 14507 14508 private int mFlags; 14509 private String mChannelId; 14510 private PendingIntent mContentIntent; 14511 private PendingIntent mDeleteIntent; 14512 private boolean mSuppressShowOverApps; 14513 14514 /** 14515 * Create a {@link TvExtender} with default options. 14516 */ TvExtender()14517 public TvExtender() { 14518 mFlags = FLAG_AVAILABLE_ON_TV; 14519 } 14520 14521 /** 14522 * Create a {@link TvExtender} from the TvExtender options of an existing Notification. 14523 * 14524 * @param notif The notification from which to copy options. 14525 */ TvExtender(@onNull Notification notif)14526 public TvExtender(@NonNull Notification notif) { 14527 Bundle bundle = notif.extras == null ? 14528 null : notif.extras.getBundle(EXTRA_TV_EXTENDER); 14529 if (bundle != null) { 14530 mFlags = bundle.getInt(EXTRA_FLAGS); 14531 mChannelId = bundle.getString(EXTRA_CHANNEL_ID); 14532 mSuppressShowOverApps = bundle.getBoolean(EXTRA_SUPPRESS_SHOW_OVER_APPS); 14533 mContentIntent = bundle.getParcelable(EXTRA_CONTENT_INTENT, PendingIntent.class); 14534 mDeleteIntent = bundle.getParcelable(EXTRA_DELETE_INTENT, PendingIntent.class); 14535 } 14536 } 14537 14538 /** 14539 * Apply a TV extension to a notification that is being built. This is typically called by 14540 * the {@link Notification.Builder#extend(Notification.Extender)} 14541 * method of {@link Notification.Builder}. 14542 */ 14543 @Override 14544 @NonNull extend(@onNull Notification.Builder builder)14545 public Notification.Builder extend(@NonNull Notification.Builder builder) { 14546 Bundle bundle = new Bundle(); 14547 14548 bundle.putInt(EXTRA_FLAGS, mFlags); 14549 bundle.putString(EXTRA_CHANNEL_ID, mChannelId); 14550 bundle.putBoolean(EXTRA_SUPPRESS_SHOW_OVER_APPS, mSuppressShowOverApps); 14551 if (mContentIntent != null) { 14552 bundle.putParcelable(EXTRA_CONTENT_INTENT, mContentIntent); 14553 } 14554 14555 if (mDeleteIntent != null) { 14556 bundle.putParcelable(EXTRA_DELETE_INTENT, mDeleteIntent); 14557 } 14558 14559 builder.getExtras().putBundle(EXTRA_TV_EXTENDER, bundle); 14560 return builder; 14561 } 14562 14563 /** 14564 * Returns true if this notification should be shown on TV. This method returns true 14565 * if the notification was extended with a TvExtender. 14566 */ isAvailableOnTv()14567 public boolean isAvailableOnTv() { 14568 return (mFlags & FLAG_AVAILABLE_ON_TV) != 0; 14569 } 14570 14571 /** 14572 * Specifies the channel the notification should be delivered on when shown on TV. 14573 * It can be different from the channel that the notification is delivered to when 14574 * posting on a non-TV device. Prefer to use {@link setChannelId(String)}. 14575 * 14576 * @hide 14577 */ 14578 @SystemApi setChannel(String channelId)14579 public TvExtender setChannel(String channelId) { 14580 mChannelId = channelId; 14581 return this; 14582 } 14583 14584 /** 14585 * Specifies the channel the notification should be delivered on when shown on TV. 14586 * It can be different from the channel that the notification is delivered to when 14587 * posting on a non-TV device. 14588 * 14589 * @return this object for method chaining 14590 */ 14591 @NonNull setChannelId(@ullable String channelId)14592 public TvExtender setChannelId(@Nullable String channelId) { 14593 mChannelId = channelId; 14594 return this; 14595 } 14596 14597 /** 14598 * @removed 14599 * @hide 14600 */ 14601 @Deprecated 14602 @SystemApi getChannel()14603 public String getChannel() { 14604 return mChannelId; 14605 } 14606 14607 /** 14608 * Returns the id of the channel this notification posts to on TV. 14609 */ 14610 @Nullable getChannelId()14611 public String getChannelId() { 14612 return mChannelId; 14613 } 14614 14615 /** 14616 * Supplies a {@link PendingIntent} to be sent when the notification is selected on TV. 14617 * If provided, it is used instead of the content intent specified 14618 * at the level of Notification. 14619 * 14620 * @param intent the {@link PendingIntent} for the associated notification content 14621 * @return this object for method chaining 14622 */ 14623 @NonNull setContentIntent(@ullable PendingIntent intent)14624 public TvExtender setContentIntent(@Nullable PendingIntent intent) { 14625 mContentIntent = intent; 14626 return this; 14627 } 14628 14629 /** 14630 * Returns the TV-specific content intent. If this method returns null, the 14631 * main content intent on the notification should be used. 14632 * 14633 * @see Notification#contentIntent 14634 */ 14635 @Nullable getContentIntent()14636 public PendingIntent getContentIntent() { 14637 return mContentIntent; 14638 } 14639 14640 /** 14641 * Supplies a {@link PendingIntent} to send when the notification is cleared explicitly 14642 * by the user on TV. If provided, it is used instead of the delete intent specified 14643 * at the level of Notification. 14644 * 14645 * @param intent the {@link PendingIntent} for the associated notification deletion 14646 * @return this object for method chaining 14647 */ 14648 @NonNull setDeleteIntent(@ullable PendingIntent intent)14649 public TvExtender setDeleteIntent(@Nullable PendingIntent intent) { 14650 mDeleteIntent = intent; 14651 return this; 14652 } 14653 14654 /** 14655 * Returns the TV-specific delete intent. If this method returns null, the 14656 * main delete intent on the notification should be used. 14657 * 14658 * @see Notification#deleteIntent 14659 */ 14660 @Nullable getDeleteIntent()14661 public PendingIntent getDeleteIntent() { 14662 return mDeleteIntent; 14663 } 14664 14665 /** 14666 * Specifies whether this notification should suppress showing a message over top of apps 14667 * outside of the launcher. 14668 * 14669 * @param suppress whether the notification should suppress showing over apps. 14670 * @return this object for method chaining 14671 */ 14672 @NonNull setSuppressShowOverApps(boolean suppress)14673 public TvExtender setSuppressShowOverApps(boolean suppress) { 14674 mSuppressShowOverApps = suppress; 14675 return this; 14676 } 14677 14678 /** 14679 * Returns true if this notification should not show messages over top of apps 14680 * outside of the launcher. 14681 * 14682 * @hide 14683 */ 14684 @SystemApi getSuppressShowOverApps()14685 public boolean getSuppressShowOverApps() { 14686 return mSuppressShowOverApps; 14687 } 14688 14689 /** 14690 * Returns true if this notification should not show messages over top of apps 14691 * outside of the launcher. 14692 */ isSuppressShowOverApps()14693 public boolean isSuppressShowOverApps() { 14694 return mSuppressShowOverApps; 14695 } 14696 } 14697 14698 /** 14699 * Get an array of Parcelable objects from a parcelable array bundle field. 14700 * Update the bundle to have a typed array so fetches in the future don't need 14701 * to do an array copy. 14702 */ 14703 @Nullable getParcelableArrayFromBundle( Bundle bundle, String key, Class<T> itemClass)14704 private static <T extends Parcelable> T[] getParcelableArrayFromBundle( 14705 Bundle bundle, String key, Class<T> itemClass) { 14706 final Parcelable[] array = bundle.getParcelableArray(key, Parcelable.class); 14707 final Class<?> arrayClass = Array.newInstance(itemClass, 0).getClass(); 14708 if (arrayClass.isInstance(array) || array == null) { 14709 return (T[]) array; 14710 } 14711 final T[] typedArray = (T[]) Array.newInstance(itemClass, array.length); 14712 for (int i = 0; i < array.length; i++) { 14713 typedArray[i] = (T) array[i]; 14714 } 14715 bundle.putParcelableArray(key, typedArray); 14716 return typedArray; 14717 } 14718 14719 private static class BuilderRemoteViews extends RemoteViews { BuilderRemoteViews(Parcel parcel)14720 public BuilderRemoteViews(Parcel parcel) { 14721 super(parcel); 14722 } 14723 BuilderRemoteViews(ApplicationInfo appInfo, int layoutId)14724 public BuilderRemoteViews(ApplicationInfo appInfo, int layoutId) { 14725 super(appInfo, layoutId); 14726 } 14727 14728 @Override clone()14729 public BuilderRemoteViews clone() { 14730 Parcel p = Parcel.obtain(); 14731 writeToParcel(p, 0); 14732 p.setDataPosition(0); 14733 BuilderRemoteViews brv = new BuilderRemoteViews(p); 14734 p.recycle(); 14735 return brv; 14736 } 14737 14738 /** 14739 * Override and return true, since {@link RemoteViews#onLoadClass(Class)} is not overridden. 14740 * 14741 * @see RemoteViews#shouldUseStaticFilter() 14742 */ 14743 @Override shouldUseStaticFilter()14744 protected boolean shouldUseStaticFilter() { 14745 return true; 14746 } 14747 } 14748 14749 /** 14750 * A result object where information about the template that was created is saved. 14751 */ 14752 private static class TemplateBindResult { 14753 boolean mRightIconVisible; 14754 float mRightIconWidthDp; 14755 float mRightIconHeightDp; 14756 14757 /** 14758 * The margin end that needs to be added to the heading so that it won't overlap 14759 * with the large icon. This value includes the space required to accommodate the large 14760 * icon, but should be added to the space needed to accommodate the expander. This does 14761 * not include the 16dp content margin that all notification views must have. 14762 */ 14763 public final MarginSet mHeadingExtraMarginSet = new MarginSet(); 14764 14765 /** 14766 * The margin end that needs to be added to the heading so that it won't overlap 14767 * with the large icon. This value includes the space required to accommodate the large 14768 * icon as well as the expander. This does not include the 16dp content margin that all 14769 * notification views must have. 14770 */ 14771 public final MarginSet mHeadingFullMarginSet = new MarginSet(); 14772 14773 /** 14774 * The margin end that needs to be added to the title text of the big state 14775 * so that it won't overlap with the large icon, but assuming the text can run under 14776 * the expander when that icon is not visible. 14777 */ 14778 public final MarginSet mTitleMarginSet = new MarginSet(); 14779 setRightIconState(boolean visible, float widthDp, float heightDp, float marginEndDpIfVisible, float spaceForExpanderDp)14780 public void setRightIconState(boolean visible, float widthDp, float heightDp, 14781 float marginEndDpIfVisible, float spaceForExpanderDp) { 14782 mRightIconVisible = visible; 14783 mRightIconWidthDp = widthDp; 14784 mRightIconHeightDp = heightDp; 14785 mHeadingExtraMarginSet.setValues( 14786 /* valueIfGone = */ 0, 14787 /* valueIfVisible = */ marginEndDpIfVisible); 14788 mHeadingFullMarginSet.setValues( 14789 /* valueIfGone = */ spaceForExpanderDp, 14790 /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp); 14791 mTitleMarginSet.setValues( 14792 /* valueIfGone = */ 0, 14793 /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp); 14794 } 14795 14796 /** 14797 * This contains the end margins for a view when the right icon is visible or not. These 14798 * values are both needed so that NotificationGroupingUtil can 'move' the right_icon to the 14799 * left_icon and adjust the margins, and to undo that change as well. 14800 */ 14801 private class MarginSet { 14802 private float mValueIfGone; 14803 private float mValueIfVisible; 14804 setValues(float valueIfGone, float valueIfVisible)14805 public void setValues(float valueIfGone, float valueIfVisible) { 14806 mValueIfGone = valueIfGone; 14807 mValueIfVisible = valueIfVisible; 14808 } 14809 applyToView(@onNull RemoteViews views, @IdRes int viewId)14810 public void applyToView(@NonNull RemoteViews views, @IdRes int viewId) { 14811 applyToView(views, viewId, 0); 14812 } 14813 applyToView(@onNull RemoteViews views, @IdRes int viewId, float extraMarginDp)14814 public void applyToView(@NonNull RemoteViews views, @IdRes int viewId, 14815 float extraMarginDp) { 14816 final float marginEndDp = getDpValue() + extraMarginDp; 14817 if (viewId == R.id.notification_header) { 14818 views.setFloat(R.id.notification_header, 14819 "setTopLineExtraMarginEndDp", marginEndDp); 14820 } else if (viewId == R.id.text || viewId == R.id.big_text) { 14821 if (mValueIfGone != 0) { 14822 throw new RuntimeException("Programming error: `text` and `big_text` use " 14823 + "ImageFloatingTextView which can either show a margin or not; " 14824 + "thus mValueIfGone must be 0, but it was " + mValueIfGone); 14825 } 14826 // Note that the caller must set "setNumIndentLines" to a positive int in order 14827 // for this margin to do anything at all. 14828 views.setFloat(viewId, "setImageEndMarginDp", mValueIfVisible); 14829 views.setBoolean(viewId, "setHasImage", mRightIconVisible); 14830 // Apply just the *extra* margin as the view layout margin; this will be 14831 // unchanged depending on the visibility of the image, but it means that the 14832 // extra margin applies to *every* line of text instead of just indented lines. 14833 views.setViewLayoutMargin(viewId, RemoteViews.MARGIN_END, 14834 extraMarginDp, TypedValue.COMPLEX_UNIT_DIP); 14835 } else { 14836 views.setViewLayoutMargin(viewId, RemoteViews.MARGIN_END, 14837 marginEndDp, TypedValue.COMPLEX_UNIT_DIP); 14838 } 14839 if (mRightIconVisible) { 14840 views.setIntTag(viewId, R.id.tag_margin_end_when_icon_visible, 14841 TypedValue.createComplexDimension( 14842 mValueIfVisible + extraMarginDp, TypedValue.COMPLEX_UNIT_DIP)); 14843 views.setIntTag(viewId, R.id.tag_margin_end_when_icon_gone, 14844 TypedValue.createComplexDimension( 14845 mValueIfGone + extraMarginDp, TypedValue.COMPLEX_UNIT_DIP)); 14846 } 14847 } 14848 getDpValue()14849 public float getDpValue() { 14850 return mRightIconVisible ? mValueIfVisible : mValueIfGone; 14851 } 14852 } 14853 } 14854 14855 private static class StandardTemplateParams { 14856 /** 14857 * Notifications will be minimally decorated with ONLY an icon and expander: 14858 * <li>A large icon is never shown. 14859 * <li>A progress bar is never shown. 14860 * <li>The expanded and heads up states do not show actions, even if provided. 14861 */ 14862 public static final int DECORATION_MINIMAL = 1; 14863 14864 /** 14865 * Notifications will be partially decorated with AT LEAST an icon and expander: 14866 * <li>A large icon is shown if provided. 14867 * <li>A progress bar is shown if provided and enough space remains below the content. 14868 * <li>Actions are shown in the expanded and heads up states. 14869 */ 14870 public static final int DECORATION_PARTIAL = 2; 14871 14872 public static int VIEW_TYPE_UNSPECIFIED = 0; 14873 public static int VIEW_TYPE_NORMAL = 1; 14874 public static int VIEW_TYPE_EXPANDED = 2; 14875 public static int VIEW_TYPE_HEADS_UP = 3; 14876 public static int VIEW_TYPE_MINIMIZED = 4; // header only for minimized state 14877 public static int VIEW_TYPE_PUBLIC = 5; // header only for automatic public version 14878 public static int VIEW_TYPE_GROUP_HEADER = 6; // header only for top of group 14879 14880 int mViewType = VIEW_TYPE_UNSPECIFIED; 14881 boolean mHeaderless; 14882 boolean mHideAppName; 14883 boolean mHideTitle; 14884 boolean mHideSubText; 14885 boolean mHideTime; 14886 boolean mHideActions; 14887 boolean mHideProgress; 14888 boolean mHideSnoozeButton; 14889 boolean mHideLeftIcon; 14890 boolean mHideRightIcon; 14891 Icon mPromotedPicture; 14892 boolean mCallStyleActions; 14893 boolean mAllowTextWithProgress; 14894 int mTitleViewId; 14895 int mTextViewId; 14896 @Nullable CharSequence mTitle; 14897 @Nullable CharSequence mText; 14898 @Nullable CharSequence mHeaderTextSecondary; 14899 @Nullable CharSequence mSubText; 14900 int maxRemoteInputHistory = Style.MAX_REMOTE_INPUT_HISTORY_LINES; 14901 boolean allowColorization = true; 14902 boolean mHighlightExpander = false; 14903 reset()14904 final StandardTemplateParams reset() { 14905 mViewType = VIEW_TYPE_UNSPECIFIED; 14906 mHeaderless = false; 14907 mHideAppName = false; 14908 mHideTitle = false; 14909 mHideSubText = false; 14910 mHideTime = false; 14911 mHideActions = false; 14912 mHideProgress = false; 14913 mHideSnoozeButton = false; 14914 mHideLeftIcon = false; 14915 mHideRightIcon = false; 14916 mPromotedPicture = null; 14917 mCallStyleActions = false; 14918 mAllowTextWithProgress = false; 14919 mTitleViewId = R.id.title; 14920 mTextViewId = R.id.text; 14921 mTitle = null; 14922 mText = null; 14923 mSubText = null; 14924 mHeaderTextSecondary = null; 14925 maxRemoteInputHistory = Style.MAX_REMOTE_INPUT_HISTORY_LINES; 14926 allowColorization = true; 14927 mHighlightExpander = false; 14928 return this; 14929 } 14930 hasTitle()14931 final boolean hasTitle() { 14932 return !TextUtils.isEmpty(mTitle) && !mHideTitle; 14933 } 14934 viewType(int viewType)14935 final StandardTemplateParams viewType(int viewType) { 14936 mViewType = viewType; 14937 return this; 14938 } 14939 headerless(boolean headerless)14940 public StandardTemplateParams headerless(boolean headerless) { 14941 mHeaderless = headerless; 14942 return this; 14943 } 14944 hideAppName(boolean hideAppName)14945 public StandardTemplateParams hideAppName(boolean hideAppName) { 14946 mHideAppName = hideAppName; 14947 return this; 14948 } 14949 hideSubText(boolean hideSubText)14950 public StandardTemplateParams hideSubText(boolean hideSubText) { 14951 mHideSubText = hideSubText; 14952 return this; 14953 } 14954 hideTime(boolean hideTime)14955 public StandardTemplateParams hideTime(boolean hideTime) { 14956 mHideTime = hideTime; 14957 return this; 14958 } 14959 hideActions(boolean hideActions)14960 final StandardTemplateParams hideActions(boolean hideActions) { 14961 this.mHideActions = hideActions; 14962 return this; 14963 } 14964 hideProgress(boolean hideProgress)14965 final StandardTemplateParams hideProgress(boolean hideProgress) { 14966 this.mHideProgress = hideProgress; 14967 return this; 14968 } 14969 hideTitle(boolean hideTitle)14970 final StandardTemplateParams hideTitle(boolean hideTitle) { 14971 this.mHideTitle = hideTitle; 14972 return this; 14973 } 14974 callStyleActions(boolean callStyleActions)14975 final StandardTemplateParams callStyleActions(boolean callStyleActions) { 14976 this.mCallStyleActions = callStyleActions; 14977 return this; 14978 } 14979 allowTextWithProgress(boolean allowTextWithProgress)14980 final StandardTemplateParams allowTextWithProgress(boolean allowTextWithProgress) { 14981 this.mAllowTextWithProgress = allowTextWithProgress; 14982 return this; 14983 } 14984 hideSnoozeButton(boolean hideSnoozeButton)14985 final StandardTemplateParams hideSnoozeButton(boolean hideSnoozeButton) { 14986 this.mHideSnoozeButton = hideSnoozeButton; 14987 return this; 14988 } 14989 promotedPicture(Icon promotedPicture)14990 final StandardTemplateParams promotedPicture(Icon promotedPicture) { 14991 this.mPromotedPicture = promotedPicture; 14992 return this; 14993 } 14994 titleViewId(int titleViewId)14995 public StandardTemplateParams titleViewId(int titleViewId) { 14996 mTitleViewId = titleViewId; 14997 return this; 14998 } 14999 textViewId(int textViewId)15000 public StandardTemplateParams textViewId(int textViewId) { 15001 mTextViewId = textViewId; 15002 return this; 15003 } 15004 title(@ullable CharSequence title)15005 final StandardTemplateParams title(@Nullable CharSequence title) { 15006 this.mTitle = title; 15007 return this; 15008 } 15009 text(@ullable CharSequence text)15010 final StandardTemplateParams text(@Nullable CharSequence text) { 15011 this.mText = text; 15012 return this; 15013 } 15014 summaryText(@ullable CharSequence text)15015 final StandardTemplateParams summaryText(@Nullable CharSequence text) { 15016 this.mSubText = text; 15017 return this; 15018 } 15019 headerTextSecondary(@ullable CharSequence text)15020 final StandardTemplateParams headerTextSecondary(@Nullable CharSequence text) { 15021 this.mHeaderTextSecondary = text; 15022 return this; 15023 } 15024 15025 hideLeftIcon(boolean hideLeftIcon)15026 final StandardTemplateParams hideLeftIcon(boolean hideLeftIcon) { 15027 this.mHideLeftIcon = hideLeftIcon; 15028 return this; 15029 } 15030 hideRightIcon(boolean hideRightIcon)15031 final StandardTemplateParams hideRightIcon(boolean hideRightIcon) { 15032 this.mHideRightIcon = hideRightIcon; 15033 return this; 15034 } 15035 disallowColorization()15036 final StandardTemplateParams disallowColorization() { 15037 this.allowColorization = false; 15038 return this; 15039 } 15040 highlightExpander(boolean highlight)15041 final StandardTemplateParams highlightExpander(boolean highlight) { 15042 this.mHighlightExpander = highlight; 15043 return this; 15044 } 15045 fillTextsFrom(Builder b)15046 final StandardTemplateParams fillTextsFrom(Builder b) { 15047 Bundle extras = b.mN.extras; 15048 this.mTitle = b.processLegacyText(extras.getCharSequence(EXTRA_TITLE)); 15049 this.mText = b.processLegacyText(extras.getCharSequence(EXTRA_TEXT)); 15050 this.mSubText = extras.getCharSequence(EXTRA_SUB_TEXT); 15051 return this; 15052 } 15053 15054 /** 15055 * Set the maximum lines of remote input history lines allowed. 15056 * @param maxRemoteInputHistory The number of lines. 15057 * @return The builder for method chaining. 15058 */ setMaxRemoteInputHistory(int maxRemoteInputHistory)15059 public StandardTemplateParams setMaxRemoteInputHistory(int maxRemoteInputHistory) { 15060 this.maxRemoteInputHistory = maxRemoteInputHistory; 15061 return this; 15062 } 15063 decorationType(int decorationType)15064 public StandardTemplateParams decorationType(int decorationType) { 15065 hideTitle(true); 15066 // Minimally decorated custom views do not show certain pieces of chrome that have 15067 // always been shown when using DecoratedCustomViewStyle. 15068 boolean hideOtherFields = decorationType <= DECORATION_MINIMAL; 15069 hideLeftIcon(false); // The left icon decoration is better than showing nothing. 15070 hideRightIcon(hideOtherFields); 15071 hideProgress(hideOtherFields); 15072 hideActions(hideOtherFields); 15073 return this; 15074 } 15075 } 15076 15077 /** 15078 * A utility which stores and calculates the palette of colors used to color notifications. 15079 * @hide 15080 */ 15081 public static class Colors { 15082 private int mPaletteIsForRawColor = COLOR_INVALID; 15083 private boolean mPaletteIsForColorized = false; 15084 private boolean mPaletteIsForNightMode = false; 15085 // The following colors are the palette 15086 private int mBackgroundColor = COLOR_INVALID; 15087 private int mProtectionColor = COLOR_INVALID; 15088 private int mPrimaryTextColor = COLOR_INVALID; 15089 private int mSecondaryTextColor = COLOR_INVALID; 15090 private int mPrimaryAccentColor = COLOR_INVALID; 15091 private int mSecondaryAccentColor = COLOR_INVALID; 15092 private int mTertiaryAccentColor = COLOR_INVALID; 15093 private int mOnTertiaryAccentTextColor = COLOR_INVALID; 15094 private int mTertiaryFixedDimAccentColor = COLOR_INVALID; 15095 private int mOnTertiaryFixedAccentTextColor = COLOR_INVALID; 15096 15097 private int mErrorColor = COLOR_INVALID; 15098 private int mContrastColor = COLOR_INVALID; 15099 private int mRippleAlpha = 0x33; 15100 15101 /** 15102 * A utility for obtaining a TypedArray of the given DayNight-styled attributes, which 15103 * returns null when the context is a mock with no theme. 15104 * 15105 * NOTE: Calling this method is expensive, as creating a new ContextThemeWrapper 15106 * instances can allocate as much as 5MB of memory, so its important to call this method 15107 * only when necessary, getting as many attributes as possible from each call. 15108 * 15109 * @see Resources.Theme#obtainStyledAttributes(int[]) 15110 */ 15111 @Nullable obtainDayNightAttributes(@onNull Context ctx, @NonNull @StyleableRes int[] attrs)15112 private static TypedArray obtainDayNightAttributes(@NonNull Context ctx, 15113 @NonNull @StyleableRes int[] attrs) { 15114 // when testing, the mock context may have no theme 15115 if (ctx.getTheme() == null) { 15116 return null; 15117 } 15118 Resources.Theme theme = new ContextThemeWrapper(ctx, 15119 R.style.Theme_DeviceDefault_DayNight).getTheme(); 15120 return theme.obtainStyledAttributes(attrs); 15121 } 15122 15123 /** A null-safe wrapper of TypedArray.getColor because mocks return null */ getColor(@ullable TypedArray ta, int index, @ColorInt int defValue)15124 private static @ColorInt int getColor(@Nullable TypedArray ta, int index, 15125 @ColorInt int defValue) { 15126 return ta == null ? defValue : ta.getColor(index, defValue); 15127 } 15128 15129 /** 15130 * Resolve the palette. If the inputs have not changed, this will be a no-op. 15131 * This does not handle invalidating the resolved colors when the context itself changes, 15132 * because that case does not happen in the current notification inflation pipeline; we will 15133 * recreate a new builder (and thus a new palette) when reinflating notifications for a new 15134 * theme (admittedly, we do the same for night mode, but that's easy to check). 15135 * 15136 * @param ctx the builder context. 15137 * @param rawColor the notification's color; may be COLOR_DEFAULT, but may never have alpha. 15138 * @param isColorized whether the notification is colorized. 15139 * @param nightMode whether the UI is in night mode. 15140 */ resolvePalette(Context ctx, int rawColor, boolean isColorized, boolean nightMode)15141 public void resolvePalette(Context ctx, int rawColor, 15142 boolean isColorized, boolean nightMode) { 15143 if (mPaletteIsForRawColor == rawColor 15144 && mPaletteIsForColorized == isColorized 15145 && mPaletteIsForNightMode == nightMode) { 15146 return; 15147 } 15148 mPaletteIsForRawColor = rawColor; 15149 mPaletteIsForColorized = isColorized; 15150 mPaletteIsForNightMode = nightMode; 15151 15152 if (isColorized) { 15153 if (rawColor == COLOR_DEFAULT) { 15154 mBackgroundColor = ctx.getColor(R.color.materialColorSecondary); 15155 } else { 15156 mBackgroundColor = rawColor; 15157 } 15158 if (Flags.uiRichOngoing()) { 15159 boolean isBgDark = Notification.Builder.isColorDark(mBackgroundColor); 15160 int onSurfaceColorExtreme = isBgDark ? Color.WHITE : Color.BLACK; 15161 mPrimaryTextColor = ContrastColorUtil.ensureContrast( 15162 ColorUtils.blendARGB(mBackgroundColor, onSurfaceColorExtreme, 0.9f), 15163 mBackgroundColor, isBgDark, 4.5); 15164 mSecondaryTextColor = ContrastColorUtil.ensureContrast( 15165 ColorUtils.blendARGB(mBackgroundColor, onSurfaceColorExtreme, 0.8f), 15166 mBackgroundColor, isBgDark, 4.5); 15167 } else { 15168 mPrimaryTextColor = ContrastColorUtil.findAlphaToMeetContrast( 15169 ContrastColorUtil.resolvePrimaryColor(ctx, mBackgroundColor, nightMode), 15170 mBackgroundColor, 4.5); 15171 mSecondaryTextColor = ContrastColorUtil.findAlphaToMeetContrast( 15172 ContrastColorUtil.resolveSecondaryColor(ctx, 15173 mBackgroundColor, nightMode), mBackgroundColor, 4.5); 15174 } 15175 mContrastColor = mPrimaryTextColor; 15176 mPrimaryAccentColor = mPrimaryTextColor; 15177 mSecondaryAccentColor = mSecondaryTextColor; 15178 mTertiaryAccentColor = flattenAlpha(mPrimaryTextColor, mBackgroundColor); 15179 mOnTertiaryAccentTextColor = mBackgroundColor; 15180 mTertiaryFixedDimAccentColor = mTertiaryAccentColor; 15181 mOnTertiaryFixedAccentTextColor = mOnTertiaryAccentTextColor; 15182 mErrorColor = mPrimaryTextColor; 15183 mRippleAlpha = 0x33; 15184 } else { 15185 int[] attrs = { 15186 R.attr.colorError, 15187 R.attr.colorControlHighlight 15188 }; 15189 15190 mBackgroundColor = ctx.getColor(R.color.materialColorSurfaceContainerHigh); 15191 mPrimaryTextColor = ctx.getColor(R.color.materialColorOnSurface); 15192 mSecondaryTextColor = ctx.getColor(R.color.materialColorOnSurfaceVariant); 15193 mPrimaryAccentColor = ctx.getColor(R.color.materialColorPrimary); 15194 mSecondaryAccentColor = ctx.getColor(R.color.materialColorSecondary); 15195 mTertiaryAccentColor = ctx.getColor(R.color.materialColorTertiary); 15196 mOnTertiaryAccentTextColor = ctx.getColor(R.color.materialColorOnTertiary); 15197 mTertiaryFixedDimAccentColor = ctx.getColor( 15198 R.color.materialColorTertiaryFixedDim); 15199 mOnTertiaryFixedAccentTextColor = ctx.getColor( 15200 R.color.materialColorOnTertiaryFixed); 15201 15202 try (TypedArray ta = obtainDayNightAttributes(ctx, attrs)) { 15203 mErrorColor = getColor(ta, 0, COLOR_INVALID); 15204 mRippleAlpha = Color.alpha(getColor(ta, 1, 0x33ffffff)); 15205 } 15206 mContrastColor = calculateContrastColor(ctx, rawColor, mPrimaryAccentColor, 15207 mBackgroundColor, nightMode); 15208 15209 // make sure every color has a valid value 15210 if (mPrimaryTextColor == COLOR_INVALID) { 15211 mPrimaryTextColor = ContrastColorUtil.resolvePrimaryColor( 15212 ctx, mBackgroundColor, nightMode); 15213 } 15214 if (mSecondaryTextColor == COLOR_INVALID) { 15215 mSecondaryTextColor = ContrastColorUtil.resolveSecondaryColor( 15216 ctx, mBackgroundColor, nightMode); 15217 } 15218 if (mPrimaryAccentColor == COLOR_INVALID) { 15219 mPrimaryAccentColor = mContrastColor; 15220 } 15221 if (mSecondaryAccentColor == COLOR_INVALID) { 15222 mSecondaryAccentColor = mContrastColor; 15223 } 15224 if (mTertiaryAccentColor == COLOR_INVALID) { 15225 mTertiaryAccentColor = mContrastColor; 15226 } 15227 if (mOnTertiaryAccentTextColor == COLOR_INVALID) { 15228 mOnTertiaryAccentTextColor = ColorUtils.setAlphaComponent( 15229 ContrastColorUtil.resolvePrimaryColor( 15230 ctx, mTertiaryAccentColor, nightMode), 0xFF); 15231 } 15232 if (mTertiaryFixedDimAccentColor == COLOR_INVALID) { 15233 mTertiaryFixedDimAccentColor = mContrastColor; 15234 } 15235 if (mOnTertiaryFixedAccentTextColor == COLOR_INVALID) { 15236 mOnTertiaryFixedAccentTextColor = ColorUtils.setAlphaComponent( 15237 ContrastColorUtil.resolvePrimaryColor( 15238 ctx, mTertiaryFixedDimAccentColor, nightMode), 0xFF); 15239 } 15240 if (mErrorColor == COLOR_INVALID) { 15241 mErrorColor = mPrimaryTextColor; 15242 } 15243 } 15244 // make sure every color has a valid value 15245 mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.9f); 15246 } 15247 15248 /** calculates the contrast color for the non-colorized notifications */ calculateContrastColor(Context ctx, @ColorInt int rawColor, @ColorInt int accentColor, @ColorInt int backgroundColor, boolean nightMode)15249 private static @ColorInt int calculateContrastColor(Context ctx, @ColorInt int rawColor, 15250 @ColorInt int accentColor, @ColorInt int backgroundColor, boolean nightMode) { 15251 int color; 15252 if (rawColor == COLOR_DEFAULT) { 15253 color = accentColor; 15254 if (color == COLOR_INVALID) { 15255 color = ContrastColorUtil.resolveDefaultColor(ctx, backgroundColor, nightMode); 15256 } 15257 } else { 15258 color = ContrastColorUtil.resolveContrastColor(ctx, rawColor, backgroundColor, 15259 nightMode); 15260 } 15261 return flattenAlpha(color, backgroundColor); 15262 } 15263 15264 /** remove any alpha by manually blending it with the given background. */ flattenAlpha(@olorInt int color, @ColorInt int background)15265 private static @ColorInt int flattenAlpha(@ColorInt int color, @ColorInt int background) { 15266 return Color.alpha(color) == 0xff ? color 15267 : ContrastColorUtil.compositeColors(color, background); 15268 } 15269 15270 /** @return the notification's background color */ getBackgroundColor()15271 public @ColorInt int getBackgroundColor() { 15272 return mBackgroundColor; 15273 } 15274 15275 /** 15276 * @return the "surface protection" color from the theme, 15277 * or a variant of the normal background color when colorized. 15278 */ getProtectionColor()15279 public @ColorInt int getProtectionColor() { 15280 return mProtectionColor; 15281 } 15282 15283 /** @return the color for the most prominent text */ getPrimaryTextColor()15284 public @ColorInt int getPrimaryTextColor() { 15285 return mPrimaryTextColor; 15286 } 15287 15288 /** @return the color for less prominent text */ getSecondaryTextColor()15289 public @ColorInt int getSecondaryTextColor() { 15290 return mSecondaryTextColor; 15291 } 15292 15293 /** @return the theme's accent color for colored UI elements. */ getPrimaryAccentColor()15294 public @ColorInt int getPrimaryAccentColor() { 15295 return mPrimaryAccentColor; 15296 } 15297 15298 /** @return the theme's secondary accent color for colored UI elements. */ getSecondaryAccentColor()15299 public @ColorInt int getSecondaryAccentColor() { 15300 return mSecondaryAccentColor; 15301 } 15302 15303 /** @return the theme's tertiary accent color for colored UI elements. */ getTertiaryAccentColor()15304 public @ColorInt int getTertiaryAccentColor() { 15305 return mTertiaryAccentColor; 15306 } 15307 15308 /** @return the theme's text color to be used on the tertiary accent color. */ getOnTertiaryAccentTextColor()15309 public @ColorInt int getOnTertiaryAccentTextColor() { 15310 return mOnTertiaryAccentTextColor; 15311 } 15312 15313 /** @return the theme's tertiary fixed dim accent color for colored UI elements. */ getTertiaryFixedDimAccentColor()15314 public @ColorInt int getTertiaryFixedDimAccentColor() { 15315 return mTertiaryFixedDimAccentColor; 15316 } 15317 15318 /** @return the theme's text color to be used on the tertiary fixed accent color. */ getOnTertiaryFixedAccentTextColor()15319 public @ColorInt int getOnTertiaryFixedAccentTextColor() { 15320 return mOnTertiaryFixedAccentTextColor; 15321 } 15322 15323 /** 15324 * @return the contrast-adjusted version of the color provided by the app, or the 15325 * primary text color when colorized. 15326 */ getContrastColor()15327 public @ColorInt int getContrastColor() { 15328 return mContrastColor; 15329 } 15330 15331 /** @return the theme's error color, or the primary text color when colorized. */ getErrorColor()15332 public @ColorInt int getErrorColor() { 15333 return mErrorColor; 15334 } 15335 15336 /** @return the alpha component of the current theme's control highlight color. */ getRippleAlpha()15337 public int getRippleAlpha() { 15338 return mRippleAlpha; 15339 } 15340 } 15341 } 15342