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 21 import static com.android.internal.util.Preconditions.checkArgument; 22 23 import static java.util.Collections.unmodifiableList; 24 import static java.util.Objects.requireNonNull; 25 26 import android.annotation.IntDef; 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.annotation.SuppressLint; 30 import android.annotation.SystemApi; 31 import android.app.PendingIntent; 32 import android.os.Parcel; 33 import android.os.Parcelable; 34 import android.text.TextUtils; 35 36 import androidx.annotation.RequiresApi; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Objects; 43 44 /** 45 * Data for a safety source issue in the Safety Center page. 46 * 47 * <p>An issue represents an actionable matter relating to a particular safety source. 48 * 49 * <p>The safety issue will contain localized messages to be shown in UI explaining the potential 50 * threat or warning and suggested fixes, as well as actions a user is allowed to take from the UI 51 * to resolve the issue. 52 * 53 * @hide 54 */ 55 @SystemApi 56 @RequiresApi(TIRAMISU) 57 public final class SafetySourceIssue implements Parcelable { 58 59 /** Indicates that the risk associated with the issue is related to a user's device safety. */ 60 public static final int ISSUE_CATEGORY_DEVICE = 100; 61 62 /** Indicates that the risk associated with the issue is related to a user's account safety. */ 63 public static final int ISSUE_CATEGORY_ACCOUNT = 200; 64 65 /** Indicates that the risk associated with the issue is related to a user's general safety. */ 66 public static final int ISSUE_CATEGORY_GENERAL = 300; 67 68 /** 69 * All possible issue categories. 70 * 71 * <p>An issue's category represents a specific area of safety that the issue relates to. 72 * 73 * <p>An issue can only have one associated category. If the issue relates to multiple areas of 74 * safety, then choose the closest area or default to {@link #ISSUE_CATEGORY_GENERAL}. 75 * 76 * @hide 77 * @see Builder#setIssueCategory(int) 78 */ 79 @IntDef( 80 prefix = {"ISSUE_CATEGORY_"}, 81 value = { 82 ISSUE_CATEGORY_DEVICE, 83 ISSUE_CATEGORY_ACCOUNT, 84 ISSUE_CATEGORY_GENERAL, 85 }) 86 @Retention(RetentionPolicy.SOURCE) 87 public @interface IssueCategory {} 88 89 @NonNull 90 public static final Creator<SafetySourceIssue> CREATOR = 91 new Creator<SafetySourceIssue>() { 92 @Override 93 public SafetySourceIssue 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 int severityLevel = in.readInt(); 99 int issueCategory = in.readInt(); 100 List<Action> actions = requireNonNull(in.createTypedArrayList(Action.CREATOR)); 101 PendingIntent onDismissPendingIntent = 102 in.readTypedObject(PendingIntent.CREATOR); 103 String issueTypeId = in.readString(); 104 Builder builder = 105 new Builder(id, title, summary, severityLevel, issueTypeId) 106 .setSubtitle(subtitle) 107 .setIssueCategory(issueCategory) 108 .setOnDismissPendingIntent(onDismissPendingIntent); 109 for (int i = 0; i < actions.size(); i++) { 110 builder.addAction(actions.get(i)); 111 } 112 return builder.build(); 113 } 114 115 @Override 116 public SafetySourceIssue[] newArray(int size) { 117 return new SafetySourceIssue[size]; 118 } 119 }; 120 121 @NonNull private final String mId; 122 @NonNull private final CharSequence mTitle; 123 @Nullable private final CharSequence mSubtitle; 124 @NonNull private final CharSequence mSummary; 125 @SafetySourceData.SeverityLevel private final int mSeverityLevel; 126 private final List<Action> mActions; 127 @Nullable private final PendingIntent mOnDismissPendingIntent; 128 @IssueCategory private final int mIssueCategory; 129 @NonNull private final String mIssueTypeId; 130 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)131 private SafetySourceIssue( 132 @NonNull String id, 133 @NonNull CharSequence title, 134 @Nullable CharSequence subtitle, 135 @NonNull CharSequence summary, 136 @SafetySourceData.SeverityLevel int severityLevel, 137 @IssueCategory int issueCategory, 138 @NonNull List<Action> actions, 139 @Nullable PendingIntent onDismissPendingIntent, 140 @NonNull String issueTypeId) { 141 this.mId = id; 142 this.mTitle = title; 143 this.mSubtitle = subtitle; 144 this.mSummary = summary; 145 this.mSeverityLevel = severityLevel; 146 this.mIssueCategory = issueCategory; 147 this.mActions = actions; 148 this.mOnDismissPendingIntent = onDismissPendingIntent; 149 this.mIssueTypeId = issueTypeId; 150 } 151 152 /** 153 * Returns the identifier for this issue. 154 * 155 * <p>This id should uniquely identify the safety risk represented by this issue. Safety issues 156 * will be deduped by this id to be shown in the UI. 157 * 158 * <p>On multiple instances of providing the same issue to be represented in Safety Center, 159 * provide the same id across all instances. 160 */ 161 @NonNull getId()162 public String getId() { 163 return mId; 164 } 165 166 /** Returns the localized title of the issue to be displayed in the UI. */ 167 @NonNull getTitle()168 public CharSequence getTitle() { 169 return mTitle; 170 } 171 172 /** Returns the localized subtitle of the issue to be displayed in the UI. */ 173 @Nullable getSubtitle()174 public CharSequence getSubtitle() { 175 return mSubtitle; 176 } 177 178 /** Returns the localized summary of the issue to be displayed in the UI. */ 179 @NonNull getSummary()180 public CharSequence getSummary() { 181 return mSummary; 182 } 183 184 /** Returns the {@link SafetySourceData.SeverityLevel} of the issue. */ 185 @SafetySourceData.SeverityLevel getSeverityLevel()186 public int getSeverityLevel() { 187 return mSeverityLevel; 188 } 189 190 /** 191 * Returns the category of the risk associated with the issue. 192 * 193 * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. 194 */ 195 @IssueCategory getIssueCategory()196 public int getIssueCategory() { 197 return mIssueCategory; 198 } 199 200 /** 201 * Returns a list of {@link Action}s representing actions supported in the UI for this issue. 202 * 203 * <p>Each issue must contain at least one action, in order to help the user resolve the issue. 204 * 205 * <p>In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most 206 * two actions supported from the UI. 207 */ 208 @NonNull getActions()209 public List<Action> getActions() { 210 return mActions; 211 } 212 213 /** 214 * Returns the optional {@link PendingIntent} that will be invoked when an issue is dismissed. 215 * 216 * <p>When a safety issue is dismissed in Safety Center page, the issue is removed from view in 217 * Safety Center page. This method returns an additional optional action specified by the safety 218 * source that should be invoked on issue dismissal. The action contained in the {@link 219 * PendingIntent} cannot start an activity. 220 */ 221 @Nullable getOnDismissPendingIntent()222 public PendingIntent getOnDismissPendingIntent() { 223 return mOnDismissPendingIntent; 224 } 225 226 /** 227 * Returns the identifier for the type of this issue. 228 * 229 * <p>The issue type should indicate the underlying basis for the issue, for e.g. a pending 230 * update or a disabled security feature. 231 * 232 * <p>The difference between this id and {@link #getId()} is that the issue type id is meant to 233 * be used for logging and should therefore contain no personally identifiable information (PII) 234 * (e.g. for account name). 235 * 236 * <p>On multiple instances of providing the same issue to be represented in Safety Center, 237 * provide the same issue type id across all instances. 238 */ 239 @NonNull getIssueTypeId()240 public String getIssueTypeId() { 241 return mIssueTypeId; 242 } 243 244 @Override describeContents()245 public int describeContents() { 246 return 0; 247 } 248 249 @Override writeToParcel(@onNull Parcel dest, int flags)250 public void writeToParcel(@NonNull Parcel dest, int flags) { 251 dest.writeString(mId); 252 TextUtils.writeToParcel(mTitle, dest, flags); 253 TextUtils.writeToParcel(mSubtitle, dest, flags); 254 TextUtils.writeToParcel(mSummary, dest, flags); 255 dest.writeInt(mSeverityLevel); 256 dest.writeInt(mIssueCategory); 257 dest.writeTypedList(mActions); 258 dest.writeTypedObject(mOnDismissPendingIntent, flags); 259 dest.writeString(mIssueTypeId); 260 } 261 262 @Override equals(Object o)263 public boolean equals(Object o) { 264 if (this == o) return true; 265 if (!(o instanceof SafetySourceIssue)) return false; 266 SafetySourceIssue that = (SafetySourceIssue) o; 267 return mSeverityLevel == that.mSeverityLevel 268 && TextUtils.equals(mId, that.mId) 269 && TextUtils.equals(mTitle, that.mTitle) 270 && TextUtils.equals(mSubtitle, that.mSubtitle) 271 && TextUtils.equals(mSummary, that.mSummary) 272 && mIssueCategory == that.mIssueCategory 273 && mActions.equals(that.mActions) 274 && Objects.equals(mOnDismissPendingIntent, that.mOnDismissPendingIntent) 275 && TextUtils.equals(mIssueTypeId, that.mIssueTypeId); 276 } 277 278 @Override hashCode()279 public int hashCode() { 280 return Objects.hash( 281 mId, 282 mTitle, 283 mSubtitle, 284 mSummary, 285 mSeverityLevel, 286 mIssueCategory, 287 mActions, 288 mOnDismissPendingIntent, 289 mIssueTypeId); 290 } 291 292 @Override toString()293 public String toString() { 294 return "SafetySourceIssue{" 295 + "mId=" 296 + mId 297 + "mTitle=" 298 + mTitle 299 + ", mSubtitle=" 300 + mSubtitle 301 + ", mSummary=" 302 + mSummary 303 + ", mSeverityLevel=" 304 + mSeverityLevel 305 + ", mIssueCategory=" 306 + mIssueCategory 307 + ", mActions=" 308 + mActions 309 + ", mOnDismissPendingIntent=" 310 + mOnDismissPendingIntent 311 + ", mIssueTypeId=" 312 + mIssueTypeId 313 + '}'; 314 } 315 316 /** 317 * Data for an action supported from a safety issue {@link SafetySourceIssue} in the Safety 318 * Center page. 319 * 320 * <p>The purpose of the action is to allow the user to address the safety issue, either by 321 * performing a fix suggested in the issue, or by navigating the user to the source of the issue 322 * where they can be exposed to detail about the issue and further suggestions to resolve it. 323 * 324 * <p>The user will be allowed to invoke the action from the UI by clicking on a UI element and 325 * consequently resolve the issue. 326 * 327 * @hide 328 */ 329 @SystemApi 330 public static final class Action implements Parcelable { 331 332 @NonNull 333 public static final Creator<Action> CREATOR = 334 new Creator<Action>() { 335 @Override 336 public Action createFromParcel(Parcel in) { 337 String id = in.readString(); 338 CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 339 PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR); 340 return new Builder(id, label, pendingIntent) 341 .setWillResolve(in.readBoolean()) 342 .setSuccessMessage( 343 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)) 344 .build(); 345 } 346 347 @Override 348 public Action[] newArray(int size) { 349 return new Action[size]; 350 } 351 }; 352 353 @NonNull private final String mId; 354 @NonNull private final CharSequence mLabel; 355 @NonNull private final PendingIntent mPendingIntent; 356 private final boolean mWillResolve; 357 @Nullable private final CharSequence mSuccessMessage; 358 Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, @Nullable CharSequence successMessage)359 private Action( 360 @NonNull String id, 361 @NonNull CharSequence label, 362 @NonNull PendingIntent pendingIntent, 363 boolean willResolve, 364 @Nullable CharSequence successMessage) { 365 mId = id; 366 mLabel = label; 367 mPendingIntent = pendingIntent; 368 mWillResolve = willResolve; 369 mSuccessMessage = successMessage; 370 } 371 372 /** 373 * Returns the ID of the action, unique among actions in a given {@link SafetySourceIssue}. 374 */ 375 @NonNull getId()376 public String getId() { 377 return mId; 378 } 379 380 /** 381 * Returns the localized label of the action to be displayed in the UI. 382 * 383 * <p>The label should indicate what action will be performed if when invoked. 384 */ 385 @NonNull getLabel()386 public CharSequence getLabel() { 387 return mLabel; 388 } 389 390 /** 391 * Returns a {@link PendingIntent} to be fired when the action is clicked on. 392 * 393 * <p>The {@link PendingIntent} should perform the action referred to by {@link 394 * #getLabel()}. 395 */ 396 @NonNull getPendingIntent()397 public PendingIntent getPendingIntent() { 398 return mPendingIntent; 399 } 400 401 /** 402 * Returns whether invoking this action will fix or address the issue sufficiently for it to 403 * be considered resolved i.e. the issue will no longer need to be conveyed to the user in 404 * the UI. 405 */ willResolve()406 public boolean willResolve() { 407 return mWillResolve; 408 } 409 410 /** 411 * Returns the optional localized message to be displayed in the UI when the action is 412 * invoked and completes successfully. 413 */ 414 @Nullable getSuccessMessage()415 public CharSequence getSuccessMessage() { 416 return mSuccessMessage; 417 } 418 419 @Override describeContents()420 public int describeContents() { 421 return 0; 422 } 423 424 @Override writeToParcel(@onNull Parcel dest, int flags)425 public void writeToParcel(@NonNull Parcel dest, int flags) { 426 dest.writeString(mId); 427 TextUtils.writeToParcel(mLabel, dest, flags); 428 dest.writeTypedObject(mPendingIntent, flags); 429 dest.writeBoolean(mWillResolve); 430 TextUtils.writeToParcel(mSuccessMessage, dest, flags); 431 } 432 433 @Override equals(Object o)434 public boolean equals(Object o) { 435 if (this == o) return true; 436 if (!(o instanceof Action)) return false; 437 Action that = (Action) o; 438 return mId.equals(that.mId) 439 && TextUtils.equals(mLabel, that.mLabel) 440 && mPendingIntent.equals(that.mPendingIntent) 441 && mWillResolve == that.mWillResolve 442 && TextUtils.equals(mSuccessMessage, that.mSuccessMessage); 443 } 444 445 @Override hashCode()446 public int hashCode() { 447 return Objects.hash(mId, mLabel, mPendingIntent, mWillResolve, mSuccessMessage); 448 } 449 450 @Override toString()451 public String toString() { 452 return "Action{" 453 + "mId=" 454 + mId 455 + ", mLabel=" 456 + mLabel 457 + ", mPendingIntent=" 458 + mPendingIntent 459 + ", mWillResolve=" 460 + mWillResolve 461 + ", mSuccessMessage=" 462 + mSuccessMessage 463 + '}'; 464 } 465 466 /** Builder class for {@link Action}. */ 467 public static final class Builder { 468 469 @NonNull private final String mId; 470 @NonNull private final CharSequence mLabel; 471 @NonNull private final PendingIntent mPendingIntent; 472 private boolean mWillResolve = false; 473 @Nullable private CharSequence mSuccessMessage; 474 475 /** Creates a {@link Builder} for an {@link Action}. */ Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)476 public Builder( 477 @NonNull String id, 478 @NonNull CharSequence label, 479 @NonNull PendingIntent pendingIntent) { 480 mId = requireNonNull(id); 481 mLabel = requireNonNull(label); 482 mPendingIntent = requireNonNull(pendingIntent); 483 } 484 485 /** 486 * Sets whether the action will resolve the safety issue. Defaults to {@code false}. 487 * 488 * @see #willResolve() 489 */ 490 @SuppressLint("MissingGetterMatchingBuilder") 491 @NonNull setWillResolve(boolean willResolve)492 public Builder setWillResolve(boolean willResolve) { 493 mWillResolve = willResolve; 494 return this; 495 } 496 497 /** 498 * Sets the optional localized message to be displayed in the UI when the action is 499 * invoked and completes successfully. 500 */ 501 @NonNull setSuccessMessage(@ullable CharSequence successMessage)502 public Builder setSuccessMessage(@Nullable CharSequence successMessage) { 503 mSuccessMessage = successMessage; 504 return this; 505 } 506 507 /** Creates the {@link Action} defined by this {@link Builder}. */ 508 @NonNull build()509 public Action build() { 510 return new Action(mId, mLabel, mPendingIntent, mWillResolve, mSuccessMessage); 511 } 512 } 513 } 514 515 /** Builder class for {@link SafetySourceIssue}. */ 516 public static final class Builder { 517 518 @NonNull private final String mId; 519 @NonNull private final CharSequence mTitle; 520 @NonNull private final CharSequence mSummary; 521 @SafetySourceData.SeverityLevel private final int mSeverityLevel; 522 @NonNull private final String mIssueTypeId; 523 private final List<Action> mActions = new ArrayList<>(); 524 525 @Nullable private CharSequence mSubtitle; 526 @IssueCategory private int mIssueCategory = ISSUE_CATEGORY_GENERAL; 527 @Nullable private PendingIntent mOnDismissPendingIntent; 528 529 /** 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)530 public Builder( 531 @NonNull String id, 532 @NonNull CharSequence title, 533 @NonNull CharSequence summary, 534 @SafetySourceData.SeverityLevel int severityLevel, 535 @NonNull String issueTypeId) { 536 this.mId = requireNonNull(id); 537 this.mTitle = requireNonNull(title); 538 this.mSummary = requireNonNull(summary); 539 this.mSeverityLevel = validateSeverityLevel(severityLevel); 540 this.mIssueTypeId = requireNonNull(issueTypeId); 541 } 542 543 /** Sets the localized subtitle. */ 544 @NonNull setSubtitle(@ullable CharSequence subtitle)545 public Builder setSubtitle(@Nullable CharSequence subtitle) { 546 mSubtitle = subtitle; 547 return this; 548 } 549 550 /** 551 * Sets the category of the risk associated with the issue. 552 * 553 * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. 554 */ 555 @NonNull setIssueCategory(@ssueCategory int issueCategory)556 public Builder setIssueCategory(@IssueCategory int issueCategory) { 557 mIssueCategory = validateIssueCategory(issueCategory); 558 return this; 559 } 560 561 /** Adds data for an {@link Action} to be shown in UI. */ 562 @NonNull addAction(@onNull Action actionData)563 public Builder addAction(@NonNull Action actionData) { 564 mActions.add(requireNonNull(actionData)); 565 return this; 566 } 567 568 /** Clears data for all the {@link Action}s that were added to this {@link Builder}. */ 569 @NonNull clearActions()570 public Builder clearActions() { 571 mActions.clear(); 572 return this; 573 } 574 575 /** 576 * Sets an optional {@link PendingIntent} to be invoked when an issue is dismissed from the 577 * UI. 578 * 579 * <p>In particular, if the source would like to be notified of issue dismissals in Safety 580 * Center in order to be able to dismiss or ignore issues at the source, then set this 581 * field. The action contained in the {@link PendingIntent} must not start an activity. 582 * 583 * @see #getOnDismissPendingIntent() 584 */ 585 @NonNull setOnDismissPendingIntent(@ullable PendingIntent onDismissPendingIntent)586 public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) { 587 checkArgument( 588 onDismissPendingIntent == null || !onDismissPendingIntent.isActivity(), 589 "Safety source issue on dismiss pending intent must not start an activity"); 590 mOnDismissPendingIntent = onDismissPendingIntent; 591 return this; 592 } 593 594 /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */ 595 @NonNull build()596 public SafetySourceIssue build() { 597 List<SafetySourceIssue.Action> actions = unmodifiableList(new ArrayList<>(mActions)); 598 checkArgument(!actions.isEmpty(), "Safety source issue must contain at least 1 action"); 599 checkArgument( 600 actions.size() <= 2, 601 "Safety source issue must not contain more than 2 actions"); 602 return new SafetySourceIssue( 603 mId, 604 mTitle, 605 mSubtitle, 606 mSummary, 607 mSeverityLevel, 608 mIssueCategory, 609 actions, 610 mOnDismissPendingIntent, 611 mIssueTypeId); 612 } 613 } 614 615 @SafetySourceData.SeverityLevel validateSeverityLevel(int value)616 private static int validateSeverityLevel(int value) { 617 switch (value) { 618 case SafetySourceData.SEVERITY_LEVEL_INFORMATION: 619 case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: 620 case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: 621 return value; 622 case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: 623 throw new IllegalArgumentException( 624 "SeverityLevel for SafetySourceIssue must not be " 625 + "SEVERITY_LEVEL_UNSPECIFIED"); 626 default: 627 } 628 throw new IllegalArgumentException( 629 String.format("Unexpected SeverityLevel for SafetySourceIssue: %s", value)); 630 } 631 632 @IssueCategory validateIssueCategory(int value)633 private static int validateIssueCategory(int value) { 634 switch (value) { 635 case ISSUE_CATEGORY_DEVICE: 636 case ISSUE_CATEGORY_ACCOUNT: 637 case ISSUE_CATEGORY_GENERAL: 638 return value; 639 default: 640 } 641 throw new IllegalArgumentException( 642 String.format("Unexpected IssueCategory for SafetySourceIssue: %s", value)); 643 } 644 } 645