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.config; 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.Objects.requireNonNull; 23 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.annotation.StringRes; 28 import android.annotation.SystemApi; 29 import android.content.res.Resources; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.util.ArraySet; 33 34 import androidx.annotation.RequiresApi; 35 36 import com.android.modules.utils.build.SdkLevel; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.List; 41 import java.util.Objects; 42 import java.util.Set; 43 44 /** 45 * Data class used to represent the initial configuration of a safety source. 46 * 47 * @hide 48 */ 49 @SystemApi 50 @RequiresApi(TIRAMISU) 51 public final class SafetySource implements Parcelable { 52 53 /** 54 * Static safety source. 55 * 56 * <p>A static safety source is a source completely defined in the Safety Center configuration. 57 * The source is displayed with no icon and neither the description displayed nor the tap 58 * behavior can be changed at runtime. A static safety source cannot have any issue associated 59 * with it. 60 */ 61 public static final int SAFETY_SOURCE_TYPE_STATIC = 1; 62 63 /** 64 * Dynamic safety source. 65 * 66 * <p>The status, description, tap behavior, and related issues of a dynamic safety source can 67 * be set at runtime by the package that owns the source. The source is displayed with an icon 68 * reflecting the status when part of a collapsible safety sources group. 69 */ 70 public static final int SAFETY_SOURCE_TYPE_DYNAMIC = 2; 71 72 /** 73 * Issue-only safety source. 74 * 75 * <p>An issue-only safety source is not displayed as an entry in the Safety Center page. The 76 * package that owns an issue-only safety source can set the list of issues associated with the 77 * source at runtime. 78 */ 79 public static final int SAFETY_SOURCE_TYPE_ISSUE_ONLY = 3; 80 81 /** 82 * All possible safety source types. 83 * 84 * @hide 85 */ 86 @IntDef( 87 prefix = {"SAFETY_SOURCE_TYPE_"}, 88 value = { 89 SAFETY_SOURCE_TYPE_STATIC, 90 SAFETY_SOURCE_TYPE_DYNAMIC, 91 SAFETY_SOURCE_TYPE_ISSUE_ONLY 92 }) 93 @Retention(RetentionPolicy.SOURCE) 94 public @interface SafetySourceType {} 95 96 /** Profile property unspecified. */ 97 public static final int PROFILE_NONE = 0; 98 99 /** 100 * Even when the active user has managed enabled profiles, a visible safety source will be 101 * displayed as a single entry for the primary profile. For dynamic sources, refresh requests 102 * will be sent to and set requests will be accepted from the primary profile only. 103 */ 104 public static final int PROFILE_PRIMARY = 1; 105 106 /** 107 * When the user has managed enabled profiles, a visible safety source will be displayed as 108 * multiple entries one for each enabled profile. For dynamic sources, refresh requests will be 109 * sent to and set requests will be accepted from all profiles. 110 */ 111 public static final int PROFILE_ALL = 2; 112 113 /** 114 * All possible profile configurations for a safety source. 115 * 116 * @hide 117 */ 118 @IntDef( 119 prefix = {"PROFILE_"}, 120 value = {PROFILE_NONE, PROFILE_PRIMARY, PROFILE_ALL}) 121 @Retention(RetentionPolicy.SOURCE) 122 public @interface Profile {} 123 124 /** 125 * The dynamic safety source will create an enabled entry in the Safety Center page until a set 126 * request is received. 127 */ 128 public static final int INITIAL_DISPLAY_STATE_ENABLED = 0; 129 130 /** 131 * The dynamic safety source will create a disabled entry in the Safety Center page until a set 132 * request is received. 133 */ 134 public static final int INITIAL_DISPLAY_STATE_DISABLED = 1; 135 136 /** 137 * The dynamic safety source will have no entry in the Safety Center page until a set request is 138 * received. 139 */ 140 public static final int INITIAL_DISPLAY_STATE_HIDDEN = 2; 141 142 /** 143 * All possible initial display states for a dynamic safety source. 144 * 145 * @hide 146 */ 147 @IntDef( 148 prefix = {"INITIAL_DISPLAY_STATE_"}, 149 value = { 150 INITIAL_DISPLAY_STATE_ENABLED, 151 INITIAL_DISPLAY_STATE_DISABLED, 152 INITIAL_DISPLAY_STATE_HIDDEN 153 }) 154 @Retention(RetentionPolicy.SOURCE) 155 public @interface InitialDisplayState {} 156 157 @NonNull 158 public static final Creator<SafetySource> CREATOR = 159 new Creator<SafetySource>() { 160 @Override 161 public SafetySource createFromParcel(Parcel in) { 162 int type = in.readInt(); 163 Builder builder = 164 new Builder(type) 165 .setId(in.readString()) 166 .setPackageName(in.readString()) 167 .setTitleResId(in.readInt()) 168 .setTitleForWorkResId(in.readInt()) 169 .setSummaryResId(in.readInt()) 170 .setIntentAction(in.readString()) 171 .setProfile(in.readInt()) 172 .setInitialDisplayState(in.readInt()) 173 .setMaxSeverityLevel(in.readInt()) 174 .setSearchTermsResId(in.readInt()) 175 .setLoggingAllowed(in.readBoolean()) 176 .setRefreshOnPageOpenAllowed(in.readBoolean()); 177 if (SdkLevel.isAtLeastU()) { 178 builder.setNotificationsAllowed(in.readBoolean()); 179 builder.setDeduplicationGroup(in.readString()); 180 List<String> certs = in.createStringArrayList(); 181 for (int i = 0; i < certs.size(); i++) { 182 builder.addPackageCertificateHash(certs.get(i)); 183 } 184 } 185 return builder.build(); 186 } 187 188 @Override 189 public SafetySource[] newArray(int size) { 190 return new SafetySource[size]; 191 } 192 }; 193 194 @SafetySourceType private final int mType; 195 @NonNull private final String mId; 196 @Nullable private final String mPackageName; 197 @StringRes private final int mTitleResId; 198 @StringRes private final int mTitleForWorkResId; 199 @StringRes private final int mSummaryResId; 200 @Nullable private final String mIntentAction; 201 @Profile private final int mProfile; 202 @InitialDisplayState private final int mInitialDisplayState; 203 private final int mMaxSeverityLevel; 204 @StringRes private final int mSearchTermsResId; 205 private final boolean mLoggingAllowed; 206 private final boolean mRefreshOnPageOpenAllowed; 207 private final boolean mNotificationsAllowed; 208 @Nullable final String mDeduplicationGroup; 209 @NonNull private final Set<String> mPackageCertificateHashes; 210 SafetySource( @afetySourceType int type, @NonNull String id, @Nullable String packageName, @StringRes int titleResId, @StringRes int titleForWorkResId, @StringRes int summaryResId, @Nullable String intentAction, @Profile int profile, @InitialDisplayState int initialDisplayState, int maxSeverityLevel, @StringRes int searchTermsResId, boolean loggingAllowed, boolean refreshOnPageOpenAllowed, boolean notificationsAllowed, @Nullable String deduplicationGroup, @NonNull Set<String> packageCertificateHashes)211 private SafetySource( 212 @SafetySourceType int type, 213 @NonNull String id, 214 @Nullable String packageName, 215 @StringRes int titleResId, 216 @StringRes int titleForWorkResId, 217 @StringRes int summaryResId, 218 @Nullable String intentAction, 219 @Profile int profile, 220 @InitialDisplayState int initialDisplayState, 221 int maxSeverityLevel, 222 @StringRes int searchTermsResId, 223 boolean loggingAllowed, 224 boolean refreshOnPageOpenAllowed, 225 boolean notificationsAllowed, 226 @Nullable String deduplicationGroup, 227 @NonNull Set<String> packageCertificateHashes) { 228 mType = type; 229 mId = id; 230 mPackageName = packageName; 231 mTitleResId = titleResId; 232 mTitleForWorkResId = titleForWorkResId; 233 mSummaryResId = summaryResId; 234 mIntentAction = intentAction; 235 mProfile = profile; 236 mInitialDisplayState = initialDisplayState; 237 mMaxSeverityLevel = maxSeverityLevel; 238 mSearchTermsResId = searchTermsResId; 239 mLoggingAllowed = loggingAllowed; 240 mRefreshOnPageOpenAllowed = refreshOnPageOpenAllowed; 241 mNotificationsAllowed = notificationsAllowed; 242 mDeduplicationGroup = deduplicationGroup; 243 mPackageCertificateHashes = Set.copyOf(packageCertificateHashes); 244 } 245 246 /** Returns the type of this safety source. */ 247 @SafetySourceType getType()248 public int getType() { 249 return mType; 250 } 251 252 /** 253 * Returns the id of this safety source. 254 * 255 * <p>The id is unique among safety sources in a Safety Center configuration. 256 */ 257 @NonNull getId()258 public String getId() { 259 return mId; 260 } 261 262 /** 263 * Returns the package name of this safety source. 264 * 265 * <p>This is the package that owns the source. The package will receive refresh requests, and 266 * it can send set requests for the source. The package is also used to create an explicit 267 * pending intent from the intent action in the package context. 268 * 269 * @throws UnsupportedOperationException if the source is of type {@link 270 * SafetySource#SAFETY_SOURCE_TYPE_STATIC} even if the optional package name field for the 271 * source is set, for sources of type {@link SafetySource#SAFETY_SOURCE_TYPE_STATIC} use 272 * {@link SafetySource#getOptionalPackageName()} 273 */ 274 @NonNull getPackageName()275 public String getPackageName() { 276 if (mType == SAFETY_SOURCE_TYPE_STATIC) { 277 throw new UnsupportedOperationException( 278 "getPackageName unsupported for static safety source"); 279 } 280 return mPackageName; 281 } 282 283 /** 284 * Returns the package name of this safety source or null if undefined. 285 * 286 * <p>This is the package that owns the source. 287 * 288 * <p>The package is always defined for sources of type dynamic and issue-only. The package will 289 * receive refresh requests, and it can send set requests for sources of type dynamic and 290 * issue-only. The package is also used to create an explicit pending intent in the package 291 * context from the intent action if defined. 292 * 293 * <p>The package is optional for sources of type static. If present, the package is used to 294 * create an explicit pending intent in the package context from the intent action. 295 */ 296 @Nullable 297 @RequiresApi(UPSIDE_DOWN_CAKE) getOptionalPackageName()298 public String getOptionalPackageName() { 299 if (!SdkLevel.isAtLeastU()) { 300 throw new UnsupportedOperationException(); 301 } 302 return mPackageName; 303 } 304 305 /** 306 * Returns the resource id of the title of this safety source. 307 * 308 * <p>The id refers to a string resource that is either accessible from any resource context or 309 * that is accessible from the same resource context that was used to load the Safety Center 310 * configuration. The id is {@link Resources#ID_NULL} when a title is not provided. 311 * 312 * @throws UnsupportedOperationException if the source is of type {@link 313 * SafetySource#SAFETY_SOURCE_TYPE_ISSUE_ONLY} 314 */ 315 @StringRes getTitleResId()316 public int getTitleResId() { 317 if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) { 318 throw new UnsupportedOperationException( 319 "getTitleResId unsupported for issue-only safety source"); 320 } 321 return mTitleResId; 322 } 323 324 /** 325 * Returns the resource id of the title for work of this safety source. 326 * 327 * <p>The id refers to a string resource that is either accessible from any resource context or 328 * that is accessible from the same resource context that was used to load the Safety Center 329 * configuration. The id is {@link Resources#ID_NULL} when a title for work is not provided. 330 * 331 * @throws UnsupportedOperationException if the source is of type {@link 332 * SafetySource#SAFETY_SOURCE_TYPE_ISSUE_ONLY} or if the profile property of the source is 333 * set to {@link SafetySource#PROFILE_PRIMARY} 334 */ 335 @StringRes getTitleForWorkResId()336 public int getTitleForWorkResId() { 337 if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) { 338 throw new UnsupportedOperationException( 339 "getTitleForWorkResId unsupported for issue-only safety source"); 340 } 341 if (mProfile == PROFILE_PRIMARY) { 342 throw new UnsupportedOperationException( 343 "getTitleForWorkResId unsupported for primary profile safety source"); 344 } 345 return mTitleForWorkResId; 346 } 347 348 /** 349 * Returns the resource id of the summary of this safety source. 350 * 351 * <p>The id refers to a string resource that is either accessible from any resource context or 352 * that is accessible from the same resource context that was used to load the Safety Center 353 * configuration. The id is {@link Resources#ID_NULL} when a summary is not provided. 354 * 355 * @throws UnsupportedOperationException if the source is of type {@link 356 * SafetySource#SAFETY_SOURCE_TYPE_ISSUE_ONLY} 357 */ 358 @StringRes getSummaryResId()359 public int getSummaryResId() { 360 if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) { 361 throw new UnsupportedOperationException( 362 "getSummaryResId unsupported for issue-only safety source"); 363 } 364 return mSummaryResId; 365 } 366 367 /** 368 * Returns the intent action of this safety source. 369 * 370 * <p>An intent created from the intent action should resolve to a public activity. If the 371 * source is displayed as an entry in the Safety Center page, and if the action is set to {@code 372 * null} or if it does not resolve to an activity the source will be marked as disabled. 373 * 374 * @throws UnsupportedOperationException if the source is of type {@link 375 * SafetySource#SAFETY_SOURCE_TYPE_ISSUE_ONLY} 376 */ 377 @Nullable getIntentAction()378 public String getIntentAction() { 379 if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) { 380 throw new UnsupportedOperationException( 381 "getIntentAction unsupported for issue-only safety source"); 382 } 383 return mIntentAction; 384 } 385 386 /** Returns the profile property of this safety source. */ 387 @Profile getProfile()388 public int getProfile() { 389 return mProfile; 390 } 391 392 /** 393 * Returns the initial display state of this safety source. 394 * 395 * @throws UnsupportedOperationException if the source is of type {@link 396 * SafetySource#SAFETY_SOURCE_TYPE_STATIC} or {@link 397 * SafetySource#SAFETY_SOURCE_TYPE_ISSUE_ONLY} 398 */ 399 @InitialDisplayState getInitialDisplayState()400 public int getInitialDisplayState() { 401 if (mType == SAFETY_SOURCE_TYPE_STATIC) { 402 throw new UnsupportedOperationException( 403 "getInitialDisplayState unsupported for static safety source"); 404 } 405 if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) { 406 throw new UnsupportedOperationException( 407 "getInitialDisplayState unsupported for issue-only safety source"); 408 } 409 return mInitialDisplayState; 410 } 411 412 /** 413 * Returns the maximum severity level of this safety source. 414 * 415 * <p>The maximum severity level dictates the maximum severity level values that can be used in 416 * the source status or the source issues when setting the source data at runtime. A source can 417 * always send a status severity level of at least {@link 418 * android.safetycenter.SafetySourceData#SEVERITY_LEVEL_INFORMATION} even if the maximum 419 * severity level is set to a lower value. 420 * 421 * @throws UnsupportedOperationException if the source is of type {@link 422 * SafetySource#SAFETY_SOURCE_TYPE_STATIC} 423 */ getMaxSeverityLevel()424 public int getMaxSeverityLevel() { 425 if (mType == SAFETY_SOURCE_TYPE_STATIC) { 426 throw new UnsupportedOperationException( 427 "getMaxSeverityLevel unsupported for static safety source"); 428 } 429 return mMaxSeverityLevel; 430 } 431 432 /** 433 * Returns the resource id of the search terms of this safety source. 434 * 435 * <p>The id refers to a string resource that is either accessible from any resource context or 436 * that is accessible from the same resource context that was used to load the Safety Center 437 * configuration. The id is {@link Resources#ID_NULL} when search terms are not provided. 438 * 439 * @throws UnsupportedOperationException if the source is of type {@link 440 * SafetySource#SAFETY_SOURCE_TYPE_ISSUE_ONLY} 441 */ 442 @StringRes getSearchTermsResId()443 public int getSearchTermsResId() { 444 if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) { 445 throw new UnsupportedOperationException( 446 "getSearchTermsResId unsupported for issue-only safety source"); 447 } 448 return mSearchTermsResId; 449 } 450 451 /** 452 * Returns the logging allowed property of this safety source. 453 * 454 * @throws UnsupportedOperationException if the source is of type {@link 455 * SafetySource#SAFETY_SOURCE_TYPE_STATIC} 456 */ isLoggingAllowed()457 public boolean isLoggingAllowed() { 458 if (mType == SAFETY_SOURCE_TYPE_STATIC) { 459 throw new UnsupportedOperationException( 460 "isLoggingAllowed unsupported for static safety source"); 461 } 462 return mLoggingAllowed; 463 } 464 465 /** 466 * Returns the refresh on page open allowed property of this safety source. 467 * 468 * <p>If set to {@code true}, a refresh request will be sent to the source when the Safety 469 * Center page is opened. 470 * 471 * @throws UnsupportedOperationException if the source is of type {@link 472 * SafetySource#SAFETY_SOURCE_TYPE_STATIC} 473 */ isRefreshOnPageOpenAllowed()474 public boolean isRefreshOnPageOpenAllowed() { 475 if (mType == SAFETY_SOURCE_TYPE_STATIC) { 476 throw new UnsupportedOperationException( 477 "isRefreshOnPageOpenAllowed unsupported for static safety source"); 478 } 479 return mRefreshOnPageOpenAllowed; 480 } 481 482 /** 483 * Returns whether Safety Center may post Notifications about issues reported by this {@link 484 * SafetySource}. 485 * 486 * @see Builder#setNotificationsAllowed(boolean) 487 */ 488 @RequiresApi(UPSIDE_DOWN_CAKE) areNotificationsAllowed()489 public boolean areNotificationsAllowed() { 490 if (!SdkLevel.isAtLeastU()) { 491 throw new UnsupportedOperationException(); 492 } 493 return mNotificationsAllowed; 494 } 495 496 /** 497 * Returns the deduplication group this source belongs to. 498 * 499 * <p>Sources which are part of the same deduplication group can coordinate to deduplicate their 500 * issues. 501 */ 502 @Nullable 503 @RequiresApi(UPSIDE_DOWN_CAKE) getDeduplicationGroup()504 public String getDeduplicationGroup() { 505 if (!SdkLevel.isAtLeastU()) { 506 throw new UnsupportedOperationException(); 507 } 508 return mDeduplicationGroup; 509 } 510 511 /** 512 * Returns a set of package certificate hashes representing valid signed packages that represent 513 * this {@link SafetySource}. 514 * 515 * <p>If one or more certificate hashes are set, Safety Center will validate that a package 516 * calling {@link android.safetycenter.SafetyCenterManager#setSafetySourceData} is signed with 517 * one of the certificates provided. 518 * 519 * <p>The default value is an empty {@code Set}, in which case only the package name is 520 * validated. 521 * 522 * @see Builder#addPackageCertificateHash(String) 523 */ 524 @NonNull 525 @RequiresApi(UPSIDE_DOWN_CAKE) getPackageCertificateHashes()526 public Set<String> getPackageCertificateHashes() { 527 if (!SdkLevel.isAtLeastU()) { 528 throw new UnsupportedOperationException(); 529 } 530 return mPackageCertificateHashes; 531 } 532 533 @Override equals(Object o)534 public boolean equals(Object o) { 535 if (this == o) return true; 536 if (!(o instanceof SafetySource)) return false; 537 SafetySource that = (SafetySource) o; 538 return mType == that.mType 539 && Objects.equals(mId, that.mId) 540 && Objects.equals(mPackageName, that.mPackageName) 541 && mTitleResId == that.mTitleResId 542 && mTitleForWorkResId == that.mTitleForWorkResId 543 && mSummaryResId == that.mSummaryResId 544 && Objects.equals(mIntentAction, that.mIntentAction) 545 && mProfile == that.mProfile 546 && mInitialDisplayState == that.mInitialDisplayState 547 && mMaxSeverityLevel == that.mMaxSeverityLevel 548 && mSearchTermsResId == that.mSearchTermsResId 549 && mLoggingAllowed == that.mLoggingAllowed 550 && mRefreshOnPageOpenAllowed == that.mRefreshOnPageOpenAllowed 551 && mNotificationsAllowed == that.mNotificationsAllowed 552 && Objects.equals(mDeduplicationGroup, that.mDeduplicationGroup) 553 && Objects.equals(mPackageCertificateHashes, that.mPackageCertificateHashes); 554 } 555 556 @Override hashCode()557 public int hashCode() { 558 return Objects.hash( 559 mType, 560 mId, 561 mPackageName, 562 mTitleResId, 563 mTitleForWorkResId, 564 mSummaryResId, 565 mIntentAction, 566 mProfile, 567 mInitialDisplayState, 568 mMaxSeverityLevel, 569 mSearchTermsResId, 570 mLoggingAllowed, 571 mRefreshOnPageOpenAllowed, 572 mNotificationsAllowed, 573 mDeduplicationGroup, 574 mPackageCertificateHashes); 575 } 576 577 @Override toString()578 public String toString() { 579 return "SafetySource{" 580 + "mType=" 581 + mType 582 + ", mId=" 583 + mId 584 + ", mPackageName=" 585 + mPackageName 586 + ", mTitleResId=" 587 + mTitleResId 588 + ", mTitleForWorkResId=" 589 + mTitleForWorkResId 590 + ", mSummaryResId=" 591 + mSummaryResId 592 + ", mIntentAction=" 593 + mIntentAction 594 + ", mProfile=" 595 + mProfile 596 + ", mInitialDisplayState=" 597 + mInitialDisplayState 598 + ", mMaxSeverityLevel=" 599 + mMaxSeverityLevel 600 + ", mSearchTermsResId=" 601 + mSearchTermsResId 602 + ", mLoggingAllowed=" 603 + mLoggingAllowed 604 + ", mRefreshOnPageOpenAllowed=" 605 + mRefreshOnPageOpenAllowed 606 + ", mNotificationsAllowed=" 607 + mNotificationsAllowed 608 + ", mDeduplicationGroup=" 609 + mDeduplicationGroup 610 + ", mPackageCertificateHashes=" 611 + mPackageCertificateHashes 612 + '}'; 613 } 614 615 @Override describeContents()616 public int describeContents() { 617 return 0; 618 } 619 620 @Override writeToParcel(@onNull Parcel dest, int flags)621 public void writeToParcel(@NonNull Parcel dest, int flags) { 622 dest.writeInt(mType); 623 dest.writeString(mId); 624 dest.writeString(mPackageName); 625 dest.writeInt(mTitleResId); 626 dest.writeInt(mTitleForWorkResId); 627 dest.writeInt(mSummaryResId); 628 dest.writeString(mIntentAction); 629 dest.writeInt(mProfile); 630 dest.writeInt(mInitialDisplayState); 631 dest.writeInt(mMaxSeverityLevel); 632 dest.writeInt(mSearchTermsResId); 633 dest.writeBoolean(mLoggingAllowed); 634 dest.writeBoolean(mRefreshOnPageOpenAllowed); 635 if (SdkLevel.isAtLeastU()) { 636 dest.writeBoolean(mNotificationsAllowed); 637 dest.writeString(mDeduplicationGroup); 638 dest.writeStringList(List.copyOf(mPackageCertificateHashes)); 639 } 640 } 641 642 /** Builder class for {@link SafetySource}. */ 643 public static final class Builder { 644 645 @SafetySourceType private final int mType; 646 @Nullable private String mId; 647 @Nullable private String mPackageName; 648 @Nullable @StringRes private Integer mTitleResId; 649 @Nullable @StringRes private Integer mTitleForWorkResId; 650 @Nullable @StringRes private Integer mSummaryResId; 651 @Nullable private String mIntentAction; 652 @Nullable @Profile private Integer mProfile; 653 @Nullable @InitialDisplayState private Integer mInitialDisplayState; 654 @Nullable private Integer mMaxSeverityLevel; 655 @Nullable @StringRes private Integer mSearchTermsResId; 656 @Nullable private Boolean mLoggingAllowed; 657 @Nullable private Boolean mRefreshOnPageOpenAllowed; 658 @Nullable private Boolean mNotificationsAllowed; 659 @Nullable private String mDeduplicationGroup; 660 @NonNull private final ArraySet<String> mPackageCertificateHashes = new ArraySet<>(); 661 662 /** Creates a {@link Builder} for a {@link SafetySource}. */ Builder(@afetySourceType int type)663 public Builder(@SafetySourceType int type) { 664 mType = type; 665 } 666 667 /** Creates a {@link Builder} with the values from the given {@link SafetySource}. */ 668 @RequiresApi(UPSIDE_DOWN_CAKE) Builder(@onNull SafetySource safetySource)669 public Builder(@NonNull SafetySource safetySource) { 670 if (!SdkLevel.isAtLeastU()) { 671 throw new UnsupportedOperationException(); 672 } 673 requireNonNull(safetySource); 674 mType = safetySource.mType; 675 mId = safetySource.mId; 676 mPackageName = safetySource.mPackageName; 677 mTitleResId = safetySource.mTitleResId; 678 mTitleForWorkResId = safetySource.mTitleForWorkResId; 679 mSummaryResId = safetySource.mSummaryResId; 680 mIntentAction = safetySource.mIntentAction; 681 mProfile = safetySource.mProfile; 682 mInitialDisplayState = safetySource.mInitialDisplayState; 683 mMaxSeverityLevel = safetySource.mMaxSeverityLevel; 684 mSearchTermsResId = safetySource.mSearchTermsResId; 685 mLoggingAllowed = safetySource.mLoggingAllowed; 686 mRefreshOnPageOpenAllowed = safetySource.mRefreshOnPageOpenAllowed; 687 mNotificationsAllowed = safetySource.mNotificationsAllowed; 688 mDeduplicationGroup = safetySource.mDeduplicationGroup; 689 mPackageCertificateHashes.addAll(safetySource.mPackageCertificateHashes); 690 } 691 692 /** 693 * Sets the id of this safety source. 694 * 695 * <p>The id must be unique among safety sources in a Safety Center configuration. 696 */ 697 @NonNull setId(@ullable String id)698 public Builder setId(@Nullable String id) { 699 mId = id; 700 return this; 701 } 702 703 /** 704 * Sets the package name of this safety source. 705 * 706 * <p>This is the package that owns the source. The package will receive refresh requests 707 * and it can send set requests for the source. 708 * 709 * <p>The package name is required for sources of type dynamic and issue-only. The package 710 * name is prohibited for sources of type static. 711 */ 712 @NonNull setPackageName(@ullable String packageName)713 public Builder setPackageName(@Nullable String packageName) { 714 mPackageName = packageName; 715 return this; 716 } 717 718 /** 719 * Sets the resource id of the title of this safety source. 720 * 721 * <p>The id must refer to a string resource that is either accessible from any resource 722 * context or that is accessible from the same resource context that was used to load the 723 * Safety Center config. The id defaults to {@link Resources#ID_NULL} when a title is not 724 * provided. 725 * 726 * <p>The title is required for sources of type static and for sources of type dynamic that 727 * are not hidden and that do not provide search terms. The title is prohibited for sources 728 * of type issue-only. 729 */ 730 @NonNull setTitleResId(@tringRes int titleResId)731 public Builder setTitleResId(@StringRes int titleResId) { 732 mTitleResId = titleResId; 733 return this; 734 } 735 736 /** 737 * Sets the resource id of the title for work of this safety source. 738 * 739 * <p>The id must refer to a string resource that is either accessible from any resource 740 * context or that is accessible from the same resource context that was used to load the 741 * Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when a title 742 * for work is not provided. 743 * 744 * <p>The title for work is required if the profile property of the source is set to {@link 745 * SafetySource#PROFILE_ALL} and either the source is of type static or the source is a 746 * source of type dynamic that is not hidden and that does not provide search terms. The 747 * title for work is prohibited for sources of type issue-only and if the profile property 748 * of the source is not set to {@link SafetySource#PROFILE_ALL}. 749 */ 750 @NonNull setTitleForWorkResId(@tringRes int titleForWorkResId)751 public Builder setTitleForWorkResId(@StringRes int titleForWorkResId) { 752 mTitleForWorkResId = titleForWorkResId; 753 return this; 754 } 755 756 /** 757 * Sets the resource id of the summary of this safety source. 758 * 759 * <p>The id must refer to a string resource that is either accessible from any resource 760 * context or that is accessible from the same resource context that was used to load the 761 * Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when a summary 762 * is not provided. 763 * 764 * <p>The summary is required for sources of type dynamic that are not hidden. The summary 765 * is prohibited for sources of type issue-only. 766 */ 767 @NonNull setSummaryResId(@tringRes int summaryResId)768 public Builder setSummaryResId(@StringRes int summaryResId) { 769 mSummaryResId = summaryResId; 770 return this; 771 } 772 773 /** 774 * Sets the intent action of this safety source. 775 * 776 * <p>An intent created from the intent action should resolve to a public activity. If the 777 * source is displayed as an entry in the Safety Center page, and if the action is set to 778 * {@code null} or if it does not resolve to an activity the source will be marked as 779 * disabled. 780 * 781 * <p>The intent action is required for sources of type static and for sources of type 782 * dynamic that are enabled. The intent action is prohibited for sources of type issue-only. 783 */ 784 @NonNull setIntentAction(@ullable String intentAction)785 public Builder setIntentAction(@Nullable String intentAction) { 786 mIntentAction = intentAction; 787 return this; 788 } 789 790 /** 791 * Sets the profile property of this safety source. 792 * 793 * <p>The profile property is explicitly required for all source types. 794 */ 795 @NonNull setProfile(@rofile int profile)796 public Builder setProfile(@Profile int profile) { 797 mProfile = profile; 798 return this; 799 } 800 801 /** 802 * Sets the initial display state of this safety source. 803 * 804 * <p>The initial display state is prohibited for sources of type static and issue-only. 805 */ 806 @NonNull setInitialDisplayState(@nitialDisplayState int initialDisplayState)807 public Builder setInitialDisplayState(@InitialDisplayState int initialDisplayState) { 808 mInitialDisplayState = initialDisplayState; 809 return this; 810 } 811 812 /** 813 * Sets the maximum severity level of this safety source. 814 * 815 * <p>The maximum severity level dictates the maximum severity level values that can be used 816 * in the source status or the source issues when setting the source data at runtime. A 817 * source can always send a status severity level of at least {@link 818 * android.safetycenter.SafetySourceData#SEVERITY_LEVEL_INFORMATION} even if the maximum 819 * severity level is set to a lower value. 820 * 821 * <p>The maximum severity level is prohibited for sources of type static. 822 */ 823 @NonNull setMaxSeverityLevel(int maxSeverityLevel)824 public Builder setMaxSeverityLevel(int maxSeverityLevel) { 825 mMaxSeverityLevel = maxSeverityLevel; 826 return this; 827 } 828 829 /** 830 * Sets the resource id of the search terms of this safety source. 831 * 832 * <p>The id must refer to a string resource that is either accessible from any resource 833 * context or that is accessible from the same resource context that was used to load the 834 * Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when search 835 * terms are not provided. 836 * 837 * <p>The search terms are prohibited for sources of type issue-only. 838 */ 839 @NonNull setSearchTermsResId(@tringRes int searchTermsResId)840 public Builder setSearchTermsResId(@StringRes int searchTermsResId) { 841 mSearchTermsResId = searchTermsResId; 842 return this; 843 } 844 845 /** 846 * Sets the logging allowed property of this safety source. 847 * 848 * <p>The logging allowed property defaults to {@code true}. 849 * 850 * <p>The logging allowed property is prohibited for sources of type static. 851 */ 852 @NonNull setLoggingAllowed(boolean loggingAllowed)853 public Builder setLoggingAllowed(boolean loggingAllowed) { 854 mLoggingAllowed = loggingAllowed; 855 return this; 856 } 857 858 /** 859 * Sets the refresh on page open allowed property of this safety source. 860 * 861 * <p>If set to {@code true}, a refresh request will be sent to the source when the Safety 862 * Center page is opened. The refresh on page open allowed property defaults to {@code 863 * false}. 864 * 865 * <p>The refresh on page open allowed property is prohibited for sources of type static. 866 */ 867 @NonNull setRefreshOnPageOpenAllowed(boolean refreshOnPageOpenAllowed)868 public Builder setRefreshOnPageOpenAllowed(boolean refreshOnPageOpenAllowed) { 869 mRefreshOnPageOpenAllowed = refreshOnPageOpenAllowed; 870 return this; 871 } 872 873 /** 874 * Sets the {@link #areNotificationsAllowed()} property of this {@link SafetySource}. 875 * 876 * <p>If set to {@code true} Safety Center may post Notifications about issues reported by 877 * this source. 878 * 879 * <p>The default value is {@code false}. 880 * 881 * @see #areNotificationsAllowed() 882 */ 883 @NonNull 884 @RequiresApi(UPSIDE_DOWN_CAKE) setNotificationsAllowed(boolean notificationsAllowed)885 public Builder setNotificationsAllowed(boolean notificationsAllowed) { 886 if (!SdkLevel.isAtLeastU()) { 887 throw new UnsupportedOperationException(); 888 } 889 mNotificationsAllowed = notificationsAllowed; 890 return this; 891 } 892 893 /** 894 * Sets the deduplication group for this source. 895 * 896 * <p>Sources which are part of the same deduplication group can coordinate to deduplicate 897 * issues that they're sending to SafetyCenter by providing the same deduplication 898 * identifier with those issues. 899 * 900 * <p>The deduplication group property is prohibited for sources of type static. 901 */ 902 @NonNull 903 @RequiresApi(UPSIDE_DOWN_CAKE) setDeduplicationGroup(@ullable String deduplicationGroup)904 public Builder setDeduplicationGroup(@Nullable String deduplicationGroup) { 905 if (!SdkLevel.isAtLeastU()) { 906 throw new UnsupportedOperationException(); 907 } 908 mDeduplicationGroup = deduplicationGroup; 909 return this; 910 } 911 912 /** 913 * Adds a package certificate hash to the {@link #getPackageCertificateHashes()} property of 914 * this {@link SafetySource}. 915 * 916 * @see #getPackageCertificateHashes() 917 */ 918 @NonNull 919 @RequiresApi(UPSIDE_DOWN_CAKE) addPackageCertificateHash(@onNull String packageCertificateHash)920 public Builder addPackageCertificateHash(@NonNull String packageCertificateHash) { 921 if (!SdkLevel.isAtLeastU()) { 922 throw new UnsupportedOperationException(); 923 } 924 mPackageCertificateHashes.add(packageCertificateHash); 925 return this; 926 } 927 928 /** 929 * Creates the {@link SafetySource} defined by this {@link Builder}. 930 * 931 * <p>Throws an {@link IllegalStateException} if any constraint on the safety source is 932 * violated. 933 */ 934 @NonNull build()935 public SafetySource build() { 936 int type = mType; 937 if (type != SAFETY_SOURCE_TYPE_STATIC 938 && type != SAFETY_SOURCE_TYPE_DYNAMIC 939 && type != SAFETY_SOURCE_TYPE_ISSUE_ONLY) { 940 throw new IllegalStateException("Unexpected type"); 941 } 942 boolean isStatic = type == SAFETY_SOURCE_TYPE_STATIC; 943 boolean isDynamic = type == SAFETY_SOURCE_TYPE_DYNAMIC; 944 boolean isIssueOnly = type == SAFETY_SOURCE_TYPE_ISSUE_ONLY; 945 946 String id = mId; 947 BuilderUtils.validateId(id, "id", true, false); 948 949 String packageName = mPackageName; 950 BuilderUtils.validateAttribute( 951 packageName, 952 "packageName", 953 isDynamic || isIssueOnly, 954 isStatic && !SdkLevel.isAtLeastU()); 955 956 int initialDisplayState = 957 BuilderUtils.validateIntDef( 958 mInitialDisplayState, 959 "initialDisplayState", 960 false, 961 isStatic || isIssueOnly, 962 INITIAL_DISPLAY_STATE_ENABLED, 963 INITIAL_DISPLAY_STATE_ENABLED, 964 INITIAL_DISPLAY_STATE_DISABLED, 965 INITIAL_DISPLAY_STATE_HIDDEN); 966 boolean isEnabled = initialDisplayState == INITIAL_DISPLAY_STATE_ENABLED; 967 boolean isHidden = initialDisplayState == INITIAL_DISPLAY_STATE_HIDDEN; 968 boolean isDynamicNotHidden = isDynamic && !isHidden; 969 970 int profile = 971 BuilderUtils.validateIntDef( 972 mProfile, 973 "profile", 974 true, 975 false, 976 PROFILE_NONE, 977 PROFILE_PRIMARY, 978 PROFILE_ALL); 979 boolean hasWork = profile == PROFILE_ALL; 980 981 int searchTermsResId = 982 BuilderUtils.validateResId( 983 mSearchTermsResId, "searchTerms", false, isIssueOnly); 984 boolean isDynamicHiddenWithSearch = 985 isDynamic && isHidden && searchTermsResId != Resources.ID_NULL; 986 987 boolean titleRequired = isDynamicNotHidden || isDynamicHiddenWithSearch || isStatic; 988 int titleResId = 989 BuilderUtils.validateResId(mTitleResId, "title", titleRequired, isIssueOnly); 990 991 int titleForWorkResId = 992 BuilderUtils.validateResId( 993 mTitleForWorkResId, 994 "titleForWork", 995 hasWork && titleRequired, 996 !hasWork || isIssueOnly); 997 998 int summaryResId = 999 BuilderUtils.validateResId( 1000 mSummaryResId, "summary", isDynamicNotHidden, isIssueOnly); 1001 1002 String intentAction = mIntentAction; 1003 BuilderUtils.validateAttribute( 1004 intentAction, 1005 "intentAction", 1006 (isDynamic && isEnabled) || isStatic, 1007 isIssueOnly); 1008 1009 int maxSeverityLevel = 1010 BuilderUtils.validateInteger( 1011 mMaxSeverityLevel, 1012 "maxSeverityLevel", 1013 false, 1014 isStatic, 1015 Integer.MAX_VALUE); 1016 1017 boolean loggingAllowed = 1018 BuilderUtils.validateBoolean( 1019 mLoggingAllowed, "loggingAllowed", false, isStatic, true); 1020 1021 boolean refreshOnPageOpenAllowed = 1022 BuilderUtils.validateBoolean( 1023 mRefreshOnPageOpenAllowed, 1024 "refreshOnPageOpenAllowed", 1025 false, 1026 isStatic, 1027 false); 1028 1029 String deduplicationGroup = mDeduplicationGroup; 1030 boolean notificationsAllowed = false; 1031 Set<String> packageCertificateHashes = Set.copyOf(mPackageCertificateHashes); 1032 if (SdkLevel.isAtLeastU()) { 1033 notificationsAllowed = 1034 BuilderUtils.validateBoolean( 1035 mNotificationsAllowed, 1036 "notificationsAllowed", 1037 false, 1038 isStatic, 1039 false); 1040 1041 BuilderUtils.validateAttribute( 1042 deduplicationGroup, "deduplicationGroup", false, isStatic); 1043 BuilderUtils.validateCollection( 1044 packageCertificateHashes, "packageCertificateHashes", false, isStatic); 1045 } 1046 1047 return new SafetySource( 1048 type, 1049 id, 1050 packageName, 1051 titleResId, 1052 titleForWorkResId, 1053 summaryResId, 1054 intentAction, 1055 profile, 1056 initialDisplayState, 1057 maxSeverityLevel, 1058 searchTermsResId, 1059 loggingAllowed, 1060 refreshOnPageOpenAllowed, 1061 notificationsAllowed, 1062 deduplicationGroup, 1063 packageCertificateHashes); 1064 } 1065 } 1066 } 1067