1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.safetycenter;
18 
19 import static android.os.Build.VERSION_CODES.TIRAMISU;
20 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
21 
22 import static java.util.Collections.unmodifiableList;
23 import static java.util.Objects.requireNonNull;
24 
25 import android.annotation.IntDef;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.SuppressLint;
29 import android.annotation.SystemApi;
30 import android.app.PendingIntent;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.safetycenter.config.SafetySourcesGroup;
34 import android.text.TextUtils;
35 
36 import androidx.annotation.RequiresApi;
37 
38 import com.android.modules.utils.build.SdkLevel;
39 
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Objects;
45 
46 /**
47  * An issue in the Safety Center.
48  *
49  * <p>An issue represents an actionable matter on the device of elevated importance.
50  *
51  * <p>It contains localized messages to display to the user, explaining the underlying threat or
52  * warning and suggested fixes, and contains actions that a user may take from the UI to resolve the
53  * issue.
54  *
55  * <p>Issues are ephemeral and disappear when resolved by user action or dismissal.
56  *
57  * @hide
58  */
59 @SystemApi
60 @RequiresApi(TIRAMISU)
61 public final class SafetyCenterIssue implements Parcelable {
62 
63     /** Indicates that this is low-severity, and informational. */
64     public static final int ISSUE_SEVERITY_LEVEL_OK = 2100;
65 
66     /** Indicates that this issue describes a safety recommendation. */
67     public static final int ISSUE_SEVERITY_LEVEL_RECOMMENDATION = 2200;
68 
69     /** Indicates that this issue describes a critical safety warning. */
70     public static final int ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING = 2300;
71 
72     /**
73      * All possible severity levels for a {@link SafetyCenterIssue}.
74      *
75      * @hide
76      * @see SafetyCenterIssue#getSeverityLevel()
77      * @see Builder#setSeverityLevel(int)
78      */
79     @Retention(RetentionPolicy.SOURCE)
80     @IntDef(
81             prefix = "ISSUE_SEVERITY_LEVEL_",
82             value = {
83                 ISSUE_SEVERITY_LEVEL_OK,
84                 ISSUE_SEVERITY_LEVEL_RECOMMENDATION,
85                 ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING,
86             })
87     public @interface IssueSeverityLevel {}
88 
89     @NonNull
90     public static final Creator<SafetyCenterIssue> CREATOR =
91             new Creator<SafetyCenterIssue>() {
92                 @Override
93                 public SafetyCenterIssue createFromParcel(Parcel in) {
94                     String id = in.readString();
95                     CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
96                     CharSequence subtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
97                     CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
98                     SafetyCenterIssue.Builder builder =
99                             new Builder(id, title, summary)
100                                     .setSubtitle(subtitle)
101                                     .setSeverityLevel(in.readInt())
102                                     .setDismissible(in.readBoolean())
103                                     .setShouldConfirmDismissal(in.readBoolean())
104                                     .setActions(in.createTypedArrayList(Action.CREATOR));
105                     if (SdkLevel.isAtLeastU()) {
106                         builder.setAttributionTitle(
107                                 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in));
108                         builder.setGroupId(in.readString());
109                     }
110                     return builder.build();
111                 }
112 
113                 @Override
114                 public SafetyCenterIssue[] newArray(int size) {
115                     return new SafetyCenterIssue[size];
116                 }
117             };
118 
119     @NonNull private final String mId;
120     @NonNull private final CharSequence mTitle;
121     @Nullable private final CharSequence mSubtitle;
122     @NonNull private final CharSequence mSummary;
123     @IssueSeverityLevel private final int mSeverityLevel;
124     private final boolean mDismissible;
125     private final boolean mShouldConfirmDismissal;
126     @NonNull private final List<Action> mActions;
127     @Nullable private final CharSequence mAttributionTitle;
128     @Nullable private final String mGroupId;
129 
SafetyCenterIssue( @onNull String id, @NonNull CharSequence title, @Nullable CharSequence subtitle, @NonNull CharSequence summary, @IssueSeverityLevel int severityLevel, boolean isDismissible, boolean shouldConfirmDismissal, @NonNull List<Action> actions, @Nullable CharSequence attributionTitle, @Nullable String groupId)130     private SafetyCenterIssue(
131             @NonNull String id,
132             @NonNull CharSequence title,
133             @Nullable CharSequence subtitle,
134             @NonNull CharSequence summary,
135             @IssueSeverityLevel int severityLevel,
136             boolean isDismissible,
137             boolean shouldConfirmDismissal,
138             @NonNull List<Action> actions,
139             @Nullable CharSequence attributionTitle,
140             @Nullable String groupId) {
141         mId = id;
142         mTitle = title;
143         mSubtitle = subtitle;
144         mSummary = summary;
145         mSeverityLevel = severityLevel;
146         mDismissible = isDismissible;
147         mShouldConfirmDismissal = shouldConfirmDismissal;
148         mActions = actions;
149         mAttributionTitle = attributionTitle;
150         mGroupId = groupId;
151     }
152 
153     /**
154      * Returns the encoded string ID which uniquely identifies this issue within the Safety Center
155      * on the device for the current user across all profiles and accounts.
156      */
157     @NonNull
getId()158     public String getId() {
159         return mId;
160     }
161 
162     /** Returns the title that describes this issue. */
163     @NonNull
getTitle()164     public CharSequence getTitle() {
165         return mTitle;
166     }
167 
168     /** Returns the subtitle of this issue, or {@code null} if it has none. */
169     @Nullable
getSubtitle()170     public CharSequence getSubtitle() {
171         return mSubtitle;
172     }
173 
174     /** Returns the summary text that describes this issue. */
175     @NonNull
getSummary()176     public CharSequence getSummary() {
177         return mSummary;
178     }
179 
180     /**
181      * Returns the attribution title of this issue, or {@code null} if it has none.
182      *
183      * <p>This is displayed in the UI and helps to attribute issue cards to a particular source.
184      *
185      * @throws UnsupportedOperationException if accessed from a version lower than {@link
186      *     UPSIDE_DOWN_CAKE}
187      */
188     @Nullable
189     @RequiresApi(UPSIDE_DOWN_CAKE)
getAttributionTitle()190     public CharSequence getAttributionTitle() {
191         if (!SdkLevel.isAtLeastU()) {
192             throw new UnsupportedOperationException(
193                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
194         }
195         return mAttributionTitle;
196     }
197 
198     /** Returns the {@link IssueSeverityLevel} of this issue. */
199     @IssueSeverityLevel
getSeverityLevel()200     public int getSeverityLevel() {
201         return mSeverityLevel;
202     }
203 
204     /** Returns {@code true} if this issue can be dismissed. */
isDismissible()205     public boolean isDismissible() {
206         return mDismissible;
207     }
208 
209     /** Returns {@code true} if this issue should have its dismissal confirmed. */
shouldConfirmDismissal()210     public boolean shouldConfirmDismissal() {
211         return mShouldConfirmDismissal;
212     }
213 
214     /**
215      * Returns the ordered list of {@link Action} objects that may be taken to resolve this issue.
216      *
217      * <p>An issue may have 0-2 actions. The first action will be considered the "Primary" action of
218      * the issue.
219      */
220     @NonNull
getActions()221     public List<Action> getActions() {
222         return mActions;
223     }
224 
225     /**
226      * Returns the ID of the {@link SafetySourcesGroup} that this issue belongs to, or {@code null}
227      * if it has none.
228      *
229      * <p>This ID is used for displaying the issue on its corresponding subpage in the Safety Center
230      * UI.
231      *
232      * @throws UnsupportedOperationException if accessed from a version lower than {@link
233      *     UPSIDE_DOWN_CAKE}
234      */
235     @Nullable
236     @RequiresApi(UPSIDE_DOWN_CAKE)
getGroupId()237     public String getGroupId() {
238         if (!SdkLevel.isAtLeastU()) {
239             throw new UnsupportedOperationException(
240                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
241         }
242         return mGroupId;
243     }
244 
245     @Override
equals(Object o)246     public boolean equals(Object o) {
247         if (this == o) return true;
248         if (!(o instanceof SafetyCenterIssue)) return false;
249         SafetyCenterIssue that = (SafetyCenterIssue) o;
250         return mSeverityLevel == that.mSeverityLevel
251                 && mDismissible == that.mDismissible
252                 && mShouldConfirmDismissal == that.mShouldConfirmDismissal
253                 && Objects.equals(mId, that.mId)
254                 && TextUtils.equals(mTitle, that.mTitle)
255                 && TextUtils.equals(mSubtitle, that.mSubtitle)
256                 && TextUtils.equals(mSummary, that.mSummary)
257                 && Objects.equals(mActions, that.mActions)
258                 && TextUtils.equals(mAttributionTitle, that.mAttributionTitle)
259                 && Objects.equals(mGroupId, that.mGroupId);
260     }
261 
262     @Override
hashCode()263     public int hashCode() {
264         return Objects.hash(
265                 mId,
266                 mTitle,
267                 mSubtitle,
268                 mSummary,
269                 mSeverityLevel,
270                 mDismissible,
271                 mShouldConfirmDismissal,
272                 mActions,
273                 mAttributionTitle,
274                 mGroupId);
275     }
276 
277     @Override
toString()278     public String toString() {
279         return "SafetyCenterIssue{"
280                 + "mId="
281                 + mId
282                 + ", mTitle="
283                 + mTitle
284                 + ", mSubtitle="
285                 + mSubtitle
286                 + ", mSummary="
287                 + mSummary
288                 + ", mSeverityLevel="
289                 + mSeverityLevel
290                 + ", mDismissible="
291                 + mDismissible
292                 + ", mConfirmDismissal="
293                 + mShouldConfirmDismissal
294                 + ", mActions="
295                 + mActions
296                 + ", mAttributionTitle="
297                 + mAttributionTitle
298                 + ", mGroupId="
299                 + mGroupId
300                 + '}';
301     }
302 
303     @Override
describeContents()304     public int describeContents() {
305         return 0;
306     }
307 
308     @Override
writeToParcel(@onNull Parcel dest, int flags)309     public void writeToParcel(@NonNull Parcel dest, int flags) {
310         dest.writeString(mId);
311         TextUtils.writeToParcel(mTitle, dest, flags);
312         TextUtils.writeToParcel(mSubtitle, dest, flags);
313         TextUtils.writeToParcel(mSummary, dest, flags);
314         dest.writeInt(mSeverityLevel);
315         dest.writeBoolean(mDismissible);
316         dest.writeBoolean(mShouldConfirmDismissal);
317         dest.writeTypedList(mActions);
318         if (SdkLevel.isAtLeastU()) {
319             TextUtils.writeToParcel(mAttributionTitle, dest, flags);
320             dest.writeString(mGroupId);
321         }
322     }
323 
324     /** Builder class for {@link SafetyCenterIssue}. */
325     public static final class Builder {
326 
327         @NonNull private String mId;
328         @NonNull private CharSequence mTitle;
329         @NonNull private CharSequence mSummary;
330         @Nullable private CharSequence mSubtitle;
331         @IssueSeverityLevel private int mSeverityLevel = ISSUE_SEVERITY_LEVEL_OK;
332         private boolean mDismissible = true;
333         private boolean mShouldConfirmDismissal = true;
334         private List<Action> mActions = new ArrayList<>();
335         @Nullable private CharSequence mAttributionTitle;
336         @Nullable private String mGroupId;
337 
338         /**
339          * Creates a {@link Builder} for a {@link SafetyCenterIssue}.
340          *
341          * @param id a unique encoded string ID, see {@link #getId()} for details
342          * @param title a title that describes this issue
343          * @param summary a summary of this issue
344          */
Builder( @onNull String id, @NonNull CharSequence title, @NonNull CharSequence summary)345         public Builder(
346                 @NonNull String id, @NonNull CharSequence title, @NonNull CharSequence summary) {
347             mId = requireNonNull(id);
348             mTitle = requireNonNull(title);
349             mSummary = requireNonNull(summary);
350         }
351 
352         /** Creates a {@link Builder} with the values from the given {@link SafetyCenterIssue}. */
Builder(@onNull SafetyCenterIssue issue)353         public Builder(@NonNull SafetyCenterIssue issue) {
354             mId = issue.mId;
355             mTitle = issue.mTitle;
356             mSubtitle = issue.mSubtitle;
357             mSummary = issue.mSummary;
358             mSeverityLevel = issue.mSeverityLevel;
359             mDismissible = issue.mDismissible;
360             mShouldConfirmDismissal = issue.mShouldConfirmDismissal;
361             mActions = new ArrayList<>(issue.mActions);
362             mAttributionTitle = issue.mAttributionTitle;
363             mGroupId = issue.mGroupId;
364         }
365 
366         /** Sets the ID for this issue. */
367         @NonNull
setId(@onNull String id)368         public Builder setId(@NonNull String id) {
369             mId = requireNonNull(id);
370             return this;
371         }
372 
373         /** Sets the title for this issue. */
374         @NonNull
setTitle(@onNull CharSequence title)375         public Builder setTitle(@NonNull CharSequence title) {
376             mTitle = requireNonNull(title);
377             return this;
378         }
379 
380         /** Sets or clears the optional subtitle for this issue. */
381         @NonNull
setSubtitle(@ullable CharSequence subtitle)382         public Builder setSubtitle(@Nullable CharSequence subtitle) {
383             mSubtitle = subtitle;
384             return this;
385         }
386 
387         /** Sets the summary for this issue. */
388         @NonNull
setSummary(@onNull CharSequence summary)389         public Builder setSummary(@NonNull CharSequence summary) {
390             mSummary = requireNonNull(summary);
391             return this;
392         }
393 
394         /**
395          * Sets or clears the optional attribution title for this issue.
396          *
397          * <p>This is displayed in the UI and helps to attribute issue cards to a particular source.
398          *
399          * @throws UnsupportedOperationException if accessed from a version lower than {@link
400          *     UPSIDE_DOWN_CAKE}
401          */
402         @NonNull
403         @RequiresApi(UPSIDE_DOWN_CAKE)
setAttributionTitle(@ullable CharSequence attributionTitle)404         public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) {
405             if (!SdkLevel.isAtLeastU()) {
406                 throw new UnsupportedOperationException(
407                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
408             }
409             mAttributionTitle = attributionTitle;
410             return this;
411         }
412 
413         /**
414          * Sets {@link IssueSeverityLevel} for this issue. Defaults to {@link
415          * #ISSUE_SEVERITY_LEVEL_OK}.
416          */
417         @NonNull
setSeverityLevel(@ssueSeverityLevel int severityLevel)418         public Builder setSeverityLevel(@IssueSeverityLevel int severityLevel) {
419             mSeverityLevel = validateIssueSeverityLevel(severityLevel);
420             return this;
421         }
422 
423         /** Sets whether this issue can be dismissed. Defaults to {@code true}. */
424         @NonNull
setDismissible(boolean dismissible)425         public Builder setDismissible(boolean dismissible) {
426             mDismissible = dismissible;
427             return this;
428         }
429 
430         /**
431          * Sets whether this issue should have its dismissal confirmed. Defaults to {@code true}.
432          */
433         @NonNull
setShouldConfirmDismissal(boolean confirmDismissal)434         public Builder setShouldConfirmDismissal(boolean confirmDismissal) {
435             mShouldConfirmDismissal = confirmDismissal;
436             return this;
437         }
438 
439         /**
440          * Sets the list of potential actions to be taken to resolve this issue. Defaults to an
441          * empty list.
442          */
443         @NonNull
setActions(@onNull List<Action> actions)444         public Builder setActions(@NonNull List<Action> actions) {
445             mActions = requireNonNull(actions);
446             return this;
447         }
448 
449         /**
450          * Sets the ID of {@link SafetySourcesGroup} that this issue belongs to. Defaults to a
451          * {@code null} value.
452          *
453          * <p>This ID is used for displaying the issue on its corresponding subpage in the Safety
454          * Center UI.
455          *
456          * @throws UnsupportedOperationException if accessed from a version lower than {@link
457          *     UPSIDE_DOWN_CAKE}
458          */
459         @NonNull
460         @RequiresApi(UPSIDE_DOWN_CAKE)
setGroupId(@ullable String groupId)461         public Builder setGroupId(@Nullable String groupId) {
462             if (!SdkLevel.isAtLeastU()) {
463                 throw new UnsupportedOperationException(
464                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
465             }
466             mGroupId = groupId;
467             return this;
468         }
469 
470         /** Creates the {@link SafetyCenterIssue} defined by this {@link Builder}. */
471         @NonNull
build()472         public SafetyCenterIssue build() {
473             return new SafetyCenterIssue(
474                     mId,
475                     mTitle,
476                     mSubtitle,
477                     mSummary,
478                     mSeverityLevel,
479                     mDismissible,
480                     mShouldConfirmDismissal,
481                     unmodifiableList(new ArrayList<>(mActions)),
482                     mAttributionTitle,
483                     mGroupId);
484         }
485     }
486 
487     /**
488      * An action that can be taken to resolve a given issue.
489      *
490      * <p>When a user initiates an {@link Action}, that action's associated {@link PendingIntent}
491      * will be executed, and the {@code successMessage} will be displayed if present.
492      */
493     public static final class Action implements Parcelable {
494 
495         @NonNull
496         public static final Creator<Action> CREATOR =
497                 new Creator<Action>() {
498                     @Override
499                     public Action createFromParcel(Parcel in) {
500                         String id = in.readString();
501                         CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
502                         PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
503                         Builder builder =
504                                 new Builder(id, label, pendingIntent)
505                                         .setWillResolve(in.readBoolean())
506                                         .setIsInFlight(in.readBoolean())
507                                         .setSuccessMessage(
508                                                 TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(
509                                                         in));
510                         if (SdkLevel.isAtLeastU()) {
511                             ConfirmationDialogDetails confirmationDialogDetails =
512                                     in.readTypedObject(ConfirmationDialogDetails.CREATOR);
513                             builder.setConfirmationDialogDetails(confirmationDialogDetails);
514                         }
515                         return builder.build();
516                     }
517 
518                     @Override
519                     public Action[] newArray(int size) {
520                         return new Action[size];
521                     }
522                 };
523 
524         @NonNull private final String mId;
525         @NonNull private final CharSequence mLabel;
526         @NonNull private final PendingIntent mPendingIntent;
527         private final boolean mWillResolve;
528         private final boolean mInFlight;
529         @Nullable private final CharSequence mSuccessMessage;
530         @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails;
531 
Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, boolean inFlight, @Nullable CharSequence successMessage, @Nullable ConfirmationDialogDetails confirmationDialogDetails)532         private Action(
533                 @NonNull String id,
534                 @NonNull CharSequence label,
535                 @NonNull PendingIntent pendingIntent,
536                 boolean willResolve,
537                 boolean inFlight,
538                 @Nullable CharSequence successMessage,
539                 @Nullable ConfirmationDialogDetails confirmationDialogDetails) {
540             mId = id;
541             mLabel = label;
542             mPendingIntent = pendingIntent;
543             mWillResolve = willResolve;
544             mInFlight = inFlight;
545             mSuccessMessage = successMessage;
546             mConfirmationDialogDetails = confirmationDialogDetails;
547         }
548 
549         /** Returns the ID of this action. */
550         @NonNull
getId()551         public String getId() {
552             return mId;
553         }
554 
555         /** Returns a label describing this {@link Action}. */
556         @NonNull
getLabel()557         public CharSequence getLabel() {
558             return mLabel;
559         }
560 
561         /** Returns the {@link PendingIntent} to execute when this {@link Action} is taken. */
562         @NonNull
getPendingIntent()563         public PendingIntent getPendingIntent() {
564             return mPendingIntent;
565         }
566 
567         /**
568          * Returns whether invoking this action will fix or address the issue sufficiently for it to
569          * be considered resolved (i.e. the issue will no longer need to be conveyed to the user in
570          * the UI).
571          */
willResolve()572         public boolean willResolve() {
573             return mWillResolve;
574         }
575 
576         /**
577          * Returns whether this action is currently being executed (i.e. the user clicked on a
578          * button that triggered this action, and now the Safety Center is waiting for the action's
579          * result).
580          */
isInFlight()581         public boolean isInFlight() {
582             return mInFlight;
583         }
584 
585         /**
586          * Returns the success message to display after successfully completing this {@link Action}
587          * or {@code null} if none should be displayed.
588          */
589         @Nullable
getSuccessMessage()590         public CharSequence getSuccessMessage() {
591             return mSuccessMessage;
592         }
593 
594         /**
595          * Returns the optional data to be displayed in the confirmation dialog prior to launching
596          * the {@link PendingIntent} when the action is clicked on.
597          */
598         @Nullable
599         @RequiresApi(UPSIDE_DOWN_CAKE)
getConfirmationDialogDetails()600         public ConfirmationDialogDetails getConfirmationDialogDetails() {
601             if (!SdkLevel.isAtLeastU()) {
602                 throw new UnsupportedOperationException(
603                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
604             }
605             return mConfirmationDialogDetails;
606         }
607 
608         @Override
equals(Object o)609         public boolean equals(Object o) {
610             if (this == o) return true;
611             if (!(o instanceof Action)) return false;
612             Action action = (Action) o;
613             return Objects.equals(mId, action.mId)
614                     && TextUtils.equals(mLabel, action.mLabel)
615                     && Objects.equals(mPendingIntent, action.mPendingIntent)
616                     && mWillResolve == action.mWillResolve
617                     && mInFlight == action.mInFlight
618                     && TextUtils.equals(mSuccessMessage, action.mSuccessMessage)
619                     && Objects.equals(
620                             mConfirmationDialogDetails, action.mConfirmationDialogDetails);
621         }
622 
623         @Override
hashCode()624         public int hashCode() {
625             return Objects.hash(
626                     mId,
627                     mLabel,
628                     mSuccessMessage,
629                     mWillResolve,
630                     mInFlight,
631                     mPendingIntent,
632                     mConfirmationDialogDetails);
633         }
634 
635         @Override
toString()636         public String toString() {
637             return "Action{"
638                     + "mId="
639                     + mId
640                     + ", mLabel="
641                     + mLabel
642                     + ", mPendingIntent="
643                     + mPendingIntent
644                     + ", mWillResolve="
645                     + mWillResolve
646                     + ", mInFlight="
647                     + mInFlight
648                     + ", mSuccessMessage="
649                     + mSuccessMessage
650                     + ", mConfirmationDialogDetails="
651                     + mConfirmationDialogDetails
652                     + '}';
653         }
654 
655         @Override
describeContents()656         public int describeContents() {
657             return 0;
658         }
659 
660         @Override
writeToParcel(@onNull Parcel dest, int flags)661         public void writeToParcel(@NonNull Parcel dest, int flags) {
662             dest.writeString(mId);
663             TextUtils.writeToParcel(mLabel, dest, flags);
664             dest.writeTypedObject(mPendingIntent, flags);
665             dest.writeBoolean(mWillResolve);
666             dest.writeBoolean(mInFlight);
667             TextUtils.writeToParcel(mSuccessMessage, dest, flags);
668             if (SdkLevel.isAtLeastU()) {
669                 dest.writeTypedObject(mConfirmationDialogDetails, flags);
670             }
671         }
672 
673         /** Data for an action confirmation dialog to be shown before action is executed. */
674         @RequiresApi(UPSIDE_DOWN_CAKE)
675         public static final class ConfirmationDialogDetails implements Parcelable {
676 
677             @NonNull
678             public static final Creator<ConfirmationDialogDetails> CREATOR =
679                     new Creator<ConfirmationDialogDetails>() {
680                         @Override
681                         public ConfirmationDialogDetails createFromParcel(Parcel in) {
682                             CharSequence title =
683                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
684                             CharSequence text =
685                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
686                             CharSequence acceptButtonText =
687                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
688                             CharSequence denyButtonText =
689                                     TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
690                             return new ConfirmationDialogDetails(
691                                     title, text, acceptButtonText, denyButtonText);
692                         }
693 
694                         @Override
695                         public ConfirmationDialogDetails[] newArray(int size) {
696                             return new ConfirmationDialogDetails[size];
697                         }
698                     };
699 
700             @NonNull private final CharSequence mTitle;
701             @NonNull private final CharSequence mText;
702             @NonNull private final CharSequence mAcceptButtonText;
703             @NonNull private final CharSequence mDenyButtonText;
704 
ConfirmationDialogDetails( @onNull CharSequence title, @NonNull CharSequence text, @NonNull CharSequence acceptButtonText, @NonNull CharSequence denyButtonText)705             public ConfirmationDialogDetails(
706                     @NonNull CharSequence title,
707                     @NonNull CharSequence text,
708                     @NonNull CharSequence acceptButtonText,
709                     @NonNull CharSequence denyButtonText) {
710                 mTitle = requireNonNull(title);
711                 mText = requireNonNull(text);
712                 mAcceptButtonText = requireNonNull(acceptButtonText);
713                 mDenyButtonText = requireNonNull(denyButtonText);
714             }
715 
716             /** Returns the title of action confirmation dialog. */
717             @NonNull
getTitle()718             public CharSequence getTitle() {
719                 return mTitle;
720             }
721 
722             /** Returns the text of action confirmation dialog. */
723             @NonNull
getText()724             public CharSequence getText() {
725                 return mText;
726             }
727 
728             /** Returns the text of the button to accept action execution. */
729             @NonNull
getAcceptButtonText()730             public CharSequence getAcceptButtonText() {
731                 return mAcceptButtonText;
732             }
733 
734             /** Returns the text of the button to deny action execution. */
735             @NonNull
getDenyButtonText()736             public CharSequence getDenyButtonText() {
737                 return mDenyButtonText;
738             }
739 
740             @Override
describeContents()741             public int describeContents() {
742                 return 0;
743             }
744 
745             @Override
writeToParcel(@onNull Parcel dest, int flags)746             public void writeToParcel(@NonNull Parcel dest, int flags) {
747                 TextUtils.writeToParcel(mTitle, dest, flags);
748                 TextUtils.writeToParcel(mText, dest, flags);
749                 TextUtils.writeToParcel(mAcceptButtonText, dest, flags);
750                 TextUtils.writeToParcel(mDenyButtonText, dest, flags);
751             }
752 
753             @Override
equals(Object o)754             public boolean equals(Object o) {
755                 if (this == o) return true;
756                 if (!(o instanceof ConfirmationDialogDetails)) return false;
757                 ConfirmationDialogDetails that = (ConfirmationDialogDetails) o;
758                 return TextUtils.equals(mTitle, that.mTitle)
759                         && TextUtils.equals(mText, that.mText)
760                         && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText)
761                         && TextUtils.equals(mDenyButtonText, that.mDenyButtonText);
762             }
763 
764             @Override
hashCode()765             public int hashCode() {
766                 return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText);
767             }
768 
769             @Override
toString()770             public String toString() {
771                 return "ConfirmationDialogDetails{"
772                         + "mTitle="
773                         + mTitle
774                         + ", mText="
775                         + mText
776                         + ", mAcceptButtonText="
777                         + mAcceptButtonText
778                         + ", mDenyButtonText="
779                         + mDenyButtonText
780                         + '}';
781             }
782         }
783 
784         /** Builder class for {@link Action}. */
785         public static final class Builder {
786 
787             @NonNull private String mId;
788             @NonNull private CharSequence mLabel;
789             @NonNull private PendingIntent mPendingIntent;
790             private boolean mWillResolve;
791             private boolean mInFlight;
792             @Nullable private CharSequence mSuccessMessage;
793             @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails;
794 
795             /**
796              * Creates a new {@link Builder} for an {@link Action}.
797              *
798              * @param id a unique ID for this action
799              * @param label a label describing this action
800              * @param pendingIntent a {@link PendingIntent} to be sent when this action is taken
801              */
Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)802             public Builder(
803                     @NonNull String id,
804                     @NonNull CharSequence label,
805                     @NonNull PendingIntent pendingIntent) {
806                 mId = requireNonNull(id);
807                 mLabel = requireNonNull(label);
808                 mPendingIntent = requireNonNull(pendingIntent);
809             }
810 
811             /** Creates a {@link Builder} with the values from the given {@link Action}. */
812             @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull Action action)813             public Builder(@NonNull Action action) {
814                 if (!SdkLevel.isAtLeastU()) {
815                     throw new UnsupportedOperationException(
816                             "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
817                 }
818                 requireNonNull(action);
819                 mId = action.mId;
820                 mLabel = action.mLabel;
821                 mPendingIntent = action.mPendingIntent;
822                 mWillResolve = action.mWillResolve;
823                 mInFlight = action.mInFlight;
824                 mSuccessMessage = action.mSuccessMessage;
825                 mConfirmationDialogDetails = action.mConfirmationDialogDetails;
826             }
827 
828             /** Sets the ID of this {@link Action} */
829             @NonNull
setId(@onNull String id)830             public Builder setId(@NonNull String id) {
831                 mId = requireNonNull(id);
832                 return this;
833             }
834 
835             /** Sets the label of this {@link Action}. */
836             @NonNull
setLabel(@onNull CharSequence label)837             public Builder setLabel(@NonNull CharSequence label) {
838                 mLabel = requireNonNull(label);
839                 return this;
840             }
841 
842             /** Sets the {@link PendingIntent} to be sent when this {@link Action} is taken. */
843             @NonNull
setPendingIntent(@onNull PendingIntent pendingIntent)844             public Builder setPendingIntent(@NonNull PendingIntent pendingIntent) {
845                 mPendingIntent = requireNonNull(pendingIntent);
846                 return this;
847             }
848 
849             /**
850              * Sets whether this action will resolve the issue when executed. Defaults to {@code
851              * false}.
852              *
853              * @see #willResolve()
854              */
855             @SuppressLint("MissingGetterMatchingBuilder")
856             @NonNull
setWillResolve(boolean willResolve)857             public Builder setWillResolve(boolean willResolve) {
858                 mWillResolve = willResolve;
859                 return this;
860             }
861 
862             /**
863              * Sets a boolean that indicates whether this action is currently being executed (i.e.
864              * the user clicked on a button that triggered this action, and now the Safety Center is
865              * waiting for the action's result). Defaults to {@code false}.
866              *
867              * @see #isInFlight()
868              */
869             @SuppressLint("MissingGetterMatchingBuilder")
870             @NonNull
setIsInFlight(boolean inFlight)871             public Builder setIsInFlight(boolean inFlight) {
872                 mInFlight = inFlight;
873                 return this;
874             }
875 
876             /**
877              * Sets or clears the optional success message to be displayed when this {@link Action}
878              * completes.
879              */
880             @NonNull
setSuccessMessage(@ullable CharSequence successMessage)881             public Builder setSuccessMessage(@Nullable CharSequence successMessage) {
882                 mSuccessMessage = successMessage;
883                 return this;
884             }
885 
886             /**
887              * Sets the optional data to be displayed in the confirmation dialog prior to launching
888              * the {@link PendingIntent} when the action is clicked on.
889              */
890             @NonNull
891             @RequiresApi(UPSIDE_DOWN_CAKE)
setConfirmationDialogDetails( @ullable ConfirmationDialogDetails confirmationDialogDetails)892             public Builder setConfirmationDialogDetails(
893                     @Nullable ConfirmationDialogDetails confirmationDialogDetails) {
894                 if (!SdkLevel.isAtLeastU()) {
895                     throw new UnsupportedOperationException(
896                             "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
897                 }
898                 mConfirmationDialogDetails = confirmationDialogDetails;
899                 return this;
900             }
901 
902             /** Creates the {@link Action} defined by this {@link Builder}. */
903             @NonNull
build()904             public Action build() {
905                 return new Action(
906                         mId,
907                         mLabel,
908                         mPendingIntent,
909                         mWillResolve,
910                         mInFlight,
911                         mSuccessMessage,
912                         mConfirmationDialogDetails);
913             }
914         }
915     }
916 
917     @IssueSeverityLevel
validateIssueSeverityLevel(int value)918     private static int validateIssueSeverityLevel(int value) {
919         switch (value) {
920             case ISSUE_SEVERITY_LEVEL_OK:
921             case ISSUE_SEVERITY_LEVEL_RECOMMENDATION:
922             case ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING:
923                 return value;
924             default:
925         }
926         throw new IllegalArgumentException(
927                 "Unexpected IssueSeverityLevel for SafetyCenterIssue: " + value);
928     }
929 }
930