1 /* 2 * Copyright (C) 2021 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.safetycenter; 18 19 import static android.os.Build.VERSION_CODES.TIRAMISU; 20 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 21 22 import static com.android.internal.util.Preconditions.checkArgument; 23 24 import static java.util.Collections.unmodifiableList; 25 import static java.util.Objects.requireNonNull; 26 27 import android.annotation.IntDef; 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.annotation.SuppressLint; 31 import android.annotation.SystemApi; 32 import android.annotation.TargetApi; 33 import android.app.PendingIntent; 34 import android.os.Build; 35 import android.os.Parcel; 36 import android.os.Parcelable; 37 import android.text.TextUtils; 38 39 import androidx.annotation.RequiresApi; 40 41 import com.android.modules.utils.build.SdkLevel; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.ArrayList; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Objects; 49 import java.util.Set; 50 51 /** 52 * Data for a safety source issue in the Safety Center page. 53 * 54 * <p>An issue represents an actionable matter relating to a particular safety source. 55 * 56 * <p>The safety issue will contain localized messages to be shown in UI explaining the potential 57 * threat or warning and suggested fixes, as well as actions a user is allowed to take from the UI 58 * to resolve the issue. 59 * 60 * @hide 61 */ 62 @SystemApi 63 @RequiresApi(TIRAMISU) 64 public final class SafetySourceIssue implements Parcelable { 65 66 /** Indicates that the risk associated with the issue is related to a user's device safety. */ 67 public static final int ISSUE_CATEGORY_DEVICE = 100; 68 69 /** Indicates that the risk associated with the issue is related to a user's account safety. */ 70 public static final int ISSUE_CATEGORY_ACCOUNT = 200; 71 72 /** 73 * Indicates that the risk associated with the issue is related to a user's general safety. 74 * 75 * <p>This is the default. It is a generic value used when the category is not known or is not 76 * relevant. 77 */ 78 public static final int ISSUE_CATEGORY_GENERAL = 300; 79 80 /** Indicates that the risk associated with the issue is related to a user's data. */ 81 @RequiresApi(UPSIDE_DOWN_CAKE) 82 public static final int ISSUE_CATEGORY_DATA = 400; 83 84 /** Indicates that the risk associated with the issue is related to a user's passwords. */ 85 @RequiresApi(UPSIDE_DOWN_CAKE) 86 public static final int ISSUE_CATEGORY_PASSWORDS = 500; 87 88 /** Indicates that the risk associated with the issue is related to a user's personal safety. */ 89 @RequiresApi(UPSIDE_DOWN_CAKE) 90 public static final int ISSUE_CATEGORY_PERSONAL_SAFETY = 600; 91 92 /** 93 * All possible issue categories. 94 * 95 * <p>An issue's category represents a specific area of safety that the issue relates to. 96 * 97 * <p>An issue can only have one associated category. If the issue relates to multiple areas of 98 * safety, then choose the closest area or default to {@link #ISSUE_CATEGORY_GENERAL}. 99 * 100 * @hide 101 * @see Builder#setIssueCategory(int) 102 */ 103 @IntDef( 104 prefix = {"ISSUE_CATEGORY_"}, 105 value = { 106 ISSUE_CATEGORY_DEVICE, 107 ISSUE_CATEGORY_ACCOUNT, 108 ISSUE_CATEGORY_GENERAL, 109 ISSUE_CATEGORY_DATA, 110 ISSUE_CATEGORY_PASSWORDS, 111 ISSUE_CATEGORY_PERSONAL_SAFETY 112 }) 113 @Retention(RetentionPolicy.SOURCE) 114 @TargetApi(UPSIDE_DOWN_CAKE) 115 public @interface IssueCategory {} 116 117 /** Value signifying that the source has not specified a particular notification behavior. */ 118 @RequiresApi(UPSIDE_DOWN_CAKE) 119 public static final int NOTIFICATION_BEHAVIOR_UNSPECIFIED = 0; 120 121 /** An issue which Safety Center should never notify the user about. */ 122 @RequiresApi(UPSIDE_DOWN_CAKE) 123 public static final int NOTIFICATION_BEHAVIOR_NEVER = 100; 124 125 /** 126 * An issue which Safety Center may notify the user about after a delay if it has not been 127 * resolved. Safety Center does not provide any guarantee about the duration of the delay. 128 */ 129 @RequiresApi(UPSIDE_DOWN_CAKE) 130 public static final int NOTIFICATION_BEHAVIOR_DELAYED = 200; 131 132 /** An issue which Safety Center may notify the user about immediately. */ 133 @RequiresApi(UPSIDE_DOWN_CAKE) 134 public static final int NOTIFICATION_BEHAVIOR_IMMEDIATELY = 300; 135 136 /** 137 * All possible notification behaviors. 138 * 139 * <p>The notification behavior of a {@link SafetySourceIssue} determines if and when Safety 140 * Center should notify the user about it. 141 * 142 * @hide 143 * @see Builder#setNotificationBehavior(int) 144 */ 145 @IntDef( 146 prefix = {"NOTIFICATION_BEHAVIOR_"}, 147 value = { 148 NOTIFICATION_BEHAVIOR_UNSPECIFIED, 149 NOTIFICATION_BEHAVIOR_NEVER, 150 NOTIFICATION_BEHAVIOR_DELAYED, 151 NOTIFICATION_BEHAVIOR_IMMEDIATELY 152 }) 153 @Retention(RetentionPolicy.SOURCE) 154 @TargetApi(UPSIDE_DOWN_CAKE) 155 public @interface NotificationBehavior {} 156 157 /** 158 * An issue which requires manual user input to be resolved. 159 * 160 * <p>This is the default. 161 */ 162 @RequiresApi(UPSIDE_DOWN_CAKE) 163 public static final int ISSUE_ACTIONABILITY_MANUAL = 0; 164 165 /** 166 * An issue which is just a "tip" and may not require any user input. 167 * 168 * <p>It is still possible to provide {@link Action}s to e.g. "learn more" about it or 169 * acknowledge it. 170 */ 171 @RequiresApi(UPSIDE_DOWN_CAKE) 172 public static final int ISSUE_ACTIONABILITY_TIP = 100; 173 174 /** 175 * An issue which has already been actioned and may not require any user input. 176 * 177 * <p>It is still possible to provide {@link Action}s to e.g. "learn more" about it or 178 * acknowledge it. 179 */ 180 @RequiresApi(UPSIDE_DOWN_CAKE) 181 public static final int ISSUE_ACTIONABILITY_AUTOMATIC = 200; 182 183 /** 184 * All possible issue actionability. 185 * 186 * <p>An issue's actionability represent what action is expected from the user as a result of 187 * showing them this issue. 188 * 189 * <p>If the user needs to manually resolve it; this is typically achieved using an {@link 190 * Action} (e.g. by resolving the issue directly through the Safety Center screen, or by 191 * navigating to another page). 192 * 193 * <p>If the issue does not need to be resolved manually by the user, it is possible not to 194 * provide any {@link Action}. However, this may still be desirable to e.g. to "learn more" 195 * about it or acknowledge it. 196 * 197 * @hide 198 * @see Builder#setIssueActionability(int) 199 */ 200 @IntDef( 201 prefix = {"ISSUE_ACTIONABILITY_"}, 202 value = { 203 ISSUE_ACTIONABILITY_MANUAL, 204 ISSUE_ACTIONABILITY_TIP, 205 ISSUE_ACTIONABILITY_AUTOMATIC 206 }) 207 @Retention(RetentionPolicy.SOURCE) 208 @TargetApi(UPSIDE_DOWN_CAKE) 209 public @interface IssueActionability {} 210 211 @NonNull 212 public static final Creator<SafetySourceIssue> CREATOR = 213 new Creator<SafetySourceIssue>() { 214 @Override 215 public SafetySourceIssue createFromParcel(Parcel in) { 216 String id = in.readString(); 217 CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 218 CharSequence subtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 219 CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 220 int severityLevel = in.readInt(); 221 int issueCategory = in.readInt(); 222 List<Action> actions = requireNonNull(in.createTypedArrayList(Action.CREATOR)); 223 PendingIntent onDismissPendingIntent = 224 in.readTypedObject(PendingIntent.CREATOR); 225 String issueTypeId = in.readString(); 226 Builder builder = 227 new Builder(id, title, summary, severityLevel, issueTypeId) 228 .setSubtitle(subtitle) 229 .setIssueCategory(issueCategory) 230 .setOnDismissPendingIntent(onDismissPendingIntent); 231 for (int i = 0; i < actions.size(); i++) { 232 builder.addAction(actions.get(i)); 233 } 234 if (SdkLevel.isAtLeastU()) { 235 builder.setCustomNotification(in.readTypedObject(Notification.CREATOR)); 236 builder.setNotificationBehavior(in.readInt()); 237 builder.setAttributionTitle( 238 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)); 239 builder.setDeduplicationId(in.readString()); 240 builder.setIssueActionability(in.readInt()); 241 } 242 return builder.build(); 243 } 244 245 @Override 246 public SafetySourceIssue[] newArray(int size) { 247 return new SafetySourceIssue[size]; 248 } 249 }; 250 251 @NonNull private final String mId; 252 @NonNull private final CharSequence mTitle; 253 @Nullable private final CharSequence mSubtitle; 254 @NonNull private final CharSequence mSummary; 255 @SafetySourceData.SeverityLevel private final int mSeverityLevel; 256 private final List<Action> mActions; 257 @Nullable private final PendingIntent mOnDismissPendingIntent; 258 @IssueCategory private final int mIssueCategory; 259 @NonNull private final String mIssueTypeId; 260 @Nullable private final Notification mCustomNotification; 261 @NotificationBehavior private final int mNotificationBehavior; 262 @Nullable private final CharSequence mAttributionTitle; 263 @Nullable private final String mDeduplicationId; 264 @IssueActionability private final int mIssueActionability; 265 SafetySourceIssue( @onNull String id, @NonNull CharSequence title, @Nullable CharSequence subtitle, @NonNull CharSequence summary, @SafetySourceData.SeverityLevel int severityLevel, @IssueCategory int issueCategory, @NonNull List<Action> actions, @Nullable PendingIntent onDismissPendingIntent, @NonNull String issueTypeId, @Nullable Notification customNotification, @NotificationBehavior int notificationBehavior, @Nullable CharSequence attributionTitle, @Nullable String deduplicationId, @IssueActionability int issueActionability)266 private SafetySourceIssue( 267 @NonNull String id, 268 @NonNull CharSequence title, 269 @Nullable CharSequence subtitle, 270 @NonNull CharSequence summary, 271 @SafetySourceData.SeverityLevel int severityLevel, 272 @IssueCategory int issueCategory, 273 @NonNull List<Action> actions, 274 @Nullable PendingIntent onDismissPendingIntent, 275 @NonNull String issueTypeId, 276 @Nullable Notification customNotification, 277 @NotificationBehavior int notificationBehavior, 278 @Nullable CharSequence attributionTitle, 279 @Nullable String deduplicationId, 280 @IssueActionability int issueActionability) { 281 this.mId = id; 282 this.mTitle = title; 283 this.mSubtitle = subtitle; 284 this.mSummary = summary; 285 this.mSeverityLevel = severityLevel; 286 this.mIssueCategory = issueCategory; 287 this.mActions = actions; 288 this.mOnDismissPendingIntent = onDismissPendingIntent; 289 this.mIssueTypeId = issueTypeId; 290 this.mCustomNotification = customNotification; 291 this.mNotificationBehavior = notificationBehavior; 292 this.mAttributionTitle = attributionTitle; 293 this.mDeduplicationId = deduplicationId; 294 this.mIssueActionability = issueActionability; 295 } 296 297 /** 298 * Returns the identifier for this issue. 299 * 300 * <p>This id should uniquely identify the safety risk represented by this issue. Safety issues 301 * will be deduped by this id to be shown in the UI. 302 * 303 * <p>On multiple instances of providing the same issue to be represented in Safety Center, 304 * provide the same id across all instances. 305 */ 306 @NonNull getId()307 public String getId() { 308 return mId; 309 } 310 311 /** Returns the localized title of the issue to be displayed in the UI. */ 312 @NonNull getTitle()313 public CharSequence getTitle() { 314 return mTitle; 315 } 316 317 /** Returns the localized subtitle of the issue to be displayed in the UI. */ 318 @Nullable getSubtitle()319 public CharSequence getSubtitle() { 320 return mSubtitle; 321 } 322 323 /** Returns the localized summary of the issue to be displayed in the UI. */ 324 @NonNull getSummary()325 public CharSequence getSummary() { 326 return mSummary; 327 } 328 329 /** 330 * Returns the localized attribution title of the issue to be displayed in the UI. 331 * 332 * <p>This is displayed in the UI and helps to attribute issue cards to a particular source. If 333 * this value is {@code null}, the title of the group that contains the Safety Source will be 334 * used. 335 */ 336 @Nullable 337 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) getAttributionTitle()338 public CharSequence getAttributionTitle() { 339 if (!SdkLevel.isAtLeastU()) { 340 throw new UnsupportedOperationException(); 341 } 342 return mAttributionTitle; 343 } 344 345 /** Returns the {@link SafetySourceData.SeverityLevel} of the issue. */ 346 @SafetySourceData.SeverityLevel getSeverityLevel()347 public int getSeverityLevel() { 348 return mSeverityLevel; 349 } 350 351 /** 352 * Returns the category of the risk associated with the issue. 353 * 354 * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. 355 */ 356 @IssueCategory getIssueCategory()357 public int getIssueCategory() { 358 return mIssueCategory; 359 } 360 361 /** 362 * Returns a list of {@link Action}s representing actions supported in the UI for this issue. 363 * 364 * <p>Each issue must contain at least one action, in order to help the user resolve the issue. 365 * 366 * <p>In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most 367 * two actions supported from the UI. 368 */ 369 @NonNull getActions()370 public List<Action> getActions() { 371 return mActions; 372 } 373 374 /** 375 * Returns the optional {@link PendingIntent} that will be invoked when an issue is dismissed. 376 * 377 * <p>When a safety issue is dismissed in Safety Center page, the issue is removed from view in 378 * Safety Center page. This method returns an additional optional action specified by the safety 379 * source that should be invoked on issue dismissal. The action contained in the {@link 380 * PendingIntent} cannot start an activity. 381 */ 382 @Nullable getOnDismissPendingIntent()383 public PendingIntent getOnDismissPendingIntent() { 384 return mOnDismissPendingIntent; 385 } 386 387 /** 388 * Returns the identifier for the type of this issue. 389 * 390 * <p>The issue type should indicate the underlying basis for the issue, for e.g. a pending 391 * update or a disabled security feature. 392 * 393 * <p>The difference between this id and {@link #getId()} is that the issue type id is meant to 394 * be used for logging and should therefore contain no personally identifiable information (PII) 395 * (e.g. for account name). 396 * 397 * <p>On multiple instances of providing the same issue to be represented in Safety Center, 398 * provide the same issue type id across all instances. 399 */ 400 @NonNull getIssueTypeId()401 public String getIssueTypeId() { 402 return mIssueTypeId; 403 } 404 405 /** 406 * Returns the optional custom {@link Notification} for this issue which overrides the title, 407 * text and actions for any {@link android.app.Notification} generated for this {@link 408 * SafetySourceIssue}. 409 * 410 * <p>Safety Center may still generate a default notification from the other details of this 411 * issue when no custom notification has been set. See {@link #getNotificationBehavior()} for 412 * details 413 * 414 * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification 415 * @see #getNotificationBehavior() 416 */ 417 @Nullable 418 @RequiresApi(UPSIDE_DOWN_CAKE) getCustomNotification()419 public Notification getCustomNotification() { 420 if (!SdkLevel.isAtLeastU()) { 421 throw new UnsupportedOperationException(); 422 } 423 return mCustomNotification; 424 } 425 426 /** 427 * Returns the {@link NotificationBehavior} for this issue which determines if and when Safety 428 * Center will post a notification for this issue. 429 * 430 * <p>Any notification will be based on the {@link #getCustomNotification()} if set, or the 431 * other properties of this issue otherwise. 432 * 433 * <ul> 434 * <li>If {@link #NOTIFICATION_BEHAVIOR_IMMEDIATELY} then Safety Center will immediately 435 * create and post a notification 436 * <li>If {@link #NOTIFICATION_BEHAVIOR_DELAYED} then a notification will only be posted after 437 * a delay, if this issue has not been resolved. 438 * <li>If {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED} then a notification may or may not be 439 * posted, the exact behavior is defined by Safety Center. 440 * <li>If {@link #NOTIFICATION_BEHAVIOR_NEVER} Safety Center will never post a notification 441 * about this issue. Sources should specify this behavior when they wish to handle their 442 * own notifications. When this behavior is set sources should not set a custom 443 * notification. 444 * </ul> 445 * 446 * @see Builder#setNotificationBehavior(int) 447 */ 448 @NotificationBehavior 449 @RequiresApi(UPSIDE_DOWN_CAKE) getNotificationBehavior()450 public int getNotificationBehavior() { 451 if (!SdkLevel.isAtLeastU()) { 452 throw new UnsupportedOperationException(); 453 } 454 return mNotificationBehavior; 455 } 456 457 /** 458 * Returns the identifier used to deduplicate this issue against other issues with the same 459 * deduplication identifiers. 460 * 461 * <p>Deduplication identifier will be used to identify duplicate issues. This identifier 462 * applies across all safety sources which are part of the same deduplication group. 463 * Deduplication groups can be set, for each source, in the SafetyCenter config. Therefore, two 464 * issues are considered duplicate if their sources are part of the same deduplication group and 465 * they have the same deduplication identifier. 466 * 467 * <p>Out of all issues that are found to be duplicates, only one will be shown in the UI (the 468 * one with the highest severity, or in case of same severities, the one placed highest in the 469 * config). 470 * 471 * <p>Expected usage implies different sources will coordinate to set the same deduplication 472 * identifiers on issues that they want to deduplicate. 473 * 474 * <p>This shouldn't be a default mechanism for deduplication of issues. Most of the time 475 * sources should coordinate or communicate to only send the issue from one of them. That would 476 * also allow sources to choose which one will be displaying the issue, instead of depending on 477 * severity and config order. This API should only be needed if for some reason this isn't 478 * possible, for example, when sources can't communicate with each other and/or send issues at 479 * different times and/or issues can be of different severities. 480 */ 481 @Nullable 482 @RequiresApi(UPSIDE_DOWN_CAKE) getDeduplicationId()483 public String getDeduplicationId() { 484 if (!SdkLevel.isAtLeastU()) { 485 throw new UnsupportedOperationException(); 486 } 487 return mDeduplicationId; 488 } 489 490 /** 491 * Returns the {@link IssueActionability} for this issue which determines what type of action is 492 * required from the user: 493 * 494 * <ul> 495 * <li>If {@link #ISSUE_ACTIONABILITY_MANUAL} then user input is required to resolve the issue 496 * <li>If {@link #ISSUE_ACTIONABILITY_TIP} then the user needs to review this issue as a tip 497 * to improve their overall safety, and possibly acknowledge it 498 * <li>If {@link #ISSUE_ACTIONABILITY_AUTOMATIC} then the user needs to review this issue as 499 * something that has been resolved on their behalf, and possibly acknowledge it 500 * </ul> 501 * 502 * @see Builder#setIssueActionability(int) 503 */ 504 @IssueActionability 505 @RequiresApi(UPSIDE_DOWN_CAKE) getIssueActionability()506 public int getIssueActionability() { 507 if (!SdkLevel.isAtLeastU()) { 508 throw new UnsupportedOperationException(); 509 } 510 return mIssueActionability; 511 } 512 513 @Override describeContents()514 public int describeContents() { 515 return 0; 516 } 517 518 @Override writeToParcel(@onNull Parcel dest, int flags)519 public void writeToParcel(@NonNull Parcel dest, int flags) { 520 dest.writeString(mId); 521 TextUtils.writeToParcel(mTitle, dest, flags); 522 TextUtils.writeToParcel(mSubtitle, dest, flags); 523 TextUtils.writeToParcel(mSummary, dest, flags); 524 dest.writeInt(mSeverityLevel); 525 dest.writeInt(mIssueCategory); 526 dest.writeTypedList(mActions); 527 dest.writeTypedObject(mOnDismissPendingIntent, flags); 528 dest.writeString(mIssueTypeId); 529 if (SdkLevel.isAtLeastU()) { 530 dest.writeTypedObject(mCustomNotification, flags); 531 dest.writeInt(mNotificationBehavior); 532 TextUtils.writeToParcel(mAttributionTitle, dest, flags); 533 dest.writeString(mDeduplicationId); 534 dest.writeInt(mIssueActionability); 535 } 536 } 537 538 @Override equals(Object o)539 public boolean equals(Object o) { 540 if (this == o) return true; 541 if (!(o instanceof SafetySourceIssue)) return false; 542 SafetySourceIssue that = (SafetySourceIssue) o; 543 return mSeverityLevel == that.mSeverityLevel 544 && TextUtils.equals(mId, that.mId) 545 && TextUtils.equals(mTitle, that.mTitle) 546 && TextUtils.equals(mSubtitle, that.mSubtitle) 547 && TextUtils.equals(mSummary, that.mSummary) 548 && mIssueCategory == that.mIssueCategory 549 && mActions.equals(that.mActions) 550 && Objects.equals(mOnDismissPendingIntent, that.mOnDismissPendingIntent) 551 && TextUtils.equals(mIssueTypeId, that.mIssueTypeId) 552 && Objects.equals(mCustomNotification, that.mCustomNotification) 553 && mNotificationBehavior == that.mNotificationBehavior 554 && TextUtils.equals(mAttributionTitle, that.mAttributionTitle) 555 && TextUtils.equals(mDeduplicationId, that.mDeduplicationId) 556 && mIssueActionability == that.mIssueActionability; 557 } 558 559 @Override hashCode()560 public int hashCode() { 561 return Objects.hash( 562 mId, 563 mTitle, 564 mSubtitle, 565 mSummary, 566 mSeverityLevel, 567 mIssueCategory, 568 mActions, 569 mOnDismissPendingIntent, 570 mIssueTypeId, 571 mCustomNotification, 572 mNotificationBehavior, 573 mAttributionTitle, 574 mDeduplicationId, 575 mIssueActionability); 576 } 577 578 @Override toString()579 public String toString() { 580 return "SafetySourceIssue{" 581 + "mId=" 582 + mId 583 + "mTitle=" 584 + mTitle 585 + ", mSubtitle=" 586 + mSubtitle 587 + ", mSummary=" 588 + mSummary 589 + ", mSeverityLevel=" 590 + mSeverityLevel 591 + ", mIssueCategory=" 592 + mIssueCategory 593 + ", mActions=" 594 + mActions 595 + ", mOnDismissPendingIntent=" 596 + mOnDismissPendingIntent 597 + ", mIssueTypeId=" 598 + mIssueTypeId 599 + ", mCustomNotification=" 600 + mCustomNotification 601 + ", mNotificationBehavior=" 602 + mNotificationBehavior 603 + ", mAttributionTitle=" 604 + mAttributionTitle 605 + ", mDeduplicationId=" 606 + mDeduplicationId 607 + ", mIssueActionability=" 608 + mIssueActionability 609 + '}'; 610 } 611 612 /** 613 * Data for an action supported from a safety issue {@link SafetySourceIssue} in the Safety 614 * Center page. 615 * 616 * <p>The purpose of the action is to allow the user to address the safety issue, either by 617 * performing a fix suggested in the issue, or by navigating the user to the source of the issue 618 * where they can be exposed to detail about the issue and further suggestions to resolve it. 619 * 620 * <p>The user will be allowed to invoke the action from the UI by clicking on a UI element and 621 * consequently resolve the issue. 622 */ 623 public static final class Action implements Parcelable { 624 625 @NonNull 626 public static final Creator<Action> CREATOR = 627 new Creator<Action>() { 628 @Override 629 public Action createFromParcel(Parcel in) { 630 String id = in.readString(); 631 CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 632 PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR); 633 Builder builder = 634 new Builder(id, label, pendingIntent) 635 .setWillResolve(in.readBoolean()) 636 .setSuccessMessage( 637 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel( 638 in)); 639 if (SdkLevel.isAtLeastU()) { 640 ConfirmationDialogDetails confirmationDialogDetails = 641 in.readTypedObject(ConfirmationDialogDetails.CREATOR); 642 builder.setConfirmationDialogDetails(confirmationDialogDetails); 643 } 644 return builder.build(); 645 } 646 647 @Override 648 public Action[] newArray(int size) { 649 return new Action[size]; 650 } 651 }; 652 enforceUniqueActionIds( @onNull List<SafetySourceIssue.Action> actions, @NonNull String message)653 private static void enforceUniqueActionIds( 654 @NonNull List<SafetySourceIssue.Action> actions, @NonNull String message) { 655 Set<String> actionIds = new HashSet<>(); 656 for (int i = 0; i < actions.size(); i++) { 657 SafetySourceIssue.Action action = actions.get(i); 658 659 String actionId = action.getId(); 660 checkArgument(!actionIds.contains(actionId), message); 661 actionIds.add(actionId); 662 } 663 } 664 665 @NonNull private final String mId; 666 @NonNull private final CharSequence mLabel; 667 @NonNull private final PendingIntent mPendingIntent; 668 private final boolean mWillResolve; 669 @Nullable private final CharSequence mSuccessMessage; 670 @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails; 671 Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, @Nullable CharSequence successMessage, @Nullable ConfirmationDialogDetails confirmationDialogDetails)672 private Action( 673 @NonNull String id, 674 @NonNull CharSequence label, 675 @NonNull PendingIntent pendingIntent, 676 boolean willResolve, 677 @Nullable CharSequence successMessage, 678 @Nullable ConfirmationDialogDetails confirmationDialogDetails) { 679 mId = id; 680 mLabel = label; 681 mPendingIntent = pendingIntent; 682 mWillResolve = willResolve; 683 mSuccessMessage = successMessage; 684 mConfirmationDialogDetails = confirmationDialogDetails; 685 } 686 687 /** 688 * Returns the ID of the action, unique among actions in a given {@link SafetySourceIssue}. 689 */ 690 @NonNull getId()691 public String getId() { 692 return mId; 693 } 694 695 /** 696 * Returns the localized label of the action to be displayed in the UI. 697 * 698 * <p>The label should indicate what action will be performed if when invoked. 699 */ 700 @NonNull getLabel()701 public CharSequence getLabel() { 702 return mLabel; 703 } 704 705 /** 706 * Returns a {@link PendingIntent} to be fired when the action is clicked on. 707 * 708 * <p>The {@link PendingIntent} should perform the action referred to by {@link 709 * #getLabel()}. 710 */ 711 @NonNull getPendingIntent()712 public PendingIntent getPendingIntent() { 713 return mPendingIntent; 714 } 715 716 /** 717 * Returns whether invoking this action will fix or address the issue sufficiently for it to 718 * be considered resolved i.e. the issue will no longer need to be conveyed to the user in 719 * the UI. 720 */ willResolve()721 public boolean willResolve() { 722 return mWillResolve; 723 } 724 725 /** 726 * Returns the optional localized message to be displayed in the UI when the action is 727 * invoked and completes successfully. 728 */ 729 @Nullable getSuccessMessage()730 public CharSequence getSuccessMessage() { 731 return mSuccessMessage; 732 } 733 734 /** 735 * Returns the optional data to be displayed in the confirmation dialog prior to launching 736 * the {@link PendingIntent} when the action is clicked on. 737 */ 738 @Nullable 739 @RequiresApi(UPSIDE_DOWN_CAKE) getConfirmationDialogDetails()740 public ConfirmationDialogDetails getConfirmationDialogDetails() { 741 if (!SdkLevel.isAtLeastU()) { 742 throw new UnsupportedOperationException(); 743 } 744 return mConfirmationDialogDetails; 745 } 746 747 @Override describeContents()748 public int describeContents() { 749 return 0; 750 } 751 752 @Override writeToParcel(@onNull Parcel dest, int flags)753 public void writeToParcel(@NonNull Parcel dest, int flags) { 754 dest.writeString(mId); 755 TextUtils.writeToParcel(mLabel, dest, flags); 756 dest.writeTypedObject(mPendingIntent, flags); 757 dest.writeBoolean(mWillResolve); 758 TextUtils.writeToParcel(mSuccessMessage, dest, flags); 759 if (SdkLevel.isAtLeastU()) { 760 dest.writeTypedObject(mConfirmationDialogDetails, flags); 761 } 762 } 763 764 @Override equals(Object o)765 public boolean equals(Object o) { 766 if (this == o) return true; 767 if (!(o instanceof Action)) return false; 768 Action that = (Action) o; 769 return mId.equals(that.mId) 770 && TextUtils.equals(mLabel, that.mLabel) 771 && mPendingIntent.equals(that.mPendingIntent) 772 && mWillResolve == that.mWillResolve 773 && TextUtils.equals(mSuccessMessage, that.mSuccessMessage) 774 && Objects.equals(mConfirmationDialogDetails, that.mConfirmationDialogDetails); 775 } 776 777 @Override hashCode()778 public int hashCode() { 779 return Objects.hash( 780 mId, 781 mLabel, 782 mPendingIntent, 783 mWillResolve, 784 mSuccessMessage, 785 mConfirmationDialogDetails); 786 } 787 788 @Override toString()789 public String toString() { 790 return "Action{" 791 + "mId=" 792 + mId 793 + ", mLabel=" 794 + mLabel 795 + ", mPendingIntent=" 796 + mPendingIntent 797 + ", mWillResolve=" 798 + mWillResolve 799 + ", mSuccessMessage=" 800 + mSuccessMessage 801 + ", mConfirmationDialogDetails=" 802 + mConfirmationDialogDetails 803 + '}'; 804 } 805 806 /** Data for an action confirmation dialog to be shown before action is executed. */ 807 @RequiresApi(UPSIDE_DOWN_CAKE) 808 public static final class ConfirmationDialogDetails implements Parcelable { 809 810 @NonNull 811 public static final Creator<ConfirmationDialogDetails> CREATOR = 812 new Creator<ConfirmationDialogDetails>() { 813 @Override 814 public ConfirmationDialogDetails createFromParcel(Parcel in) { 815 CharSequence title = 816 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 817 CharSequence text = 818 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 819 CharSequence acceptButtonText = 820 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 821 CharSequence denyButtonText = 822 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 823 return new ConfirmationDialogDetails( 824 title, text, acceptButtonText, denyButtonText); 825 } 826 827 @Override 828 public ConfirmationDialogDetails[] newArray(int size) { 829 return new ConfirmationDialogDetails[size]; 830 } 831 }; 832 833 @NonNull private final CharSequence mTitle; 834 @NonNull private final CharSequence mText; 835 @NonNull private final CharSequence mAcceptButtonText; 836 @NonNull private final CharSequence mDenyButtonText; 837 ConfirmationDialogDetails( @onNull CharSequence title, @NonNull CharSequence text, @NonNull CharSequence acceptButtonText, @NonNull CharSequence denyButtonText)838 public ConfirmationDialogDetails( 839 @NonNull CharSequence title, 840 @NonNull CharSequence text, 841 @NonNull CharSequence acceptButtonText, 842 @NonNull CharSequence denyButtonText) { 843 mTitle = requireNonNull(title); 844 mText = requireNonNull(text); 845 mAcceptButtonText = requireNonNull(acceptButtonText); 846 mDenyButtonText = requireNonNull(denyButtonText); 847 } 848 849 /** Returns the title of action confirmation dialog. */ 850 @NonNull getTitle()851 public CharSequence getTitle() { 852 return mTitle; 853 } 854 855 /** Returns the text of action confirmation dialog. */ 856 @NonNull getText()857 public CharSequence getText() { 858 return mText; 859 } 860 861 /** Returns the text of the button to accept action execution. */ 862 @NonNull getAcceptButtonText()863 public CharSequence getAcceptButtonText() { 864 return mAcceptButtonText; 865 } 866 867 /** Returns the text of the button to deny action execution. */ 868 @NonNull getDenyButtonText()869 public CharSequence getDenyButtonText() { 870 return mDenyButtonText; 871 } 872 873 @Override describeContents()874 public int describeContents() { 875 return 0; 876 } 877 878 @Override writeToParcel(@onNull Parcel dest, int flags)879 public void writeToParcel(@NonNull Parcel dest, int flags) { 880 TextUtils.writeToParcel(mTitle, dest, flags); 881 TextUtils.writeToParcel(mText, dest, flags); 882 TextUtils.writeToParcel(mAcceptButtonText, dest, flags); 883 TextUtils.writeToParcel(mDenyButtonText, dest, flags); 884 } 885 886 @Override equals(Object o)887 public boolean equals(Object o) { 888 if (this == o) return true; 889 if (!(o instanceof ConfirmationDialogDetails)) return false; 890 ConfirmationDialogDetails that = (ConfirmationDialogDetails) o; 891 return TextUtils.equals(mTitle, that.mTitle) 892 && TextUtils.equals(mText, that.mText) 893 && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText) 894 && TextUtils.equals(mDenyButtonText, that.mDenyButtonText); 895 } 896 897 @Override hashCode()898 public int hashCode() { 899 return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText); 900 } 901 902 @Override toString()903 public String toString() { 904 return "ConfirmationDialogDetails{" 905 + "mTitle=" 906 + mTitle 907 + ", mText=" 908 + mText 909 + ", mAcceptButtonText=" 910 + mAcceptButtonText 911 + ", mDenyButtonText=" 912 + mDenyButtonText 913 + '}'; 914 } 915 } 916 917 /** Builder class for {@link Action}. */ 918 public static final class Builder { 919 920 @NonNull private final String mId; 921 @NonNull private final CharSequence mLabel; 922 @NonNull private final PendingIntent mPendingIntent; 923 private boolean mWillResolve = false; 924 @Nullable private CharSequence mSuccessMessage; 925 @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails; 926 927 /** Creates a {@link Builder} for an {@link Action}. */ Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)928 public Builder( 929 @NonNull String id, 930 @NonNull CharSequence label, 931 @NonNull PendingIntent pendingIntent) { 932 mId = requireNonNull(id); 933 mLabel = requireNonNull(label); 934 mPendingIntent = requireNonNull(pendingIntent); 935 } 936 937 /** Creates a {@link Builder} with the values from the given {@link Action}. */ 938 @RequiresApi(UPSIDE_DOWN_CAKE) Builder(@onNull Action action)939 public Builder(@NonNull Action action) { 940 if (!SdkLevel.isAtLeastU()) { 941 throw new UnsupportedOperationException(); 942 } 943 requireNonNull(action); 944 mId = action.mId; 945 mLabel = action.mLabel; 946 mPendingIntent = action.mPendingIntent; 947 mWillResolve = action.mWillResolve; 948 mSuccessMessage = action.mSuccessMessage; 949 mConfirmationDialogDetails = action.mConfirmationDialogDetails; 950 } 951 952 /** 953 * Sets whether the action will resolve the safety issue. Defaults to {@code false}. 954 * 955 * <p>Note: It is not allowed for resolvable actions to have a {@link PendingIntent} 956 * that launches activity. When extra confirmation is needed consider using {@link 957 * Builder#setConfirmationDialogDetails}. 958 * 959 * @see #willResolve() 960 */ 961 @SuppressLint("MissingGetterMatchingBuilder") 962 @NonNull setWillResolve(boolean willResolve)963 public Builder setWillResolve(boolean willResolve) { 964 mWillResolve = willResolve; 965 return this; 966 } 967 968 /** 969 * Sets the optional localized message to be displayed in the UI when the action is 970 * invoked and completes successfully. 971 */ 972 @NonNull setSuccessMessage(@ullable CharSequence successMessage)973 public Builder setSuccessMessage(@Nullable CharSequence successMessage) { 974 mSuccessMessage = successMessage; 975 return this; 976 } 977 978 /** 979 * Sets the optional data to be displayed in the confirmation dialog prior to launching 980 * the {@link PendingIntent} when the action is clicked on. 981 */ 982 @NonNull 983 @RequiresApi(UPSIDE_DOWN_CAKE) setConfirmationDialogDetails( @ullable ConfirmationDialogDetails confirmationDialogDetails)984 public Builder setConfirmationDialogDetails( 985 @Nullable ConfirmationDialogDetails confirmationDialogDetails) { 986 if (!SdkLevel.isAtLeastU()) { 987 throw new UnsupportedOperationException(); 988 } 989 mConfirmationDialogDetails = confirmationDialogDetails; 990 return this; 991 } 992 993 /** Creates the {@link Action} defined by this {@link Builder}. */ 994 @NonNull build()995 public Action build() { 996 if (SdkLevel.isAtLeastU()) { 997 boolean willResolveWithActivity = mWillResolve && mPendingIntent.isActivity(); 998 checkArgument( 999 !willResolveWithActivity, 1000 "Launching activity from Action that should resolve the" 1001 + " SafetySourceIssue is not allowed. Consider using setting a" 1002 + " Confirmation if needed, and either set the willResolve to" 1003 + " false or make PendingIntent to start a service/send a" 1004 + " broadcast."); 1005 } 1006 return new Action( 1007 mId, 1008 mLabel, 1009 mPendingIntent, 1010 mWillResolve, 1011 mSuccessMessage, 1012 mConfirmationDialogDetails); 1013 } 1014 } 1015 } 1016 1017 /** 1018 * Data for Safety Center to use when constructing a system {@link android.app.Notification} 1019 * about a related {@link SafetySourceIssue}. 1020 * 1021 * <p>Safety Center can construct a default notification for any issue, but sources may use 1022 * {@link Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification)} if 1023 * they want to override the title, text or actions. 1024 * 1025 * @see #getCustomNotification() 1026 * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification) 1027 * @see #getNotificationBehavior() 1028 */ 1029 @RequiresApi(UPSIDE_DOWN_CAKE) 1030 public static final class Notification implements Parcelable { 1031 1032 @NonNull 1033 public static final Creator<Notification> CREATOR = 1034 new Creator<Notification>() { 1035 @Override 1036 public Notification createFromParcel(Parcel in) { 1037 return new Builder( 1038 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in), 1039 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)) 1040 .addActions(in.createTypedArrayList(Action.CREATOR)) 1041 .build(); 1042 } 1043 1044 @Override 1045 public Notification[] newArray(int size) { 1046 return new Notification[size]; 1047 } 1048 }; 1049 1050 @NonNull private final CharSequence mTitle; 1051 @NonNull private final CharSequence mText; 1052 @NonNull private final List<Action> mActions; 1053 Notification( @onNull CharSequence title, @NonNull CharSequence text, @NonNull List<Action> actions)1054 private Notification( 1055 @NonNull CharSequence title, 1056 @NonNull CharSequence text, 1057 @NonNull List<Action> actions) { 1058 mTitle = title; 1059 mText = text; 1060 mActions = actions; 1061 } 1062 1063 /** 1064 * Custom title which will be used instead of {@link SafetySourceIssue#getTitle()} when 1065 * building a {@link android.app.Notification} for this issue. 1066 */ 1067 @NonNull getTitle()1068 public CharSequence getTitle() { 1069 return mTitle; 1070 } 1071 1072 /** 1073 * Custom text which will be used instead of {@link SafetySourceIssue#getSummary()} when 1074 * building a {@link android.app.Notification} for this issue. 1075 */ 1076 @NonNull getText()1077 public CharSequence getText() { 1078 return mText; 1079 } 1080 1081 /** 1082 * Custom list of {@link Action} instances which will be used instead of {@link 1083 * SafetySourceIssue#getActions()} when building a {@link android.app.Notification} for this 1084 * issue. 1085 * 1086 * <p>If this list is empty then the resulting {@link android.app.Notification} will have 1087 * zero action buttons. 1088 */ 1089 @NonNull getActions()1090 public List<Action> getActions() { 1091 return mActions; 1092 } 1093 1094 @Override describeContents()1095 public int describeContents() { 1096 return 0; 1097 } 1098 1099 @Override writeToParcel(@onNull Parcel dest, int flags)1100 public void writeToParcel(@NonNull Parcel dest, int flags) { 1101 TextUtils.writeToParcel(mTitle, dest, flags); 1102 TextUtils.writeToParcel(mText, dest, flags); 1103 dest.writeTypedList(mActions); 1104 } 1105 1106 @Override equals(Object o)1107 public boolean equals(Object o) { 1108 if (this == o) return true; 1109 if (!(o instanceof Notification)) return false; 1110 Notification that = (Notification) o; 1111 return TextUtils.equals(mTitle, that.mTitle) 1112 && TextUtils.equals(mText, that.mText) 1113 && mActions.equals(that.mActions); 1114 } 1115 1116 @Override hashCode()1117 public int hashCode() { 1118 return Objects.hash(mTitle, mText, mActions); 1119 } 1120 1121 @Override toString()1122 public String toString() { 1123 return "Notification{" 1124 + "mTitle=" 1125 + mTitle 1126 + ", mText=" 1127 + mText 1128 + ", mActions=" 1129 + mActions 1130 + '}'; 1131 } 1132 1133 /** Builder for {@link SafetySourceIssue.Notification}. */ 1134 public static final class Builder { 1135 1136 @NonNull private final CharSequence mTitle; 1137 @NonNull private final CharSequence mText; 1138 @NonNull private final List<Action> mActions = new ArrayList<>(); 1139 Builder(@onNull CharSequence title, @NonNull CharSequence text)1140 public Builder(@NonNull CharSequence title, @NonNull CharSequence text) { 1141 mTitle = requireNonNull(title); 1142 mText = requireNonNull(text); 1143 } 1144 1145 /** Creates a {@link Builder} with the values from the given {@link Notification}. */ Builder(@onNull Notification notification)1146 public Builder(@NonNull Notification notification) { 1147 requireNonNull(notification); 1148 mTitle = notification.mTitle; 1149 mText = notification.mText; 1150 mActions.addAll(notification.mActions); 1151 } 1152 1153 /** Adds an {@link Action} to the custom {@link Notification}. */ 1154 @NonNull addAction(@onNull Action action)1155 public Builder addAction(@NonNull Action action) { 1156 mActions.add(requireNonNull(action)); 1157 return this; 1158 } 1159 1160 /** Adds several {@link Action}s to the custom {@link Notification}. */ 1161 @NonNull addActions(@onNull List<Action> actions)1162 public Builder addActions(@NonNull List<Action> actions) { 1163 mActions.addAll(requireNonNull(actions)); 1164 return this; 1165 } 1166 1167 /** Clears all the {@link Action}s that were added so far. */ 1168 @NonNull clearActions()1169 public Builder clearActions() { 1170 mActions.clear(); 1171 return this; 1172 } 1173 1174 /** Builds a {@link Notification} instance. */ 1175 @NonNull build()1176 public Notification build() { 1177 List<Action> actions = unmodifiableList(new ArrayList<>(mActions)); 1178 Action.enforceUniqueActionIds( 1179 actions, "Custom notification cannot have duplicate action ids"); 1180 checkArgument( 1181 actions.size() <= 2, 1182 "Custom notification must not contain more than 2 actions"); 1183 return new Notification(mTitle, mText, actions); 1184 } 1185 } 1186 } 1187 1188 /** Builder class for {@link SafetySourceIssue}. */ 1189 public static final class Builder { 1190 1191 @NonNull private final String mId; 1192 @NonNull private final CharSequence mTitle; 1193 @NonNull private final CharSequence mSummary; 1194 @SafetySourceData.SeverityLevel private final int mSeverityLevel; 1195 @NonNull private final String mIssueTypeId; 1196 private final List<Action> mActions = new ArrayList<>(); 1197 1198 @Nullable private CharSequence mSubtitle; 1199 @IssueCategory private int mIssueCategory = ISSUE_CATEGORY_GENERAL; 1200 @Nullable private PendingIntent mOnDismissPendingIntent; 1201 @Nullable private CharSequence mAttributionTitle; 1202 @Nullable private String mDeduplicationId; 1203 1204 @Nullable private Notification mCustomNotification = null; 1205 1206 @SuppressLint("NewApi") 1207 @NotificationBehavior 1208 private int mNotificationBehavior = NOTIFICATION_BEHAVIOR_UNSPECIFIED; 1209 1210 @SuppressLint("NewApi") 1211 @IssueActionability 1212 private int mIssueActionability = ISSUE_ACTIONABILITY_MANUAL; 1213 1214 /** Creates a {@link Builder} for a {@link SafetySourceIssue}. */ Builder( @onNull String id, @NonNull CharSequence title, @NonNull CharSequence summary, @SafetySourceData.SeverityLevel int severityLevel, @NonNull String issueTypeId)1215 public Builder( 1216 @NonNull String id, 1217 @NonNull CharSequence title, 1218 @NonNull CharSequence summary, 1219 @SafetySourceData.SeverityLevel int severityLevel, 1220 @NonNull String issueTypeId) { 1221 this.mId = requireNonNull(id); 1222 this.mTitle = requireNonNull(title); 1223 this.mSummary = requireNonNull(summary); 1224 this.mSeverityLevel = validateSeverityLevel(severityLevel); 1225 this.mIssueTypeId = requireNonNull(issueTypeId); 1226 } 1227 1228 /** Creates a {@link Builder} with the values from the given {@link SafetySourceIssue}. */ 1229 @RequiresApi(UPSIDE_DOWN_CAKE) Builder(@onNull SafetySourceIssue safetySourceIssue)1230 public Builder(@NonNull SafetySourceIssue safetySourceIssue) { 1231 if (!SdkLevel.isAtLeastU()) { 1232 throw new UnsupportedOperationException(); 1233 } 1234 requireNonNull(safetySourceIssue); 1235 mId = safetySourceIssue.mId; 1236 mTitle = safetySourceIssue.mTitle; 1237 mSummary = safetySourceIssue.mSummary; 1238 mSeverityLevel = safetySourceIssue.mSeverityLevel; 1239 mIssueTypeId = safetySourceIssue.mIssueTypeId; 1240 mActions.addAll(safetySourceIssue.mActions); 1241 mSubtitle = safetySourceIssue.mSubtitle; 1242 mIssueCategory = safetySourceIssue.mIssueCategory; 1243 mOnDismissPendingIntent = safetySourceIssue.mOnDismissPendingIntent; 1244 mAttributionTitle = safetySourceIssue.mAttributionTitle; 1245 mDeduplicationId = safetySourceIssue.mDeduplicationId; 1246 mCustomNotification = safetySourceIssue.mCustomNotification; 1247 mNotificationBehavior = safetySourceIssue.mNotificationBehavior; 1248 mIssueActionability = safetySourceIssue.mIssueActionability; 1249 } 1250 1251 /** Sets the localized subtitle. */ 1252 @NonNull setSubtitle(@ullable CharSequence subtitle)1253 public Builder setSubtitle(@Nullable CharSequence subtitle) { 1254 mSubtitle = subtitle; 1255 return this; 1256 } 1257 1258 /** 1259 * Sets or clears the optional attribution title for this issue. 1260 * 1261 * <p>This is displayed in the UI and helps to attribute an issue to a particular source. If 1262 * this value is {@code null}, the title of the group that contains the Safety Source will 1263 * be used. 1264 */ 1265 @NonNull 1266 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) setAttributionTitle(@ullable CharSequence attributionTitle)1267 public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) { 1268 if (!SdkLevel.isAtLeastU()) { 1269 throw new UnsupportedOperationException(); 1270 } 1271 mAttributionTitle = attributionTitle; 1272 return this; 1273 } 1274 1275 /** 1276 * Sets the category of the risk associated with the issue. 1277 * 1278 * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. 1279 */ 1280 @NonNull setIssueCategory(@ssueCategory int issueCategory)1281 public Builder setIssueCategory(@IssueCategory int issueCategory) { 1282 mIssueCategory = validateIssueCategory(issueCategory); 1283 return this; 1284 } 1285 1286 /** Adds data for an {@link Action} to be shown in UI. */ 1287 @NonNull addAction(@onNull Action actionData)1288 public Builder addAction(@NonNull Action actionData) { 1289 mActions.add(requireNonNull(actionData)); 1290 return this; 1291 } 1292 1293 /** Clears data for all the {@link Action}s that were added to this {@link Builder}. */ 1294 @NonNull clearActions()1295 public Builder clearActions() { 1296 mActions.clear(); 1297 return this; 1298 } 1299 1300 /** 1301 * Sets an optional {@link PendingIntent} to be invoked when an issue is dismissed from the 1302 * UI. 1303 * 1304 * <p>In particular, if the source would like to be notified of issue dismissals in Safety 1305 * Center in order to be able to dismiss or ignore issues at the source, then set this 1306 * field. The action contained in the {@link PendingIntent} must not start an activity. 1307 * 1308 * @see #getOnDismissPendingIntent() 1309 */ 1310 @NonNull setOnDismissPendingIntent(@ullable PendingIntent onDismissPendingIntent)1311 public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) { 1312 checkArgument( 1313 onDismissPendingIntent == null || !onDismissPendingIntent.isActivity(), 1314 "Safety source issue on dismiss pending intent must not start an activity"); 1315 mOnDismissPendingIntent = onDismissPendingIntent; 1316 return this; 1317 } 1318 1319 /** 1320 * Sets a custom {@link Notification} for this issue. 1321 * 1322 * <p>Using a custom {@link Notification} a source may specify a different {@link 1323 * Notification#getTitle()}, {@link Notification#getText()} and {@link 1324 * Notification#getActions()} for Safety Center to use when constructing a notification for 1325 * this issue. 1326 * 1327 * <p>Safety Center may still generate a default notification from the other details of this 1328 * issue when no custom notification has been set, depending on the issue's {@link 1329 * #getNotificationBehavior()}. 1330 * 1331 * @see #getCustomNotification() 1332 * @see #setNotificationBehavior(int) 1333 */ 1334 @NonNull 1335 @RequiresApi(UPSIDE_DOWN_CAKE) setCustomNotification(@ullable Notification customNotification)1336 public Builder setCustomNotification(@Nullable Notification customNotification) { 1337 if (!SdkLevel.isAtLeastU()) { 1338 throw new UnsupportedOperationException(); 1339 } 1340 mCustomNotification = customNotification; 1341 return this; 1342 } 1343 1344 /** 1345 * Sets the notification behavior of the issue. 1346 * 1347 * <p>Must be one of {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED}, {@link 1348 * #NOTIFICATION_BEHAVIOR_NEVER}, {@link #NOTIFICATION_BEHAVIOR_DELAYED} or {@link 1349 * #NOTIFICATION_BEHAVIOR_IMMEDIATELY}. See {@link #getNotificationBehavior()} for details 1350 * of how Safety Center will interpret each of these. 1351 * 1352 * @see #getNotificationBehavior() 1353 */ 1354 @NonNull 1355 @RequiresApi(UPSIDE_DOWN_CAKE) setNotificationBehavior(@otificationBehavior int notificationBehavior)1356 public Builder setNotificationBehavior(@NotificationBehavior int notificationBehavior) { 1357 if (!SdkLevel.isAtLeastU()) { 1358 throw new UnsupportedOperationException(); 1359 } 1360 mNotificationBehavior = validateNotificationBehavior(notificationBehavior); 1361 return this; 1362 } 1363 1364 /** 1365 * Sets the deduplication identifier for the issue. 1366 * 1367 * @see #getDeduplicationId() 1368 */ 1369 @NonNull 1370 @RequiresApi(UPSIDE_DOWN_CAKE) setDeduplicationId(@ullable String deduplicationId)1371 public Builder setDeduplicationId(@Nullable String deduplicationId) { 1372 if (!SdkLevel.isAtLeastU()) { 1373 throw new UnsupportedOperationException(); 1374 } 1375 mDeduplicationId = deduplicationId; 1376 return this; 1377 } 1378 1379 /** 1380 * Sets the issue actionability of the issue. 1381 * 1382 * <p>Must be one of {@link #ISSUE_ACTIONABILITY_MANUAL} (default), {@link 1383 * #ISSUE_ACTIONABILITY_TIP}, {@link #ISSUE_ACTIONABILITY_AUTOMATIC}. 1384 * 1385 * @see #getIssueActionability() 1386 */ 1387 @NonNull 1388 @RequiresApi(UPSIDE_DOWN_CAKE) setIssueActionability(@ssueActionability int issueActionability)1389 public Builder setIssueActionability(@IssueActionability int issueActionability) { 1390 if (!SdkLevel.isAtLeastU()) { 1391 throw new UnsupportedOperationException(); 1392 } 1393 mIssueActionability = validateIssueActionability(issueActionability); 1394 return this; 1395 } 1396 1397 /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */ 1398 @NonNull build()1399 public SafetySourceIssue build() { 1400 List<SafetySourceIssue.Action> actions = unmodifiableList(new ArrayList<>(mActions)); 1401 Action.enforceUniqueActionIds( 1402 actions, "Safety source issue cannot have duplicate action ids"); 1403 if (SdkLevel.isAtLeastU()) { 1404 checkArgument( 1405 mIssueActionability != ISSUE_ACTIONABILITY_MANUAL || !actions.isEmpty(), 1406 "Actionable safety source issue must contain at least 1 action"); 1407 } else { 1408 checkArgument( 1409 !actions.isEmpty(), "Safety source issue must contain at least 1 action"); 1410 } 1411 checkArgument( 1412 actions.size() <= 2, 1413 "Safety source issue must not contain more than 2 actions"); 1414 return new SafetySourceIssue( 1415 mId, 1416 mTitle, 1417 mSubtitle, 1418 mSummary, 1419 mSeverityLevel, 1420 mIssueCategory, 1421 actions, 1422 mOnDismissPendingIntent, 1423 mIssueTypeId, 1424 mCustomNotification, 1425 mNotificationBehavior, 1426 mAttributionTitle, 1427 mDeduplicationId, 1428 mIssueActionability); 1429 } 1430 } 1431 1432 @SafetySourceData.SeverityLevel validateSeverityLevel(int value)1433 private static int validateSeverityLevel(int value) { 1434 switch (value) { 1435 case SafetySourceData.SEVERITY_LEVEL_INFORMATION: 1436 case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: 1437 case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: 1438 return value; 1439 case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: 1440 throw new IllegalArgumentException( 1441 "SeverityLevel for SafetySourceIssue must not be " 1442 + "SEVERITY_LEVEL_UNSPECIFIED"); 1443 default: 1444 } 1445 throw new IllegalArgumentException( 1446 "Unexpected SeverityLevel for SafetySourceIssue: " + value); 1447 } 1448 1449 @IssueCategory validateIssueCategory(int value)1450 private static int validateIssueCategory(int value) { 1451 switch (value) { 1452 case ISSUE_CATEGORY_DEVICE: 1453 case ISSUE_CATEGORY_ACCOUNT: 1454 case ISSUE_CATEGORY_GENERAL: 1455 return value; 1456 default: 1457 } 1458 if (SdkLevel.isAtLeastU()) { 1459 switch (value) { 1460 case ISSUE_CATEGORY_DATA: 1461 case ISSUE_CATEGORY_PASSWORDS: 1462 case ISSUE_CATEGORY_PERSONAL_SAFETY: 1463 return value; 1464 default: 1465 } 1466 } 1467 throw new IllegalArgumentException( 1468 "Unexpected IssueCategory for SafetySourceIssue: " + value); 1469 } 1470 1471 @NotificationBehavior validateNotificationBehavior(int value)1472 private static int validateNotificationBehavior(int value) { 1473 switch (value) { 1474 case NOTIFICATION_BEHAVIOR_UNSPECIFIED: 1475 case NOTIFICATION_BEHAVIOR_NEVER: 1476 case NOTIFICATION_BEHAVIOR_DELAYED: 1477 case NOTIFICATION_BEHAVIOR_IMMEDIATELY: 1478 return value; 1479 default: 1480 } 1481 throw new IllegalArgumentException( 1482 "Unexpected NotificationBehavior for SafetySourceIssue: " + value); 1483 } 1484 1485 @IssueActionability validateIssueActionability(int value)1486 private static int validateIssueActionability(int value) { 1487 switch (value) { 1488 case ISSUE_ACTIONABILITY_MANUAL: 1489 case ISSUE_ACTIONABILITY_TIP: 1490 case ISSUE_ACTIONABILITY_AUTOMATIC: 1491 return value; 1492 default: 1493 } 1494 throw new IllegalArgumentException( 1495 "Unexpected IssueActionability for SafetySourceIssue: " + value); 1496 } 1497 } 1498