• 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 
21 import static com.android.internal.util.Preconditions.checkArgument;
22 
23 import static java.util.Collections.unmodifiableList;
24 import static java.util.Objects.requireNonNull;
25 
26 import android.annotation.IntDef;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.annotation.SuppressLint;
30 import android.annotation.SystemApi;
31 import android.app.PendingIntent;
32 import android.os.Parcel;
33 import android.os.Parcelable;
34 import android.text.TextUtils;
35 
36 import androidx.annotation.RequiresApi;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Objects;
43 
44 /**
45  * Data for a safety source issue in the Safety Center page.
46  *
47  * <p>An issue represents an actionable matter relating to a particular safety source.
48  *
49  * <p>The safety issue will contain localized messages to be shown in UI explaining the potential
50  * threat or warning and suggested fixes, as well as actions a user is allowed to take from the UI
51  * to resolve the issue.
52  *
53  * @hide
54  */
55 @SystemApi
56 @RequiresApi(TIRAMISU)
57 public final class SafetySourceIssue implements Parcelable {
58 
59     /** Indicates that the risk associated with the issue is related to a user's device safety. */
60     public static final int ISSUE_CATEGORY_DEVICE = 100;
61 
62     /** Indicates that the risk associated with the issue is related to a user's account safety. */
63     public static final int ISSUE_CATEGORY_ACCOUNT = 200;
64 
65     /** Indicates that the risk associated with the issue is related to a user's general safety. */
66     public static final int ISSUE_CATEGORY_GENERAL = 300;
67 
68     /**
69      * All possible issue categories.
70      *
71      * <p>An issue's category represents a specific area of safety that the issue relates to.
72      *
73      * <p>An issue can only have one associated category. If the issue relates to multiple areas of
74      * safety, then choose the closest area or default to {@link #ISSUE_CATEGORY_GENERAL}.
75      *
76      * @hide
77      * @see Builder#setIssueCategory(int)
78      */
79     @IntDef(
80             prefix = {"ISSUE_CATEGORY_"},
81             value = {
82                 ISSUE_CATEGORY_DEVICE,
83                 ISSUE_CATEGORY_ACCOUNT,
84                 ISSUE_CATEGORY_GENERAL,
85             })
86     @Retention(RetentionPolicy.SOURCE)
87     public @interface IssueCategory {}
88 
89     @NonNull
90     public static final Creator<SafetySourceIssue> CREATOR =
91             new Creator<SafetySourceIssue>() {
92                 @Override
93                 public SafetySourceIssue 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                     int severityLevel = in.readInt();
99                     int issueCategory = in.readInt();
100                     List<Action> actions = requireNonNull(in.createTypedArrayList(Action.CREATOR));
101                     PendingIntent onDismissPendingIntent =
102                             in.readTypedObject(PendingIntent.CREATOR);
103                     String issueTypeId = in.readString();
104                     Builder builder =
105                             new Builder(id, title, summary, severityLevel, issueTypeId)
106                                     .setSubtitle(subtitle)
107                                     .setIssueCategory(issueCategory)
108                                     .setOnDismissPendingIntent(onDismissPendingIntent);
109                     for (int i = 0; i < actions.size(); i++) {
110                         builder.addAction(actions.get(i));
111                     }
112                     return builder.build();
113                 }
114 
115                 @Override
116                 public SafetySourceIssue[] newArray(int size) {
117                     return new SafetySourceIssue[size];
118                 }
119             };
120 
121     @NonNull private final String mId;
122     @NonNull private final CharSequence mTitle;
123     @Nullable private final CharSequence mSubtitle;
124     @NonNull private final CharSequence mSummary;
125     @SafetySourceData.SeverityLevel private final int mSeverityLevel;
126     private final List<Action> mActions;
127     @Nullable private final PendingIntent mOnDismissPendingIntent;
128     @IssueCategory private final int mIssueCategory;
129     @NonNull private final String mIssueTypeId;
130 
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)131     private SafetySourceIssue(
132             @NonNull String id,
133             @NonNull CharSequence title,
134             @Nullable CharSequence subtitle,
135             @NonNull CharSequence summary,
136             @SafetySourceData.SeverityLevel int severityLevel,
137             @IssueCategory int issueCategory,
138             @NonNull List<Action> actions,
139             @Nullable PendingIntent onDismissPendingIntent,
140             @NonNull String issueTypeId) {
141         this.mId = id;
142         this.mTitle = title;
143         this.mSubtitle = subtitle;
144         this.mSummary = summary;
145         this.mSeverityLevel = severityLevel;
146         this.mIssueCategory = issueCategory;
147         this.mActions = actions;
148         this.mOnDismissPendingIntent = onDismissPendingIntent;
149         this.mIssueTypeId = issueTypeId;
150     }
151 
152     /**
153      * Returns the identifier for this issue.
154      *
155      * <p>This id should uniquely identify the safety risk represented by this issue. Safety issues
156      * will be deduped by this id to be shown in the UI.
157      *
158      * <p>On multiple instances of providing the same issue to be represented in Safety Center,
159      * provide the same id across all instances.
160      */
161     @NonNull
getId()162     public String getId() {
163         return mId;
164     }
165 
166     /** Returns the localized title of the issue to be displayed in the UI. */
167     @NonNull
getTitle()168     public CharSequence getTitle() {
169         return mTitle;
170     }
171 
172     /** Returns the localized subtitle of the issue to be displayed in the UI. */
173     @Nullable
getSubtitle()174     public CharSequence getSubtitle() {
175         return mSubtitle;
176     }
177 
178     /** Returns the localized summary of the issue to be displayed in the UI. */
179     @NonNull
getSummary()180     public CharSequence getSummary() {
181         return mSummary;
182     }
183 
184     /** Returns the {@link SafetySourceData.SeverityLevel} of the issue. */
185     @SafetySourceData.SeverityLevel
getSeverityLevel()186     public int getSeverityLevel() {
187         return mSeverityLevel;
188     }
189 
190     /**
191      * Returns the category of the risk associated with the issue.
192      *
193      * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}.
194      */
195     @IssueCategory
getIssueCategory()196     public int getIssueCategory() {
197         return mIssueCategory;
198     }
199 
200     /**
201      * Returns a list of {@link Action}s representing actions supported in the UI for this issue.
202      *
203      * <p>Each issue must contain at least one action, in order to help the user resolve the issue.
204      *
205      * <p>In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most
206      * two actions supported from the UI.
207      */
208     @NonNull
getActions()209     public List<Action> getActions() {
210         return mActions;
211     }
212 
213     /**
214      * Returns the optional {@link PendingIntent} that will be invoked when an issue is dismissed.
215      *
216      * <p>When a safety issue is dismissed in Safety Center page, the issue is removed from view in
217      * Safety Center page. This method returns an additional optional action specified by the safety
218      * source that should be invoked on issue dismissal. The action contained in the {@link
219      * PendingIntent} cannot start an activity.
220      */
221     @Nullable
getOnDismissPendingIntent()222     public PendingIntent getOnDismissPendingIntent() {
223         return mOnDismissPendingIntent;
224     }
225 
226     /**
227      * Returns the identifier for the type of this issue.
228      *
229      * <p>The issue type should indicate the underlying basis for the issue, for e.g. a pending
230      * update or a disabled security feature.
231      *
232      * <p>The difference between this id and {@link #getId()} is that the issue type id is meant to
233      * be used for logging and should therefore contain no personally identifiable information (PII)
234      * (e.g. for account name).
235      *
236      * <p>On multiple instances of providing the same issue to be represented in Safety Center,
237      * provide the same issue type id across all instances.
238      */
239     @NonNull
getIssueTypeId()240     public String getIssueTypeId() {
241         return mIssueTypeId;
242     }
243 
244     @Override
describeContents()245     public int describeContents() {
246         return 0;
247     }
248 
249     @Override
writeToParcel(@onNull Parcel dest, int flags)250     public void writeToParcel(@NonNull Parcel dest, int flags) {
251         dest.writeString(mId);
252         TextUtils.writeToParcel(mTitle, dest, flags);
253         TextUtils.writeToParcel(mSubtitle, dest, flags);
254         TextUtils.writeToParcel(mSummary, dest, flags);
255         dest.writeInt(mSeverityLevel);
256         dest.writeInt(mIssueCategory);
257         dest.writeTypedList(mActions);
258         dest.writeTypedObject(mOnDismissPendingIntent, flags);
259         dest.writeString(mIssueTypeId);
260     }
261 
262     @Override
equals(Object o)263     public boolean equals(Object o) {
264         if (this == o) return true;
265         if (!(o instanceof SafetySourceIssue)) return false;
266         SafetySourceIssue that = (SafetySourceIssue) o;
267         return mSeverityLevel == that.mSeverityLevel
268                 && TextUtils.equals(mId, that.mId)
269                 && TextUtils.equals(mTitle, that.mTitle)
270                 && TextUtils.equals(mSubtitle, that.mSubtitle)
271                 && TextUtils.equals(mSummary, that.mSummary)
272                 && mIssueCategory == that.mIssueCategory
273                 && mActions.equals(that.mActions)
274                 && Objects.equals(mOnDismissPendingIntent, that.mOnDismissPendingIntent)
275                 && TextUtils.equals(mIssueTypeId, that.mIssueTypeId);
276     }
277 
278     @Override
hashCode()279     public int hashCode() {
280         return Objects.hash(
281                 mId,
282                 mTitle,
283                 mSubtitle,
284                 mSummary,
285                 mSeverityLevel,
286                 mIssueCategory,
287                 mActions,
288                 mOnDismissPendingIntent,
289                 mIssueTypeId);
290     }
291 
292     @Override
toString()293     public String toString() {
294         return "SafetySourceIssue{"
295                 + "mId="
296                 + mId
297                 + "mTitle="
298                 + mTitle
299                 + ", mSubtitle="
300                 + mSubtitle
301                 + ", mSummary="
302                 + mSummary
303                 + ", mSeverityLevel="
304                 + mSeverityLevel
305                 + ", mIssueCategory="
306                 + mIssueCategory
307                 + ", mActions="
308                 + mActions
309                 + ", mOnDismissPendingIntent="
310                 + mOnDismissPendingIntent
311                 + ", mIssueTypeId="
312                 + mIssueTypeId
313                 + '}';
314     }
315 
316     /**
317      * Data for an action supported from a safety issue {@link SafetySourceIssue} in the Safety
318      * Center page.
319      *
320      * <p>The purpose of the action is to allow the user to address the safety issue, either by
321      * performing a fix suggested in the issue, or by navigating the user to the source of the issue
322      * where they can be exposed to detail about the issue and further suggestions to resolve it.
323      *
324      * <p>The user will be allowed to invoke the action from the UI by clicking on a UI element and
325      * consequently resolve the issue.
326      *
327      * @hide
328      */
329     @SystemApi
330     public static final class Action implements Parcelable {
331 
332         @NonNull
333         public static final Creator<Action> CREATOR =
334                 new Creator<Action>() {
335                     @Override
336                     public Action createFromParcel(Parcel in) {
337                         String id = in.readString();
338                         CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
339                         PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
340                         return new Builder(id, label, pendingIntent)
341                                 .setWillResolve(in.readBoolean())
342                                 .setSuccessMessage(
343                                         TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in))
344                                 .build();
345                     }
346 
347                     @Override
348                     public Action[] newArray(int size) {
349                         return new Action[size];
350                     }
351                 };
352 
353         @NonNull private final String mId;
354         @NonNull private final CharSequence mLabel;
355         @NonNull private final PendingIntent mPendingIntent;
356         private final boolean mWillResolve;
357         @Nullable private final CharSequence mSuccessMessage;
358 
Action( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, boolean willResolve, @Nullable CharSequence successMessage)359         private Action(
360                 @NonNull String id,
361                 @NonNull CharSequence label,
362                 @NonNull PendingIntent pendingIntent,
363                 boolean willResolve,
364                 @Nullable CharSequence successMessage) {
365             mId = id;
366             mLabel = label;
367             mPendingIntent = pendingIntent;
368             mWillResolve = willResolve;
369             mSuccessMessage = successMessage;
370         }
371 
372         /**
373          * Returns the ID of the action, unique among actions in a given {@link SafetySourceIssue}.
374          */
375         @NonNull
getId()376         public String getId() {
377             return mId;
378         }
379 
380         /**
381          * Returns the localized label of the action to be displayed in the UI.
382          *
383          * <p>The label should indicate what action will be performed if when invoked.
384          */
385         @NonNull
getLabel()386         public CharSequence getLabel() {
387             return mLabel;
388         }
389 
390         /**
391          * Returns a {@link PendingIntent} to be fired when the action is clicked on.
392          *
393          * <p>The {@link PendingIntent} should perform the action referred to by {@link
394          * #getLabel()}.
395          */
396         @NonNull
getPendingIntent()397         public PendingIntent getPendingIntent() {
398             return mPendingIntent;
399         }
400 
401         /**
402          * Returns whether invoking this action will fix or address the issue sufficiently for it to
403          * be considered resolved i.e. the issue will no longer need to be conveyed to the user in
404          * the UI.
405          */
willResolve()406         public boolean willResolve() {
407             return mWillResolve;
408         }
409 
410         /**
411          * Returns the optional localized message to be displayed in the UI when the action is
412          * invoked and completes successfully.
413          */
414         @Nullable
getSuccessMessage()415         public CharSequence getSuccessMessage() {
416             return mSuccessMessage;
417         }
418 
419         @Override
describeContents()420         public int describeContents() {
421             return 0;
422         }
423 
424         @Override
writeToParcel(@onNull Parcel dest, int flags)425         public void writeToParcel(@NonNull Parcel dest, int flags) {
426             dest.writeString(mId);
427             TextUtils.writeToParcel(mLabel, dest, flags);
428             dest.writeTypedObject(mPendingIntent, flags);
429             dest.writeBoolean(mWillResolve);
430             TextUtils.writeToParcel(mSuccessMessage, dest, flags);
431         }
432 
433         @Override
equals(Object o)434         public boolean equals(Object o) {
435             if (this == o) return true;
436             if (!(o instanceof Action)) return false;
437             Action that = (Action) o;
438             return mId.equals(that.mId)
439                     && TextUtils.equals(mLabel, that.mLabel)
440                     && mPendingIntent.equals(that.mPendingIntent)
441                     && mWillResolve == that.mWillResolve
442                     && TextUtils.equals(mSuccessMessage, that.mSuccessMessage);
443         }
444 
445         @Override
hashCode()446         public int hashCode() {
447             return Objects.hash(mId, mLabel, mPendingIntent, mWillResolve, mSuccessMessage);
448         }
449 
450         @Override
toString()451         public String toString() {
452             return "Action{"
453                     + "mId="
454                     + mId
455                     + ", mLabel="
456                     + mLabel
457                     + ", mPendingIntent="
458                     + mPendingIntent
459                     + ", mWillResolve="
460                     + mWillResolve
461                     + ", mSuccessMessage="
462                     + mSuccessMessage
463                     + '}';
464         }
465 
466         /** Builder class for {@link Action}. */
467         public static final class Builder {
468 
469             @NonNull private final String mId;
470             @NonNull private final CharSequence mLabel;
471             @NonNull private final PendingIntent mPendingIntent;
472             private boolean mWillResolve = false;
473             @Nullable private CharSequence mSuccessMessage;
474 
475             /** Creates a {@link Builder} for an {@link Action}. */
Builder( @onNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent)476             public Builder(
477                     @NonNull String id,
478                     @NonNull CharSequence label,
479                     @NonNull PendingIntent pendingIntent) {
480                 mId = requireNonNull(id);
481                 mLabel = requireNonNull(label);
482                 mPendingIntent = requireNonNull(pendingIntent);
483             }
484 
485             /**
486              * Sets whether the action will resolve the safety issue. Defaults to {@code false}.
487              *
488              * @see #willResolve()
489              */
490             @SuppressLint("MissingGetterMatchingBuilder")
491             @NonNull
setWillResolve(boolean willResolve)492             public Builder setWillResolve(boolean willResolve) {
493                 mWillResolve = willResolve;
494                 return this;
495             }
496 
497             /**
498              * Sets the optional localized message to be displayed in the UI when the action is
499              * invoked and completes successfully.
500              */
501             @NonNull
setSuccessMessage(@ullable CharSequence successMessage)502             public Builder setSuccessMessage(@Nullable CharSequence successMessage) {
503                 mSuccessMessage = successMessage;
504                 return this;
505             }
506 
507             /** Creates the {@link Action} defined by this {@link Builder}. */
508             @NonNull
build()509             public Action build() {
510                 return new Action(mId, mLabel, mPendingIntent, mWillResolve, mSuccessMessage);
511             }
512         }
513     }
514 
515     /** Builder class for {@link SafetySourceIssue}. */
516     public static final class Builder {
517 
518         @NonNull private final String mId;
519         @NonNull private final CharSequence mTitle;
520         @NonNull private final CharSequence mSummary;
521         @SafetySourceData.SeverityLevel private final int mSeverityLevel;
522         @NonNull private final String mIssueTypeId;
523         private final List<Action> mActions = new ArrayList<>();
524 
525         @Nullable private CharSequence mSubtitle;
526         @IssueCategory private int mIssueCategory = ISSUE_CATEGORY_GENERAL;
527         @Nullable private PendingIntent mOnDismissPendingIntent;
528 
529         /** 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)530         public Builder(
531                 @NonNull String id,
532                 @NonNull CharSequence title,
533                 @NonNull CharSequence summary,
534                 @SafetySourceData.SeverityLevel int severityLevel,
535                 @NonNull String issueTypeId) {
536             this.mId = requireNonNull(id);
537             this.mTitle = requireNonNull(title);
538             this.mSummary = requireNonNull(summary);
539             this.mSeverityLevel = validateSeverityLevel(severityLevel);
540             this.mIssueTypeId = requireNonNull(issueTypeId);
541         }
542 
543         /** Sets the localized subtitle. */
544         @NonNull
setSubtitle(@ullable CharSequence subtitle)545         public Builder setSubtitle(@Nullable CharSequence subtitle) {
546             mSubtitle = subtitle;
547             return this;
548         }
549 
550         /**
551          * Sets the category of the risk associated with the issue.
552          *
553          * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}.
554          */
555         @NonNull
setIssueCategory(@ssueCategory int issueCategory)556         public Builder setIssueCategory(@IssueCategory int issueCategory) {
557             mIssueCategory = validateIssueCategory(issueCategory);
558             return this;
559         }
560 
561         /** Adds data for an {@link Action} to be shown in UI. */
562         @NonNull
addAction(@onNull Action actionData)563         public Builder addAction(@NonNull Action actionData) {
564             mActions.add(requireNonNull(actionData));
565             return this;
566         }
567 
568         /** Clears data for all the {@link Action}s that were added to this {@link Builder}. */
569         @NonNull
clearActions()570         public Builder clearActions() {
571             mActions.clear();
572             return this;
573         }
574 
575         /**
576          * Sets an optional {@link PendingIntent} to be invoked when an issue is dismissed from the
577          * UI.
578          *
579          * <p>In particular, if the source would like to be notified of issue dismissals in Safety
580          * Center in order to be able to dismiss or ignore issues at the source, then set this
581          * field. The action contained in the {@link PendingIntent} must not start an activity.
582          *
583          * @see #getOnDismissPendingIntent()
584          */
585         @NonNull
setOnDismissPendingIntent(@ullable PendingIntent onDismissPendingIntent)586         public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) {
587             checkArgument(
588                     onDismissPendingIntent == null || !onDismissPendingIntent.isActivity(),
589                     "Safety source issue on dismiss pending intent must not start an activity");
590             mOnDismissPendingIntent = onDismissPendingIntent;
591             return this;
592         }
593 
594         /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */
595         @NonNull
build()596         public SafetySourceIssue build() {
597             List<SafetySourceIssue.Action> actions = unmodifiableList(new ArrayList<>(mActions));
598             checkArgument(!actions.isEmpty(), "Safety source issue must contain at least 1 action");
599             checkArgument(
600                     actions.size() <= 2,
601                     "Safety source issue must not contain more than 2 actions");
602             return new SafetySourceIssue(
603                     mId,
604                     mTitle,
605                     mSubtitle,
606                     mSummary,
607                     mSeverityLevel,
608                     mIssueCategory,
609                     actions,
610                     mOnDismissPendingIntent,
611                     mIssueTypeId);
612         }
613     }
614 
615     @SafetySourceData.SeverityLevel
validateSeverityLevel(int value)616     private static int validateSeverityLevel(int value) {
617         switch (value) {
618             case SafetySourceData.SEVERITY_LEVEL_INFORMATION:
619             case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION:
620             case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING:
621                 return value;
622             case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED:
623                 throw new IllegalArgumentException(
624                         "SeverityLevel for SafetySourceIssue must not be "
625                                 + "SEVERITY_LEVEL_UNSPECIFIED");
626             default:
627         }
628         throw new IllegalArgumentException(
629                 String.format("Unexpected SeverityLevel for SafetySourceIssue: %s", value));
630     }
631 
632     @IssueCategory
validateIssueCategory(int value)633     private static int validateIssueCategory(int value) {
634         switch (value) {
635             case ISSUE_CATEGORY_DEVICE:
636             case ISSUE_CATEGORY_ACCOUNT:
637             case ISSUE_CATEGORY_GENERAL:
638                 return value;
639             default:
640         }
641         throw new IllegalArgumentException(
642                 String.format("Unexpected IssueCategory for SafetySourceIssue: %s", value));
643     }
644 }
645