• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.safetycenter;
18 
19 import static android.os.Build.VERSION_CODES.TIRAMISU;
20 
21 import static java.util.Collections.unmodifiableList;
22 import static java.util.Objects.requireNonNull;
23 
24 import android.annotation.IntDef;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.SuppressLint;
28 import android.annotation.SystemApi;
29 import android.app.PendingIntent;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.text.TextUtils;
33 
34 import androidx.annotation.RequiresApi;
35 
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Objects;
41 
42 /**
43  * An issue in the Safety Center.
44  *
45  * <p>An issue represents an actionable matter on the device of elevated importance.
46  *
47  * <p>It contains localized messages to display to the user, explaining the underlying threat or
48  * warning and suggested fixes, and contains actions that a user may take from the UI to resolve the
49  * issue.
50  *
51  * <p>Issues are ephemeral and disappear when resolved by user action or dismissal.
52  *
53  * @hide
54  */
55 @SystemApi
56 @RequiresApi(TIRAMISU)
57 public final class SafetyCenterIssue implements Parcelable {
58 
59     /** Indicates that this is low-severity, and informational. */
60     public static final int ISSUE_SEVERITY_LEVEL_OK = 2100;
61 
62     /** Indicates that this issue describes a safety recommendation. */
63     public static final int ISSUE_SEVERITY_LEVEL_RECOMMENDATION = 2200;
64 
65     /** Indicates that this issue describes a critical safety warning. */
66     public static final int ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING = 2300;
67 
68     /**
69      * All possible severity levels for a {@link SafetyCenterIssue}.
70      *
71      * @hide
72      * @see SafetyCenterIssue#getSeverityLevel()
73      * @see Builder#setSeverityLevel(int)
74      */
75     @Retention(RetentionPolicy.SOURCE)
76     @IntDef(
77             prefix = "ISSUE_SEVERITY_LEVEL_",
78             value = {
79                 ISSUE_SEVERITY_LEVEL_OK,
80                 ISSUE_SEVERITY_LEVEL_RECOMMENDATION,
81                 ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING,
82             })
83     public @interface IssueSeverityLevel {}
84 
85     @NonNull
86     public static final Creator<SafetyCenterIssue> CREATOR =
87             new Creator<SafetyCenterIssue>() {
88                 @Override
89                 public SafetyCenterIssue createFromParcel(Parcel in) {
90                     String id = in.readString();
91                     CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
92                     CharSequence subtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
93                     CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
94                     return new Builder(id, title, summary)
95                             .setSubtitle(subtitle)
96                             .setSeverityLevel(in.readInt())
97                             .setDismissible(in.readBoolean())
98                             .setShouldConfirmDismissal(in.readBoolean())
99                             .setActions(in.createTypedArrayList(Action.CREATOR))
100                             .build();
101                 }
102 
103                 @Override
104                 public SafetyCenterIssue[] newArray(int size) {
105                     return new SafetyCenterIssue[size];
106                 }
107             };
108 
109     @NonNull private final String mId;
110     @NonNull private final CharSequence mTitle;
111     @Nullable private final CharSequence mSubtitle;
112     @NonNull private final CharSequence mSummary;
113     @IssueSeverityLevel private final int mSeverityLevel;
114     private final boolean mDismissible;
115     private final boolean mShouldConfirmDismissal;
116     @NonNull private final List<Action> mActions;
117 
SafetyCenterIssue( @onNull String id, @NonNull CharSequence title, @Nullable CharSequence subtitle, @NonNull CharSequence summary, @IssueSeverityLevel int severityLevel, boolean isDismissible, boolean shouldConfirmDismissal, @NonNull List<Action> actions)118     private SafetyCenterIssue(
119             @NonNull String id,
120             @NonNull CharSequence title,
121             @Nullable CharSequence subtitle,
122             @NonNull CharSequence summary,
123             @IssueSeverityLevel int severityLevel,
124             boolean isDismissible,
125             boolean shouldConfirmDismissal,
126             @NonNull List<Action> actions) {
127         mId = id;
128         mTitle = title;
129         mSubtitle = subtitle;
130         mSummary = summary;
131         mSeverityLevel = severityLevel;
132         mDismissible = isDismissible;
133         mShouldConfirmDismissal = shouldConfirmDismissal;
134         mActions = actions;
135     }
136 
137     /**
138      * Returns the encoded string ID which uniquely identifies this issue within the Safety Center
139      * on the device for the current user across all profiles and accounts.
140      */
141     @NonNull
getId()142     public String getId() {
143         return mId;
144     }
145 
146     /** Returns the title that describes this issue. */
147     @NonNull
getTitle()148     public CharSequence getTitle() {
149         return mTitle;
150     }
151 
152     /** Returns the subtitle of this issue, or {@code null} if it has none. */
153     @Nullable
getSubtitle()154     public CharSequence getSubtitle() {
155         return mSubtitle;
156     }
157 
158     /** Returns the summary text that describes this issue. */
159     @NonNull
getSummary()160     public CharSequence getSummary() {
161         return mSummary;
162     }
163 
164     /** Returns the {@link IssueSeverityLevel} of this issue. */
165     @IssueSeverityLevel
getSeverityLevel()166     public int getSeverityLevel() {
167         return mSeverityLevel;
168     }
169 
170     /** Returns {@code true} if this issue can be dismissed. */
isDismissible()171     public boolean isDismissible() {
172         return mDismissible;
173     }
174 
175     /** Returns {@code true} if this issue should have its dismissal confirmed. */
shouldConfirmDismissal()176     public boolean shouldConfirmDismissal() {
177         return mShouldConfirmDismissal;
178     }
179 
180     /**
181      * Returns the ordered list of {@link Action} objects that may be taken to resolve this issue.
182      *
183      * <p>An issue may have 0-2 actions. The first action will be considered the "Primary" action of
184      * the issue.
185      */
186     @NonNull
getActions()187     public List<Action> getActions() {
188         return mActions;
189     }
190 
191     @Override
equals(Object o)192     public boolean equals(Object o) {
193         if (this == o) return true;
194         if (!(o instanceof SafetyCenterIssue)) return false;
195         SafetyCenterIssue that = (SafetyCenterIssue) o;
196         return mSeverityLevel == that.mSeverityLevel
197                 && mDismissible == that.mDismissible
198                 && mShouldConfirmDismissal == that.mShouldConfirmDismissal
199                 && Objects.equals(mId, that.mId)
200                 && TextUtils.equals(mTitle, that.mTitle)
201                 && TextUtils.equals(mSubtitle, that.mSubtitle)
202                 && TextUtils.equals(mSummary, that.mSummary)
203                 && Objects.equals(mActions, that.mActions);
204     }
205 
206     @Override
hashCode()207     public int hashCode() {
208         return Objects.hash(
209                 mId,
210                 mTitle,
211                 mSubtitle,
212                 mSummary,
213                 mSeverityLevel,
214                 mDismissible,
215                 mShouldConfirmDismissal,
216                 mActions);
217     }
218 
219     @Override
toString()220     public String toString() {
221         return "SafetyCenterIssue{"
222                 + "mId='"
223                 + mId
224                 + '\''
225                 + ", mTitle="
226                 + mTitle
227                 + ", mSubtitle="
228                 + mSubtitle
229                 + ", mSummary="
230                 + mSummary
231                 + ", mSeverityLevel="
232                 + mSeverityLevel
233                 + ", mDismissible="
234                 + mDismissible
235                 + ", mConfirmDismissal="
236                 + mShouldConfirmDismissal
237                 + ", mActions="
238                 + mActions
239                 + '}';
240     }
241 
242     @Override
describeContents()243     public int describeContents() {
244         return 0;
245     }
246 
247     @Override
writeToParcel(@onNull Parcel dest, int flags)248     public void writeToParcel(@NonNull Parcel dest, int flags) {
249         dest.writeString(mId);
250         TextUtils.writeToParcel(mTitle, dest, flags);
251         TextUtils.writeToParcel(mSubtitle, dest, flags);
252         TextUtils.writeToParcel(mSummary, dest, flags);
253         dest.writeInt(mSeverityLevel);
254         dest.writeBoolean(mDismissible);
255         dest.writeBoolean(mShouldConfirmDismissal);
256         dest.writeTypedList(mActions);
257     }
258 
259     /** Builder class for {@link SafetyCenterIssue}. */
260     public static final class Builder {
261 
262         @NonNull private String mId;
263         @NonNull private CharSequence mTitle;
264         @NonNull private CharSequence mSummary;
265         @Nullable private CharSequence mSubtitle;
266         @IssueSeverityLevel private int mSeverityLevel = ISSUE_SEVERITY_LEVEL_OK;
267         private boolean mDismissible = true;
268         private boolean mShouldConfirmDismissal = true;
269         private List<Action> mActions = new ArrayList<>();
270 
271         /**
272          * Creates a {@link Builder} for a {@link SafetyCenterIssue}.
273          *
274          * @param id a unique encoded string ID, see {@link #getId()} for details
275          * @param title a title that describes this issue
276          * @param summary a summary of this issue
277          */
Builder( @onNull String id, @NonNull CharSequence title, @NonNull CharSequence summary)278         public Builder(
279                 @NonNull String id, @NonNull CharSequence title, @NonNull CharSequence summary) {
280             mId = requireNonNull(id);
281             mTitle = requireNonNull(title);
282             mSummary = requireNonNull(summary);
283         }
284 
285         /** Creates a {@link Builder} with the values from the given {@link SafetyCenterIssue}. */
Builder(@onNull SafetyCenterIssue issue)286         public Builder(@NonNull SafetyCenterIssue issue) {
287             mId = issue.mId;
288             mTitle = issue.mTitle;
289             mSubtitle = issue.mSubtitle;
290             mSummary = issue.mSummary;
291             mSeverityLevel = issue.mSeverityLevel;
292             mDismissible = issue.mDismissible;
293             mShouldConfirmDismissal = issue.mShouldConfirmDismissal;
294             mActions = new ArrayList<>(issue.mActions);
295         }
296 
297         /** Sets the ID for this issue. */
298         @NonNull
setId(@onNull String id)299         public Builder setId(@NonNull String id) {
300             mId = requireNonNull(id);
301             return this;
302         }
303 
304         /** Sets the title for this issue. */
305         @NonNull
setTitle(@onNull CharSequence title)306         public Builder setTitle(@NonNull CharSequence title) {
307             mTitle = requireNonNull(title);
308             return this;
309         }
310 
311         /** Sets or clears the optional subtitle for this issue. */
312         @NonNull
setSubtitle(@ullable CharSequence subtitle)313         public Builder setSubtitle(@Nullable CharSequence subtitle) {
314             mSubtitle = subtitle;
315             return this;
316         }
317 
318         /** Sets the summary for this issue. */
319         @NonNull
setSummary(@onNull CharSequence summary)320         public Builder setSummary(@NonNull CharSequence summary) {
321             mSummary = requireNonNull(summary);
322             return this;
323         }
324 
325         /**
326          * Sets {@link IssueSeverityLevel} for this issue. Defaults to {@link
327          * #ISSUE_SEVERITY_LEVEL_OK}.
328          */
329         @NonNull
setSeverityLevel(@ssueSeverityLevel int severityLevel)330         public Builder setSeverityLevel(@IssueSeverityLevel int severityLevel) {
331             mSeverityLevel = validateIssueSeverityLevel(severityLevel);
332             return this;
333         }
334 
335         /** Sets whether this issue can be dismissed. Defaults to {@code true}. */
336         @NonNull
setDismissible(boolean dismissible)337         public Builder setDismissible(boolean dismissible) {
338             mDismissible = dismissible;
339             return this;
340         }
341 
342         /**
343          * Sets whether this issue should have its dismissal confirmed. Defaults to {@code true}.
344          */
345         @NonNull
setShouldConfirmDismissal(boolean confirmDismissal)346         public Builder setShouldConfirmDismissal(boolean confirmDismissal) {
347             mShouldConfirmDismissal = confirmDismissal;
348             return this;
349         }
350 
351         /**
352          * Sets the list of potential actions to be taken to resolve this issue. Defaults to an
353          * empty list.
354          */
355         @NonNull
setActions(@onNull List<Action> actions)356         public Builder setActions(@NonNull List<Action> actions) {
357             mActions = requireNonNull(actions);
358             return this;
359         }
360 
361         /** Creates the {@link SafetyCenterIssue} defined by this {@link Builder}. */
362         @NonNull
build()363         public SafetyCenterIssue build() {
364             return new SafetyCenterIssue(
365                     mId,
366                     mTitle,
367                     mSubtitle,
368                     mSummary,
369                     mSeverityLevel,
370                     mDismissible,
371                     mShouldConfirmDismissal,
372                     unmodifiableList(new ArrayList<>(mActions)));
373         }
374     }
375 
376     /**
377      * An action that can be taken to resolve a given issue.
378      *
379      * <p>When a user initiates an {@link Action}, that action's associated {@link PendingIntent}
380      * will be executed, and the {@code successMessage} will be displayed if present.
381      *
382      * @hide
383      */
384     @SystemApi
385     public static final class Action implements Parcelable {
386 
387         @NonNull
388         public static final Creator<Action> CREATOR =
389                 new Creator<Action>() {
390                     @Override
391                     public Action createFromParcel(Parcel in) {
392                         String id = in.readString();
393                         CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
394                         PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
395                         return new Builder(id, label, pendingIntent)
396                                 .setWillResolve(in.readBoolean())
397                                 .setIsInFlight(in.readBoolean())
398                                 .setSuccessMessage(
399                                         TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in))
400                                 .build();
401                     }
402 
403                     @Override
404                     public Action[] newArray(int size) {
405                         return new Action[size];
406                     }
407                 };
408 
409         @NonNull private final String mId;
410         @NonNull private final CharSequence mLabel;
411         @NonNull private final PendingIntent mPendingIntent;
412         private final boolean mWillResolve;
413         private final boolean mInFlight;
414         @Nullable private final CharSequence mSuccessMessage;
415 
Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, boolean inFlight, @Nullable CharSequence successMessage)416         private Action(
417                 @NonNull String id,
418                 @NonNull CharSequence label,
419                 @NonNull PendingIntent pendingIntent,
420                 boolean willResolve,
421                 boolean inFlight,
422                 @Nullable CharSequence successMessage) {
423             mId = id;
424             mLabel = label;
425             mPendingIntent = pendingIntent;
426             mWillResolve = willResolve;
427             mInFlight = inFlight;
428             mSuccessMessage = successMessage;
429         }
430 
431         /** Returns the ID of this action. */
432         @NonNull
getId()433         public String getId() {
434             return mId;
435         }
436 
437         /** Returns a label describing this {@link Action}. */
438         @NonNull
getLabel()439         public CharSequence getLabel() {
440             return mLabel;
441         }
442 
443         /** Returns the {@link PendingIntent} to execute when this {@link Action} is taken. */
444         @NonNull
getPendingIntent()445         public PendingIntent getPendingIntent() {
446             return mPendingIntent;
447         }
448 
449         /**
450          * Returns whether invoking this action will fix or address the issue sufficiently for it to
451          * be considered resolved (i.e. the issue will no longer need to be conveyed to the user in
452          * the UI).
453          */
willResolve()454         public boolean willResolve() {
455             return mWillResolve;
456         }
457 
458         /**
459          * Returns whether this action is currently being executed (i.e. the user clicked on a
460          * button that triggered this action, and now the Safety Center is waiting for the action's
461          * result).
462          */
isInFlight()463         public boolean isInFlight() {
464             return mInFlight;
465         }
466 
467         /**
468          * Returns the success message to display after successfully completing this {@link Action}
469          * or {@code null} if none should be displayed.
470          */
471         @Nullable
getSuccessMessage()472         public CharSequence getSuccessMessage() {
473             return mSuccessMessage;
474         }
475 
476         @Override
equals(Object o)477         public boolean equals(Object o) {
478             if (this == o) return true;
479             if (!(o instanceof Action)) return false;
480             Action action = (Action) o;
481             return Objects.equals(mId, action.mId)
482                     && TextUtils.equals(mLabel, action.mLabel)
483                     && Objects.equals(mPendingIntent, action.mPendingIntent)
484                     && mWillResolve == action.mWillResolve
485                     && mInFlight == action.mInFlight
486                     && TextUtils.equals(mSuccessMessage, action.mSuccessMessage);
487         }
488 
489         @Override
hashCode()490         public int hashCode() {
491             return Objects.hash(
492                     mId, mLabel, mSuccessMessage, mWillResolve, mInFlight, mPendingIntent);
493         }
494 
495         @Override
toString()496         public String toString() {
497             return "Action{"
498                     + "mId="
499                     + mId
500                     + ", mLabel="
501                     + mLabel
502                     + ", mPendingIntent="
503                     + mPendingIntent
504                     + ", mWillResolve="
505                     + mWillResolve
506                     + ", mInFlight="
507                     + mInFlight
508                     + ", mSuccessMessage="
509                     + mSuccessMessage
510                     + '}';
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(mLabel, dest, flags);
522             dest.writeTypedObject(mPendingIntent, flags);
523             dest.writeBoolean(mWillResolve);
524             dest.writeBoolean(mInFlight);
525             TextUtils.writeToParcel(mSuccessMessage, dest, flags);
526         }
527 
528         /** Builder class for {@link Action}. */
529         public static final class Builder {
530 
531             @NonNull private String mId;
532             @NonNull private CharSequence mLabel;
533             @NonNull private PendingIntent mPendingIntent;
534             private boolean mWillResolve;
535             private boolean mInFlight;
536             @Nullable private CharSequence mSuccessMessage;
537 
538             /**
539              * Creates a new {@link Builder} for an {@link Action}.
540              *
541              * @param id a unique ID for this action
542              * @param label a label describing this action
543              * @param pendingIntent a {@link PendingIntent} to be sent when this action is taken
544              */
Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)545             public Builder(
546                     @NonNull String id,
547                     @NonNull CharSequence label,
548                     @NonNull PendingIntent pendingIntent) {
549                 mId = requireNonNull(id);
550                 mLabel = requireNonNull(label);
551                 mPendingIntent = requireNonNull(pendingIntent);
552             }
553 
554             /** Sets the ID of this {@link Action} */
555             @NonNull
setId(@onNull String id)556             public Builder setId(@NonNull String id) {
557                 mId = requireNonNull(id);
558                 return this;
559             }
560 
561             /** Sets the label of this {@link Action}. */
562             @NonNull
setLabel(@onNull CharSequence label)563             public Builder setLabel(@NonNull CharSequence label) {
564                 mLabel = requireNonNull(label);
565                 return this;
566             }
567 
568             /** Sets the {@link PendingIntent} to be sent when this {@link Action} is taken. */
569             @NonNull
setPendingIntent(@onNull PendingIntent pendingIntent)570             public Builder setPendingIntent(@NonNull PendingIntent pendingIntent) {
571                 mPendingIntent = requireNonNull(pendingIntent);
572                 return this;
573             }
574 
575             /**
576              * Sets whether this action will resolve the issue when executed. Defaults to {@code
577              * false}.
578              *
579              * @see #willResolve()
580              */
581             @SuppressLint("MissingGetterMatchingBuilder")
582             @NonNull
setWillResolve(boolean willResolve)583             public Builder setWillResolve(boolean willResolve) {
584                 mWillResolve = willResolve;
585                 return this;
586             }
587 
588             /**
589              * Sets a boolean that indicates whether this action is currently being executed (i.e.
590              * the user clicked on a button that triggered this action, and now the Safety Center is
591              * waiting for the action's result). Defaults to {@code false}.
592              *
593              * @see #isInFlight()
594              */
595             @SuppressLint("MissingGetterMatchingBuilder")
596             @NonNull
setIsInFlight(boolean inFlight)597             public Builder setIsInFlight(boolean inFlight) {
598                 mInFlight = inFlight;
599                 return this;
600             }
601 
602             /**
603              * Sets or clears the optional success message to be displayed when this {@link Action}
604              * completes.
605              */
606             @NonNull
setSuccessMessage(@ullable CharSequence successMessage)607             public Builder setSuccessMessage(@Nullable CharSequence successMessage) {
608                 mSuccessMessage = successMessage;
609                 return this;
610             }
611 
612             /** Creates the {@link Action} defined by this {@link Builder}. */
613             @NonNull
build()614             public Action build() {
615                 return new Action(
616                         mId, mLabel, mPendingIntent, mWillResolve, mInFlight, mSuccessMessage);
617             }
618         }
619     }
620 
621     @IssueSeverityLevel
validateIssueSeverityLevel(int value)622     private static int validateIssueSeverityLevel(int value) {
623         switch (value) {
624             case ISSUE_SEVERITY_LEVEL_OK:
625             case ISSUE_SEVERITY_LEVEL_RECOMMENDATION:
626             case ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING:
627                 return value;
628             default:
629         }
630         throw new IllegalArgumentException(
631                 String.format("Unexpected IssueSeverityLevel for SafetyCenterIssue: %s", value));
632     }
633 }
634