1 /* 2 * Copyright (C) 2022 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 java.util.Collections.unmodifiableList; 23 import static java.util.Objects.requireNonNull; 24 25 import android.annotation.IntDef; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.annotation.SuppressLint; 29 import android.annotation.SystemApi; 30 import android.app.PendingIntent; 31 import android.os.Parcel; 32 import android.os.Parcelable; 33 import android.safetycenter.config.SafetySourcesGroup; 34 import android.text.TextUtils; 35 36 import androidx.annotation.RequiresApi; 37 38 import com.android.modules.utils.build.SdkLevel; 39 40 import java.lang.annotation.Retention; 41 import java.lang.annotation.RetentionPolicy; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Objects; 45 46 /** 47 * An issue in the Safety Center. 48 * 49 * <p>An issue represents an actionable matter on the device of elevated importance. 50 * 51 * <p>It contains localized messages to display to the user, explaining the underlying threat or 52 * warning and suggested fixes, and contains actions that a user may take from the UI to resolve the 53 * issue. 54 * 55 * <p>Issues are ephemeral and disappear when resolved by user action or dismissal. 56 * 57 * @hide 58 */ 59 @SystemApi 60 @RequiresApi(TIRAMISU) 61 public final class SafetyCenterIssue implements Parcelable { 62 63 /** Indicates that this is low-severity, and informational. */ 64 public static final int ISSUE_SEVERITY_LEVEL_OK = 2100; 65 66 /** Indicates that this issue describes a safety recommendation. */ 67 public static final int ISSUE_SEVERITY_LEVEL_RECOMMENDATION = 2200; 68 69 /** Indicates that this issue describes a critical safety warning. */ 70 public static final int ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING = 2300; 71 72 /** 73 * All possible severity levels for a {@link SafetyCenterIssue}. 74 * 75 * @hide 76 * @see SafetyCenterIssue#getSeverityLevel() 77 * @see Builder#setSeverityLevel(int) 78 */ 79 @Retention(RetentionPolicy.SOURCE) 80 @IntDef( 81 prefix = "ISSUE_SEVERITY_LEVEL_", 82 value = { 83 ISSUE_SEVERITY_LEVEL_OK, 84 ISSUE_SEVERITY_LEVEL_RECOMMENDATION, 85 ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING, 86 }) 87 public @interface IssueSeverityLevel {} 88 89 @NonNull 90 public static final Creator<SafetyCenterIssue> CREATOR = 91 new Creator<SafetyCenterIssue>() { 92 @Override 93 public SafetyCenterIssue createFromParcel(Parcel in) { 94 String id = in.readString(); 95 CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 96 CharSequence subtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 97 CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 98 SafetyCenterIssue.Builder builder = 99 new Builder(id, title, summary) 100 .setSubtitle(subtitle) 101 .setSeverityLevel(in.readInt()) 102 .setDismissible(in.readBoolean()) 103 .setShouldConfirmDismissal(in.readBoolean()) 104 .setActions(in.createTypedArrayList(Action.CREATOR)); 105 if (SdkLevel.isAtLeastU()) { 106 builder.setAttributionTitle( 107 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)); 108 builder.setGroupId(in.readString()); 109 } 110 return builder.build(); 111 } 112 113 @Override 114 public SafetyCenterIssue[] newArray(int size) { 115 return new SafetyCenterIssue[size]; 116 } 117 }; 118 119 @NonNull private final String mId; 120 @NonNull private final CharSequence mTitle; 121 @Nullable private final CharSequence mSubtitle; 122 @NonNull private final CharSequence mSummary; 123 @IssueSeverityLevel private final int mSeverityLevel; 124 private final boolean mDismissible; 125 private final boolean mShouldConfirmDismissal; 126 @NonNull private final List<Action> mActions; 127 @Nullable private final CharSequence mAttributionTitle; 128 @Nullable private final String mGroupId; 129 SafetyCenterIssue( @onNull String id, @NonNull CharSequence title, @Nullable CharSequence subtitle, @NonNull CharSequence summary, @IssueSeverityLevel int severityLevel, boolean isDismissible, boolean shouldConfirmDismissal, @NonNull List<Action> actions, @Nullable CharSequence attributionTitle, @Nullable String groupId)130 private SafetyCenterIssue( 131 @NonNull String id, 132 @NonNull CharSequence title, 133 @Nullable CharSequence subtitle, 134 @NonNull CharSequence summary, 135 @IssueSeverityLevel int severityLevel, 136 boolean isDismissible, 137 boolean shouldConfirmDismissal, 138 @NonNull List<Action> actions, 139 @Nullable CharSequence attributionTitle, 140 @Nullable String groupId) { 141 mId = id; 142 mTitle = title; 143 mSubtitle = subtitle; 144 mSummary = summary; 145 mSeverityLevel = severityLevel; 146 mDismissible = isDismissible; 147 mShouldConfirmDismissal = shouldConfirmDismissal; 148 mActions = actions; 149 mAttributionTitle = attributionTitle; 150 mGroupId = groupId; 151 } 152 153 /** 154 * Returns the encoded string ID which uniquely identifies this issue within the Safety Center 155 * on the device for the current user across all profiles and accounts. 156 */ 157 @NonNull getId()158 public String getId() { 159 return mId; 160 } 161 162 /** Returns the title that describes this issue. */ 163 @NonNull getTitle()164 public CharSequence getTitle() { 165 return mTitle; 166 } 167 168 /** Returns the subtitle of this issue, or {@code null} if it has none. */ 169 @Nullable getSubtitle()170 public CharSequence getSubtitle() { 171 return mSubtitle; 172 } 173 174 /** Returns the summary text that describes this issue. */ 175 @NonNull getSummary()176 public CharSequence getSummary() { 177 return mSummary; 178 } 179 180 /** 181 * Returns the attribution title of this issue, or {@code null} if it has none. 182 * 183 * <p>This is displayed in the UI and helps to attribute issue cards to a particular source. 184 * 185 * @throws UnsupportedOperationException if accessed from a version lower than {@link 186 * UPSIDE_DOWN_CAKE} 187 */ 188 @Nullable 189 @RequiresApi(UPSIDE_DOWN_CAKE) getAttributionTitle()190 public CharSequence getAttributionTitle() { 191 if (!SdkLevel.isAtLeastU()) { 192 throw new UnsupportedOperationException( 193 "Method not supported for versions lower than UPSIDE_DOWN_CAKE"); 194 } 195 return mAttributionTitle; 196 } 197 198 /** Returns the {@link IssueSeverityLevel} of this issue. */ 199 @IssueSeverityLevel getSeverityLevel()200 public int getSeverityLevel() { 201 return mSeverityLevel; 202 } 203 204 /** Returns {@code true} if this issue can be dismissed. */ isDismissible()205 public boolean isDismissible() { 206 return mDismissible; 207 } 208 209 /** Returns {@code true} if this issue should have its dismissal confirmed. */ shouldConfirmDismissal()210 public boolean shouldConfirmDismissal() { 211 return mShouldConfirmDismissal; 212 } 213 214 /** 215 * Returns the ordered list of {@link Action} objects that may be taken to resolve this issue. 216 * 217 * <p>An issue may have 0-2 actions. The first action will be considered the "Primary" action of 218 * the issue. 219 */ 220 @NonNull getActions()221 public List<Action> getActions() { 222 return mActions; 223 } 224 225 /** 226 * Returns the ID of the {@link SafetySourcesGroup} that this issue belongs to, or {@code null} 227 * if it has none. 228 * 229 * <p>This ID is used for displaying the issue on its corresponding subpage in the Safety Center 230 * UI. 231 * 232 * @throws UnsupportedOperationException if accessed from a version lower than {@link 233 * UPSIDE_DOWN_CAKE} 234 */ 235 @Nullable 236 @RequiresApi(UPSIDE_DOWN_CAKE) getGroupId()237 public String getGroupId() { 238 if (!SdkLevel.isAtLeastU()) { 239 throw new UnsupportedOperationException( 240 "Method not supported for versions lower than UPSIDE_DOWN_CAKE"); 241 } 242 return mGroupId; 243 } 244 245 @Override equals(Object o)246 public boolean equals(Object o) { 247 if (this == o) return true; 248 if (!(o instanceof SafetyCenterIssue)) return false; 249 SafetyCenterIssue that = (SafetyCenterIssue) o; 250 return mSeverityLevel == that.mSeverityLevel 251 && mDismissible == that.mDismissible 252 && mShouldConfirmDismissal == that.mShouldConfirmDismissal 253 && Objects.equals(mId, that.mId) 254 && TextUtils.equals(mTitle, that.mTitle) 255 && TextUtils.equals(mSubtitle, that.mSubtitle) 256 && TextUtils.equals(mSummary, that.mSummary) 257 && Objects.equals(mActions, that.mActions) 258 && TextUtils.equals(mAttributionTitle, that.mAttributionTitle) 259 && Objects.equals(mGroupId, that.mGroupId); 260 } 261 262 @Override hashCode()263 public int hashCode() { 264 return Objects.hash( 265 mId, 266 mTitle, 267 mSubtitle, 268 mSummary, 269 mSeverityLevel, 270 mDismissible, 271 mShouldConfirmDismissal, 272 mActions, 273 mAttributionTitle, 274 mGroupId); 275 } 276 277 @Override toString()278 public String toString() { 279 return "SafetyCenterIssue{" 280 + "mId=" 281 + mId 282 + ", mTitle=" 283 + mTitle 284 + ", mSubtitle=" 285 + mSubtitle 286 + ", mSummary=" 287 + mSummary 288 + ", mSeverityLevel=" 289 + mSeverityLevel 290 + ", mDismissible=" 291 + mDismissible 292 + ", mConfirmDismissal=" 293 + mShouldConfirmDismissal 294 + ", mActions=" 295 + mActions 296 + ", mAttributionTitle=" 297 + mAttributionTitle 298 + ", mGroupId=" 299 + mGroupId 300 + '}'; 301 } 302 303 @Override describeContents()304 public int describeContents() { 305 return 0; 306 } 307 308 @Override writeToParcel(@onNull Parcel dest, int flags)309 public void writeToParcel(@NonNull Parcel dest, int flags) { 310 dest.writeString(mId); 311 TextUtils.writeToParcel(mTitle, dest, flags); 312 TextUtils.writeToParcel(mSubtitle, dest, flags); 313 TextUtils.writeToParcel(mSummary, dest, flags); 314 dest.writeInt(mSeverityLevel); 315 dest.writeBoolean(mDismissible); 316 dest.writeBoolean(mShouldConfirmDismissal); 317 dest.writeTypedList(mActions); 318 if (SdkLevel.isAtLeastU()) { 319 TextUtils.writeToParcel(mAttributionTitle, dest, flags); 320 dest.writeString(mGroupId); 321 } 322 } 323 324 /** Builder class for {@link SafetyCenterIssue}. */ 325 public static final class Builder { 326 327 @NonNull private String mId; 328 @NonNull private CharSequence mTitle; 329 @NonNull private CharSequence mSummary; 330 @Nullable private CharSequence mSubtitle; 331 @IssueSeverityLevel private int mSeverityLevel = ISSUE_SEVERITY_LEVEL_OK; 332 private boolean mDismissible = true; 333 private boolean mShouldConfirmDismissal = true; 334 private List<Action> mActions = new ArrayList<>(); 335 @Nullable private CharSequence mAttributionTitle; 336 @Nullable private String mGroupId; 337 338 /** 339 * Creates a {@link Builder} for a {@link SafetyCenterIssue}. 340 * 341 * @param id a unique encoded string ID, see {@link #getId()} for details 342 * @param title a title that describes this issue 343 * @param summary a summary of this issue 344 */ Builder( @onNull String id, @NonNull CharSequence title, @NonNull CharSequence summary)345 public Builder( 346 @NonNull String id, @NonNull CharSequence title, @NonNull CharSequence summary) { 347 mId = requireNonNull(id); 348 mTitle = requireNonNull(title); 349 mSummary = requireNonNull(summary); 350 } 351 352 /** Creates a {@link Builder} with the values from the given {@link SafetyCenterIssue}. */ Builder(@onNull SafetyCenterIssue issue)353 public Builder(@NonNull SafetyCenterIssue issue) { 354 mId = issue.mId; 355 mTitle = issue.mTitle; 356 mSubtitle = issue.mSubtitle; 357 mSummary = issue.mSummary; 358 mSeverityLevel = issue.mSeverityLevel; 359 mDismissible = issue.mDismissible; 360 mShouldConfirmDismissal = issue.mShouldConfirmDismissal; 361 mActions = new ArrayList<>(issue.mActions); 362 mAttributionTitle = issue.mAttributionTitle; 363 mGroupId = issue.mGroupId; 364 } 365 366 /** Sets the ID for this issue. */ 367 @NonNull setId(@onNull String id)368 public Builder setId(@NonNull String id) { 369 mId = requireNonNull(id); 370 return this; 371 } 372 373 /** Sets the title for this issue. */ 374 @NonNull setTitle(@onNull CharSequence title)375 public Builder setTitle(@NonNull CharSequence title) { 376 mTitle = requireNonNull(title); 377 return this; 378 } 379 380 /** Sets or clears the optional subtitle for this issue. */ 381 @NonNull setSubtitle(@ullable CharSequence subtitle)382 public Builder setSubtitle(@Nullable CharSequence subtitle) { 383 mSubtitle = subtitle; 384 return this; 385 } 386 387 /** Sets the summary for this issue. */ 388 @NonNull setSummary(@onNull CharSequence summary)389 public Builder setSummary(@NonNull CharSequence summary) { 390 mSummary = requireNonNull(summary); 391 return this; 392 } 393 394 /** 395 * Sets or clears the optional attribution title for this issue. 396 * 397 * <p>This is displayed in the UI and helps to attribute issue cards to a particular source. 398 * 399 * @throws UnsupportedOperationException if accessed from a version lower than {@link 400 * UPSIDE_DOWN_CAKE} 401 */ 402 @NonNull 403 @RequiresApi(UPSIDE_DOWN_CAKE) setAttributionTitle(@ullable CharSequence attributionTitle)404 public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) { 405 if (!SdkLevel.isAtLeastU()) { 406 throw new UnsupportedOperationException( 407 "Method not supported for versions lower than UPSIDE_DOWN_CAKE"); 408 } 409 mAttributionTitle = attributionTitle; 410 return this; 411 } 412 413 /** 414 * Sets {@link IssueSeverityLevel} for this issue. Defaults to {@link 415 * #ISSUE_SEVERITY_LEVEL_OK}. 416 */ 417 @NonNull setSeverityLevel(@ssueSeverityLevel int severityLevel)418 public Builder setSeverityLevel(@IssueSeverityLevel int severityLevel) { 419 mSeverityLevel = validateIssueSeverityLevel(severityLevel); 420 return this; 421 } 422 423 /** Sets whether this issue can be dismissed. Defaults to {@code true}. */ 424 @NonNull setDismissible(boolean dismissible)425 public Builder setDismissible(boolean dismissible) { 426 mDismissible = dismissible; 427 return this; 428 } 429 430 /** 431 * Sets whether this issue should have its dismissal confirmed. Defaults to {@code true}. 432 */ 433 @NonNull setShouldConfirmDismissal(boolean confirmDismissal)434 public Builder setShouldConfirmDismissal(boolean confirmDismissal) { 435 mShouldConfirmDismissal = confirmDismissal; 436 return this; 437 } 438 439 /** 440 * Sets the list of potential actions to be taken to resolve this issue. Defaults to an 441 * empty list. 442 */ 443 @NonNull setActions(@onNull List<Action> actions)444 public Builder setActions(@NonNull List<Action> actions) { 445 mActions = requireNonNull(actions); 446 return this; 447 } 448 449 /** 450 * Sets the ID of {@link SafetySourcesGroup} that this issue belongs to. Defaults to a 451 * {@code null} value. 452 * 453 * <p>This ID is used for displaying the issue on its corresponding subpage in the Safety 454 * Center UI. 455 * 456 * @throws UnsupportedOperationException if accessed from a version lower than {@link 457 * UPSIDE_DOWN_CAKE} 458 */ 459 @NonNull 460 @RequiresApi(UPSIDE_DOWN_CAKE) setGroupId(@ullable String groupId)461 public Builder setGroupId(@Nullable String groupId) { 462 if (!SdkLevel.isAtLeastU()) { 463 throw new UnsupportedOperationException( 464 "Method not supported for versions lower than UPSIDE_DOWN_CAKE"); 465 } 466 mGroupId = groupId; 467 return this; 468 } 469 470 /** Creates the {@link SafetyCenterIssue} defined by this {@link Builder}. */ 471 @NonNull build()472 public SafetyCenterIssue build() { 473 return new SafetyCenterIssue( 474 mId, 475 mTitle, 476 mSubtitle, 477 mSummary, 478 mSeverityLevel, 479 mDismissible, 480 mShouldConfirmDismissal, 481 unmodifiableList(new ArrayList<>(mActions)), 482 mAttributionTitle, 483 mGroupId); 484 } 485 } 486 487 /** 488 * An action that can be taken to resolve a given issue. 489 * 490 * <p>When a user initiates an {@link Action}, that action's associated {@link PendingIntent} 491 * will be executed, and the {@code successMessage} will be displayed if present. 492 */ 493 public static final class Action implements Parcelable { 494 495 @NonNull 496 public static final Creator<Action> CREATOR = 497 new Creator<Action>() { 498 @Override 499 public Action createFromParcel(Parcel in) { 500 String id = in.readString(); 501 CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 502 PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR); 503 Builder builder = 504 new Builder(id, label, pendingIntent) 505 .setWillResolve(in.readBoolean()) 506 .setIsInFlight(in.readBoolean()) 507 .setSuccessMessage( 508 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel( 509 in)); 510 if (SdkLevel.isAtLeastU()) { 511 ConfirmationDialogDetails confirmationDialogDetails = 512 in.readTypedObject(ConfirmationDialogDetails.CREATOR); 513 builder.setConfirmationDialogDetails(confirmationDialogDetails); 514 } 515 return builder.build(); 516 } 517 518 @Override 519 public Action[] newArray(int size) { 520 return new Action[size]; 521 } 522 }; 523 524 @NonNull private final String mId; 525 @NonNull private final CharSequence mLabel; 526 @NonNull private final PendingIntent mPendingIntent; 527 private final boolean mWillResolve; 528 private final boolean mInFlight; 529 @Nullable private final CharSequence mSuccessMessage; 530 @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails; 531 Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, boolean inFlight, @Nullable CharSequence successMessage, @Nullable ConfirmationDialogDetails confirmationDialogDetails)532 private Action( 533 @NonNull String id, 534 @NonNull CharSequence label, 535 @NonNull PendingIntent pendingIntent, 536 boolean willResolve, 537 boolean inFlight, 538 @Nullable CharSequence successMessage, 539 @Nullable ConfirmationDialogDetails confirmationDialogDetails) { 540 mId = id; 541 mLabel = label; 542 mPendingIntent = pendingIntent; 543 mWillResolve = willResolve; 544 mInFlight = inFlight; 545 mSuccessMessage = successMessage; 546 mConfirmationDialogDetails = confirmationDialogDetails; 547 } 548 549 /** Returns the ID of this action. */ 550 @NonNull getId()551 public String getId() { 552 return mId; 553 } 554 555 /** Returns a label describing this {@link Action}. */ 556 @NonNull getLabel()557 public CharSequence getLabel() { 558 return mLabel; 559 } 560 561 /** Returns the {@link PendingIntent} to execute when this {@link Action} is taken. */ 562 @NonNull getPendingIntent()563 public PendingIntent getPendingIntent() { 564 return mPendingIntent; 565 } 566 567 /** 568 * Returns whether invoking this action will fix or address the issue sufficiently for it to 569 * be considered resolved (i.e. the issue will no longer need to be conveyed to the user in 570 * the UI). 571 */ willResolve()572 public boolean willResolve() { 573 return mWillResolve; 574 } 575 576 /** 577 * Returns whether this action is currently being executed (i.e. the user clicked on a 578 * button that triggered this action, and now the Safety Center is waiting for the action's 579 * result). 580 */ isInFlight()581 public boolean isInFlight() { 582 return mInFlight; 583 } 584 585 /** 586 * Returns the success message to display after successfully completing this {@link Action} 587 * or {@code null} if none should be displayed. 588 */ 589 @Nullable getSuccessMessage()590 public CharSequence getSuccessMessage() { 591 return mSuccessMessage; 592 } 593 594 /** 595 * Returns the optional data to be displayed in the confirmation dialog prior to launching 596 * the {@link PendingIntent} when the action is clicked on. 597 */ 598 @Nullable 599 @RequiresApi(UPSIDE_DOWN_CAKE) getConfirmationDialogDetails()600 public ConfirmationDialogDetails getConfirmationDialogDetails() { 601 if (!SdkLevel.isAtLeastU()) { 602 throw new UnsupportedOperationException(); 603 } 604 return mConfirmationDialogDetails; 605 } 606 607 @Override equals(Object o)608 public boolean equals(Object o) { 609 if (this == o) return true; 610 if (!(o instanceof Action)) return false; 611 Action action = (Action) o; 612 return Objects.equals(mId, action.mId) 613 && TextUtils.equals(mLabel, action.mLabel) 614 && Objects.equals(mPendingIntent, action.mPendingIntent) 615 && mWillResolve == action.mWillResolve 616 && mInFlight == action.mInFlight 617 && TextUtils.equals(mSuccessMessage, action.mSuccessMessage) 618 && Objects.equals( 619 mConfirmationDialogDetails, action.mConfirmationDialogDetails); 620 } 621 622 @Override hashCode()623 public int hashCode() { 624 return Objects.hash( 625 mId, 626 mLabel, 627 mSuccessMessage, 628 mWillResolve, 629 mInFlight, 630 mPendingIntent, 631 mConfirmationDialogDetails); 632 } 633 634 @Override toString()635 public String toString() { 636 return "Action{" 637 + "mId=" 638 + mId 639 + ", mLabel=" 640 + mLabel 641 + ", mPendingIntent=" 642 + mPendingIntent 643 + ", mWillResolve=" 644 + mWillResolve 645 + ", mInFlight=" 646 + mInFlight 647 + ", mSuccessMessage=" 648 + mSuccessMessage 649 + ", mConfirmationDialogDetails=" 650 + mConfirmationDialogDetails 651 + '}'; 652 } 653 654 @Override describeContents()655 public int describeContents() { 656 return 0; 657 } 658 659 @Override writeToParcel(@onNull Parcel dest, int flags)660 public void writeToParcel(@NonNull Parcel dest, int flags) { 661 dest.writeString(mId); 662 TextUtils.writeToParcel(mLabel, dest, flags); 663 dest.writeTypedObject(mPendingIntent, flags); 664 dest.writeBoolean(mWillResolve); 665 dest.writeBoolean(mInFlight); 666 TextUtils.writeToParcel(mSuccessMessage, dest, flags); 667 if (SdkLevel.isAtLeastU()) { 668 dest.writeTypedObject(mConfirmationDialogDetails, flags); 669 } 670 } 671 672 /** Data for an action confirmation dialog to be shown before action is executed. */ 673 @RequiresApi(UPSIDE_DOWN_CAKE) 674 public static final class ConfirmationDialogDetails implements Parcelable { 675 676 @NonNull 677 public static final Creator<ConfirmationDialogDetails> CREATOR = 678 new Creator<ConfirmationDialogDetails>() { 679 @Override 680 public ConfirmationDialogDetails createFromParcel(Parcel in) { 681 CharSequence title = 682 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 683 CharSequence text = 684 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 685 CharSequence acceptButtonText = 686 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 687 CharSequence denyButtonText = 688 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 689 return new ConfirmationDialogDetails( 690 title, text, acceptButtonText, denyButtonText); 691 } 692 693 @Override 694 public ConfirmationDialogDetails[] newArray(int size) { 695 return new ConfirmationDialogDetails[size]; 696 } 697 }; 698 699 @NonNull private final CharSequence mTitle; 700 @NonNull private final CharSequence mText; 701 @NonNull private final CharSequence mAcceptButtonText; 702 @NonNull private final CharSequence mDenyButtonText; 703 ConfirmationDialogDetails( @onNull CharSequence title, @NonNull CharSequence text, @NonNull CharSequence acceptButtonText, @NonNull CharSequence denyButtonText)704 public ConfirmationDialogDetails( 705 @NonNull CharSequence title, 706 @NonNull CharSequence text, 707 @NonNull CharSequence acceptButtonText, 708 @NonNull CharSequence denyButtonText) { 709 mTitle = requireNonNull(title); 710 mText = requireNonNull(text); 711 mAcceptButtonText = requireNonNull(acceptButtonText); 712 mDenyButtonText = requireNonNull(denyButtonText); 713 } 714 715 /** Returns the title of action confirmation dialog. */ 716 @NonNull getTitle()717 public CharSequence getTitle() { 718 return mTitle; 719 } 720 721 /** Returns the text of action confirmation dialog. */ 722 @NonNull getText()723 public CharSequence getText() { 724 return mText; 725 } 726 727 /** Returns the text of the button to accept action execution. */ 728 @NonNull getAcceptButtonText()729 public CharSequence getAcceptButtonText() { 730 return mAcceptButtonText; 731 } 732 733 /** Returns the text of the button to deny action execution. */ 734 @NonNull getDenyButtonText()735 public CharSequence getDenyButtonText() { 736 return mDenyButtonText; 737 } 738 739 @Override describeContents()740 public int describeContents() { 741 return 0; 742 } 743 744 @Override writeToParcel(@onNull Parcel dest, int flags)745 public void writeToParcel(@NonNull Parcel dest, int flags) { 746 TextUtils.writeToParcel(mTitle, dest, flags); 747 TextUtils.writeToParcel(mText, dest, flags); 748 TextUtils.writeToParcel(mAcceptButtonText, dest, flags); 749 TextUtils.writeToParcel(mDenyButtonText, dest, flags); 750 } 751 752 @Override equals(Object o)753 public boolean equals(Object o) { 754 if (this == o) return true; 755 if (!(o instanceof ConfirmationDialogDetails)) return false; 756 ConfirmationDialogDetails that = (ConfirmationDialogDetails) o; 757 return TextUtils.equals(mTitle, that.mTitle) 758 && TextUtils.equals(mText, that.mText) 759 && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText) 760 && TextUtils.equals(mDenyButtonText, that.mDenyButtonText); 761 } 762 763 @Override hashCode()764 public int hashCode() { 765 return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText); 766 } 767 768 @Override toString()769 public String toString() { 770 return "ConfirmationDialogDetails{" 771 + "mTitle=" 772 + mTitle 773 + ", mText=" 774 + mText 775 + ", mAcceptButtonText=" 776 + mAcceptButtonText 777 + ", mDenyButtonText=" 778 + mDenyButtonText 779 + '}'; 780 } 781 } 782 783 /** Builder class for {@link Action}. */ 784 public static final class Builder { 785 786 @NonNull private String mId; 787 @NonNull private CharSequence mLabel; 788 @NonNull private PendingIntent mPendingIntent; 789 private boolean mWillResolve; 790 private boolean mInFlight; 791 @Nullable private CharSequence mSuccessMessage; 792 @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails; 793 794 /** 795 * Creates a new {@link Builder} for an {@link Action}. 796 * 797 * @param id a unique ID for this action 798 * @param label a label describing this action 799 * @param pendingIntent a {@link PendingIntent} to be sent when this action is taken 800 */ Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)801 public Builder( 802 @NonNull String id, 803 @NonNull CharSequence label, 804 @NonNull PendingIntent pendingIntent) { 805 mId = requireNonNull(id); 806 mLabel = requireNonNull(label); 807 mPendingIntent = requireNonNull(pendingIntent); 808 } 809 810 /** Creates a {@link Builder} with the values from the given {@link Action}. */ 811 @RequiresApi(UPSIDE_DOWN_CAKE) Builder(@onNull Action action)812 public Builder(@NonNull Action action) { 813 if (!SdkLevel.isAtLeastU()) { 814 throw new UnsupportedOperationException(); 815 } 816 requireNonNull(action); 817 mId = action.mId; 818 mLabel = action.mLabel; 819 mPendingIntent = action.mPendingIntent; 820 mWillResolve = action.mWillResolve; 821 mInFlight = action.mInFlight; 822 mSuccessMessage = action.mSuccessMessage; 823 mConfirmationDialogDetails = action.mConfirmationDialogDetails; 824 } 825 826 /** Sets the ID of this {@link Action} */ 827 @NonNull setId(@onNull String id)828 public Builder setId(@NonNull String id) { 829 mId = requireNonNull(id); 830 return this; 831 } 832 833 /** Sets the label of this {@link Action}. */ 834 @NonNull setLabel(@onNull CharSequence label)835 public Builder setLabel(@NonNull CharSequence label) { 836 mLabel = requireNonNull(label); 837 return this; 838 } 839 840 /** Sets the {@link PendingIntent} to be sent when this {@link Action} is taken. */ 841 @NonNull setPendingIntent(@onNull PendingIntent pendingIntent)842 public Builder setPendingIntent(@NonNull PendingIntent pendingIntent) { 843 mPendingIntent = requireNonNull(pendingIntent); 844 return this; 845 } 846 847 /** 848 * Sets whether this action will resolve the issue when executed. Defaults to {@code 849 * false}. 850 * 851 * @see #willResolve() 852 */ 853 @SuppressLint("MissingGetterMatchingBuilder") 854 @NonNull setWillResolve(boolean willResolve)855 public Builder setWillResolve(boolean willResolve) { 856 mWillResolve = willResolve; 857 return this; 858 } 859 860 /** 861 * Sets a boolean that indicates whether this action is currently being executed (i.e. 862 * the user clicked on a button that triggered this action, and now the Safety Center is 863 * waiting for the action's result). Defaults to {@code false}. 864 * 865 * @see #isInFlight() 866 */ 867 @SuppressLint("MissingGetterMatchingBuilder") 868 @NonNull setIsInFlight(boolean inFlight)869 public Builder setIsInFlight(boolean inFlight) { 870 mInFlight = inFlight; 871 return this; 872 } 873 874 /** 875 * Sets or clears the optional success message to be displayed when this {@link Action} 876 * completes. 877 */ 878 @NonNull setSuccessMessage(@ullable CharSequence successMessage)879 public Builder setSuccessMessage(@Nullable CharSequence successMessage) { 880 mSuccessMessage = successMessage; 881 return this; 882 } 883 884 /** 885 * Sets the optional data to be displayed in the confirmation dialog prior to launching 886 * the {@link PendingIntent} when the action is clicked on. 887 */ 888 @NonNull 889 @RequiresApi(UPSIDE_DOWN_CAKE) setConfirmationDialogDetails( @ullable ConfirmationDialogDetails confirmationDialogDetails)890 public Builder setConfirmationDialogDetails( 891 @Nullable ConfirmationDialogDetails confirmationDialogDetails) { 892 if (!SdkLevel.isAtLeastU()) { 893 throw new UnsupportedOperationException(); 894 } 895 mConfirmationDialogDetails = confirmationDialogDetails; 896 return this; 897 } 898 899 /** Creates the {@link Action} defined by this {@link Builder}. */ 900 @NonNull build()901 public Action build() { 902 return new Action( 903 mId, 904 mLabel, 905 mPendingIntent, 906 mWillResolve, 907 mInFlight, 908 mSuccessMessage, 909 mConfirmationDialogDetails); 910 } 911 } 912 } 913 914 @IssueSeverityLevel validateIssueSeverityLevel(int value)915 private static int validateIssueSeverityLevel(int value) { 916 switch (value) { 917 case ISSUE_SEVERITY_LEVEL_OK: 918 case ISSUE_SEVERITY_LEVEL_RECOMMENDATION: 919 case ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING: 920 return value; 921 default: 922 } 923 throw new IllegalArgumentException( 924 "Unexpected IssueSeverityLevel for SafetyCenterIssue: " + value); 925 } 926 } 927