• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         }
342         return mAttributionTitle;
343     }
344 
345     /** Returns the {@link SafetySourceData.SeverityLevel} of the issue. */
346     @SafetySourceData.SeverityLevel
getSeverityLevel()347     public int getSeverityLevel() {
348         return mSeverityLevel;
349     }
350 
351     /**
352      * Returns the category of the risk associated with the issue.
353      *
354      * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}.
355      */
356     @IssueCategory
getIssueCategory()357     public int getIssueCategory() {
358         return mIssueCategory;
359     }
360 
361     /**
362      * Returns a list of {@link Action}s representing actions supported in the UI for this issue.
363      *
364      * <p>Each issue must contain at least one action, in order to help the user resolve the issue.
365      *
366      * <p>In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most
367      * two actions supported from the UI.
368      */
369     @NonNull
getActions()370     public List<Action> getActions() {
371         return mActions;
372     }
373 
374     /**
375      * Returns the optional {@link PendingIntent} that will be invoked when an issue is dismissed.
376      *
377      * <p>When a safety issue is dismissed in Safety Center page, the issue is removed from view in
378      * Safety Center page. This method returns an additional optional action specified by the safety
379      * source that should be invoked on issue dismissal. The action contained in the {@link
380      * PendingIntent} cannot start an activity.
381      */
382     @Nullable
getOnDismissPendingIntent()383     public PendingIntent getOnDismissPendingIntent() {
384         return mOnDismissPendingIntent;
385     }
386 
387     /**
388      * Returns the identifier for the type of this issue.
389      *
390      * <p>The issue type should indicate the underlying basis for the issue, for e.g. a pending
391      * update or a disabled security feature.
392      *
393      * <p>The difference between this id and {@link #getId()} is that the issue type id is meant to
394      * be used for logging and should therefore contain no personally identifiable information (PII)
395      * (e.g. for account name).
396      *
397      * <p>On multiple instances of providing the same issue to be represented in Safety Center,
398      * provide the same issue type id across all instances.
399      */
400     @NonNull
getIssueTypeId()401     public String getIssueTypeId() {
402         return mIssueTypeId;
403     }
404 
405     /**
406      * Returns the optional custom {@link Notification} for this issue which overrides the title,
407      * text and actions for any {@link android.app.Notification} generated for this {@link
408      * SafetySourceIssue}.
409      *
410      * <p>Safety Center may still generate a default notification from the other details of this
411      * issue when no custom notification has been set. See {@link #getNotificationBehavior()} for
412      * details
413      *
414      * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification
415      * @see #getNotificationBehavior()
416      */
417     @Nullable
418     @RequiresApi(UPSIDE_DOWN_CAKE)
getCustomNotification()419     public Notification getCustomNotification() {
420         if (!SdkLevel.isAtLeastU()) {
421             throw new UnsupportedOperationException();
422         }
423         return mCustomNotification;
424     }
425 
426     /**
427      * Returns the {@link NotificationBehavior} for this issue which determines if and when Safety
428      * Center will post a notification for this issue.
429      *
430      * <p>Any notification will be based on the {@link #getCustomNotification()} if set, or the
431      * other properties of this issue otherwise.
432      *
433      * <ul>
434      *   <li>If {@link #NOTIFICATION_BEHAVIOR_IMMEDIATELY} then Safety Center will immediately
435      *       create and post a notification
436      *   <li>If {@link #NOTIFICATION_BEHAVIOR_DELAYED} then a notification will only be posted after
437      *       a delay, if this issue has not been resolved.
438      *   <li>If {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED} then a notification may or may not be
439      *       posted, the exact behavior is defined by Safety Center.
440      *   <li>If {@link #NOTIFICATION_BEHAVIOR_NEVER} Safety Center will never post a notification
441      *       about this issue. Sources should specify this behavior when they wish to handle their
442      *       own notifications. When this behavior is set sources should not set a custom
443      *       notification.
444      * </ul>
445      *
446      * @see Builder#setNotificationBehavior(int)
447      */
448     @NotificationBehavior
449     @RequiresApi(UPSIDE_DOWN_CAKE)
getNotificationBehavior()450     public int getNotificationBehavior() {
451         if (!SdkLevel.isAtLeastU()) {
452             throw new UnsupportedOperationException();
453         }
454         return mNotificationBehavior;
455     }
456 
457     /**
458      * Returns the identifier used to deduplicate this issue against other issues with the same
459      * deduplication identifiers.
460      *
461      * <p>Deduplication identifier will be used to identify duplicate issues. This identifier
462      * applies across all safety sources which are part of the same deduplication group.
463      * Deduplication groups can be set, for each source, in the SafetyCenter config. Therefore, two
464      * issues are considered duplicate if their sources are part of the same deduplication group and
465      * they have the same deduplication identifier.
466      *
467      * <p>Out of all issues that are found to be duplicates, only one will be shown in the UI (the
468      * one with the highest severity, or in case of same severities, the one placed highest in the
469      * config).
470      *
471      * <p>Expected usage implies different sources will coordinate to set the same deduplication
472      * identifiers on issues that they want to deduplicate.
473      *
474      * <p>This shouldn't be a default mechanism for deduplication of issues. Most of the time
475      * sources should coordinate or communicate to only send the issue from one of them. That would
476      * also allow sources to choose which one will be displaying the issue, instead of depending on
477      * severity and config order. This API should only be needed if for some reason this isn't
478      * possible, for example, when sources can't communicate with each other and/or send issues at
479      * different times and/or issues can be of different severities.
480      */
481     @Nullable
482     @RequiresApi(UPSIDE_DOWN_CAKE)
getDeduplicationId()483     public String getDeduplicationId() {
484         if (!SdkLevel.isAtLeastU()) {
485             throw new UnsupportedOperationException();
486         }
487         return mDeduplicationId;
488     }
489 
490     /**
491      * Returns the {@link IssueActionability} for this issue which determines what type of action is
492      * required from the user:
493      *
494      * <ul>
495      *   <li>If {@link #ISSUE_ACTIONABILITY_MANUAL} then user input is required to resolve the issue
496      *   <li>If {@link #ISSUE_ACTIONABILITY_TIP} then the user needs to review this issue as a tip
497      *       to improve their overall safety, and possibly acknowledge it
498      *   <li>If {@link #ISSUE_ACTIONABILITY_AUTOMATIC} then the user needs to review this issue as
499      *       something that has been resolved on their behalf, and possibly acknowledge it
500      * </ul>
501      *
502      * @see Builder#setIssueActionability(int)
503      */
504     @IssueActionability
505     @RequiresApi(UPSIDE_DOWN_CAKE)
getIssueActionability()506     public int getIssueActionability() {
507         if (!SdkLevel.isAtLeastU()) {
508             throw new UnsupportedOperationException();
509         }
510         return mIssueActionability;
511     }
512 
513     @Override
describeContents()514     public int describeContents() {
515         return 0;
516     }
517 
518     @Override
writeToParcel(@onNull Parcel dest, int flags)519     public void writeToParcel(@NonNull Parcel dest, int flags) {
520         dest.writeString(mId);
521         TextUtils.writeToParcel(mTitle, dest, flags);
522         TextUtils.writeToParcel(mSubtitle, dest, flags);
523         TextUtils.writeToParcel(mSummary, dest, flags);
524         dest.writeInt(mSeverityLevel);
525         dest.writeInt(mIssueCategory);
526         dest.writeTypedList(mActions);
527         dest.writeTypedObject(mOnDismissPendingIntent, flags);
528         dest.writeString(mIssueTypeId);
529         if (SdkLevel.isAtLeastU()) {
530             dest.writeTypedObject(mCustomNotification, flags);
531             dest.writeInt(mNotificationBehavior);
532             TextUtils.writeToParcel(mAttributionTitle, dest, flags);
533             dest.writeString(mDeduplicationId);
534             dest.writeInt(mIssueActionability);
535         }
536     }
537 
538     @Override
equals(Object o)539     public boolean equals(Object o) {
540         if (this == o) return true;
541         if (!(o instanceof SafetySourceIssue)) return false;
542         SafetySourceIssue that = (SafetySourceIssue) o;
543         return mSeverityLevel == that.mSeverityLevel
544                 && TextUtils.equals(mId, that.mId)
545                 && TextUtils.equals(mTitle, that.mTitle)
546                 && TextUtils.equals(mSubtitle, that.mSubtitle)
547                 && TextUtils.equals(mSummary, that.mSummary)
548                 && mIssueCategory == that.mIssueCategory
549                 && mActions.equals(that.mActions)
550                 && Objects.equals(mOnDismissPendingIntent, that.mOnDismissPendingIntent)
551                 && TextUtils.equals(mIssueTypeId, that.mIssueTypeId)
552                 && Objects.equals(mCustomNotification, that.mCustomNotification)
553                 && mNotificationBehavior == that.mNotificationBehavior
554                 && TextUtils.equals(mAttributionTitle, that.mAttributionTitle)
555                 && TextUtils.equals(mDeduplicationId, that.mDeduplicationId)
556                 && mIssueActionability == that.mIssueActionability;
557     }
558 
559     @Override
hashCode()560     public int hashCode() {
561         return Objects.hash(
562                 mId,
563                 mTitle,
564                 mSubtitle,
565                 mSummary,
566                 mSeverityLevel,
567                 mIssueCategory,
568                 mActions,
569                 mOnDismissPendingIntent,
570                 mIssueTypeId,
571                 mCustomNotification,
572                 mNotificationBehavior,
573                 mAttributionTitle,
574                 mDeduplicationId,
575                 mIssueActionability);
576     }
577 
578     @Override
toString()579     public String toString() {
580         return "SafetySourceIssue{"
581                 + "mId="
582                 + mId
583                 + "mTitle="
584                 + mTitle
585                 + ", mSubtitle="
586                 + mSubtitle
587                 + ", mSummary="
588                 + mSummary
589                 + ", mSeverityLevel="
590                 + mSeverityLevel
591                 + ", mIssueCategory="
592                 + mIssueCategory
593                 + ", mActions="
594                 + mActions
595                 + ", mOnDismissPendingIntent="
596                 + mOnDismissPendingIntent
597                 + ", mIssueTypeId="
598                 + mIssueTypeId
599                 + ", mCustomNotification="
600                 + mCustomNotification
601                 + ", mNotificationBehavior="
602                 + mNotificationBehavior
603                 + ", mAttributionTitle="
604                 + mAttributionTitle
605                 + ", mDeduplicationId="
606                 + mDeduplicationId
607                 + ", mIssueActionability="
608                 + mIssueActionability
609                 + '}';
610     }
611 
612     /**
613      * Data for an action supported from a safety issue {@link SafetySourceIssue} in the Safety
614      * Center page.
615      *
616      * <p>The purpose of the action is to allow the user to address the safety issue, either by
617      * performing a fix suggested in the issue, or by navigating the user to the source of the issue
618      * where they can be exposed to detail about the issue and further suggestions to resolve it.
619      *
620      * <p>The user will be allowed to invoke the action from the UI by clicking on a UI element and
621      * consequently resolve the issue.
622      */
623     public static final class Action implements Parcelable {
624 
625         @NonNull
626         public static final Creator<Action> CREATOR =
627                 new Creator<Action>() {
628                     @Override
629                     public Action createFromParcel(Parcel in) {
630                         String id = in.readString();
631                         CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
632                         PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
633                         Builder builder =
634                                 new Builder(id, label, pendingIntent)
635                                         .setWillResolve(in.readBoolean())
636                                         .setSuccessMessage(
637                                                 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(
638                                                         in));
639                         if (SdkLevel.isAtLeastU()) {
640                             ConfirmationDialogDetails confirmationDialogDetails =
641                                     in.readTypedObject(ConfirmationDialogDetails.CREATOR);
642                             builder.setConfirmationDialogDetails(confirmationDialogDetails);
643                         }
644                         return builder.build();
645                     }
646 
647                     @Override
648                     public Action[] newArray(int size) {
649                         return new Action[size];
650                     }
651                 };
652 
enforceUniqueActionIds( @onNull List<SafetySourceIssue.Action> actions, @NonNull String message)653         private static void enforceUniqueActionIds(
654                 @NonNull List<SafetySourceIssue.Action> actions, @NonNull String message) {
655             Set<String> actionIds = new HashSet<>();
656             for (int i = 0; i < actions.size(); i++) {
657                 SafetySourceIssue.Action action = actions.get(i);
658 
659                 String actionId = action.getId();
660                 checkArgument(!actionIds.contains(actionId), message);
661                 actionIds.add(actionId);
662             }
663         }
664 
665         @NonNull private final String mId;
666         @NonNull private final CharSequence mLabel;
667         @NonNull private final PendingIntent mPendingIntent;
668         private final boolean mWillResolve;
669         @Nullable private final CharSequence mSuccessMessage;
670         @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails;
671 
Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, @Nullable CharSequence successMessage, @Nullable ConfirmationDialogDetails confirmationDialogDetails)672         private Action(
673                 @NonNull String id,
674                 @NonNull CharSequence label,
675                 @NonNull PendingIntent pendingIntent,
676                 boolean willResolve,
677                 @Nullable CharSequence successMessage,
678                 @Nullable ConfirmationDialogDetails confirmationDialogDetails) {
679             mId = id;
680             mLabel = label;
681             mPendingIntent = pendingIntent;
682             mWillResolve = willResolve;
683             mSuccessMessage = successMessage;
684             mConfirmationDialogDetails = confirmationDialogDetails;
685         }
686 
687         /**
688          * Returns the ID of the action, unique among actions in a given {@link SafetySourceIssue}.
689          */
690         @NonNull
getId()691         public String getId() {
692             return mId;
693         }
694 
695         /**
696          * Returns the localized label of the action to be displayed in the UI.
697          *
698          * <p>The label should indicate what action will be performed if when invoked.
699          */
700         @NonNull
getLabel()701         public CharSequence getLabel() {
702             return mLabel;
703         }
704 
705         /**
706          * Returns a {@link PendingIntent} to be fired when the action is clicked on.
707          *
708          * <p>The {@link PendingIntent} should perform the action referred to by {@link
709          * #getLabel()}.
710          */
711         @NonNull
getPendingIntent()712         public PendingIntent getPendingIntent() {
713             return mPendingIntent;
714         }
715 
716         /**
717          * Returns whether invoking this action will fix or address the issue sufficiently for it to
718          * be considered resolved i.e. the issue will no longer need to be conveyed to the user in
719          * the UI.
720          */
willResolve()721         public boolean willResolve() {
722             return mWillResolve;
723         }
724 
725         /**
726          * Returns the optional localized message to be displayed in the UI when the action is
727          * invoked and completes successfully.
728          */
729         @Nullable
getSuccessMessage()730         public CharSequence getSuccessMessage() {
731             return mSuccessMessage;
732         }
733 
734         /**
735          * Returns the optional data to be displayed in the confirmation dialog prior to launching
736          * the {@link PendingIntent} when the action is clicked on.
737          */
738         @Nullable
739         @RequiresApi(UPSIDE_DOWN_CAKE)
getConfirmationDialogDetails()740         public ConfirmationDialogDetails getConfirmationDialogDetails() {
741             if (!SdkLevel.isAtLeastU()) {
742                 throw new UnsupportedOperationException();
743             }
744             return mConfirmationDialogDetails;
745         }
746 
747         @Override
describeContents()748         public int describeContents() {
749             return 0;
750         }
751 
752         @Override
writeToParcel(@onNull Parcel dest, int flags)753         public void writeToParcel(@NonNull Parcel dest, int flags) {
754             dest.writeString(mId);
755             TextUtils.writeToParcel(mLabel, dest, flags);
756             dest.writeTypedObject(mPendingIntent, flags);
757             dest.writeBoolean(mWillResolve);
758             TextUtils.writeToParcel(mSuccessMessage, dest, flags);
759             if (SdkLevel.isAtLeastU()) {
760                 dest.writeTypedObject(mConfirmationDialogDetails, flags);
761             }
762         }
763 
764         @Override
equals(Object o)765         public boolean equals(Object o) {
766             if (this == o) return true;
767             if (!(o instanceof Action)) return false;
768             Action that = (Action) o;
769             return mId.equals(that.mId)
770                     && TextUtils.equals(mLabel, that.mLabel)
771                     && mPendingIntent.equals(that.mPendingIntent)
772                     && mWillResolve == that.mWillResolve
773                     && TextUtils.equals(mSuccessMessage, that.mSuccessMessage)
774                     && Objects.equals(mConfirmationDialogDetails, that.mConfirmationDialogDetails);
775         }
776 
777         @Override
hashCode()778         public int hashCode() {
779             return Objects.hash(
780                     mId,
781                     mLabel,
782                     mPendingIntent,
783                     mWillResolve,
784                     mSuccessMessage,
785                     mConfirmationDialogDetails);
786         }
787 
788         @Override
toString()789         public String toString() {
790             return "Action{"
791                     + "mId="
792                     + mId
793                     + ", mLabel="
794                     + mLabel
795                     + ", mPendingIntent="
796                     + mPendingIntent
797                     + ", mWillResolve="
798                     + mWillResolve
799                     + ", mSuccessMessage="
800                     + mSuccessMessage
801                     + ", mConfirmationDialogDetails="
802                     + mConfirmationDialogDetails
803                     + '}';
804         }
805 
806         /** Data for an action confirmation dialog to be shown before action is executed. */
807         @RequiresApi(UPSIDE_DOWN_CAKE)
808         public static final class ConfirmationDialogDetails implements Parcelable {
809 
810             @NonNull
811             public static final Creator<ConfirmationDialogDetails> CREATOR =
812                     new Creator<ConfirmationDialogDetails>() {
813                         @Override
814                         public ConfirmationDialogDetails createFromParcel(Parcel in) {
815                             CharSequence title =
816                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
817                             CharSequence text =
818                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
819                             CharSequence acceptButtonText =
820                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
821                             CharSequence denyButtonText =
822                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
823                             return new ConfirmationDialogDetails(
824                                     title, text, acceptButtonText, denyButtonText);
825                         }
826 
827                         @Override
828                         public ConfirmationDialogDetails[] newArray(int size) {
829                             return new ConfirmationDialogDetails[size];
830                         }
831                     };
832 
833             @NonNull private final CharSequence mTitle;
834             @NonNull private final CharSequence mText;
835             @NonNull private final CharSequence mAcceptButtonText;
836             @NonNull private final CharSequence mDenyButtonText;
837 
ConfirmationDialogDetails( @onNull CharSequence title, @NonNull CharSequence text, @NonNull CharSequence acceptButtonText, @NonNull CharSequence denyButtonText)838             public ConfirmationDialogDetails(
839                     @NonNull CharSequence title,
840                     @NonNull CharSequence text,
841                     @NonNull CharSequence acceptButtonText,
842                     @NonNull CharSequence denyButtonText) {
843                 mTitle = requireNonNull(title);
844                 mText = requireNonNull(text);
845                 mAcceptButtonText = requireNonNull(acceptButtonText);
846                 mDenyButtonText = requireNonNull(denyButtonText);
847             }
848 
849             /** Returns the title of action confirmation dialog. */
850             @NonNull
getTitle()851             public CharSequence getTitle() {
852                 return mTitle;
853             }
854 
855             /** Returns the text of action confirmation dialog. */
856             @NonNull
getText()857             public CharSequence getText() {
858                 return mText;
859             }
860 
861             /** Returns the text of the button to accept action execution. */
862             @NonNull
getAcceptButtonText()863             public CharSequence getAcceptButtonText() {
864                 return mAcceptButtonText;
865             }
866 
867             /** Returns the text of the button to deny action execution. */
868             @NonNull
getDenyButtonText()869             public CharSequence getDenyButtonText() {
870                 return mDenyButtonText;
871             }
872 
873             @Override
describeContents()874             public int describeContents() {
875                 return 0;
876             }
877 
878             @Override
writeToParcel(@onNull Parcel dest, int flags)879             public void writeToParcel(@NonNull Parcel dest, int flags) {
880                 TextUtils.writeToParcel(mTitle, dest, flags);
881                 TextUtils.writeToParcel(mText, dest, flags);
882                 TextUtils.writeToParcel(mAcceptButtonText, dest, flags);
883                 TextUtils.writeToParcel(mDenyButtonText, dest, flags);
884             }
885 
886             @Override
equals(Object o)887             public boolean equals(Object o) {
888                 if (this == o) return true;
889                 if (!(o instanceof ConfirmationDialogDetails)) return false;
890                 ConfirmationDialogDetails that = (ConfirmationDialogDetails) o;
891                 return TextUtils.equals(mTitle, that.mTitle)
892                         && TextUtils.equals(mText, that.mText)
893                         && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText)
894                         && TextUtils.equals(mDenyButtonText, that.mDenyButtonText);
895             }
896 
897             @Override
hashCode()898             public int hashCode() {
899                 return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText);
900             }
901 
902             @Override
toString()903             public String toString() {
904                 return "ConfirmationDialogDetails{"
905                         + "mTitle="
906                         + mTitle
907                         + ", mText="
908                         + mText
909                         + ", mAcceptButtonText="
910                         + mAcceptButtonText
911                         + ", mDenyButtonText="
912                         + mDenyButtonText
913                         + '}';
914             }
915         }
916 
917         /** Builder class for {@link Action}. */
918         public static final class Builder {
919 
920             @NonNull private final String mId;
921             @NonNull private final CharSequence mLabel;
922             @NonNull private final PendingIntent mPendingIntent;
923             private boolean mWillResolve = false;
924             @Nullable private CharSequence mSuccessMessage;
925             @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails;
926 
927             /** Creates a {@link Builder} for an {@link Action}. */
Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)928             public Builder(
929                     @NonNull String id,
930                     @NonNull CharSequence label,
931                     @NonNull PendingIntent pendingIntent) {
932                 mId = requireNonNull(id);
933                 mLabel = requireNonNull(label);
934                 mPendingIntent = requireNonNull(pendingIntent);
935             }
936 
937             /** Creates a {@link Builder} with the values from the given {@link Action}. */
938             @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull Action action)939             public Builder(@NonNull Action action) {
940                 if (!SdkLevel.isAtLeastU()) {
941                     throw new UnsupportedOperationException();
942                 }
943                 requireNonNull(action);
944                 mId = action.mId;
945                 mLabel = action.mLabel;
946                 mPendingIntent = action.mPendingIntent;
947                 mWillResolve = action.mWillResolve;
948                 mSuccessMessage = action.mSuccessMessage;
949                 mConfirmationDialogDetails = action.mConfirmationDialogDetails;
950             }
951 
952             /**
953              * Sets whether the action will resolve the safety issue. Defaults to {@code false}.
954              *
955              * <p>Note: It is not allowed for resolvable actions to have a {@link PendingIntent}
956              * that launches activity. When extra confirmation is needed consider using {@link
957              * Builder#setConfirmationDialogDetails}.
958              *
959              * @see #willResolve()
960              */
961             @SuppressLint("MissingGetterMatchingBuilder")
962             @NonNull
setWillResolve(boolean willResolve)963             public Builder setWillResolve(boolean willResolve) {
964                 mWillResolve = willResolve;
965                 return this;
966             }
967 
968             /**
969              * Sets the optional localized message to be displayed in the UI when the action is
970              * invoked and completes successfully.
971              */
972             @NonNull
setSuccessMessage(@ullable CharSequence successMessage)973             public Builder setSuccessMessage(@Nullable CharSequence successMessage) {
974                 mSuccessMessage = successMessage;
975                 return this;
976             }
977 
978             /**
979              * Sets the optional data to be displayed in the confirmation dialog prior to launching
980              * the {@link PendingIntent} when the action is clicked on.
981              */
982             @NonNull
983             @RequiresApi(UPSIDE_DOWN_CAKE)
setConfirmationDialogDetails( @ullable ConfirmationDialogDetails confirmationDialogDetails)984             public Builder setConfirmationDialogDetails(
985                     @Nullable ConfirmationDialogDetails confirmationDialogDetails) {
986                 if (!SdkLevel.isAtLeastU()) {
987                     throw new UnsupportedOperationException();
988                 }
989                 mConfirmationDialogDetails = confirmationDialogDetails;
990                 return this;
991             }
992 
993             /** Creates the {@link Action} defined by this {@link Builder}. */
994             @NonNull
build()995             public Action build() {
996                 if (SdkLevel.isAtLeastU()) {
997                     boolean willResolveWithActivity = mWillResolve && mPendingIntent.isActivity();
998                     checkArgument(
999                             !willResolveWithActivity,
1000                             "Launching activity from Action that should resolve the"
1001                                     + " SafetySourceIssue is not allowed. Consider using setting a"
1002                                     + " Confirmation if needed, and either set the willResolve to"
1003                                     + " false or make PendingIntent to start a service/send a"
1004                                     + " broadcast.");
1005                 }
1006                 return new Action(
1007                         mId,
1008                         mLabel,
1009                         mPendingIntent,
1010                         mWillResolve,
1011                         mSuccessMessage,
1012                         mConfirmationDialogDetails);
1013             }
1014         }
1015     }
1016 
1017     /**
1018      * Data for Safety Center to use when constructing a system {@link android.app.Notification}
1019      * about a related {@link SafetySourceIssue}.
1020      *
1021      * <p>Safety Center can construct a default notification for any issue, but sources may use
1022      * {@link Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification)} if
1023      * they want to override the title, text or actions.
1024      *
1025      * @see #getCustomNotification()
1026      * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification)
1027      * @see #getNotificationBehavior()
1028      */
1029     @RequiresApi(UPSIDE_DOWN_CAKE)
1030     public static final class Notification implements Parcelable {
1031 
1032         @NonNull
1033         public static final Creator<Notification> CREATOR =
1034                 new Creator<Notification>() {
1035                     @Override
1036                     public Notification createFromParcel(Parcel in) {
1037                         return new Builder(
1038                                         TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in),
1039                                         TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in))
1040                                 .addActions(in.createTypedArrayList(Action.CREATOR))
1041                                 .build();
1042                     }
1043 
1044                     @Override
1045                     public Notification[] newArray(int size) {
1046                         return new Notification[size];
1047                     }
1048                 };
1049 
1050         @NonNull private final CharSequence mTitle;
1051         @NonNull private final CharSequence mText;
1052         @NonNull private final List<Action> mActions;
1053 
Notification( @onNull CharSequence title, @NonNull CharSequence text, @NonNull List<Action> actions)1054         private Notification(
1055                 @NonNull CharSequence title,
1056                 @NonNull CharSequence text,
1057                 @NonNull List<Action> actions) {
1058             mTitle = title;
1059             mText = text;
1060             mActions = actions;
1061         }
1062 
1063         /**
1064          * Custom title which will be used instead of {@link SafetySourceIssue#getTitle()} when
1065          * building a {@link android.app.Notification} for this issue.
1066          */
1067         @NonNull
getTitle()1068         public CharSequence getTitle() {
1069             return mTitle;
1070         }
1071 
1072         /**
1073          * Custom text which will be used instead of {@link SafetySourceIssue#getSummary()} when
1074          * building a {@link android.app.Notification} for this issue.
1075          */
1076         @NonNull
getText()1077         public CharSequence getText() {
1078             return mText;
1079         }
1080 
1081         /**
1082          * Custom list of {@link Action} instances which will be used instead of {@link
1083          * SafetySourceIssue#getActions()} when building a {@link android.app.Notification} for this
1084          * issue.
1085          *
1086          * <p>If this list is empty then the resulting {@link android.app.Notification} will have
1087          * zero action buttons.
1088          */
1089         @NonNull
getActions()1090         public List<Action> getActions() {
1091             return mActions;
1092         }
1093 
1094         @Override
describeContents()1095         public int describeContents() {
1096             return 0;
1097         }
1098 
1099         @Override
writeToParcel(@onNull Parcel dest, int flags)1100         public void writeToParcel(@NonNull Parcel dest, int flags) {
1101             TextUtils.writeToParcel(mTitle, dest, flags);
1102             TextUtils.writeToParcel(mText, dest, flags);
1103             dest.writeTypedList(mActions);
1104         }
1105 
1106         @Override
equals(Object o)1107         public boolean equals(Object o) {
1108             if (this == o) return true;
1109             if (!(o instanceof Notification)) return false;
1110             Notification that = (Notification) o;
1111             return TextUtils.equals(mTitle, that.mTitle)
1112                     && TextUtils.equals(mText, that.mText)
1113                     && mActions.equals(that.mActions);
1114         }
1115 
1116         @Override
hashCode()1117         public int hashCode() {
1118             return Objects.hash(mTitle, mText, mActions);
1119         }
1120 
1121         @Override
toString()1122         public String toString() {
1123             return "Notification{"
1124                     + "mTitle="
1125                     + mTitle
1126                     + ", mText="
1127                     + mText
1128                     + ", mActions="
1129                     + mActions
1130                     + '}';
1131         }
1132 
1133         /** Builder for {@link SafetySourceIssue.Notification}. */
1134         public static final class Builder {
1135 
1136             @NonNull private final CharSequence mTitle;
1137             @NonNull private final CharSequence mText;
1138             @NonNull private final List<Action> mActions = new ArrayList<>();
1139 
Builder(@onNull CharSequence title, @NonNull CharSequence text)1140             public Builder(@NonNull CharSequence title, @NonNull CharSequence text) {
1141                 mTitle = requireNonNull(title);
1142                 mText = requireNonNull(text);
1143             }
1144 
1145             /** Creates a {@link Builder} with the values from the given {@link Notification}. */
Builder(@onNull Notification notification)1146             public Builder(@NonNull Notification notification) {
1147                 requireNonNull(notification);
1148                 mTitle = notification.mTitle;
1149                 mText = notification.mText;
1150                 mActions.addAll(notification.mActions);
1151             }
1152 
1153             /** Adds an {@link Action} to the custom {@link Notification}. */
1154             @NonNull
addAction(@onNull Action action)1155             public Builder addAction(@NonNull Action action) {
1156                 mActions.add(requireNonNull(action));
1157                 return this;
1158             }
1159 
1160             /** Adds several {@link Action}s to the custom {@link Notification}. */
1161             @NonNull
addActions(@onNull List<Action> actions)1162             public Builder addActions(@NonNull List<Action> actions) {
1163                 mActions.addAll(requireNonNull(actions));
1164                 return this;
1165             }
1166 
1167             /** Clears all the {@link Action}s that were added so far. */
1168             @NonNull
clearActions()1169             public Builder clearActions() {
1170                 mActions.clear();
1171                 return this;
1172             }
1173 
1174             /** Builds a {@link Notification} instance. */
1175             @NonNull
build()1176             public Notification build() {
1177                 List<Action> actions = unmodifiableList(new ArrayList<>(mActions));
1178                 Action.enforceUniqueActionIds(
1179                         actions, "Custom notification cannot have duplicate action ids");
1180                 checkArgument(
1181                         actions.size() <= 2,
1182                         "Custom notification must not contain more than 2 actions");
1183                 return new Notification(mTitle, mText, actions);
1184             }
1185         }
1186     }
1187 
1188     /** Builder class for {@link SafetySourceIssue}. */
1189     public static final class Builder {
1190 
1191         @NonNull private final String mId;
1192         @NonNull private final CharSequence mTitle;
1193         @NonNull private final CharSequence mSummary;
1194         @SafetySourceData.SeverityLevel private final int mSeverityLevel;
1195         @NonNull private final String mIssueTypeId;
1196         private final List<Action> mActions = new ArrayList<>();
1197 
1198         @Nullable private CharSequence mSubtitle;
1199         @IssueCategory private int mIssueCategory = ISSUE_CATEGORY_GENERAL;
1200         @Nullable private PendingIntent mOnDismissPendingIntent;
1201         @Nullable private CharSequence mAttributionTitle;
1202         @Nullable private String mDeduplicationId;
1203 
1204         @Nullable private Notification mCustomNotification = null;
1205 
1206         @SuppressLint("NewApi")
1207         @NotificationBehavior
1208         private int mNotificationBehavior = NOTIFICATION_BEHAVIOR_UNSPECIFIED;
1209 
1210         @SuppressLint("NewApi")
1211         @IssueActionability
1212         private int mIssueActionability = ISSUE_ACTIONABILITY_MANUAL;
1213 
1214         /** 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)1215         public Builder(
1216                 @NonNull String id,
1217                 @NonNull CharSequence title,
1218                 @NonNull CharSequence summary,
1219                 @SafetySourceData.SeverityLevel int severityLevel,
1220                 @NonNull String issueTypeId) {
1221             this.mId = requireNonNull(id);
1222             this.mTitle = requireNonNull(title);
1223             this.mSummary = requireNonNull(summary);
1224             this.mSeverityLevel = validateSeverityLevel(severityLevel);
1225             this.mIssueTypeId = requireNonNull(issueTypeId);
1226         }
1227 
1228         /** Creates a {@link Builder} with the values from the given {@link SafetySourceIssue}. */
1229         @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull SafetySourceIssue safetySourceIssue)1230         public Builder(@NonNull SafetySourceIssue safetySourceIssue) {
1231             if (!SdkLevel.isAtLeastU()) {
1232                 throw new UnsupportedOperationException();
1233             }
1234             requireNonNull(safetySourceIssue);
1235             mId = safetySourceIssue.mId;
1236             mTitle = safetySourceIssue.mTitle;
1237             mSummary = safetySourceIssue.mSummary;
1238             mSeverityLevel = safetySourceIssue.mSeverityLevel;
1239             mIssueTypeId = safetySourceIssue.mIssueTypeId;
1240             mActions.addAll(safetySourceIssue.mActions);
1241             mSubtitle = safetySourceIssue.mSubtitle;
1242             mIssueCategory = safetySourceIssue.mIssueCategory;
1243             mOnDismissPendingIntent = safetySourceIssue.mOnDismissPendingIntent;
1244             mAttributionTitle = safetySourceIssue.mAttributionTitle;
1245             mDeduplicationId = safetySourceIssue.mDeduplicationId;
1246             mCustomNotification = safetySourceIssue.mCustomNotification;
1247             mNotificationBehavior = safetySourceIssue.mNotificationBehavior;
1248             mIssueActionability = safetySourceIssue.mIssueActionability;
1249         }
1250 
1251         /** Sets the localized subtitle. */
1252         @NonNull
setSubtitle(@ullable CharSequence subtitle)1253         public Builder setSubtitle(@Nullable CharSequence subtitle) {
1254             mSubtitle = subtitle;
1255             return this;
1256         }
1257 
1258         /**
1259          * Sets or clears the optional attribution title for this issue.
1260          *
1261          * <p>This is displayed in the UI and helps to attribute an issue to a particular source. If
1262          * this value is {@code null}, the title of the group that contains the Safety Source will
1263          * be used.
1264          */
1265         @NonNull
1266         @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
setAttributionTitle(@ullable CharSequence attributionTitle)1267         public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) {
1268             if (!SdkLevel.isAtLeastU()) {
1269                 throw new UnsupportedOperationException();
1270             }
1271             mAttributionTitle = attributionTitle;
1272             return this;
1273         }
1274 
1275         /**
1276          * Sets the category of the risk associated with the issue.
1277          *
1278          * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}.
1279          */
1280         @NonNull
setIssueCategory(@ssueCategory int issueCategory)1281         public Builder setIssueCategory(@IssueCategory int issueCategory) {
1282             mIssueCategory = validateIssueCategory(issueCategory);
1283             return this;
1284         }
1285 
1286         /** Adds data for an {@link Action} to be shown in UI. */
1287         @NonNull
addAction(@onNull Action actionData)1288         public Builder addAction(@NonNull Action actionData) {
1289             mActions.add(requireNonNull(actionData));
1290             return this;
1291         }
1292 
1293         /** Clears data for all the {@link Action}s that were added to this {@link Builder}. */
1294         @NonNull
clearActions()1295         public Builder clearActions() {
1296             mActions.clear();
1297             return this;
1298         }
1299 
1300         /**
1301          * Sets an optional {@link PendingIntent} to be invoked when an issue is dismissed from the
1302          * UI.
1303          *
1304          * <p>In particular, if the source would like to be notified of issue dismissals in Safety
1305          * Center in order to be able to dismiss or ignore issues at the source, then set this
1306          * field. The action contained in the {@link PendingIntent} must not start an activity.
1307          *
1308          * @see #getOnDismissPendingIntent()
1309          */
1310         @NonNull
setOnDismissPendingIntent(@ullable PendingIntent onDismissPendingIntent)1311         public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) {
1312             checkArgument(
1313                     onDismissPendingIntent == null || !onDismissPendingIntent.isActivity(),
1314                     "Safety source issue on dismiss pending intent must not start an activity");
1315             mOnDismissPendingIntent = onDismissPendingIntent;
1316             return this;
1317         }
1318 
1319         /**
1320          * Sets a custom {@link Notification} for this issue.
1321          *
1322          * <p>Using a custom {@link Notification} a source may specify a different {@link
1323          * Notification#getTitle()}, {@link Notification#getText()} and {@link
1324          * Notification#getActions()} for Safety Center to use when constructing a notification for
1325          * this issue.
1326          *
1327          * <p>Safety Center may still generate a default notification from the other details of this
1328          * issue when no custom notification has been set, depending on the issue's {@link
1329          * #getNotificationBehavior()}.
1330          *
1331          * @see #getCustomNotification()
1332          * @see #setNotificationBehavior(int)
1333          */
1334         @NonNull
1335         @RequiresApi(UPSIDE_DOWN_CAKE)
setCustomNotification(@ullable Notification customNotification)1336         public Builder setCustomNotification(@Nullable Notification customNotification) {
1337             if (!SdkLevel.isAtLeastU()) {
1338                 throw new UnsupportedOperationException();
1339             }
1340             mCustomNotification = customNotification;
1341             return this;
1342         }
1343 
1344         /**
1345          * Sets the notification behavior of the issue.
1346          *
1347          * <p>Must be one of {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED}, {@link
1348          * #NOTIFICATION_BEHAVIOR_NEVER}, {@link #NOTIFICATION_BEHAVIOR_DELAYED} or {@link
1349          * #NOTIFICATION_BEHAVIOR_IMMEDIATELY}. See {@link #getNotificationBehavior()} for details
1350          * of how Safety Center will interpret each of these.
1351          *
1352          * @see #getNotificationBehavior()
1353          */
1354         @NonNull
1355         @RequiresApi(UPSIDE_DOWN_CAKE)
setNotificationBehavior(@otificationBehavior int notificationBehavior)1356         public Builder setNotificationBehavior(@NotificationBehavior int notificationBehavior) {
1357             if (!SdkLevel.isAtLeastU()) {
1358                 throw new UnsupportedOperationException();
1359             }
1360             mNotificationBehavior = validateNotificationBehavior(notificationBehavior);
1361             return this;
1362         }
1363 
1364         /**
1365          * Sets the deduplication identifier for the issue.
1366          *
1367          * @see #getDeduplicationId()
1368          */
1369         @NonNull
1370         @RequiresApi(UPSIDE_DOWN_CAKE)
setDeduplicationId(@ullable String deduplicationId)1371         public Builder setDeduplicationId(@Nullable String deduplicationId) {
1372             if (!SdkLevel.isAtLeastU()) {
1373                 throw new UnsupportedOperationException();
1374             }
1375             mDeduplicationId = deduplicationId;
1376             return this;
1377         }
1378 
1379         /**
1380          * Sets the issue actionability of the issue.
1381          *
1382          * <p>Must be one of {@link #ISSUE_ACTIONABILITY_MANUAL} (default), {@link
1383          * #ISSUE_ACTIONABILITY_TIP}, {@link #ISSUE_ACTIONABILITY_AUTOMATIC}.
1384          *
1385          * @see #getIssueActionability()
1386          */
1387         @NonNull
1388         @RequiresApi(UPSIDE_DOWN_CAKE)
setIssueActionability(@ssueActionability int issueActionability)1389         public Builder setIssueActionability(@IssueActionability int issueActionability) {
1390             if (!SdkLevel.isAtLeastU()) {
1391                 throw new UnsupportedOperationException();
1392             }
1393             mIssueActionability = validateIssueActionability(issueActionability);
1394             return this;
1395         }
1396 
1397         /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */
1398         @NonNull
build()1399         public SafetySourceIssue build() {
1400             List<SafetySourceIssue.Action> actions = unmodifiableList(new ArrayList<>(mActions));
1401             Action.enforceUniqueActionIds(
1402                     actions, "Safety source issue cannot have duplicate action ids");
1403             if (SdkLevel.isAtLeastU()) {
1404                 checkArgument(
1405                         mIssueActionability != ISSUE_ACTIONABILITY_MANUAL || !actions.isEmpty(),
1406                         "Actionable safety source issue must contain at least 1 action");
1407             } else {
1408                 checkArgument(
1409                         !actions.isEmpty(), "Safety source issue must contain at least 1 action");
1410             }
1411             checkArgument(
1412                     actions.size() <= 2,
1413                     "Safety source issue must not contain more than 2 actions");
1414             return new SafetySourceIssue(
1415                     mId,
1416                     mTitle,
1417                     mSubtitle,
1418                     mSummary,
1419                     mSeverityLevel,
1420                     mIssueCategory,
1421                     actions,
1422                     mOnDismissPendingIntent,
1423                     mIssueTypeId,
1424                     mCustomNotification,
1425                     mNotificationBehavior,
1426                     mAttributionTitle,
1427                     mDeduplicationId,
1428                     mIssueActionability);
1429         }
1430     }
1431 
1432     @SafetySourceData.SeverityLevel
validateSeverityLevel(int value)1433     private static int validateSeverityLevel(int value) {
1434         switch (value) {
1435             case SafetySourceData.SEVERITY_LEVEL_INFORMATION:
1436             case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION:
1437             case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING:
1438                 return value;
1439             case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED:
1440                 throw new IllegalArgumentException(
1441                         "SeverityLevel for SafetySourceIssue must not be "
1442                                 + "SEVERITY_LEVEL_UNSPECIFIED");
1443             default:
1444         }
1445         throw new IllegalArgumentException(
1446                 "Unexpected SeverityLevel for SafetySourceIssue: " + value);
1447     }
1448 
1449     @IssueCategory
validateIssueCategory(int value)1450     private static int validateIssueCategory(int value) {
1451         switch (value) {
1452             case ISSUE_CATEGORY_DEVICE:
1453             case ISSUE_CATEGORY_ACCOUNT:
1454             case ISSUE_CATEGORY_GENERAL:
1455                 return value;
1456             default:
1457         }
1458         if (SdkLevel.isAtLeastU()) {
1459             switch (value) {
1460                 case ISSUE_CATEGORY_DATA:
1461                 case ISSUE_CATEGORY_PASSWORDS:
1462                 case ISSUE_CATEGORY_PERSONAL_SAFETY:
1463                     return value;
1464                 default:
1465             }
1466         }
1467         throw new IllegalArgumentException(
1468                 "Unexpected IssueCategory for SafetySourceIssue: " + value);
1469     }
1470 
1471     @NotificationBehavior
validateNotificationBehavior(int value)1472     private static int validateNotificationBehavior(int value) {
1473         switch (value) {
1474             case NOTIFICATION_BEHAVIOR_UNSPECIFIED:
1475             case NOTIFICATION_BEHAVIOR_NEVER:
1476             case NOTIFICATION_BEHAVIOR_DELAYED:
1477             case NOTIFICATION_BEHAVIOR_IMMEDIATELY:
1478                 return value;
1479             default:
1480         }
1481         throw new IllegalArgumentException(
1482                 "Unexpected NotificationBehavior for SafetySourceIssue: " + value);
1483     }
1484 
1485     @IssueActionability
validateIssueActionability(int value)1486     private static int validateIssueActionability(int value) {
1487         switch (value) {
1488             case ISSUE_ACTIONABILITY_MANUAL:
1489             case ISSUE_ACTIONABILITY_TIP:
1490             case ISSUE_ACTIONABILITY_AUTOMATIC:
1491                 return value;
1492             default:
1493         }
1494         throw new IllegalArgumentException(
1495                 "Unexpected IssueActionability for SafetySourceIssue: " + value);
1496     }
1497 }
1498