• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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