1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.safetycenter;
18 
19 import static android.os.Build.VERSION_CODES.TIRAMISU;
20 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
21 
22 import static com.android.internal.util.Preconditions.checkArgument;
23 
24 import static java.util.Collections.unmodifiableList;
25 import static java.util.Objects.requireNonNull;
26 
27 import android.annotation.IntDef;
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.annotation.SuppressLint;
31 import android.annotation.SystemApi;
32 import android.annotation.TargetApi;
33 import android.app.PendingIntent;
34 import android.os.Build;
35 import android.os.Parcel;
36 import android.os.Parcelable;
37 import android.text.TextUtils;
38 
39 import androidx.annotation.RequiresApi;
40 
41 import com.android.modules.utils.build.SdkLevel;
42 
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 import java.util.ArrayList;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Objects;
49 import java.util.Set;
50 
51 /**
52  * Data for a safety source issue in the Safety Center page.
53  *
54  * <p>An issue represents an actionable matter relating to a particular safety source.
55  *
56  * <p>The safety issue will contain localized messages to be shown in UI explaining the potential
57  * threat or warning and suggested fixes, as well as actions a user is allowed to take from the UI
58  * to resolve the issue.
59  *
60  * @hide
61  */
62 @SystemApi
63 @RequiresApi(TIRAMISU)
64 public final class SafetySourceIssue implements Parcelable {
65 
66     /** Indicates that the risk associated with the issue is related to a user's device safety. */
67     public static final int ISSUE_CATEGORY_DEVICE = 100;
68 
69     /** Indicates that the risk associated with the issue is related to a user's account safety. */
70     public static final int ISSUE_CATEGORY_ACCOUNT = 200;
71 
72     /**
73      * Indicates that the risk associated with the issue is related to a user's general safety.
74      *
75      * <p>This is the default. It is a generic value used when the category is not known or is not
76      * relevant.
77      */
78     public static final int ISSUE_CATEGORY_GENERAL = 300;
79 
80     /** Indicates that the risk associated with the issue is related to a user's data. */
81     @RequiresApi(UPSIDE_DOWN_CAKE)
82     public static final int ISSUE_CATEGORY_DATA = 400;
83 
84     /** Indicates that the risk associated with the issue is related to a user's passwords. */
85     @RequiresApi(UPSIDE_DOWN_CAKE)
86     public static final int ISSUE_CATEGORY_PASSWORDS = 500;
87 
88     /** Indicates that the risk associated with the issue is related to a user's personal safety. */
89     @RequiresApi(UPSIDE_DOWN_CAKE)
90     public static final int ISSUE_CATEGORY_PERSONAL_SAFETY = 600;
91 
92     /**
93      * All possible issue categories.
94      *
95      * <p>An issue's category represents a specific area of safety that the issue relates to.
96      *
97      * <p>An issue can only have one associated category. If the issue relates to multiple areas of
98      * safety, then choose the closest area or default to {@link #ISSUE_CATEGORY_GENERAL}.
99      *
100      * @hide
101      * @see Builder#setIssueCategory(int)
102      */
103     @IntDef(
104             prefix = {"ISSUE_CATEGORY_"},
105             value = {
106                 ISSUE_CATEGORY_DEVICE,
107                 ISSUE_CATEGORY_ACCOUNT,
108                 ISSUE_CATEGORY_GENERAL,
109                 ISSUE_CATEGORY_DATA,
110                 ISSUE_CATEGORY_PASSWORDS,
111                 ISSUE_CATEGORY_PERSONAL_SAFETY
112             })
113     @Retention(RetentionPolicy.SOURCE)
114     @TargetApi(UPSIDE_DOWN_CAKE)
115     public @interface IssueCategory {}
116 
117     /** Value signifying that the source has not specified a particular notification behavior. */
118     @RequiresApi(UPSIDE_DOWN_CAKE)
119     public static final int NOTIFICATION_BEHAVIOR_UNSPECIFIED = 0;
120 
121     /** An issue which Safety Center should never notify the user about. */
122     @RequiresApi(UPSIDE_DOWN_CAKE)
123     public static final int NOTIFICATION_BEHAVIOR_NEVER = 100;
124 
125     /**
126      * An issue which Safety Center may notify the user about after a delay if it has not been
127      * resolved. Safety Center does not provide any guarantee about the duration of the delay.
128      */
129     @RequiresApi(UPSIDE_DOWN_CAKE)
130     public static final int NOTIFICATION_BEHAVIOR_DELAYED = 200;
131 
132     /** An issue which Safety Center may notify the user about immediately. */
133     @RequiresApi(UPSIDE_DOWN_CAKE)
134     public static final int NOTIFICATION_BEHAVIOR_IMMEDIATELY = 300;
135 
136     /**
137      * All possible notification behaviors.
138      *
139      * <p>The notification behavior of a {@link SafetySourceIssue} determines if and when Safety
140      * Center should notify the user about it.
141      *
142      * @hide
143      * @see Builder#setNotificationBehavior(int)
144      */
145     @IntDef(
146             prefix = {"NOTIFICATION_BEHAVIOR_"},
147             value = {
148                 NOTIFICATION_BEHAVIOR_UNSPECIFIED,
149                 NOTIFICATION_BEHAVIOR_NEVER,
150                 NOTIFICATION_BEHAVIOR_DELAYED,
151                 NOTIFICATION_BEHAVIOR_IMMEDIATELY
152             })
153     @Retention(RetentionPolicy.SOURCE)
154     @TargetApi(UPSIDE_DOWN_CAKE)
155     public @interface NotificationBehavior {}
156 
157     /**
158      * An issue which requires manual user input to be resolved.
159      *
160      * <p>This is the default.
161      */
162     @RequiresApi(UPSIDE_DOWN_CAKE)
163     public static final int ISSUE_ACTIONABILITY_MANUAL = 0;
164 
165     /**
166      * An issue which is just a "tip" and may not require any user input.
167      *
168      * <p>It is still possible to provide {@link Action}s to e.g. "learn more" about it or
169      * acknowledge it.
170      */
171     @RequiresApi(UPSIDE_DOWN_CAKE)
172     public static final int ISSUE_ACTIONABILITY_TIP = 100;
173 
174     /**
175      * An issue which has already been actioned and may not require any user input.
176      *
177      * <p>It is still possible to provide {@link Action}s to e.g. "learn more" about it or
178      * acknowledge it.
179      */
180     @RequiresApi(UPSIDE_DOWN_CAKE)
181     public static final int ISSUE_ACTIONABILITY_AUTOMATIC = 200;
182 
183     /**
184      * All possible issue actionability.
185      *
186      * <p>An issue's actionability represent what action is expected from the user as a result of
187      * showing them this issue.
188      *
189      * <p>If the user needs to manually resolve it; this is typically achieved using an {@link
190      * Action} (e.g. by resolving the issue directly through the Safety Center screen, or by
191      * navigating to another page).
192      *
193      * <p>If the issue does not need to be resolved manually by the user, it is possible not to
194      * provide any {@link Action}. However, this may still be desirable to e.g. to "learn more"
195      * about it or acknowledge it.
196      *
197      * @hide
198      * @see Builder#setIssueActionability(int)
199      */
200     @IntDef(
201             prefix = {"ISSUE_ACTIONABILITY_"},
202             value = {
203                 ISSUE_ACTIONABILITY_MANUAL,
204                 ISSUE_ACTIONABILITY_TIP,
205                 ISSUE_ACTIONABILITY_AUTOMATIC
206             })
207     @Retention(RetentionPolicy.SOURCE)
208     @TargetApi(UPSIDE_DOWN_CAKE)
209     public @interface IssueActionability {}
210 
211     @NonNull
212     public static final Creator<SafetySourceIssue> CREATOR =
213             new Creator<SafetySourceIssue>() {
214                 @Override
215                 public SafetySourceIssue createFromParcel(Parcel in) {
216                     String id = in.readString();
217                     CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
218                     CharSequence subtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
219                     CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
220                     int severityLevel = in.readInt();
221                     int issueCategory = in.readInt();
222                     List<Action> actions = requireNonNull(in.createTypedArrayList(Action.CREATOR));
223                     PendingIntent onDismissPendingIntent =
224                             in.readTypedObject(PendingIntent.CREATOR);
225                     String issueTypeId = in.readString();
226                     Builder builder =
227                             new Builder(id, title, summary, severityLevel, issueTypeId)
228                                     .setSubtitle(subtitle)
229                                     .setIssueCategory(issueCategory)
230                                     .setOnDismissPendingIntent(onDismissPendingIntent);
231                     for (int i = 0; i < actions.size(); i++) {
232                         builder.addAction(actions.get(i));
233                     }
234                     if (SdkLevel.isAtLeastU()) {
235                         builder.setCustomNotification(in.readTypedObject(Notification.CREATOR));
236                         builder.setNotificationBehavior(in.readInt());
237                         builder.setAttributionTitle(
238                                 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in));
239                         builder.setDeduplicationId(in.readString());
240                         builder.setIssueActionability(in.readInt());
241                     }
242                     return builder.build();
243                 }
244 
245                 @Override
246                 public SafetySourceIssue[] newArray(int size) {
247                     return new SafetySourceIssue[size];
248                 }
249             };
250 
251     @NonNull private final String mId;
252     @NonNull private final CharSequence mTitle;
253     @Nullable private final CharSequence mSubtitle;
254     @NonNull private final CharSequence mSummary;
255     @SafetySourceData.SeverityLevel private final int mSeverityLevel;
256     private final List<Action> mActions;
257     @Nullable private final PendingIntent mOnDismissPendingIntent;
258     @IssueCategory private final int mIssueCategory;
259     @NonNull private final String mIssueTypeId;
260     @Nullable private final Notification mCustomNotification;
261     @NotificationBehavior private final int mNotificationBehavior;
262     @Nullable private final CharSequence mAttributionTitle;
263     @Nullable private final String mDeduplicationId;
264     @IssueActionability private final int mIssueActionability;
265 
SafetySourceIssue( @onNull String id, @NonNull CharSequence title, @Nullable CharSequence subtitle, @NonNull CharSequence summary, @SafetySourceData.SeverityLevel int severityLevel, @IssueCategory int issueCategory, @NonNull List<Action> actions, @Nullable PendingIntent onDismissPendingIntent, @NonNull String issueTypeId, @Nullable Notification customNotification, @NotificationBehavior int notificationBehavior, @Nullable CharSequence attributionTitle, @Nullable String deduplicationId, @IssueActionability int issueActionability)266     private SafetySourceIssue(
267             @NonNull String id,
268             @NonNull CharSequence title,
269             @Nullable CharSequence subtitle,
270             @NonNull CharSequence summary,
271             @SafetySourceData.SeverityLevel int severityLevel,
272             @IssueCategory int issueCategory,
273             @NonNull List<Action> actions,
274             @Nullable PendingIntent onDismissPendingIntent,
275             @NonNull String issueTypeId,
276             @Nullable Notification customNotification,
277             @NotificationBehavior int notificationBehavior,
278             @Nullable CharSequence attributionTitle,
279             @Nullable String deduplicationId,
280             @IssueActionability int issueActionability) {
281         this.mId = id;
282         this.mTitle = title;
283         this.mSubtitle = subtitle;
284         this.mSummary = summary;
285         this.mSeverityLevel = severityLevel;
286         this.mIssueCategory = issueCategory;
287         this.mActions = actions;
288         this.mOnDismissPendingIntent = onDismissPendingIntent;
289         this.mIssueTypeId = issueTypeId;
290         this.mCustomNotification = customNotification;
291         this.mNotificationBehavior = notificationBehavior;
292         this.mAttributionTitle = attributionTitle;
293         this.mDeduplicationId = deduplicationId;
294         this.mIssueActionability = issueActionability;
295     }
296 
297     /**
298      * Returns the identifier for this issue.
299      *
300      * <p>This id should uniquely identify the safety risk represented by this issue. Safety issues
301      * will be deduped by this id to be shown in the UI.
302      *
303      * <p>On multiple instances of providing the same issue to be represented in Safety Center,
304      * provide the same id across all instances.
305      */
306     @NonNull
getId()307     public String getId() {
308         return mId;
309     }
310 
311     /** Returns the localized title of the issue to be displayed in the UI. */
312     @NonNull
getTitle()313     public CharSequence getTitle() {
314         return mTitle;
315     }
316 
317     /** Returns the localized subtitle of the issue to be displayed in the UI. */
318     @Nullable
getSubtitle()319     public CharSequence getSubtitle() {
320         return mSubtitle;
321     }
322 
323     /** Returns the localized summary of the issue to be displayed in the UI. */
324     @NonNull
getSummary()325     public CharSequence getSummary() {
326         return mSummary;
327     }
328 
329     /**
330      * Returns the localized attribution title of the issue to be displayed in the UI.
331      *
332      * <p>This is displayed in the UI and helps to attribute issue cards to a particular source. If
333      * this value is {@code null}, the title of the group that contains the Safety Source will be
334      * used.
335      */
336     @Nullable
337     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
getAttributionTitle()338     public CharSequence getAttributionTitle() {
339         if (!SdkLevel.isAtLeastU()) {
340             throw new UnsupportedOperationException(
341                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
342         }
343         return mAttributionTitle;
344     }
345 
346     /** Returns the {@link SafetySourceData.SeverityLevel} of the issue. */
347     @SafetySourceData.SeverityLevel
getSeverityLevel()348     public int getSeverityLevel() {
349         return mSeverityLevel;
350     }
351 
352     /**
353      * Returns the category of the risk associated with the issue.
354      *
355      * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}.
356      */
357     @IssueCategory
getIssueCategory()358     public int getIssueCategory() {
359         return mIssueCategory;
360     }
361 
362     /**
363      * Returns a list of {@link Action}s representing actions supported in the UI for this issue.
364      *
365      * <p>Each issue must contain at least one action, in order to help the user resolve the issue.
366      *
367      * <p>In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most
368      * two actions supported from the UI.
369      */
370     @NonNull
getActions()371     public List<Action> getActions() {
372         return mActions;
373     }
374 
375     /**
376      * Returns the optional {@link PendingIntent} that will be invoked when an issue is dismissed.
377      *
378      * <p>When a safety issue is dismissed in Safety Center page, the issue is removed from view in
379      * Safety Center page. This method returns an additional optional action specified by the safety
380      * source that should be invoked on issue dismissal. The action contained in the {@link
381      * PendingIntent} cannot start an activity.
382      */
383     @Nullable
getOnDismissPendingIntent()384     public PendingIntent getOnDismissPendingIntent() {
385         return mOnDismissPendingIntent;
386     }
387 
388     /**
389      * Returns the identifier for the type of this issue.
390      *
391      * <p>The issue type should indicate the underlying basis for the issue, for e.g. a pending
392      * update or a disabled security feature.
393      *
394      * <p>The difference between this id and {@link #getId()} is that the issue type id is meant to
395      * be used for logging and should therefore contain no personally identifiable information (PII)
396      * (e.g. for account name).
397      *
398      * <p>On multiple instances of providing the same issue to be represented in Safety Center,
399      * provide the same issue type id across all instances.
400      */
401     @NonNull
getIssueTypeId()402     public String getIssueTypeId() {
403         return mIssueTypeId;
404     }
405 
406     /**
407      * Returns the optional custom {@link Notification} for this issue which overrides the title,
408      * text and actions for any {@link android.app.Notification} generated for this {@link
409      * SafetySourceIssue}.
410      *
411      * <p>Safety Center may still generate a default notification from the other details of this
412      * issue when no custom notification has been set. See {@link #getNotificationBehavior()} for
413      * details
414      *
415      * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification
416      * @see #getNotificationBehavior()
417      */
418     @Nullable
419     @RequiresApi(UPSIDE_DOWN_CAKE)
getCustomNotification()420     public Notification getCustomNotification() {
421         if (!SdkLevel.isAtLeastU()) {
422             throw new UnsupportedOperationException(
423                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
424         }
425         return mCustomNotification;
426     }
427 
428     /**
429      * Returns the {@link NotificationBehavior} for this issue which determines if and when Safety
430      * Center will post a notification for this issue.
431      *
432      * <p>Any notification will be based on the {@link #getCustomNotification()} if set, or the
433      * other properties of this issue otherwise.
434      *
435      * <ul>
436      *   <li>If {@link #NOTIFICATION_BEHAVIOR_IMMEDIATELY} then Safety Center will immediately
437      *       create and post a notification
438      *   <li>If {@link #NOTIFICATION_BEHAVIOR_DELAYED} then a notification will only be posted after
439      *       a delay, if this issue has not been resolved.
440      *   <li>If {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED} then a notification may or may not be
441      *       posted, the exact behavior is defined by Safety Center.
442      *   <li>If {@link #NOTIFICATION_BEHAVIOR_NEVER} Safety Center will never post a notification
443      *       about this issue. Sources should specify this behavior when they wish to handle their
444      *       own notifications. When this behavior is set sources should not set a custom
445      *       notification.
446      * </ul>
447      *
448      * @see Builder#setNotificationBehavior(int)
449      */
450     @NotificationBehavior
451     @RequiresApi(UPSIDE_DOWN_CAKE)
getNotificationBehavior()452     public int getNotificationBehavior() {
453         if (!SdkLevel.isAtLeastU()) {
454             throw new UnsupportedOperationException(
455                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
456         }
457         return mNotificationBehavior;
458     }
459 
460     /**
461      * Returns the identifier used to deduplicate this issue against other issues with the same
462      * deduplication identifiers.
463      *
464      * <p>Deduplication identifier will be used to identify duplicate issues. This identifier
465      * applies across all safety sources which are part of the same deduplication group.
466      * Deduplication groups can be set, for each source, in the SafetyCenter config. Therefore, two
467      * issues are considered duplicate if their sources are part of the same deduplication group and
468      * they have the same deduplication identifier.
469      *
470      * <p>Out of all issues that are found to be duplicates, only one will be shown in the UI (the
471      * one with the highest severity, or in case of same severities, the one placed highest in the
472      * config).
473      *
474      * <p>Expected usage implies different sources will coordinate to set the same deduplication
475      * identifiers on issues that they want to deduplicate.
476      *
477      * <p>This shouldn't be a default mechanism for deduplication of issues. Most of the time
478      * sources should coordinate or communicate to only send the issue from one of them. That would
479      * also allow sources to choose which one will be displaying the issue, instead of depending on
480      * severity and config order. This API should only be needed if for some reason this isn't
481      * possible, for example, when sources can't communicate with each other and/or send issues at
482      * different times and/or issues can be of different severities.
483      */
484     @Nullable
485     @RequiresApi(UPSIDE_DOWN_CAKE)
getDeduplicationId()486     public String getDeduplicationId() {
487         if (!SdkLevel.isAtLeastU()) {
488             throw new UnsupportedOperationException(
489                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
490         }
491         return mDeduplicationId;
492     }
493 
494     /**
495      * Returns the {@link IssueActionability} for this issue which determines what type of action is
496      * required from the user:
497      *
498      * <ul>
499      *   <li>If {@link #ISSUE_ACTIONABILITY_MANUAL} then user input is required to resolve the issue
500      *   <li>If {@link #ISSUE_ACTIONABILITY_TIP} then the user needs to review this issue as a tip
501      *       to improve their overall safety, and possibly acknowledge it
502      *   <li>If {@link #ISSUE_ACTIONABILITY_AUTOMATIC} then the user needs to review this issue as
503      *       something that has been resolved on their behalf, and possibly acknowledge it
504      * </ul>
505      *
506      * @see Builder#setIssueActionability(int)
507      */
508     @IssueActionability
509     @RequiresApi(UPSIDE_DOWN_CAKE)
getIssueActionability()510     public int getIssueActionability() {
511         if (!SdkLevel.isAtLeastU()) {
512             throw new UnsupportedOperationException(
513                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
514         }
515         return mIssueActionability;
516     }
517 
518     @Override
describeContents()519     public int describeContents() {
520         return 0;
521     }
522 
523     @Override
writeToParcel(@onNull Parcel dest, int flags)524     public void writeToParcel(@NonNull Parcel dest, int flags) {
525         dest.writeString(mId);
526         TextUtils.writeToParcel(mTitle, dest, flags);
527         TextUtils.writeToParcel(mSubtitle, dest, flags);
528         TextUtils.writeToParcel(mSummary, dest, flags);
529         dest.writeInt(mSeverityLevel);
530         dest.writeInt(mIssueCategory);
531         dest.writeTypedList(mActions);
532         dest.writeTypedObject(mOnDismissPendingIntent, flags);
533         dest.writeString(mIssueTypeId);
534         if (SdkLevel.isAtLeastU()) {
535             dest.writeTypedObject(mCustomNotification, flags);
536             dest.writeInt(mNotificationBehavior);
537             TextUtils.writeToParcel(mAttributionTitle, dest, flags);
538             dest.writeString(mDeduplicationId);
539             dest.writeInt(mIssueActionability);
540         }
541     }
542 
543     @Override
equals(Object o)544     public boolean equals(Object o) {
545         if (this == o) return true;
546         if (!(o instanceof SafetySourceIssue)) return false;
547         SafetySourceIssue that = (SafetySourceIssue) o;
548         return mSeverityLevel == that.mSeverityLevel
549                 && TextUtils.equals(mId, that.mId)
550                 && TextUtils.equals(mTitle, that.mTitle)
551                 && TextUtils.equals(mSubtitle, that.mSubtitle)
552                 && TextUtils.equals(mSummary, that.mSummary)
553                 && mIssueCategory == that.mIssueCategory
554                 && mActions.equals(that.mActions)
555                 && Objects.equals(mOnDismissPendingIntent, that.mOnDismissPendingIntent)
556                 && TextUtils.equals(mIssueTypeId, that.mIssueTypeId)
557                 && Objects.equals(mCustomNotification, that.mCustomNotification)
558                 && mNotificationBehavior == that.mNotificationBehavior
559                 && TextUtils.equals(mAttributionTitle, that.mAttributionTitle)
560                 && TextUtils.equals(mDeduplicationId, that.mDeduplicationId)
561                 && mIssueActionability == that.mIssueActionability;
562     }
563 
564     @Override
hashCode()565     public int hashCode() {
566         return Objects.hash(
567                 mId,
568                 mTitle,
569                 mSubtitle,
570                 mSummary,
571                 mSeverityLevel,
572                 mIssueCategory,
573                 mActions,
574                 mOnDismissPendingIntent,
575                 mIssueTypeId,
576                 mCustomNotification,
577                 mNotificationBehavior,
578                 mAttributionTitle,
579                 mDeduplicationId,
580                 mIssueActionability);
581     }
582 
583     @Override
toString()584     public String toString() {
585         return "SafetySourceIssue{"
586                 + "mId="
587                 + mId
588                 + "mTitle="
589                 + mTitle
590                 + ", mSubtitle="
591                 + mSubtitle
592                 + ", mSummary="
593                 + mSummary
594                 + ", mSeverityLevel="
595                 + mSeverityLevel
596                 + ", mIssueCategory="
597                 + mIssueCategory
598                 + ", mActions="
599                 + mActions
600                 + ", mOnDismissPendingIntent="
601                 + mOnDismissPendingIntent
602                 + ", mIssueTypeId="
603                 + mIssueTypeId
604                 + ", mCustomNotification="
605                 + mCustomNotification
606                 + ", mNotificationBehavior="
607                 + mNotificationBehavior
608                 + ", mAttributionTitle="
609                 + mAttributionTitle
610                 + ", mDeduplicationId="
611                 + mDeduplicationId
612                 + ", mIssueActionability="
613                 + mIssueActionability
614                 + '}';
615     }
616 
617     /**
618      * Data for an action supported from a safety issue {@link SafetySourceIssue} in the Safety
619      * Center page.
620      *
621      * <p>The purpose of the action is to allow the user to address the safety issue, either by
622      * performing a fix suggested in the issue, or by navigating the user to the source of the issue
623      * where they can be exposed to detail about the issue and further suggestions to resolve it.
624      *
625      * <p>The user will be allowed to invoke the action from the UI by clicking on a UI element and
626      * consequently resolve the issue.
627      */
628     public static final class Action implements Parcelable {
629 
630         @NonNull
631         public static final Creator<Action> CREATOR =
632                 new Creator<Action>() {
633                     @Override
634                     public Action createFromParcel(Parcel in) {
635                         String id = in.readString();
636                         CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
637                         PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
638                         Builder builder =
639                                 new Builder(id, label, pendingIntent)
640                                         .setWillResolve(in.readBoolean())
641                                         .setSuccessMessage(
642                                                 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(
643                                                         in));
644                         if (SdkLevel.isAtLeastU()) {
645                             ConfirmationDialogDetails confirmationDialogDetails =
646                                     in.readTypedObject(ConfirmationDialogDetails.CREATOR);
647                             builder.setConfirmationDialogDetails(confirmationDialogDetails);
648                         }
649                         return builder.build();
650                     }
651 
652                     @Override
653                     public Action[] newArray(int size) {
654                         return new Action[size];
655                     }
656                 };
657 
enforceUniqueActionIds( @onNull List<SafetySourceIssue.Action> actions, @NonNull String message)658         private static void enforceUniqueActionIds(
659                 @NonNull List<SafetySourceIssue.Action> actions, @NonNull String message) {
660             Set<String> actionIds = new HashSet<>();
661             for (int i = 0; i < actions.size(); i++) {
662                 SafetySourceIssue.Action action = actions.get(i);
663 
664                 String actionId = action.getId();
665                 checkArgument(!actionIds.contains(actionId), message);
666                 actionIds.add(actionId);
667             }
668         }
669 
670         @NonNull private final String mId;
671         @NonNull private final CharSequence mLabel;
672         @NonNull private final PendingIntent mPendingIntent;
673         private final boolean mWillResolve;
674         @Nullable private final CharSequence mSuccessMessage;
675         @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails;
676 
Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, @Nullable CharSequence successMessage, @Nullable ConfirmationDialogDetails confirmationDialogDetails)677         private Action(
678                 @NonNull String id,
679                 @NonNull CharSequence label,
680                 @NonNull PendingIntent pendingIntent,
681                 boolean willResolve,
682                 @Nullable CharSequence successMessage,
683                 @Nullable ConfirmationDialogDetails confirmationDialogDetails) {
684             mId = id;
685             mLabel = label;
686             mPendingIntent = pendingIntent;
687             mWillResolve = willResolve;
688             mSuccessMessage = successMessage;
689             mConfirmationDialogDetails = confirmationDialogDetails;
690         }
691 
692         /**
693          * Returns the ID of the action, unique among actions in a given {@link SafetySourceIssue}.
694          */
695         @NonNull
getId()696         public String getId() {
697             return mId;
698         }
699 
700         /**
701          * Returns the localized label of the action to be displayed in the UI.
702          *
703          * <p>The label should indicate what action will be performed if when invoked.
704          */
705         @NonNull
getLabel()706         public CharSequence getLabel() {
707             return mLabel;
708         }
709 
710         /**
711          * Returns a {@link PendingIntent} to be fired when the action is clicked on.
712          *
713          * <p>The {@link PendingIntent} should perform the action referred to by {@link
714          * #getLabel()}.
715          */
716         @NonNull
getPendingIntent()717         public PendingIntent getPendingIntent() {
718             return mPendingIntent;
719         }
720 
721         /**
722          * Returns whether invoking this action will fix or address the issue sufficiently for it to
723          * be considered resolved i.e. the issue will no longer need to be conveyed to the user in
724          * the UI.
725          */
willResolve()726         public boolean willResolve() {
727             return mWillResolve;
728         }
729 
730         /**
731          * Returns the optional localized message to be displayed in the UI when the action is
732          * invoked and completes successfully.
733          */
734         @Nullable
getSuccessMessage()735         public CharSequence getSuccessMessage() {
736             return mSuccessMessage;
737         }
738 
739         /**
740          * Returns the optional data to be displayed in the confirmation dialog prior to launching
741          * the {@link PendingIntent} when the action is clicked on.
742          */
743         @Nullable
744         @RequiresApi(UPSIDE_DOWN_CAKE)
getConfirmationDialogDetails()745         public ConfirmationDialogDetails getConfirmationDialogDetails() {
746             if (!SdkLevel.isAtLeastU()) {
747                 throw new UnsupportedOperationException(
748                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
749             }
750             return mConfirmationDialogDetails;
751         }
752 
753         @Override
describeContents()754         public int describeContents() {
755             return 0;
756         }
757 
758         @Override
writeToParcel(@onNull Parcel dest, int flags)759         public void writeToParcel(@NonNull Parcel dest, int flags) {
760             dest.writeString(mId);
761             TextUtils.writeToParcel(mLabel, dest, flags);
762             dest.writeTypedObject(mPendingIntent, flags);
763             dest.writeBoolean(mWillResolve);
764             TextUtils.writeToParcel(mSuccessMessage, dest, flags);
765             if (SdkLevel.isAtLeastU()) {
766                 dest.writeTypedObject(mConfirmationDialogDetails, flags);
767             }
768         }
769 
770         @Override
equals(Object o)771         public boolean equals(Object o) {
772             if (this == o) return true;
773             if (!(o instanceof Action)) return false;
774             Action that = (Action) o;
775             return mId.equals(that.mId)
776                     && TextUtils.equals(mLabel, that.mLabel)
777                     && mPendingIntent.equals(that.mPendingIntent)
778                     && mWillResolve == that.mWillResolve
779                     && TextUtils.equals(mSuccessMessage, that.mSuccessMessage)
780                     && Objects.equals(mConfirmationDialogDetails, that.mConfirmationDialogDetails);
781         }
782 
783         @Override
hashCode()784         public int hashCode() {
785             return Objects.hash(
786                     mId,
787                     mLabel,
788                     mPendingIntent,
789                     mWillResolve,
790                     mSuccessMessage,
791                     mConfirmationDialogDetails);
792         }
793 
794         @Override
toString()795         public String toString() {
796             return "Action{"
797                     + "mId="
798                     + mId
799                     + ", mLabel="
800                     + mLabel
801                     + ", mPendingIntent="
802                     + mPendingIntent
803                     + ", mWillResolve="
804                     + mWillResolve
805                     + ", mSuccessMessage="
806                     + mSuccessMessage
807                     + ", mConfirmationDialogDetails="
808                     + mConfirmationDialogDetails
809                     + '}';
810         }
811 
812         /** Data for an action confirmation dialog to be shown before action is executed. */
813         @RequiresApi(UPSIDE_DOWN_CAKE)
814         public static final class ConfirmationDialogDetails implements Parcelable {
815 
816             @NonNull
817             public static final Creator<ConfirmationDialogDetails> CREATOR =
818                     new Creator<ConfirmationDialogDetails>() {
819                         @Override
820                         public ConfirmationDialogDetails createFromParcel(Parcel in) {
821                             CharSequence title =
822                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
823                             CharSequence text =
824                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
825                             CharSequence acceptButtonText =
826                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
827                             CharSequence denyButtonText =
828                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
829                             return new ConfirmationDialogDetails(
830                                     title, text, acceptButtonText, denyButtonText);
831                         }
832 
833                         @Override
834                         public ConfirmationDialogDetails[] newArray(int size) {
835                             return new ConfirmationDialogDetails[size];
836                         }
837                     };
838 
839             @NonNull private final CharSequence mTitle;
840             @NonNull private final CharSequence mText;
841             @NonNull private final CharSequence mAcceptButtonText;
842             @NonNull private final CharSequence mDenyButtonText;
843 
ConfirmationDialogDetails( @onNull CharSequence title, @NonNull CharSequence text, @NonNull CharSequence acceptButtonText, @NonNull CharSequence denyButtonText)844             public ConfirmationDialogDetails(
845                     @NonNull CharSequence title,
846                     @NonNull CharSequence text,
847                     @NonNull CharSequence acceptButtonText,
848                     @NonNull CharSequence denyButtonText) {
849                 mTitle = requireNonNull(title);
850                 mText = requireNonNull(text);
851                 mAcceptButtonText = requireNonNull(acceptButtonText);
852                 mDenyButtonText = requireNonNull(denyButtonText);
853             }
854 
855             /** Returns the title of action confirmation dialog. */
856             @NonNull
getTitle()857             public CharSequence getTitle() {
858                 return mTitle;
859             }
860 
861             /** Returns the text of action confirmation dialog. */
862             @NonNull
getText()863             public CharSequence getText() {
864                 return mText;
865             }
866 
867             /** Returns the text of the button to accept action execution. */
868             @NonNull
getAcceptButtonText()869             public CharSequence getAcceptButtonText() {
870                 return mAcceptButtonText;
871             }
872 
873             /** Returns the text of the button to deny action execution. */
874             @NonNull
getDenyButtonText()875             public CharSequence getDenyButtonText() {
876                 return mDenyButtonText;
877             }
878 
879             @Override
describeContents()880             public int describeContents() {
881                 return 0;
882             }
883 
884             @Override
writeToParcel(@onNull Parcel dest, int flags)885             public void writeToParcel(@NonNull Parcel dest, int flags) {
886                 TextUtils.writeToParcel(mTitle, dest, flags);
887                 TextUtils.writeToParcel(mText, dest, flags);
888                 TextUtils.writeToParcel(mAcceptButtonText, dest, flags);
889                 TextUtils.writeToParcel(mDenyButtonText, dest, flags);
890             }
891 
892             @Override
equals(Object o)893             public boolean equals(Object o) {
894                 if (this == o) return true;
895                 if (!(o instanceof ConfirmationDialogDetails)) return false;
896                 ConfirmationDialogDetails that = (ConfirmationDialogDetails) o;
897                 return TextUtils.equals(mTitle, that.mTitle)
898                         && TextUtils.equals(mText, that.mText)
899                         && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText)
900                         && TextUtils.equals(mDenyButtonText, that.mDenyButtonText);
901             }
902 
903             @Override
hashCode()904             public int hashCode() {
905                 return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText);
906             }
907 
908             @Override
toString()909             public String toString() {
910                 return "ConfirmationDialogDetails{"
911                         + "mTitle="
912                         + mTitle
913                         + ", mText="
914                         + mText
915                         + ", mAcceptButtonText="
916                         + mAcceptButtonText
917                         + ", mDenyButtonText="
918                         + mDenyButtonText
919                         + '}';
920             }
921         }
922 
923         /** Builder class for {@link Action}. */
924         public static final class Builder {
925 
926             @NonNull private final String mId;
927             @NonNull private final CharSequence mLabel;
928             @NonNull private final PendingIntent mPendingIntent;
929             private boolean mWillResolve = false;
930             @Nullable private CharSequence mSuccessMessage;
931             @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails;
932 
933             /** Creates a {@link Builder} for an {@link Action}. */
Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)934             public Builder(
935                     @NonNull String id,
936                     @NonNull CharSequence label,
937                     @NonNull PendingIntent pendingIntent) {
938                 mId = requireNonNull(id);
939                 mLabel = requireNonNull(label);
940                 mPendingIntent = requireNonNull(pendingIntent);
941             }
942 
943             /** Creates a {@link Builder} with the values from the given {@link Action}. */
944             @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull Action action)945             public Builder(@NonNull Action action) {
946                 if (!SdkLevel.isAtLeastU()) {
947                     throw new UnsupportedOperationException(
948                             "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
949                 }
950                 requireNonNull(action);
951                 mId = action.mId;
952                 mLabel = action.mLabel;
953                 mPendingIntent = action.mPendingIntent;
954                 mWillResolve = action.mWillResolve;
955                 mSuccessMessage = action.mSuccessMessage;
956                 mConfirmationDialogDetails = action.mConfirmationDialogDetails;
957             }
958 
959             // TODO(b/303443020): Add setters for id, label, and pendingIntent
960 
961             /**
962              * Sets whether the action will resolve the safety issue. Defaults to {@code false}.
963              *
964              * <p>Note: It is not allowed for resolvable actions to have a {@link PendingIntent}
965              * that launches activity. When extra confirmation is needed consider using {@link
966              * Builder#setConfirmationDialogDetails}.
967              *
968              * @see #willResolve()
969              */
970             @SuppressLint("MissingGetterMatchingBuilder")
971             @NonNull
setWillResolve(boolean willResolve)972             public Builder setWillResolve(boolean willResolve) {
973                 mWillResolve = willResolve;
974                 return this;
975             }
976 
977             /**
978              * Sets the optional localized message to be displayed in the UI when the action is
979              * invoked and completes successfully.
980              */
981             @NonNull
setSuccessMessage(@ullable CharSequence successMessage)982             public Builder setSuccessMessage(@Nullable CharSequence successMessage) {
983                 mSuccessMessage = successMessage;
984                 return this;
985             }
986 
987             /**
988              * Sets the optional data to be displayed in the confirmation dialog prior to launching
989              * the {@link PendingIntent} when the action is clicked on.
990              */
991             @NonNull
992             @RequiresApi(UPSIDE_DOWN_CAKE)
setConfirmationDialogDetails( @ullable ConfirmationDialogDetails confirmationDialogDetails)993             public Builder setConfirmationDialogDetails(
994                     @Nullable ConfirmationDialogDetails confirmationDialogDetails) {
995                 if (!SdkLevel.isAtLeastU()) {
996                     throw new UnsupportedOperationException(
997                             "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
998                 }
999                 mConfirmationDialogDetails = confirmationDialogDetails;
1000                 return this;
1001             }
1002 
1003             /** Creates the {@link Action} defined by this {@link Builder}. */
1004             @NonNull
build()1005             public Action build() {
1006                 if (SdkLevel.isAtLeastU()) {
1007                     boolean willResolveWithActivity = mWillResolve && mPendingIntent.isActivity();
1008                     checkArgument(
1009                             !willResolveWithActivity,
1010                             "Launching activity from Action that should resolve the"
1011                                     + " SafetySourceIssue is not allowed. Consider using setting a"
1012                                     + " Confirmation if needed, and either set the willResolve to"
1013                                     + " false or make PendingIntent to start a service/send a"
1014                                     + " broadcast.");
1015                 }
1016                 return new Action(
1017                         mId,
1018                         mLabel,
1019                         mPendingIntent,
1020                         mWillResolve,
1021                         mSuccessMessage,
1022                         mConfirmationDialogDetails);
1023             }
1024         }
1025     }
1026 
1027     /**
1028      * Data for Safety Center to use when constructing a system {@link android.app.Notification}
1029      * about a related {@link SafetySourceIssue}.
1030      *
1031      * <p>Safety Center can construct a default notification for any issue, but sources may use
1032      * {@link Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification)} if
1033      * they want to override the title, text or actions.
1034      *
1035      * @see #getCustomNotification()
1036      * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification)
1037      * @see #getNotificationBehavior()
1038      */
1039     @RequiresApi(UPSIDE_DOWN_CAKE)
1040     public static final class Notification implements Parcelable {
1041 
1042         @NonNull
1043         public static final Creator<Notification> CREATOR =
1044                 new Creator<Notification>() {
1045                     @Override
1046                     public Notification createFromParcel(Parcel in) {
1047                         return new Builder(
1048                                         TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in),
1049                                         TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in))
1050                                 .addActions(in.createTypedArrayList(Action.CREATOR))
1051                                 .build();
1052                     }
1053 
1054                     @Override
1055                     public Notification[] newArray(int size) {
1056                         return new Notification[size];
1057                     }
1058                 };
1059 
1060         @NonNull private final CharSequence mTitle;
1061         @NonNull private final CharSequence mText;
1062         @NonNull private final List<Action> mActions;
1063 
Notification( @onNull CharSequence title, @NonNull CharSequence text, @NonNull List<Action> actions)1064         private Notification(
1065                 @NonNull CharSequence title,
1066                 @NonNull CharSequence text,
1067                 @NonNull List<Action> actions) {
1068             mTitle = title;
1069             mText = text;
1070             mActions = actions;
1071         }
1072 
1073         /**
1074          * Custom title which will be used instead of {@link SafetySourceIssue#getTitle()} when
1075          * building a {@link android.app.Notification} for this issue.
1076          */
1077         @NonNull
getTitle()1078         public CharSequence getTitle() {
1079             return mTitle;
1080         }
1081 
1082         /**
1083          * Custom text which will be used instead of {@link SafetySourceIssue#getSummary()} when
1084          * building a {@link android.app.Notification} for this issue.
1085          */
1086         @NonNull
getText()1087         public CharSequence getText() {
1088             return mText;
1089         }
1090 
1091         /**
1092          * Custom list of {@link Action} instances which will be used instead of {@link
1093          * SafetySourceIssue#getActions()} when building a {@link android.app.Notification} for this
1094          * issue.
1095          *
1096          * <p>If this list is empty then the resulting {@link android.app.Notification} will have
1097          * zero action buttons.
1098          */
1099         @NonNull
getActions()1100         public List<Action> getActions() {
1101             return mActions;
1102         }
1103 
1104         @Override
describeContents()1105         public int describeContents() {
1106             return 0;
1107         }
1108 
1109         @Override
writeToParcel(@onNull Parcel dest, int flags)1110         public void writeToParcel(@NonNull Parcel dest, int flags) {
1111             TextUtils.writeToParcel(mTitle, dest, flags);
1112             TextUtils.writeToParcel(mText, dest, flags);
1113             dest.writeTypedList(mActions);
1114         }
1115 
1116         @Override
equals(Object o)1117         public boolean equals(Object o) {
1118             if (this == o) return true;
1119             if (!(o instanceof Notification)) return false;
1120             Notification that = (Notification) o;
1121             return TextUtils.equals(mTitle, that.mTitle)
1122                     && TextUtils.equals(mText, that.mText)
1123                     && mActions.equals(that.mActions);
1124         }
1125 
1126         @Override
hashCode()1127         public int hashCode() {
1128             return Objects.hash(mTitle, mText, mActions);
1129         }
1130 
1131         @Override
toString()1132         public String toString() {
1133             return "Notification{"
1134                     + "mTitle="
1135                     + mTitle
1136                     + ", mText="
1137                     + mText
1138                     + ", mActions="
1139                     + mActions
1140                     + '}';
1141         }
1142 
1143         /** Builder for {@link SafetySourceIssue.Notification}. */
1144         public static final class Builder {
1145 
1146             @NonNull private final CharSequence mTitle;
1147             @NonNull private final CharSequence mText;
1148             @NonNull private final List<Action> mActions = new ArrayList<>();
1149 
Builder(@onNull CharSequence title, @NonNull CharSequence text)1150             public Builder(@NonNull CharSequence title, @NonNull CharSequence text) {
1151                 mTitle = requireNonNull(title);
1152                 mText = requireNonNull(text);
1153             }
1154 
1155             /** Creates a {@link Builder} with the values from the given {@link Notification}. */
Builder(@onNull Notification notification)1156             public Builder(@NonNull Notification notification) {
1157                 requireNonNull(notification);
1158                 mTitle = notification.mTitle;
1159                 mText = notification.mText;
1160                 mActions.addAll(notification.mActions);
1161             }
1162 
1163             /** Adds an {@link Action} to the custom {@link Notification}. */
1164             @NonNull
addAction(@onNull Action action)1165             public Builder addAction(@NonNull Action action) {
1166                 mActions.add(requireNonNull(action));
1167                 return this;
1168             }
1169 
1170             /** Adds several {@link Action}s to the custom {@link Notification}. */
1171             @NonNull
addActions(@onNull List<Action> actions)1172             public Builder addActions(@NonNull List<Action> actions) {
1173                 mActions.addAll(requireNonNull(actions));
1174                 return this;
1175             }
1176 
1177             /** Clears all the {@link Action}s that were added so far. */
1178             @NonNull
clearActions()1179             public Builder clearActions() {
1180                 mActions.clear();
1181                 return this;
1182             }
1183 
1184             /** Builds a {@link Notification} instance. */
1185             @NonNull
build()1186             public Notification build() {
1187                 List<Action> actions = unmodifiableList(new ArrayList<>(mActions));
1188                 Action.enforceUniqueActionIds(
1189                         actions, "Custom notification cannot have duplicate action ids");
1190                 checkArgument(
1191                         actions.size() <= 2,
1192                         "Custom notification must not contain more than 2 actions");
1193                 return new Notification(mTitle, mText, actions);
1194             }
1195         }
1196     }
1197 
1198     /** Builder class for {@link SafetySourceIssue}. */
1199     public static final class Builder {
1200 
1201         @NonNull private final String mId;
1202         @NonNull private final CharSequence mTitle;
1203         @NonNull private final CharSequence mSummary;
1204         @SafetySourceData.SeverityLevel private final int mSeverityLevel;
1205         @NonNull private final String mIssueTypeId;
1206         private final List<Action> mActions = new ArrayList<>();
1207 
1208         @Nullable private CharSequence mSubtitle;
1209         @IssueCategory private int mIssueCategory = ISSUE_CATEGORY_GENERAL;
1210         @Nullable private PendingIntent mOnDismissPendingIntent;
1211         @Nullable private CharSequence mAttributionTitle;
1212         @Nullable private String mDeduplicationId;
1213 
1214         @Nullable private Notification mCustomNotification = null;
1215 
1216         @SuppressLint("NewApi")
1217         @NotificationBehavior
1218         private int mNotificationBehavior = NOTIFICATION_BEHAVIOR_UNSPECIFIED;
1219 
1220         @SuppressLint("NewApi")
1221         @IssueActionability
1222         private int mIssueActionability = ISSUE_ACTIONABILITY_MANUAL;
1223 
1224         /** Creates a {@link Builder} for a {@link SafetySourceIssue}. */
Builder( @onNull String id, @NonNull CharSequence title, @NonNull CharSequence summary, @SafetySourceData.SeverityLevel int severityLevel, @NonNull String issueTypeId)1225         public Builder(
1226                 @NonNull String id,
1227                 @NonNull CharSequence title,
1228                 @NonNull CharSequence summary,
1229                 @SafetySourceData.SeverityLevel int severityLevel,
1230                 @NonNull String issueTypeId) {
1231             this.mId = requireNonNull(id);
1232             this.mTitle = requireNonNull(title);
1233             this.mSummary = requireNonNull(summary);
1234             this.mSeverityLevel = validateSeverityLevel(severityLevel);
1235             this.mIssueTypeId = requireNonNull(issueTypeId);
1236         }
1237 
1238         /** Creates a {@link Builder} with the values from the given {@link SafetySourceIssue}. */
1239         @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull SafetySourceIssue safetySourceIssue)1240         public Builder(@NonNull SafetySourceIssue safetySourceIssue) {
1241             if (!SdkLevel.isAtLeastU()) {
1242                 throw new UnsupportedOperationException(
1243                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
1244             }
1245             requireNonNull(safetySourceIssue);
1246             mId = safetySourceIssue.mId;
1247             mTitle = safetySourceIssue.mTitle;
1248             mSummary = safetySourceIssue.mSummary;
1249             mSeverityLevel = safetySourceIssue.mSeverityLevel;
1250             mIssueTypeId = safetySourceIssue.mIssueTypeId;
1251             mActions.addAll(safetySourceIssue.mActions);
1252             mSubtitle = safetySourceIssue.mSubtitle;
1253             mIssueCategory = safetySourceIssue.mIssueCategory;
1254             mOnDismissPendingIntent = safetySourceIssue.mOnDismissPendingIntent;
1255             mAttributionTitle = safetySourceIssue.mAttributionTitle;
1256             mDeduplicationId = safetySourceIssue.mDeduplicationId;
1257             mCustomNotification = safetySourceIssue.mCustomNotification;
1258             mNotificationBehavior = safetySourceIssue.mNotificationBehavior;
1259             mIssueActionability = safetySourceIssue.mIssueActionability;
1260         }
1261 
1262         /** Sets the localized subtitle. */
1263         @NonNull
setSubtitle(@ullable CharSequence subtitle)1264         public Builder setSubtitle(@Nullable CharSequence subtitle) {
1265             mSubtitle = subtitle;
1266             return this;
1267         }
1268 
1269         /**
1270          * Sets or clears the optional attribution title for this issue.
1271          *
1272          * <p>This is displayed in the UI and helps to attribute an issue to a particular source. If
1273          * this value is {@code null}, the title of the group that contains the Safety Source will
1274          * be used.
1275          */
1276         @NonNull
1277         @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
setAttributionTitle(@ullable CharSequence attributionTitle)1278         public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) {
1279             if (!SdkLevel.isAtLeastU()) {
1280                 throw new UnsupportedOperationException(
1281                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
1282             }
1283             mAttributionTitle = attributionTitle;
1284             return this;
1285         }
1286 
1287         /**
1288          * Sets the category of the risk associated with the issue.
1289          *
1290          * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}.
1291          */
1292         @NonNull
setIssueCategory(@ssueCategory int issueCategory)1293         public Builder setIssueCategory(@IssueCategory int issueCategory) {
1294             mIssueCategory = validateIssueCategory(issueCategory);
1295             return this;
1296         }
1297 
1298         /** Adds data for an {@link Action} to be shown in UI. */
1299         @NonNull
addAction(@onNull Action actionData)1300         public Builder addAction(@NonNull Action actionData) {
1301             mActions.add(requireNonNull(actionData));
1302             return this;
1303         }
1304 
1305         /** Clears data for all the {@link Action}s that were added to this {@link Builder}. */
1306         @NonNull
clearActions()1307         public Builder clearActions() {
1308             mActions.clear();
1309             return this;
1310         }
1311 
1312         /**
1313          * Sets an optional {@link PendingIntent} to be invoked when an issue is dismissed from the
1314          * UI.
1315          *
1316          * <p>In particular, if the source would like to be notified of issue dismissals in Safety
1317          * Center in order to be able to dismiss or ignore issues at the source, then set this
1318          * field. The action contained in the {@link PendingIntent} must not start an activity.
1319          *
1320          * @see #getOnDismissPendingIntent()
1321          */
1322         @NonNull
setOnDismissPendingIntent(@ullable PendingIntent onDismissPendingIntent)1323         public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) {
1324             checkArgument(
1325                     onDismissPendingIntent == null || !onDismissPendingIntent.isActivity(),
1326                     "Safety source issue on dismiss pending intent must not start an activity");
1327             mOnDismissPendingIntent = onDismissPendingIntent;
1328             return this;
1329         }
1330 
1331         /**
1332          * Sets a custom {@link Notification} for this issue.
1333          *
1334          * <p>Using a custom {@link Notification} a source may specify a different {@link
1335          * Notification#getTitle()}, {@link Notification#getText()} and {@link
1336          * Notification#getActions()} for Safety Center to use when constructing a notification for
1337          * this issue.
1338          *
1339          * <p>Safety Center may still generate a default notification from the other details of this
1340          * issue when no custom notification has been set, depending on the issue's {@link
1341          * #getNotificationBehavior()}.
1342          *
1343          * @see #getCustomNotification()
1344          * @see #setNotificationBehavior(int)
1345          */
1346         @NonNull
1347         @RequiresApi(UPSIDE_DOWN_CAKE)
setCustomNotification(@ullable Notification customNotification)1348         public Builder setCustomNotification(@Nullable Notification customNotification) {
1349             if (!SdkLevel.isAtLeastU()) {
1350                 throw new UnsupportedOperationException(
1351                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
1352             }
1353             mCustomNotification = customNotification;
1354             return this;
1355         }
1356 
1357         /**
1358          * Sets the notification behavior of the issue.
1359          *
1360          * <p>Must be one of {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED}, {@link
1361          * #NOTIFICATION_BEHAVIOR_NEVER}, {@link #NOTIFICATION_BEHAVIOR_DELAYED} or {@link
1362          * #NOTIFICATION_BEHAVIOR_IMMEDIATELY}. See {@link #getNotificationBehavior()} for details
1363          * of how Safety Center will interpret each of these.
1364          *
1365          * @see #getNotificationBehavior()
1366          */
1367         @NonNull
1368         @RequiresApi(UPSIDE_DOWN_CAKE)
setNotificationBehavior(@otificationBehavior int notificationBehavior)1369         public Builder setNotificationBehavior(@NotificationBehavior int notificationBehavior) {
1370             if (!SdkLevel.isAtLeastU()) {
1371                 throw new UnsupportedOperationException(
1372                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
1373             }
1374             mNotificationBehavior = validateNotificationBehavior(notificationBehavior);
1375             return this;
1376         }
1377 
1378         /**
1379          * Sets the deduplication identifier for the issue.
1380          *
1381          * @see #getDeduplicationId()
1382          */
1383         @NonNull
1384         @RequiresApi(UPSIDE_DOWN_CAKE)
setDeduplicationId(@ullable String deduplicationId)1385         public Builder setDeduplicationId(@Nullable String deduplicationId) {
1386             if (!SdkLevel.isAtLeastU()) {
1387                 throw new UnsupportedOperationException(
1388                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
1389             }
1390             mDeduplicationId = deduplicationId;
1391             return this;
1392         }
1393 
1394         /**
1395          * Sets the issue actionability of the issue.
1396          *
1397          * <p>Must be one of {@link #ISSUE_ACTIONABILITY_MANUAL} (default), {@link
1398          * #ISSUE_ACTIONABILITY_TIP}, {@link #ISSUE_ACTIONABILITY_AUTOMATIC}.
1399          *
1400          * @see #getIssueActionability()
1401          */
1402         @NonNull
1403         @RequiresApi(UPSIDE_DOWN_CAKE)
setIssueActionability(@ssueActionability int issueActionability)1404         public Builder setIssueActionability(@IssueActionability int issueActionability) {
1405             if (!SdkLevel.isAtLeastU()) {
1406                 throw new UnsupportedOperationException(
1407                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
1408             }
1409             mIssueActionability = validateIssueActionability(issueActionability);
1410             return this;
1411         }
1412 
1413         /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */
1414         @NonNull
build()1415         public SafetySourceIssue build() {
1416             List<SafetySourceIssue.Action> actions = unmodifiableList(new ArrayList<>(mActions));
1417             Action.enforceUniqueActionIds(
1418                     actions, "Safety source issue cannot have duplicate action ids");
1419             if (SdkLevel.isAtLeastU()) {
1420                 checkArgument(
1421                         mIssueActionability != ISSUE_ACTIONABILITY_MANUAL || !actions.isEmpty(),
1422                         "Actionable safety source issue must contain at least 1 action");
1423             } else {
1424                 checkArgument(
1425                         !actions.isEmpty(), "Safety source issue must contain at least 1 action");
1426             }
1427             checkArgument(
1428                     actions.size() <= 2,
1429                     "Safety source issue must not contain more than 2 actions");
1430             return new SafetySourceIssue(
1431                     mId,
1432                     mTitle,
1433                     mSubtitle,
1434                     mSummary,
1435                     mSeverityLevel,
1436                     mIssueCategory,
1437                     actions,
1438                     mOnDismissPendingIntent,
1439                     mIssueTypeId,
1440                     mCustomNotification,
1441                     mNotificationBehavior,
1442                     mAttributionTitle,
1443                     mDeduplicationId,
1444                     mIssueActionability);
1445         }
1446     }
1447 
1448     @SafetySourceData.SeverityLevel
validateSeverityLevel(int value)1449     private static int validateSeverityLevel(int value) {
1450         switch (value) {
1451             case SafetySourceData.SEVERITY_LEVEL_INFORMATION:
1452             case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION:
1453             case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING:
1454                 return value;
1455             case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED:
1456                 throw new IllegalArgumentException(
1457                         "SeverityLevel for SafetySourceIssue must not be "
1458                                 + "SEVERITY_LEVEL_UNSPECIFIED");
1459             default:
1460         }
1461         throw new IllegalArgumentException(
1462                 "Unexpected SeverityLevel for SafetySourceIssue: " + value);
1463     }
1464 
1465     @IssueCategory
validateIssueCategory(int value)1466     private static int validateIssueCategory(int value) {
1467         switch (value) {
1468             case ISSUE_CATEGORY_DEVICE:
1469             case ISSUE_CATEGORY_ACCOUNT:
1470             case ISSUE_CATEGORY_GENERAL:
1471                 return value;
1472             default:
1473         }
1474         if (SdkLevel.isAtLeastU()) {
1475             switch (value) {
1476                 case ISSUE_CATEGORY_DATA:
1477                 case ISSUE_CATEGORY_PASSWORDS:
1478                 case ISSUE_CATEGORY_PERSONAL_SAFETY:
1479                     return value;
1480                 default:
1481             }
1482         }
1483         throw new IllegalArgumentException(
1484                 "Unexpected IssueCategory for SafetySourceIssue: " + value);
1485     }
1486 
1487     @NotificationBehavior
validateNotificationBehavior(int value)1488     private static int validateNotificationBehavior(int value) {
1489         switch (value) {
1490             case NOTIFICATION_BEHAVIOR_UNSPECIFIED:
1491             case NOTIFICATION_BEHAVIOR_NEVER:
1492             case NOTIFICATION_BEHAVIOR_DELAYED:
1493             case NOTIFICATION_BEHAVIOR_IMMEDIATELY:
1494                 return value;
1495             default:
1496         }
1497         throw new IllegalArgumentException(
1498                 "Unexpected NotificationBehavior for SafetySourceIssue: " + value);
1499     }
1500 
1501     @IssueActionability
validateIssueActionability(int value)1502     private static int validateIssueActionability(int value) {
1503         switch (value) {
1504             case ISSUE_ACTIONABILITY_MANUAL:
1505             case ISSUE_ACTIONABILITY_TIP:
1506             case ISSUE_ACTIONABILITY_AUTOMATIC:
1507                 return value;
1508             default:
1509         }
1510         throw new IllegalArgumentException(
1511                 "Unexpected IssueActionability for SafetySourceIssue: " + value);
1512     }
1513 }
1514