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 21 import static java.util.Collections.unmodifiableList; 22 import static java.util.Objects.requireNonNull; 23 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.annotation.SuppressLint; 28 import android.annotation.SystemApi; 29 import android.app.PendingIntent; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.text.TextUtils; 33 34 import androidx.annotation.RequiresApi; 35 36 import java.lang.annotation.Retention; 37 import java.lang.annotation.RetentionPolicy; 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Objects; 41 42 /** 43 * An issue in the Safety Center. 44 * 45 * <p>An issue represents an actionable matter on the device of elevated importance. 46 * 47 * <p>It contains localized messages to display to the user, explaining the underlying threat or 48 * warning and suggested fixes, and contains actions that a user may take from the UI to resolve the 49 * issue. 50 * 51 * <p>Issues are ephemeral and disappear when resolved by user action or dismissal. 52 * 53 * @hide 54 */ 55 @SystemApi 56 @RequiresApi(TIRAMISU) 57 public final class SafetyCenterIssue implements Parcelable { 58 59 /** Indicates that this is low-severity, and informational. */ 60 public static final int ISSUE_SEVERITY_LEVEL_OK = 2100; 61 62 /** Indicates that this issue describes a safety recommendation. */ 63 public static final int ISSUE_SEVERITY_LEVEL_RECOMMENDATION = 2200; 64 65 /** Indicates that this issue describes a critical safety warning. */ 66 public static final int ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING = 2300; 67 68 /** 69 * All possible severity levels for a {@link SafetyCenterIssue}. 70 * 71 * @hide 72 * @see SafetyCenterIssue#getSeverityLevel() 73 * @see Builder#setSeverityLevel(int) 74 */ 75 @Retention(RetentionPolicy.SOURCE) 76 @IntDef( 77 prefix = "ISSUE_SEVERITY_LEVEL_", 78 value = { 79 ISSUE_SEVERITY_LEVEL_OK, 80 ISSUE_SEVERITY_LEVEL_RECOMMENDATION, 81 ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING, 82 }) 83 public @interface IssueSeverityLevel {} 84 85 @NonNull 86 public static final Creator<SafetyCenterIssue> CREATOR = 87 new Creator<SafetyCenterIssue>() { 88 @Override 89 public SafetyCenterIssue createFromParcel(Parcel in) { 90 String id = in.readString(); 91 CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 92 CharSequence subtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 93 CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 94 return new Builder(id, title, summary) 95 .setSubtitle(subtitle) 96 .setSeverityLevel(in.readInt()) 97 .setDismissible(in.readBoolean()) 98 .setShouldConfirmDismissal(in.readBoolean()) 99 .setActions(in.createTypedArrayList(Action.CREATOR)) 100 .build(); 101 } 102 103 @Override 104 public SafetyCenterIssue[] newArray(int size) { 105 return new SafetyCenterIssue[size]; 106 } 107 }; 108 109 @NonNull private final String mId; 110 @NonNull private final CharSequence mTitle; 111 @Nullable private final CharSequence mSubtitle; 112 @NonNull private final CharSequence mSummary; 113 @IssueSeverityLevel private final int mSeverityLevel; 114 private final boolean mDismissible; 115 private final boolean mShouldConfirmDismissal; 116 @NonNull private final List<Action> mActions; 117 SafetyCenterIssue( @onNull String id, @NonNull CharSequence title, @Nullable CharSequence subtitle, @NonNull CharSequence summary, @IssueSeverityLevel int severityLevel, boolean isDismissible, boolean shouldConfirmDismissal, @NonNull List<Action> actions)118 private SafetyCenterIssue( 119 @NonNull String id, 120 @NonNull CharSequence title, 121 @Nullable CharSequence subtitle, 122 @NonNull CharSequence summary, 123 @IssueSeverityLevel int severityLevel, 124 boolean isDismissible, 125 boolean shouldConfirmDismissal, 126 @NonNull List<Action> actions) { 127 mId = id; 128 mTitle = title; 129 mSubtitle = subtitle; 130 mSummary = summary; 131 mSeverityLevel = severityLevel; 132 mDismissible = isDismissible; 133 mShouldConfirmDismissal = shouldConfirmDismissal; 134 mActions = actions; 135 } 136 137 /** 138 * Returns the encoded string ID which uniquely identifies this issue within the Safety Center 139 * on the device for the current user across all profiles and accounts. 140 */ 141 @NonNull getId()142 public String getId() { 143 return mId; 144 } 145 146 /** Returns the title that describes this issue. */ 147 @NonNull getTitle()148 public CharSequence getTitle() { 149 return mTitle; 150 } 151 152 /** Returns the subtitle of this issue, or {@code null} if it has none. */ 153 @Nullable getSubtitle()154 public CharSequence getSubtitle() { 155 return mSubtitle; 156 } 157 158 /** Returns the summary text that describes this issue. */ 159 @NonNull getSummary()160 public CharSequence getSummary() { 161 return mSummary; 162 } 163 164 /** Returns the {@link IssueSeverityLevel} of this issue. */ 165 @IssueSeverityLevel getSeverityLevel()166 public int getSeverityLevel() { 167 return mSeverityLevel; 168 } 169 170 /** Returns {@code true} if this issue can be dismissed. */ isDismissible()171 public boolean isDismissible() { 172 return mDismissible; 173 } 174 175 /** Returns {@code true} if this issue should have its dismissal confirmed. */ shouldConfirmDismissal()176 public boolean shouldConfirmDismissal() { 177 return mShouldConfirmDismissal; 178 } 179 180 /** 181 * Returns the ordered list of {@link Action} objects that may be taken to resolve this issue. 182 * 183 * <p>An issue may have 0-2 actions. The first action will be considered the "Primary" action of 184 * the issue. 185 */ 186 @NonNull getActions()187 public List<Action> getActions() { 188 return mActions; 189 } 190 191 @Override equals(Object o)192 public boolean equals(Object o) { 193 if (this == o) return true; 194 if (!(o instanceof SafetyCenterIssue)) return false; 195 SafetyCenterIssue that = (SafetyCenterIssue) o; 196 return mSeverityLevel == that.mSeverityLevel 197 && mDismissible == that.mDismissible 198 && mShouldConfirmDismissal == that.mShouldConfirmDismissal 199 && Objects.equals(mId, that.mId) 200 && TextUtils.equals(mTitle, that.mTitle) 201 && TextUtils.equals(mSubtitle, that.mSubtitle) 202 && TextUtils.equals(mSummary, that.mSummary) 203 && Objects.equals(mActions, that.mActions); 204 } 205 206 @Override hashCode()207 public int hashCode() { 208 return Objects.hash( 209 mId, 210 mTitle, 211 mSubtitle, 212 mSummary, 213 mSeverityLevel, 214 mDismissible, 215 mShouldConfirmDismissal, 216 mActions); 217 } 218 219 @Override toString()220 public String toString() { 221 return "SafetyCenterIssue{" 222 + "mId='" 223 + mId 224 + '\'' 225 + ", mTitle=" 226 + mTitle 227 + ", mSubtitle=" 228 + mSubtitle 229 + ", mSummary=" 230 + mSummary 231 + ", mSeverityLevel=" 232 + mSeverityLevel 233 + ", mDismissible=" 234 + mDismissible 235 + ", mConfirmDismissal=" 236 + mShouldConfirmDismissal 237 + ", mActions=" 238 + mActions 239 + '}'; 240 } 241 242 @Override describeContents()243 public int describeContents() { 244 return 0; 245 } 246 247 @Override writeToParcel(@onNull Parcel dest, int flags)248 public void writeToParcel(@NonNull Parcel dest, int flags) { 249 dest.writeString(mId); 250 TextUtils.writeToParcel(mTitle, dest, flags); 251 TextUtils.writeToParcel(mSubtitle, dest, flags); 252 TextUtils.writeToParcel(mSummary, dest, flags); 253 dest.writeInt(mSeverityLevel); 254 dest.writeBoolean(mDismissible); 255 dest.writeBoolean(mShouldConfirmDismissal); 256 dest.writeTypedList(mActions); 257 } 258 259 /** Builder class for {@link SafetyCenterIssue}. */ 260 public static final class Builder { 261 262 @NonNull private String mId; 263 @NonNull private CharSequence mTitle; 264 @NonNull private CharSequence mSummary; 265 @Nullable private CharSequence mSubtitle; 266 @IssueSeverityLevel private int mSeverityLevel = ISSUE_SEVERITY_LEVEL_OK; 267 private boolean mDismissible = true; 268 private boolean mShouldConfirmDismissal = true; 269 private List<Action> mActions = new ArrayList<>(); 270 271 /** 272 * Creates a {@link Builder} for a {@link SafetyCenterIssue}. 273 * 274 * @param id a unique encoded string ID, see {@link #getId()} for details 275 * @param title a title that describes this issue 276 * @param summary a summary of this issue 277 */ Builder( @onNull String id, @NonNull CharSequence title, @NonNull CharSequence summary)278 public Builder( 279 @NonNull String id, @NonNull CharSequence title, @NonNull CharSequence summary) { 280 mId = requireNonNull(id); 281 mTitle = requireNonNull(title); 282 mSummary = requireNonNull(summary); 283 } 284 285 /** Creates a {@link Builder} with the values from the given {@link SafetyCenterIssue}. */ Builder(@onNull SafetyCenterIssue issue)286 public Builder(@NonNull SafetyCenterIssue issue) { 287 mId = issue.mId; 288 mTitle = issue.mTitle; 289 mSubtitle = issue.mSubtitle; 290 mSummary = issue.mSummary; 291 mSeverityLevel = issue.mSeverityLevel; 292 mDismissible = issue.mDismissible; 293 mShouldConfirmDismissal = issue.mShouldConfirmDismissal; 294 mActions = new ArrayList<>(issue.mActions); 295 } 296 297 /** Sets the ID for this issue. */ 298 @NonNull setId(@onNull String id)299 public Builder setId(@NonNull String id) { 300 mId = requireNonNull(id); 301 return this; 302 } 303 304 /** Sets the title for this issue. */ 305 @NonNull setTitle(@onNull CharSequence title)306 public Builder setTitle(@NonNull CharSequence title) { 307 mTitle = requireNonNull(title); 308 return this; 309 } 310 311 /** Sets or clears the optional subtitle for this issue. */ 312 @NonNull setSubtitle(@ullable CharSequence subtitle)313 public Builder setSubtitle(@Nullable CharSequence subtitle) { 314 mSubtitle = subtitle; 315 return this; 316 } 317 318 /** Sets the summary for this issue. */ 319 @NonNull setSummary(@onNull CharSequence summary)320 public Builder setSummary(@NonNull CharSequence summary) { 321 mSummary = requireNonNull(summary); 322 return this; 323 } 324 325 /** 326 * Sets {@link IssueSeverityLevel} for this issue. Defaults to {@link 327 * #ISSUE_SEVERITY_LEVEL_OK}. 328 */ 329 @NonNull setSeverityLevel(@ssueSeverityLevel int severityLevel)330 public Builder setSeverityLevel(@IssueSeverityLevel int severityLevel) { 331 mSeverityLevel = validateIssueSeverityLevel(severityLevel); 332 return this; 333 } 334 335 /** Sets whether this issue can be dismissed. Defaults to {@code true}. */ 336 @NonNull setDismissible(boolean dismissible)337 public Builder setDismissible(boolean dismissible) { 338 mDismissible = dismissible; 339 return this; 340 } 341 342 /** 343 * Sets whether this issue should have its dismissal confirmed. Defaults to {@code true}. 344 */ 345 @NonNull setShouldConfirmDismissal(boolean confirmDismissal)346 public Builder setShouldConfirmDismissal(boolean confirmDismissal) { 347 mShouldConfirmDismissal = confirmDismissal; 348 return this; 349 } 350 351 /** 352 * Sets the list of potential actions to be taken to resolve this issue. Defaults to an 353 * empty list. 354 */ 355 @NonNull setActions(@onNull List<Action> actions)356 public Builder setActions(@NonNull List<Action> actions) { 357 mActions = requireNonNull(actions); 358 return this; 359 } 360 361 /** Creates the {@link SafetyCenterIssue} defined by this {@link Builder}. */ 362 @NonNull build()363 public SafetyCenterIssue build() { 364 return new SafetyCenterIssue( 365 mId, 366 mTitle, 367 mSubtitle, 368 mSummary, 369 mSeverityLevel, 370 mDismissible, 371 mShouldConfirmDismissal, 372 unmodifiableList(new ArrayList<>(mActions))); 373 } 374 } 375 376 /** 377 * An action that can be taken to resolve a given issue. 378 * 379 * <p>When a user initiates an {@link Action}, that action's associated {@link PendingIntent} 380 * will be executed, and the {@code successMessage} will be displayed if present. 381 * 382 * @hide 383 */ 384 @SystemApi 385 public static final class Action implements Parcelable { 386 387 @NonNull 388 public static final Creator<Action> CREATOR = 389 new Creator<Action>() { 390 @Override 391 public Action createFromParcel(Parcel in) { 392 String id = in.readString(); 393 CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 394 PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR); 395 return new Builder(id, label, pendingIntent) 396 .setWillResolve(in.readBoolean()) 397 .setIsInFlight(in.readBoolean()) 398 .setSuccessMessage( 399 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)) 400 .build(); 401 } 402 403 @Override 404 public Action[] newArray(int size) { 405 return new Action[size]; 406 } 407 }; 408 409 @NonNull private final String mId; 410 @NonNull private final CharSequence mLabel; 411 @NonNull private final PendingIntent mPendingIntent; 412 private final boolean mWillResolve; 413 private final boolean mInFlight; 414 @Nullable private final CharSequence mSuccessMessage; 415 Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, boolean inFlight, @Nullable CharSequence successMessage)416 private Action( 417 @NonNull String id, 418 @NonNull CharSequence label, 419 @NonNull PendingIntent pendingIntent, 420 boolean willResolve, 421 boolean inFlight, 422 @Nullable CharSequence successMessage) { 423 mId = id; 424 mLabel = label; 425 mPendingIntent = pendingIntent; 426 mWillResolve = willResolve; 427 mInFlight = inFlight; 428 mSuccessMessage = successMessage; 429 } 430 431 /** Returns the ID of this action. */ 432 @NonNull getId()433 public String getId() { 434 return mId; 435 } 436 437 /** Returns a label describing this {@link Action}. */ 438 @NonNull getLabel()439 public CharSequence getLabel() { 440 return mLabel; 441 } 442 443 /** Returns the {@link PendingIntent} to execute when this {@link Action} is taken. */ 444 @NonNull getPendingIntent()445 public PendingIntent getPendingIntent() { 446 return mPendingIntent; 447 } 448 449 /** 450 * Returns whether invoking this action will fix or address the issue sufficiently for it to 451 * be considered resolved (i.e. the issue will no longer need to be conveyed to the user in 452 * the UI). 453 */ willResolve()454 public boolean willResolve() { 455 return mWillResolve; 456 } 457 458 /** 459 * Returns whether this action is currently being executed (i.e. the user clicked on a 460 * button that triggered this action, and now the Safety Center is waiting for the action's 461 * result). 462 */ isInFlight()463 public boolean isInFlight() { 464 return mInFlight; 465 } 466 467 /** 468 * Returns the success message to display after successfully completing this {@link Action} 469 * or {@code null} if none should be displayed. 470 */ 471 @Nullable getSuccessMessage()472 public CharSequence getSuccessMessage() { 473 return mSuccessMessage; 474 } 475 476 @Override equals(Object o)477 public boolean equals(Object o) { 478 if (this == o) return true; 479 if (!(o instanceof Action)) return false; 480 Action action = (Action) o; 481 return Objects.equals(mId, action.mId) 482 && TextUtils.equals(mLabel, action.mLabel) 483 && Objects.equals(mPendingIntent, action.mPendingIntent) 484 && mWillResolve == action.mWillResolve 485 && mInFlight == action.mInFlight 486 && TextUtils.equals(mSuccessMessage, action.mSuccessMessage); 487 } 488 489 @Override hashCode()490 public int hashCode() { 491 return Objects.hash( 492 mId, mLabel, mSuccessMessage, mWillResolve, mInFlight, mPendingIntent); 493 } 494 495 @Override toString()496 public String toString() { 497 return "Action{" 498 + "mId=" 499 + mId 500 + ", mLabel=" 501 + mLabel 502 + ", mPendingIntent=" 503 + mPendingIntent 504 + ", mWillResolve=" 505 + mWillResolve 506 + ", mInFlight=" 507 + mInFlight 508 + ", mSuccessMessage=" 509 + mSuccessMessage 510 + '}'; 511 } 512 513 @Override describeContents()514 public int describeContents() { 515 return 0; 516 } 517 518 @Override writeToParcel(@onNull Parcel dest, int flags)519 public void writeToParcel(@NonNull Parcel dest, int flags) { 520 dest.writeString(mId); 521 TextUtils.writeToParcel(mLabel, dest, flags); 522 dest.writeTypedObject(mPendingIntent, flags); 523 dest.writeBoolean(mWillResolve); 524 dest.writeBoolean(mInFlight); 525 TextUtils.writeToParcel(mSuccessMessage, dest, flags); 526 } 527 528 /** Builder class for {@link Action}. */ 529 public static final class Builder { 530 531 @NonNull private String mId; 532 @NonNull private CharSequence mLabel; 533 @NonNull private PendingIntent mPendingIntent; 534 private boolean mWillResolve; 535 private boolean mInFlight; 536 @Nullable private CharSequence mSuccessMessage; 537 538 /** 539 * Creates a new {@link Builder} for an {@link Action}. 540 * 541 * @param id a unique ID for this action 542 * @param label a label describing this action 543 * @param pendingIntent a {@link PendingIntent} to be sent when this action is taken 544 */ Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)545 public Builder( 546 @NonNull String id, 547 @NonNull CharSequence label, 548 @NonNull PendingIntent pendingIntent) { 549 mId = requireNonNull(id); 550 mLabel = requireNonNull(label); 551 mPendingIntent = requireNonNull(pendingIntent); 552 } 553 554 /** Sets the ID of this {@link Action} */ 555 @NonNull setId(@onNull String id)556 public Builder setId(@NonNull String id) { 557 mId = requireNonNull(id); 558 return this; 559 } 560 561 /** Sets the label of this {@link Action}. */ 562 @NonNull setLabel(@onNull CharSequence label)563 public Builder setLabel(@NonNull CharSequence label) { 564 mLabel = requireNonNull(label); 565 return this; 566 } 567 568 /** Sets the {@link PendingIntent} to be sent when this {@link Action} is taken. */ 569 @NonNull setPendingIntent(@onNull PendingIntent pendingIntent)570 public Builder setPendingIntent(@NonNull PendingIntent pendingIntent) { 571 mPendingIntent = requireNonNull(pendingIntent); 572 return this; 573 } 574 575 /** 576 * Sets whether this action will resolve the issue when executed. Defaults to {@code 577 * false}. 578 * 579 * @see #willResolve() 580 */ 581 @SuppressLint("MissingGetterMatchingBuilder") 582 @NonNull setWillResolve(boolean willResolve)583 public Builder setWillResolve(boolean willResolve) { 584 mWillResolve = willResolve; 585 return this; 586 } 587 588 /** 589 * Sets a boolean that indicates whether this action is currently being executed (i.e. 590 * the user clicked on a button that triggered this action, and now the Safety Center is 591 * waiting for the action's result). Defaults to {@code false}. 592 * 593 * @see #isInFlight() 594 */ 595 @SuppressLint("MissingGetterMatchingBuilder") 596 @NonNull setIsInFlight(boolean inFlight)597 public Builder setIsInFlight(boolean inFlight) { 598 mInFlight = inFlight; 599 return this; 600 } 601 602 /** 603 * Sets or clears the optional success message to be displayed when this {@link Action} 604 * completes. 605 */ 606 @NonNull setSuccessMessage(@ullable CharSequence successMessage)607 public Builder setSuccessMessage(@Nullable CharSequence successMessage) { 608 mSuccessMessage = successMessage; 609 return this; 610 } 611 612 /** Creates the {@link Action} defined by this {@link Builder}. */ 613 @NonNull build()614 public Action build() { 615 return new Action( 616 mId, mLabel, mPendingIntent, mWillResolve, mInFlight, mSuccessMessage); 617 } 618 } 619 } 620 621 @IssueSeverityLevel validateIssueSeverityLevel(int value)622 private static int validateIssueSeverityLevel(int value) { 623 switch (value) { 624 case ISSUE_SEVERITY_LEVEL_OK: 625 case ISSUE_SEVERITY_LEVEL_RECOMMENDATION: 626 case ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING: 627 return value; 628 default: 629 } 630 throw new IllegalArgumentException( 631 String.format("Unexpected IssueSeverityLevel for SafetyCenterIssue: %s", value)); 632 } 633 } 634