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 on 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 on 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 on 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 on 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 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 604 } 605 return mConfirmationDialogDetails; 606 } 607 608 @Override equals(Object o)609 public boolean equals(Object o) { 610 if (this == o) return true; 611 if (!(o instanceof Action)) return false; 612 Action action = (Action) o; 613 return Objects.equals(mId, action.mId) 614 && TextUtils.equals(mLabel, action.mLabel) 615 && Objects.equals(mPendingIntent, action.mPendingIntent) 616 && mWillResolve == action.mWillResolve 617 && mInFlight == action.mInFlight 618 && TextUtils.equals(mSuccessMessage, action.mSuccessMessage) 619 && Objects.equals( 620 mConfirmationDialogDetails, action.mConfirmationDialogDetails); 621 } 622 623 @Override hashCode()624 public int hashCode() { 625 return Objects.hash( 626 mId, 627 mLabel, 628 mSuccessMessage, 629 mWillResolve, 630 mInFlight, 631 mPendingIntent, 632 mConfirmationDialogDetails); 633 } 634 635 @Override toString()636 public String toString() { 637 return "Action{" 638 + "mId=" 639 + mId 640 + ", mLabel=" 641 + mLabel 642 + ", mPendingIntent=" 643 + mPendingIntent 644 + ", mWillResolve=" 645 + mWillResolve 646 + ", mInFlight=" 647 + mInFlight 648 + ", mSuccessMessage=" 649 + mSuccessMessage 650 + ", mConfirmationDialogDetails=" 651 + mConfirmationDialogDetails 652 + '}'; 653 } 654 655 @Override describeContents()656 public int describeContents() { 657 return 0; 658 } 659 660 @Override writeToParcel(@onNull Parcel dest, int flags)661 public void writeToParcel(@NonNull Parcel dest, int flags) { 662 dest.writeString(mId); 663 TextUtils.writeToParcel(mLabel, dest, flags); 664 dest.writeTypedObject(mPendingIntent, flags); 665 dest.writeBoolean(mWillResolve); 666 dest.writeBoolean(mInFlight); 667 TextUtils.writeToParcel(mSuccessMessage, dest, flags); 668 if (SdkLevel.isAtLeastU()) { 669 dest.writeTypedObject(mConfirmationDialogDetails, flags); 670 } 671 } 672 673 /** Data for an action confirmation dialog to be shown before action is executed. */ 674 @RequiresApi(UPSIDE_DOWN_CAKE) 675 public static final class ConfirmationDialogDetails implements Parcelable { 676 677 @NonNull 678 public static final Creator<ConfirmationDialogDetails> CREATOR = 679 new Creator<ConfirmationDialogDetails>() { 680 @Override 681 public ConfirmationDialogDetails createFromParcel(Parcel in) { 682 CharSequence title = 683 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 684 CharSequence text = 685 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 686 CharSequence acceptButtonText = 687 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 688 CharSequence denyButtonText = 689 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 690 return new ConfirmationDialogDetails( 691 title, text, acceptButtonText, denyButtonText); 692 } 693 694 @Override 695 public ConfirmationDialogDetails[] newArray(int size) { 696 return new ConfirmationDialogDetails[size]; 697 } 698 }; 699 700 @NonNull private final CharSequence mTitle; 701 @NonNull private final CharSequence mText; 702 @NonNull private final CharSequence mAcceptButtonText; 703 @NonNull private final CharSequence mDenyButtonText; 704 ConfirmationDialogDetails( @onNull CharSequence title, @NonNull CharSequence text, @NonNull CharSequence acceptButtonText, @NonNull CharSequence denyButtonText)705 public ConfirmationDialogDetails( 706 @NonNull CharSequence title, 707 @NonNull CharSequence text, 708 @NonNull CharSequence acceptButtonText, 709 @NonNull CharSequence denyButtonText) { 710 mTitle = requireNonNull(title); 711 mText = requireNonNull(text); 712 mAcceptButtonText = requireNonNull(acceptButtonText); 713 mDenyButtonText = requireNonNull(denyButtonText); 714 } 715 716 /** Returns the title of action confirmation dialog. */ 717 @NonNull getTitle()718 public CharSequence getTitle() { 719 return mTitle; 720 } 721 722 /** Returns the text of action confirmation dialog. */ 723 @NonNull getText()724 public CharSequence getText() { 725 return mText; 726 } 727 728 /** Returns the text of the button to accept action execution. */ 729 @NonNull getAcceptButtonText()730 public CharSequence getAcceptButtonText() { 731 return mAcceptButtonText; 732 } 733 734 /** Returns the text of the button to deny action execution. */ 735 @NonNull getDenyButtonText()736 public CharSequence getDenyButtonText() { 737 return mDenyButtonText; 738 } 739 740 @Override describeContents()741 public int describeContents() { 742 return 0; 743 } 744 745 @Override writeToParcel(@onNull Parcel dest, int flags)746 public void writeToParcel(@NonNull Parcel dest, int flags) { 747 TextUtils.writeToParcel(mTitle, dest, flags); 748 TextUtils.writeToParcel(mText, dest, flags); 749 TextUtils.writeToParcel(mAcceptButtonText, dest, flags); 750 TextUtils.writeToParcel(mDenyButtonText, dest, flags); 751 } 752 753 @Override equals(Object o)754 public boolean equals(Object o) { 755 if (this == o) return true; 756 if (!(o instanceof ConfirmationDialogDetails)) return false; 757 ConfirmationDialogDetails that = (ConfirmationDialogDetails) o; 758 return TextUtils.equals(mTitle, that.mTitle) 759 && TextUtils.equals(mText, that.mText) 760 && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText) 761 && TextUtils.equals(mDenyButtonText, that.mDenyButtonText); 762 } 763 764 @Override hashCode()765 public int hashCode() { 766 return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText); 767 } 768 769 @Override toString()770 public String toString() { 771 return "ConfirmationDialogDetails{" 772 + "mTitle=" 773 + mTitle 774 + ", mText=" 775 + mText 776 + ", mAcceptButtonText=" 777 + mAcceptButtonText 778 + ", mDenyButtonText=" 779 + mDenyButtonText 780 + '}'; 781 } 782 } 783 784 /** Builder class for {@link Action}. */ 785 public static final class Builder { 786 787 @NonNull private String mId; 788 @NonNull private CharSequence mLabel; 789 @NonNull private PendingIntent mPendingIntent; 790 private boolean mWillResolve; 791 private boolean mInFlight; 792 @Nullable private CharSequence mSuccessMessage; 793 @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails; 794 795 /** 796 * Creates a new {@link Builder} for an {@link Action}. 797 * 798 * @param id a unique ID for this action 799 * @param label a label describing this action 800 * @param pendingIntent a {@link PendingIntent} to be sent when this action is taken 801 */ Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)802 public Builder( 803 @NonNull String id, 804 @NonNull CharSequence label, 805 @NonNull PendingIntent pendingIntent) { 806 mId = requireNonNull(id); 807 mLabel = requireNonNull(label); 808 mPendingIntent = requireNonNull(pendingIntent); 809 } 810 811 /** Creates a {@link Builder} with the values from the given {@link Action}. */ 812 @RequiresApi(UPSIDE_DOWN_CAKE) Builder(@onNull Action action)813 public Builder(@NonNull Action action) { 814 if (!SdkLevel.isAtLeastU()) { 815 throw new UnsupportedOperationException( 816 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 817 } 818 requireNonNull(action); 819 mId = action.mId; 820 mLabel = action.mLabel; 821 mPendingIntent = action.mPendingIntent; 822 mWillResolve = action.mWillResolve; 823 mInFlight = action.mInFlight; 824 mSuccessMessage = action.mSuccessMessage; 825 mConfirmationDialogDetails = action.mConfirmationDialogDetails; 826 } 827 828 /** Sets the ID of this {@link Action} */ 829 @NonNull setId(@onNull String id)830 public Builder setId(@NonNull String id) { 831 mId = requireNonNull(id); 832 return this; 833 } 834 835 /** Sets the label of this {@link Action}. */ 836 @NonNull setLabel(@onNull CharSequence label)837 public Builder setLabel(@NonNull CharSequence label) { 838 mLabel = requireNonNull(label); 839 return this; 840 } 841 842 /** Sets the {@link PendingIntent} to be sent when this {@link Action} is taken. */ 843 @NonNull setPendingIntent(@onNull PendingIntent pendingIntent)844 public Builder setPendingIntent(@NonNull PendingIntent pendingIntent) { 845 mPendingIntent = requireNonNull(pendingIntent); 846 return this; 847 } 848 849 /** 850 * Sets whether this action will resolve the issue when executed. Defaults to {@code 851 * false}. 852 * 853 * @see #willResolve() 854 */ 855 @SuppressLint("MissingGetterMatchingBuilder") 856 @NonNull setWillResolve(boolean willResolve)857 public Builder setWillResolve(boolean willResolve) { 858 mWillResolve = willResolve; 859 return this; 860 } 861 862 /** 863 * Sets a boolean that indicates whether this action is currently being executed (i.e. 864 * the user clicked on a button that triggered this action, and now the Safety Center is 865 * waiting for the action's result). Defaults to {@code false}. 866 * 867 * @see #isInFlight() 868 */ 869 @SuppressLint("MissingGetterMatchingBuilder") 870 @NonNull setIsInFlight(boolean inFlight)871 public Builder setIsInFlight(boolean inFlight) { 872 mInFlight = inFlight; 873 return this; 874 } 875 876 /** 877 * Sets or clears the optional success message to be displayed when this {@link Action} 878 * completes. 879 */ 880 @NonNull setSuccessMessage(@ullable CharSequence successMessage)881 public Builder setSuccessMessage(@Nullable CharSequence successMessage) { 882 mSuccessMessage = successMessage; 883 return this; 884 } 885 886 /** 887 * Sets the optional data to be displayed in the confirmation dialog prior to launching 888 * the {@link PendingIntent} when the action is clicked on. 889 */ 890 @NonNull 891 @RequiresApi(UPSIDE_DOWN_CAKE) setConfirmationDialogDetails( @ullable ConfirmationDialogDetails confirmationDialogDetails)892 public Builder setConfirmationDialogDetails( 893 @Nullable ConfirmationDialogDetails confirmationDialogDetails) { 894 if (!SdkLevel.isAtLeastU()) { 895 throw new UnsupportedOperationException( 896 "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); 897 } 898 mConfirmationDialogDetails = confirmationDialogDetails; 899 return this; 900 } 901 902 /** Creates the {@link Action} defined by this {@link Builder}. */ 903 @NonNull build()904 public Action build() { 905 return new Action( 906 mId, 907 mLabel, 908 mPendingIntent, 909 mWillResolve, 910 mInFlight, 911 mSuccessMessage, 912 mConfirmationDialogDetails); 913 } 914 } 915 } 916 917 @IssueSeverityLevel validateIssueSeverityLevel(int value)918 private static int validateIssueSeverityLevel(int value) { 919 switch (value) { 920 case ISSUE_SEVERITY_LEVEL_OK: 921 case ISSUE_SEVERITY_LEVEL_RECOMMENDATION: 922 case ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING: 923 return value; 924 default: 925 } 926 throw new IllegalArgumentException( 927 "Unexpected IssueSeverityLevel for SafetyCenterIssue: " + value); 928 } 929 } 930