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