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