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 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 342 } 343 return mAttributionTitle; 344 } 345 346 /** Returns the {@link SafetySourceData.SeverityLevel} of the issue. */ 347 @SafetySourceData.SeverityLevel getSeverityLevel()348 public int getSeverityLevel() { 349 return mSeverityLevel; 350 } 351 352 /** 353 * Returns the category of the risk associated with the issue. 354 * 355 * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. 356 */ 357 @IssueCategory getIssueCategory()358 public int getIssueCategory() { 359 return mIssueCategory; 360 } 361 362 /** 363 * Returns a list of {@link Action}s representing actions supported in the UI for this issue. 364 * 365 * <p>Each issue must contain at least one action, in order to help the user resolve the issue. 366 * 367 * <p>In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most 368 * two actions supported from the UI. 369 */ 370 @NonNull getActions()371 public List<Action> getActions() { 372 return mActions; 373 } 374 375 /** 376 * Returns the optional {@link PendingIntent} that will be invoked when an issue is dismissed. 377 * 378 * <p>When a safety issue is dismissed in Safety Center page, the issue is removed from view in 379 * Safety Center page. This method returns an additional optional action specified by the safety 380 * source that should be invoked on issue dismissal. The action contained in the {@link 381 * PendingIntent} cannot start an activity. 382 */ 383 @Nullable getOnDismissPendingIntent()384 public PendingIntent getOnDismissPendingIntent() { 385 return mOnDismissPendingIntent; 386 } 387 388 /** 389 * Returns the identifier for the type of this issue. 390 * 391 * <p>The issue type should indicate the underlying basis for the issue, for e.g. a pending 392 * update or a disabled security feature. 393 * 394 * <p>The difference between this id and {@link #getId()} is that the issue type id is meant to 395 * be used for logging and should therefore contain no personally identifiable information (PII) 396 * (e.g. for account name). 397 * 398 * <p>On multiple instances of providing the same issue to be represented in Safety Center, 399 * provide the same issue type id across all instances. 400 */ 401 @NonNull getIssueTypeId()402 public String getIssueTypeId() { 403 return mIssueTypeId; 404 } 405 406 /** 407 * Returns the optional custom {@link Notification} for this issue which overrides the title, 408 * text and actions for any {@link android.app.Notification} generated for this {@link 409 * SafetySourceIssue}. 410 * 411 * <p>Safety Center may still generate a default notification from the other details of this 412 * issue when no custom notification has been set. See {@link #getNotificationBehavior()} for 413 * details 414 * 415 * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification 416 * @see #getNotificationBehavior() 417 */ 418 @Nullable 419 @RequiresApi(UPSIDE_DOWN_CAKE) getCustomNotification()420 public Notification getCustomNotification() { 421 if (!SdkLevel.isAtLeastU()) { 422 throw new UnsupportedOperationException( 423 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 424 } 425 return mCustomNotification; 426 } 427 428 /** 429 * Returns the {@link NotificationBehavior} for this issue which determines if and when Safety 430 * Center will post a notification for this issue. 431 * 432 * <p>Any notification will be based on the {@link #getCustomNotification()} if set, or the 433 * other properties of this issue otherwise. 434 * 435 * <ul> 436 * <li>If {@link #NOTIFICATION_BEHAVIOR_IMMEDIATELY} then Safety Center will immediately 437 * create and post a notification 438 * <li>If {@link #NOTIFICATION_BEHAVIOR_DELAYED} then a notification will only be posted after 439 * a delay, if this issue has not been resolved. 440 * <li>If {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED} then a notification may or may not be 441 * posted, the exact behavior is defined by Safety Center. 442 * <li>If {@link #NOTIFICATION_BEHAVIOR_NEVER} Safety Center will never post a notification 443 * about this issue. Sources should specify this behavior when they wish to handle their 444 * own notifications. When this behavior is set sources should not set a custom 445 * notification. 446 * </ul> 447 * 448 * @see Builder#setNotificationBehavior(int) 449 */ 450 @NotificationBehavior 451 @RequiresApi(UPSIDE_DOWN_CAKE) getNotificationBehavior()452 public int getNotificationBehavior() { 453 if (!SdkLevel.isAtLeastU()) { 454 throw new UnsupportedOperationException( 455 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 456 } 457 return mNotificationBehavior; 458 } 459 460 /** 461 * Returns the identifier used to deduplicate this issue against other issues with the same 462 * deduplication identifiers. 463 * 464 * <p>Deduplication identifier will be used to identify duplicate issues. This identifier 465 * applies across all safety sources which are part of the same deduplication group. 466 * Deduplication groups can be set, for each source, in the SafetyCenter config. Therefore, two 467 * issues are considered duplicate if their sources are part of the same deduplication group and 468 * they have the same deduplication identifier. 469 * 470 * <p>Out of all issues that are found to be duplicates, only one will be shown in the UI (the 471 * one with the highest severity, or in case of same severities, the one placed highest in the 472 * config). 473 * 474 * <p>Expected usage implies different sources will coordinate to set the same deduplication 475 * identifiers on issues that they want to deduplicate. 476 * 477 * <p>This shouldn't be a default mechanism for deduplication of issues. Most of the time 478 * sources should coordinate or communicate to only send the issue from one of them. That would 479 * also allow sources to choose which one will be displaying the issue, instead of depending on 480 * severity and config order. This API should only be needed if for some reason this isn't 481 * possible, for example, when sources can't communicate with each other and/or send issues at 482 * different times and/or issues can be of different severities. 483 */ 484 @Nullable 485 @RequiresApi(UPSIDE_DOWN_CAKE) getDeduplicationId()486 public String getDeduplicationId() { 487 if (!SdkLevel.isAtLeastU()) { 488 throw new UnsupportedOperationException( 489 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 490 } 491 return mDeduplicationId; 492 } 493 494 /** 495 * Returns the {@link IssueActionability} for this issue which determines what type of action is 496 * required from the user: 497 * 498 * <ul> 499 * <li>If {@link #ISSUE_ACTIONABILITY_MANUAL} then user input is required to resolve the issue 500 * <li>If {@link #ISSUE_ACTIONABILITY_TIP} then the user needs to review this issue as a tip 501 * to improve their overall safety, and possibly acknowledge it 502 * <li>If {@link #ISSUE_ACTIONABILITY_AUTOMATIC} then the user needs to review this issue as 503 * something that has been resolved on their behalf, and possibly acknowledge it 504 * </ul> 505 * 506 * @see Builder#setIssueActionability(int) 507 */ 508 @IssueActionability 509 @RequiresApi(UPSIDE_DOWN_CAKE) getIssueActionability()510 public int getIssueActionability() { 511 if (!SdkLevel.isAtLeastU()) { 512 throw new UnsupportedOperationException( 513 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 514 } 515 return mIssueActionability; 516 } 517 518 @Override describeContents()519 public int describeContents() { 520 return 0; 521 } 522 523 @Override writeToParcel(@onNull Parcel dest, int flags)524 public void writeToParcel(@NonNull Parcel dest, int flags) { 525 dest.writeString(mId); 526 TextUtils.writeToParcel(mTitle, dest, flags); 527 TextUtils.writeToParcel(mSubtitle, dest, flags); 528 TextUtils.writeToParcel(mSummary, dest, flags); 529 dest.writeInt(mSeverityLevel); 530 dest.writeInt(mIssueCategory); 531 dest.writeTypedList(mActions); 532 dest.writeTypedObject(mOnDismissPendingIntent, flags); 533 dest.writeString(mIssueTypeId); 534 if (SdkLevel.isAtLeastU()) { 535 dest.writeTypedObject(mCustomNotification, flags); 536 dest.writeInt(mNotificationBehavior); 537 TextUtils.writeToParcel(mAttributionTitle, dest, flags); 538 dest.writeString(mDeduplicationId); 539 dest.writeInt(mIssueActionability); 540 } 541 } 542 543 @Override equals(Object o)544 public boolean equals(Object o) { 545 if (this == o) return true; 546 if (!(o instanceof SafetySourceIssue)) return false; 547 SafetySourceIssue that = (SafetySourceIssue) o; 548 return mSeverityLevel == that.mSeverityLevel 549 && TextUtils.equals(mId, that.mId) 550 && TextUtils.equals(mTitle, that.mTitle) 551 && TextUtils.equals(mSubtitle, that.mSubtitle) 552 && TextUtils.equals(mSummary, that.mSummary) 553 && mIssueCategory == that.mIssueCategory 554 && mActions.equals(that.mActions) 555 && Objects.equals(mOnDismissPendingIntent, that.mOnDismissPendingIntent) 556 && TextUtils.equals(mIssueTypeId, that.mIssueTypeId) 557 && Objects.equals(mCustomNotification, that.mCustomNotification) 558 && mNotificationBehavior == that.mNotificationBehavior 559 && TextUtils.equals(mAttributionTitle, that.mAttributionTitle) 560 && TextUtils.equals(mDeduplicationId, that.mDeduplicationId) 561 && mIssueActionability == that.mIssueActionability; 562 } 563 564 @Override hashCode()565 public int hashCode() { 566 return Objects.hash( 567 mId, 568 mTitle, 569 mSubtitle, 570 mSummary, 571 mSeverityLevel, 572 mIssueCategory, 573 mActions, 574 mOnDismissPendingIntent, 575 mIssueTypeId, 576 mCustomNotification, 577 mNotificationBehavior, 578 mAttributionTitle, 579 mDeduplicationId, 580 mIssueActionability); 581 } 582 583 @Override toString()584 public String toString() { 585 return "SafetySourceIssue{" 586 + "mId=" 587 + mId 588 + "mTitle=" 589 + mTitle 590 + ", mSubtitle=" 591 + mSubtitle 592 + ", mSummary=" 593 + mSummary 594 + ", mSeverityLevel=" 595 + mSeverityLevel 596 + ", mIssueCategory=" 597 + mIssueCategory 598 + ", mActions=" 599 + mActions 600 + ", mOnDismissPendingIntent=" 601 + mOnDismissPendingIntent 602 + ", mIssueTypeId=" 603 + mIssueTypeId 604 + ", mCustomNotification=" 605 + mCustomNotification 606 + ", mNotificationBehavior=" 607 + mNotificationBehavior 608 + ", mAttributionTitle=" 609 + mAttributionTitle 610 + ", mDeduplicationId=" 611 + mDeduplicationId 612 + ", mIssueActionability=" 613 + mIssueActionability 614 + '}'; 615 } 616 617 /** 618 * Data for an action supported from a safety issue {@link SafetySourceIssue} in the Safety 619 * Center page. 620 * 621 * <p>The purpose of the action is to allow the user to address the safety issue, either by 622 * performing a fix suggested in the issue, or by navigating the user to the source of the issue 623 * where they can be exposed to detail about the issue and further suggestions to resolve it. 624 * 625 * <p>The user will be allowed to invoke the action from the UI by clicking on a UI element and 626 * consequently resolve the issue. 627 */ 628 public static final class Action implements Parcelable { 629 630 @NonNull 631 public static final Creator<Action> CREATOR = 632 new Creator<Action>() { 633 @Override 634 public Action createFromParcel(Parcel in) { 635 String id = in.readString(); 636 CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 637 PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR); 638 Builder builder = 639 new Builder(id, label, pendingIntent) 640 .setWillResolve(in.readBoolean()) 641 .setSuccessMessage( 642 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel( 643 in)); 644 if (SdkLevel.isAtLeastU()) { 645 ConfirmationDialogDetails confirmationDialogDetails = 646 in.readTypedObject(ConfirmationDialogDetails.CREATOR); 647 builder.setConfirmationDialogDetails(confirmationDialogDetails); 648 } 649 return builder.build(); 650 } 651 652 @Override 653 public Action[] newArray(int size) { 654 return new Action[size]; 655 } 656 }; 657 enforceUniqueActionIds( @onNull List<SafetySourceIssue.Action> actions, @NonNull String message)658 private static void enforceUniqueActionIds( 659 @NonNull List<SafetySourceIssue.Action> actions, @NonNull String message) { 660 Set<String> actionIds = new HashSet<>(); 661 for (int i = 0; i < actions.size(); i++) { 662 SafetySourceIssue.Action action = actions.get(i); 663 664 String actionId = action.getId(); 665 checkArgument(!actionIds.contains(actionId), message); 666 actionIds.add(actionId); 667 } 668 } 669 670 @NonNull private final String mId; 671 @NonNull private final CharSequence mLabel; 672 @NonNull private final PendingIntent mPendingIntent; 673 private final boolean mWillResolve; 674 @Nullable private final CharSequence mSuccessMessage; 675 @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails; 676 Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, @Nullable CharSequence successMessage, @Nullable ConfirmationDialogDetails confirmationDialogDetails)677 private Action( 678 @NonNull String id, 679 @NonNull CharSequence label, 680 @NonNull PendingIntent pendingIntent, 681 boolean willResolve, 682 @Nullable CharSequence successMessage, 683 @Nullable ConfirmationDialogDetails confirmationDialogDetails) { 684 mId = id; 685 mLabel = label; 686 mPendingIntent = pendingIntent; 687 mWillResolve = willResolve; 688 mSuccessMessage = successMessage; 689 mConfirmationDialogDetails = confirmationDialogDetails; 690 } 691 692 /** 693 * Returns the ID of the action, unique among actions in a given {@link SafetySourceIssue}. 694 */ 695 @NonNull getId()696 public String getId() { 697 return mId; 698 } 699 700 /** 701 * Returns the localized label of the action to be displayed in the UI. 702 * 703 * <p>The label should indicate what action will be performed if when invoked. 704 */ 705 @NonNull getLabel()706 public CharSequence getLabel() { 707 return mLabel; 708 } 709 710 /** 711 * Returns a {@link PendingIntent} to be fired when the action is clicked on. 712 * 713 * <p>The {@link PendingIntent} should perform the action referred to by {@link 714 * #getLabel()}. 715 */ 716 @NonNull getPendingIntent()717 public PendingIntent getPendingIntent() { 718 return mPendingIntent; 719 } 720 721 /** 722 * Returns whether invoking this action will fix or address the issue sufficiently for it to 723 * be considered resolved i.e. the issue will no longer need to be conveyed to the user in 724 * the UI. 725 */ willResolve()726 public boolean willResolve() { 727 return mWillResolve; 728 } 729 730 /** 731 * Returns the optional localized message to be displayed in the UI when the action is 732 * invoked and completes successfully. 733 */ 734 @Nullable getSuccessMessage()735 public CharSequence getSuccessMessage() { 736 return mSuccessMessage; 737 } 738 739 /** 740 * Returns the optional data to be displayed in the confirmation dialog prior to launching 741 * the {@link PendingIntent} when the action is clicked on. 742 */ 743 @Nullable 744 @RequiresApi(UPSIDE_DOWN_CAKE) getConfirmationDialogDetails()745 public ConfirmationDialogDetails getConfirmationDialogDetails() { 746 if (!SdkLevel.isAtLeastU()) { 747 throw new UnsupportedOperationException( 748 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 749 } 750 return mConfirmationDialogDetails; 751 } 752 753 @Override describeContents()754 public int describeContents() { 755 return 0; 756 } 757 758 @Override writeToParcel(@onNull Parcel dest, int flags)759 public void writeToParcel(@NonNull Parcel dest, int flags) { 760 dest.writeString(mId); 761 TextUtils.writeToParcel(mLabel, dest, flags); 762 dest.writeTypedObject(mPendingIntent, flags); 763 dest.writeBoolean(mWillResolve); 764 TextUtils.writeToParcel(mSuccessMessage, dest, flags); 765 if (SdkLevel.isAtLeastU()) { 766 dest.writeTypedObject(mConfirmationDialogDetails, flags); 767 } 768 } 769 770 @Override equals(Object o)771 public boolean equals(Object o) { 772 if (this == o) return true; 773 if (!(o instanceof Action)) return false; 774 Action that = (Action) o; 775 return mId.equals(that.mId) 776 && TextUtils.equals(mLabel, that.mLabel) 777 && mPendingIntent.equals(that.mPendingIntent) 778 && mWillResolve == that.mWillResolve 779 && TextUtils.equals(mSuccessMessage, that.mSuccessMessage) 780 && Objects.equals(mConfirmationDialogDetails, that.mConfirmationDialogDetails); 781 } 782 783 @Override hashCode()784 public int hashCode() { 785 return Objects.hash( 786 mId, 787 mLabel, 788 mPendingIntent, 789 mWillResolve, 790 mSuccessMessage, 791 mConfirmationDialogDetails); 792 } 793 794 @Override toString()795 public String toString() { 796 return "Action{" 797 + "mId=" 798 + mId 799 + ", mLabel=" 800 + mLabel 801 + ", mPendingIntent=" 802 + mPendingIntent 803 + ", mWillResolve=" 804 + mWillResolve 805 + ", mSuccessMessage=" 806 + mSuccessMessage 807 + ", mConfirmationDialogDetails=" 808 + mConfirmationDialogDetails 809 + '}'; 810 } 811 812 /** Data for an action confirmation dialog to be shown before action is executed. */ 813 @RequiresApi(UPSIDE_DOWN_CAKE) 814 public static final class ConfirmationDialogDetails implements Parcelable { 815 816 @NonNull 817 public static final Creator<ConfirmationDialogDetails> CREATOR = 818 new Creator<ConfirmationDialogDetails>() { 819 @Override 820 public ConfirmationDialogDetails createFromParcel(Parcel in) { 821 CharSequence title = 822 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 823 CharSequence text = 824 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 825 CharSequence acceptButtonText = 826 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 827 CharSequence denyButtonText = 828 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 829 return new ConfirmationDialogDetails( 830 title, text, acceptButtonText, denyButtonText); 831 } 832 833 @Override 834 public ConfirmationDialogDetails[] newArray(int size) { 835 return new ConfirmationDialogDetails[size]; 836 } 837 }; 838 839 @NonNull private final CharSequence mTitle; 840 @NonNull private final CharSequence mText; 841 @NonNull private final CharSequence mAcceptButtonText; 842 @NonNull private final CharSequence mDenyButtonText; 843 ConfirmationDialogDetails( @onNull CharSequence title, @NonNull CharSequence text, @NonNull CharSequence acceptButtonText, @NonNull CharSequence denyButtonText)844 public ConfirmationDialogDetails( 845 @NonNull CharSequence title, 846 @NonNull CharSequence text, 847 @NonNull CharSequence acceptButtonText, 848 @NonNull CharSequence denyButtonText) { 849 mTitle = requireNonNull(title); 850 mText = requireNonNull(text); 851 mAcceptButtonText = requireNonNull(acceptButtonText); 852 mDenyButtonText = requireNonNull(denyButtonText); 853 } 854 855 /** Returns the title of action confirmation dialog. */ 856 @NonNull getTitle()857 public CharSequence getTitle() { 858 return mTitle; 859 } 860 861 /** Returns the text of action confirmation dialog. */ 862 @NonNull getText()863 public CharSequence getText() { 864 return mText; 865 } 866 867 /** Returns the text of the button to accept action execution. */ 868 @NonNull getAcceptButtonText()869 public CharSequence getAcceptButtonText() { 870 return mAcceptButtonText; 871 } 872 873 /** Returns the text of the button to deny action execution. */ 874 @NonNull getDenyButtonText()875 public CharSequence getDenyButtonText() { 876 return mDenyButtonText; 877 } 878 879 @Override describeContents()880 public int describeContents() { 881 return 0; 882 } 883 884 @Override writeToParcel(@onNull Parcel dest, int flags)885 public void writeToParcel(@NonNull Parcel dest, int flags) { 886 TextUtils.writeToParcel(mTitle, dest, flags); 887 TextUtils.writeToParcel(mText, dest, flags); 888 TextUtils.writeToParcel(mAcceptButtonText, dest, flags); 889 TextUtils.writeToParcel(mDenyButtonText, dest, flags); 890 } 891 892 @Override equals(Object o)893 public boolean equals(Object o) { 894 if (this == o) return true; 895 if (!(o instanceof ConfirmationDialogDetails)) return false; 896 ConfirmationDialogDetails that = (ConfirmationDialogDetails) o; 897 return TextUtils.equals(mTitle, that.mTitle) 898 && TextUtils.equals(mText, that.mText) 899 && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText) 900 && TextUtils.equals(mDenyButtonText, that.mDenyButtonText); 901 } 902 903 @Override hashCode()904 public int hashCode() { 905 return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText); 906 } 907 908 @Override toString()909 public String toString() { 910 return "ConfirmationDialogDetails{" 911 + "mTitle=" 912 + mTitle 913 + ", mText=" 914 + mText 915 + ", mAcceptButtonText=" 916 + mAcceptButtonText 917 + ", mDenyButtonText=" 918 + mDenyButtonText 919 + '}'; 920 } 921 } 922 923 /** Builder class for {@link Action}. */ 924 public static final class Builder { 925 926 @NonNull private final String mId; 927 @NonNull private final CharSequence mLabel; 928 @NonNull private final PendingIntent mPendingIntent; 929 private boolean mWillResolve = false; 930 @Nullable private CharSequence mSuccessMessage; 931 @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails; 932 933 /** Creates a {@link Builder} for an {@link Action}. */ Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)934 public Builder( 935 @NonNull String id, 936 @NonNull CharSequence label, 937 @NonNull PendingIntent pendingIntent) { 938 mId = requireNonNull(id); 939 mLabel = requireNonNull(label); 940 mPendingIntent = requireNonNull(pendingIntent); 941 } 942 943 /** Creates a {@link Builder} with the values from the given {@link Action}. */ 944 @RequiresApi(UPSIDE_DOWN_CAKE) Builder(@onNull Action action)945 public Builder(@NonNull Action action) { 946 if (!SdkLevel.isAtLeastU()) { 947 throw new UnsupportedOperationException( 948 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 949 } 950 requireNonNull(action); 951 mId = action.mId; 952 mLabel = action.mLabel; 953 mPendingIntent = action.mPendingIntent; 954 mWillResolve = action.mWillResolve; 955 mSuccessMessage = action.mSuccessMessage; 956 mConfirmationDialogDetails = action.mConfirmationDialogDetails; 957 } 958 959 // TODO(b/303443020): Add setters for id, label, and pendingIntent 960 961 /** 962 * Sets whether the action will resolve the safety issue. Defaults to {@code false}. 963 * 964 * <p>Note: It is not allowed for resolvable actions to have a {@link PendingIntent} 965 * that launches activity. When extra confirmation is needed consider using {@link 966 * Builder#setConfirmationDialogDetails}. 967 * 968 * @see #willResolve() 969 */ 970 @SuppressLint("MissingGetterMatchingBuilder") 971 @NonNull setWillResolve(boolean willResolve)972 public Builder setWillResolve(boolean willResolve) { 973 mWillResolve = willResolve; 974 return this; 975 } 976 977 /** 978 * Sets the optional localized message to be displayed in the UI when the action is 979 * invoked and completes successfully. 980 */ 981 @NonNull setSuccessMessage(@ullable CharSequence successMessage)982 public Builder setSuccessMessage(@Nullable CharSequence successMessage) { 983 mSuccessMessage = successMessage; 984 return this; 985 } 986 987 /** 988 * Sets the optional data to be displayed in the confirmation dialog prior to launching 989 * the {@link PendingIntent} when the action is clicked on. 990 */ 991 @NonNull 992 @RequiresApi(UPSIDE_DOWN_CAKE) setConfirmationDialogDetails( @ullable ConfirmationDialogDetails confirmationDialogDetails)993 public Builder setConfirmationDialogDetails( 994 @Nullable ConfirmationDialogDetails confirmationDialogDetails) { 995 if (!SdkLevel.isAtLeastU()) { 996 throw new UnsupportedOperationException( 997 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 998 } 999 mConfirmationDialogDetails = confirmationDialogDetails; 1000 return this; 1001 } 1002 1003 /** Creates the {@link Action} defined by this {@link Builder}. */ 1004 @NonNull build()1005 public Action build() { 1006 if (SdkLevel.isAtLeastU()) { 1007 boolean willResolveWithActivity = mWillResolve && mPendingIntent.isActivity(); 1008 checkArgument( 1009 !willResolveWithActivity, 1010 "Launching activity from Action that should resolve the" 1011 + " SafetySourceIssue is not allowed. Consider using setting a" 1012 + " Confirmation if needed, and either set the willResolve to" 1013 + " false or make PendingIntent to start a service/send a" 1014 + " broadcast."); 1015 } 1016 return new Action( 1017 mId, 1018 mLabel, 1019 mPendingIntent, 1020 mWillResolve, 1021 mSuccessMessage, 1022 mConfirmationDialogDetails); 1023 } 1024 } 1025 } 1026 1027 /** 1028 * Data for Safety Center to use when constructing a system {@link android.app.Notification} 1029 * about a related {@link SafetySourceIssue}. 1030 * 1031 * <p>Safety Center can construct a default notification for any issue, but sources may use 1032 * {@link Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification)} if 1033 * they want to override the title, text or actions. 1034 * 1035 * @see #getCustomNotification() 1036 * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification) 1037 * @see #getNotificationBehavior() 1038 */ 1039 @RequiresApi(UPSIDE_DOWN_CAKE) 1040 public static final class Notification implements Parcelable { 1041 1042 @NonNull 1043 public static final Creator<Notification> CREATOR = 1044 new Creator<Notification>() { 1045 @Override 1046 public Notification createFromParcel(Parcel in) { 1047 return new Builder( 1048 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in), 1049 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)) 1050 .addActions(in.createTypedArrayList(Action.CREATOR)) 1051 .build(); 1052 } 1053 1054 @Override 1055 public Notification[] newArray(int size) { 1056 return new Notification[size]; 1057 } 1058 }; 1059 1060 @NonNull private final CharSequence mTitle; 1061 @NonNull private final CharSequence mText; 1062 @NonNull private final List<Action> mActions; 1063 Notification( @onNull CharSequence title, @NonNull CharSequence text, @NonNull List<Action> actions)1064 private Notification( 1065 @NonNull CharSequence title, 1066 @NonNull CharSequence text, 1067 @NonNull List<Action> actions) { 1068 mTitle = title; 1069 mText = text; 1070 mActions = actions; 1071 } 1072 1073 /** 1074 * Custom title which will be used instead of {@link SafetySourceIssue#getTitle()} when 1075 * building a {@link android.app.Notification} for this issue. 1076 */ 1077 @NonNull getTitle()1078 public CharSequence getTitle() { 1079 return mTitle; 1080 } 1081 1082 /** 1083 * Custom text which will be used instead of {@link SafetySourceIssue#getSummary()} when 1084 * building a {@link android.app.Notification} for this issue. 1085 */ 1086 @NonNull getText()1087 public CharSequence getText() { 1088 return mText; 1089 } 1090 1091 /** 1092 * Custom list of {@link Action} instances which will be used instead of {@link 1093 * SafetySourceIssue#getActions()} when building a {@link android.app.Notification} for this 1094 * issue. 1095 * 1096 * <p>If this list is empty then the resulting {@link android.app.Notification} will have 1097 * zero action buttons. 1098 */ 1099 @NonNull getActions()1100 public List<Action> getActions() { 1101 return mActions; 1102 } 1103 1104 @Override describeContents()1105 public int describeContents() { 1106 return 0; 1107 } 1108 1109 @Override writeToParcel(@onNull Parcel dest, int flags)1110 public void writeToParcel(@NonNull Parcel dest, int flags) { 1111 TextUtils.writeToParcel(mTitle, dest, flags); 1112 TextUtils.writeToParcel(mText, dest, flags); 1113 dest.writeTypedList(mActions); 1114 } 1115 1116 @Override equals(Object o)1117 public boolean equals(Object o) { 1118 if (this == o) return true; 1119 if (!(o instanceof Notification)) return false; 1120 Notification that = (Notification) o; 1121 return TextUtils.equals(mTitle, that.mTitle) 1122 && TextUtils.equals(mText, that.mText) 1123 && mActions.equals(that.mActions); 1124 } 1125 1126 @Override hashCode()1127 public int hashCode() { 1128 return Objects.hash(mTitle, mText, mActions); 1129 } 1130 1131 @Override toString()1132 public String toString() { 1133 return "Notification{" 1134 + "mTitle=" 1135 + mTitle 1136 + ", mText=" 1137 + mText 1138 + ", mActions=" 1139 + mActions 1140 + '}'; 1141 } 1142 1143 /** Builder for {@link SafetySourceIssue.Notification}. */ 1144 public static final class Builder { 1145 1146 @NonNull private final CharSequence mTitle; 1147 @NonNull private final CharSequence mText; 1148 @NonNull private final List<Action> mActions = new ArrayList<>(); 1149 Builder(@onNull CharSequence title, @NonNull CharSequence text)1150 public Builder(@NonNull CharSequence title, @NonNull CharSequence text) { 1151 mTitle = requireNonNull(title); 1152 mText = requireNonNull(text); 1153 } 1154 1155 /** Creates a {@link Builder} with the values from the given {@link Notification}. */ Builder(@onNull Notification notification)1156 public Builder(@NonNull Notification notification) { 1157 requireNonNull(notification); 1158 mTitle = notification.mTitle; 1159 mText = notification.mText; 1160 mActions.addAll(notification.mActions); 1161 } 1162 1163 /** Adds an {@link Action} to the custom {@link Notification}. */ 1164 @NonNull addAction(@onNull Action action)1165 public Builder addAction(@NonNull Action action) { 1166 mActions.add(requireNonNull(action)); 1167 return this; 1168 } 1169 1170 /** Adds several {@link Action}s to the custom {@link Notification}. */ 1171 @NonNull addActions(@onNull List<Action> actions)1172 public Builder addActions(@NonNull List<Action> actions) { 1173 mActions.addAll(requireNonNull(actions)); 1174 return this; 1175 } 1176 1177 /** Clears all the {@link Action}s that were added so far. */ 1178 @NonNull clearActions()1179 public Builder clearActions() { 1180 mActions.clear(); 1181 return this; 1182 } 1183 1184 /** Builds a {@link Notification} instance. */ 1185 @NonNull build()1186 public Notification build() { 1187 List<Action> actions = unmodifiableList(new ArrayList<>(mActions)); 1188 Action.enforceUniqueActionIds( 1189 actions, "Custom notification cannot have duplicate action ids"); 1190 checkArgument( 1191 actions.size() <= 2, 1192 "Custom notification must not contain more than 2 actions"); 1193 return new Notification(mTitle, mText, actions); 1194 } 1195 } 1196 } 1197 1198 /** Builder class for {@link SafetySourceIssue}. */ 1199 public static final class Builder { 1200 1201 @NonNull private final String mId; 1202 @NonNull private final CharSequence mTitle; 1203 @NonNull private final CharSequence mSummary; 1204 @SafetySourceData.SeverityLevel private final int mSeverityLevel; 1205 @NonNull private final String mIssueTypeId; 1206 private final List<Action> mActions = new ArrayList<>(); 1207 1208 @Nullable private CharSequence mSubtitle; 1209 @IssueCategory private int mIssueCategory = ISSUE_CATEGORY_GENERAL; 1210 @Nullable private PendingIntent mOnDismissPendingIntent; 1211 @Nullable private CharSequence mAttributionTitle; 1212 @Nullable private String mDeduplicationId; 1213 1214 @Nullable private Notification mCustomNotification = null; 1215 1216 @SuppressLint("NewApi") 1217 @NotificationBehavior 1218 private int mNotificationBehavior = NOTIFICATION_BEHAVIOR_UNSPECIFIED; 1219 1220 @SuppressLint("NewApi") 1221 @IssueActionability 1222 private int mIssueActionability = ISSUE_ACTIONABILITY_MANUAL; 1223 1224 /** 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)1225 public Builder( 1226 @NonNull String id, 1227 @NonNull CharSequence title, 1228 @NonNull CharSequence summary, 1229 @SafetySourceData.SeverityLevel int severityLevel, 1230 @NonNull String issueTypeId) { 1231 this.mId = requireNonNull(id); 1232 this.mTitle = requireNonNull(title); 1233 this.mSummary = requireNonNull(summary); 1234 this.mSeverityLevel = validateSeverityLevel(severityLevel); 1235 this.mIssueTypeId = requireNonNull(issueTypeId); 1236 } 1237 1238 /** Creates a {@link Builder} with the values from the given {@link SafetySourceIssue}. */ 1239 @RequiresApi(UPSIDE_DOWN_CAKE) Builder(@onNull SafetySourceIssue safetySourceIssue)1240 public Builder(@NonNull SafetySourceIssue safetySourceIssue) { 1241 if (!SdkLevel.isAtLeastU()) { 1242 throw new UnsupportedOperationException( 1243 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 1244 } 1245 requireNonNull(safetySourceIssue); 1246 mId = safetySourceIssue.mId; 1247 mTitle = safetySourceIssue.mTitle; 1248 mSummary = safetySourceIssue.mSummary; 1249 mSeverityLevel = safetySourceIssue.mSeverityLevel; 1250 mIssueTypeId = safetySourceIssue.mIssueTypeId; 1251 mActions.addAll(safetySourceIssue.mActions); 1252 mSubtitle = safetySourceIssue.mSubtitle; 1253 mIssueCategory = safetySourceIssue.mIssueCategory; 1254 mOnDismissPendingIntent = safetySourceIssue.mOnDismissPendingIntent; 1255 mAttributionTitle = safetySourceIssue.mAttributionTitle; 1256 mDeduplicationId = safetySourceIssue.mDeduplicationId; 1257 mCustomNotification = safetySourceIssue.mCustomNotification; 1258 mNotificationBehavior = safetySourceIssue.mNotificationBehavior; 1259 mIssueActionability = safetySourceIssue.mIssueActionability; 1260 } 1261 1262 /** Sets the localized subtitle. */ 1263 @NonNull setSubtitle(@ullable CharSequence subtitle)1264 public Builder setSubtitle(@Nullable CharSequence subtitle) { 1265 mSubtitle = subtitle; 1266 return this; 1267 } 1268 1269 /** 1270 * Sets or clears the optional attribution title for this issue. 1271 * 1272 * <p>This is displayed in the UI and helps to attribute an issue to a particular source. If 1273 * this value is {@code null}, the title of the group that contains the Safety Source will 1274 * be used. 1275 */ 1276 @NonNull 1277 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) setAttributionTitle(@ullable CharSequence attributionTitle)1278 public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) { 1279 if (!SdkLevel.isAtLeastU()) { 1280 throw new UnsupportedOperationException( 1281 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 1282 } 1283 mAttributionTitle = attributionTitle; 1284 return this; 1285 } 1286 1287 /** 1288 * Sets the category of the risk associated with the issue. 1289 * 1290 * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. 1291 */ 1292 @NonNull setIssueCategory(@ssueCategory int issueCategory)1293 public Builder setIssueCategory(@IssueCategory int issueCategory) { 1294 mIssueCategory = validateIssueCategory(issueCategory); 1295 return this; 1296 } 1297 1298 /** Adds data for an {@link Action} to be shown in UI. */ 1299 @NonNull addAction(@onNull Action actionData)1300 public Builder addAction(@NonNull Action actionData) { 1301 mActions.add(requireNonNull(actionData)); 1302 return this; 1303 } 1304 1305 /** Clears data for all the {@link Action}s that were added to this {@link Builder}. */ 1306 @NonNull clearActions()1307 public Builder clearActions() { 1308 mActions.clear(); 1309 return this; 1310 } 1311 1312 /** 1313 * Sets an optional {@link PendingIntent} to be invoked when an issue is dismissed from the 1314 * UI. 1315 * 1316 * <p>In particular, if the source would like to be notified of issue dismissals in Safety 1317 * Center in order to be able to dismiss or ignore issues at the source, then set this 1318 * field. The action contained in the {@link PendingIntent} must not start an activity. 1319 * 1320 * @see #getOnDismissPendingIntent() 1321 */ 1322 @NonNull setOnDismissPendingIntent(@ullable PendingIntent onDismissPendingIntent)1323 public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) { 1324 checkArgument( 1325 onDismissPendingIntent == null || !onDismissPendingIntent.isActivity(), 1326 "Safety source issue on dismiss pending intent must not start an activity"); 1327 mOnDismissPendingIntent = onDismissPendingIntent; 1328 return this; 1329 } 1330 1331 /** 1332 * Sets a custom {@link Notification} for this issue. 1333 * 1334 * <p>Using a custom {@link Notification} a source may specify a different {@link 1335 * Notification#getTitle()}, {@link Notification#getText()} and {@link 1336 * Notification#getActions()} for Safety Center to use when constructing a notification for 1337 * this issue. 1338 * 1339 * <p>Safety Center may still generate a default notification from the other details of this 1340 * issue when no custom notification has been set, depending on the issue's {@link 1341 * #getNotificationBehavior()}. 1342 * 1343 * @see #getCustomNotification() 1344 * @see #setNotificationBehavior(int) 1345 */ 1346 @NonNull 1347 @RequiresApi(UPSIDE_DOWN_CAKE) setCustomNotification(@ullable Notification customNotification)1348 public Builder setCustomNotification(@Nullable Notification customNotification) { 1349 if (!SdkLevel.isAtLeastU()) { 1350 throw new UnsupportedOperationException( 1351 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 1352 } 1353 mCustomNotification = customNotification; 1354 return this; 1355 } 1356 1357 /** 1358 * Sets the notification behavior of the issue. 1359 * 1360 * <p>Must be one of {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED}, {@link 1361 * #NOTIFICATION_BEHAVIOR_NEVER}, {@link #NOTIFICATION_BEHAVIOR_DELAYED} or {@link 1362 * #NOTIFICATION_BEHAVIOR_IMMEDIATELY}. See {@link #getNotificationBehavior()} for details 1363 * of how Safety Center will interpret each of these. 1364 * 1365 * @see #getNotificationBehavior() 1366 */ 1367 @NonNull 1368 @RequiresApi(UPSIDE_DOWN_CAKE) setNotificationBehavior(@otificationBehavior int notificationBehavior)1369 public Builder setNotificationBehavior(@NotificationBehavior int notificationBehavior) { 1370 if (!SdkLevel.isAtLeastU()) { 1371 throw new UnsupportedOperationException( 1372 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 1373 } 1374 mNotificationBehavior = validateNotificationBehavior(notificationBehavior); 1375 return this; 1376 } 1377 1378 /** 1379 * Sets the deduplication identifier for the issue. 1380 * 1381 * @see #getDeduplicationId() 1382 */ 1383 @NonNull 1384 @RequiresApi(UPSIDE_DOWN_CAKE) setDeduplicationId(@ullable String deduplicationId)1385 public Builder setDeduplicationId(@Nullable String deduplicationId) { 1386 if (!SdkLevel.isAtLeastU()) { 1387 throw new UnsupportedOperationException( 1388 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 1389 } 1390 mDeduplicationId = deduplicationId; 1391 return this; 1392 } 1393 1394 /** 1395 * Sets the issue actionability of the issue. 1396 * 1397 * <p>Must be one of {@link #ISSUE_ACTIONABILITY_MANUAL} (default), {@link 1398 * #ISSUE_ACTIONABILITY_TIP}, {@link #ISSUE_ACTIONABILITY_AUTOMATIC}. 1399 * 1400 * @see #getIssueActionability() 1401 */ 1402 @NonNull 1403 @RequiresApi(UPSIDE_DOWN_CAKE) setIssueActionability(@ssueActionability int issueActionability)1404 public Builder setIssueActionability(@IssueActionability int issueActionability) { 1405 if (!SdkLevel.isAtLeastU()) { 1406 throw new UnsupportedOperationException( 1407 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 1408 } 1409 mIssueActionability = validateIssueActionability(issueActionability); 1410 return this; 1411 } 1412 1413 /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */ 1414 @NonNull build()1415 public SafetySourceIssue build() { 1416 List<SafetySourceIssue.Action> actions = unmodifiableList(new ArrayList<>(mActions)); 1417 Action.enforceUniqueActionIds( 1418 actions, "Safety source issue cannot have duplicate action ids"); 1419 if (SdkLevel.isAtLeastU()) { 1420 checkArgument( 1421 mIssueActionability != ISSUE_ACTIONABILITY_MANUAL || !actions.isEmpty(), 1422 "Actionable safety source issue must contain at least 1 action"); 1423 } else { 1424 checkArgument( 1425 !actions.isEmpty(), "Safety source issue must contain at least 1 action"); 1426 } 1427 checkArgument( 1428 actions.size() <= 2, 1429 "Safety source issue must not contain more than 2 actions"); 1430 return new SafetySourceIssue( 1431 mId, 1432 mTitle, 1433 mSubtitle, 1434 mSummary, 1435 mSeverityLevel, 1436 mIssueCategory, 1437 actions, 1438 mOnDismissPendingIntent, 1439 mIssueTypeId, 1440 mCustomNotification, 1441 mNotificationBehavior, 1442 mAttributionTitle, 1443 mDeduplicationId, 1444 mIssueActionability); 1445 } 1446 } 1447 1448 @SafetySourceData.SeverityLevel validateSeverityLevel(int value)1449 private static int validateSeverityLevel(int value) { 1450 switch (value) { 1451 case SafetySourceData.SEVERITY_LEVEL_INFORMATION: 1452 case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: 1453 case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: 1454 return value; 1455 case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: 1456 throw new IllegalArgumentException( 1457 "SeverityLevel for SafetySourceIssue must not be " 1458 + "SEVERITY_LEVEL_UNSPECIFIED"); 1459 default: 1460 } 1461 throw new IllegalArgumentException( 1462 "Unexpected SeverityLevel for SafetySourceIssue: " + value); 1463 } 1464 1465 @IssueCategory validateIssueCategory(int value)1466 private static int validateIssueCategory(int value) { 1467 switch (value) { 1468 case ISSUE_CATEGORY_DEVICE: 1469 case ISSUE_CATEGORY_ACCOUNT: 1470 case ISSUE_CATEGORY_GENERAL: 1471 return value; 1472 default: 1473 } 1474 if (SdkLevel.isAtLeastU()) { 1475 switch (value) { 1476 case ISSUE_CATEGORY_DATA: 1477 case ISSUE_CATEGORY_PASSWORDS: 1478 case ISSUE_CATEGORY_PERSONAL_SAFETY: 1479 return value; 1480 default: 1481 } 1482 } 1483 throw new IllegalArgumentException( 1484 "Unexpected IssueCategory for SafetySourceIssue: " + value); 1485 } 1486 1487 @NotificationBehavior validateNotificationBehavior(int value)1488 private static int validateNotificationBehavior(int value) { 1489 switch (value) { 1490 case NOTIFICATION_BEHAVIOR_UNSPECIFIED: 1491 case NOTIFICATION_BEHAVIOR_NEVER: 1492 case NOTIFICATION_BEHAVIOR_DELAYED: 1493 case NOTIFICATION_BEHAVIOR_IMMEDIATELY: 1494 return value; 1495 default: 1496 } 1497 throw new IllegalArgumentException( 1498 "Unexpected NotificationBehavior for SafetySourceIssue: " + value); 1499 } 1500 1501 @IssueActionability validateIssueActionability(int value)1502 private static int validateIssueActionability(int value) { 1503 switch (value) { 1504 case ISSUE_ACTIONABILITY_MANUAL: 1505 case ISSUE_ACTIONABILITY_TIP: 1506 case ISSUE_ACTIONABILITY_AUTOMATIC: 1507 return value; 1508 default: 1509 } 1510 throw new IllegalArgumentException( 1511 "Unexpected IssueActionability for SafetySourceIssue: " + value); 1512 } 1513 } 1514