| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.safetycenter; |
| |
| import static android.os.Build.VERSION_CODES.TIRAMISU; |
| |
| import static com.android.internal.util.Preconditions.checkArgument; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SuppressLint; |
| import android.annotation.SystemApi; |
| import android.app.PendingIntent; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.text.TextUtils; |
| |
| import androidx.annotation.RequiresApi; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * Data for a safety source issue in the Safety Center page. |
| * |
| * <p>An issue represents an actionable matter relating to a particular safety source. |
| * |
| * <p>The safety issue will contain localized messages to be shown in UI explaining the potential |
| * threat or warning and suggested fixes, as well as actions a user is allowed to take from the UI |
| * to resolve the issue. |
| * |
| * @hide |
| */ |
| @SystemApi |
| @RequiresApi(TIRAMISU) |
| public final class SafetySourceIssue implements Parcelable { |
| |
| /** |
| * Indicates an informational message. |
| * |
| * <p>This severity will be reflected in the UI through a green icon. |
| * |
| * <p>Issues with this severity will be dismissible by the user from the UI, and will not |
| * trigger a confirmation dialog upon a user attempting to dismiss the warning. |
| */ |
| public static final int SEVERITY_LEVEL_INFORMATION = 200; |
| |
| /** |
| * Indicates a medium-severity issue which the user is encouraged to act on. |
| * |
| * <p>This severity will be reflected in the UI through a yellow icon. |
| * |
| * <p>Issues with this severity will be dismissible by the user from the UI, and will trigger a |
| * confirmation dialog upon a user attempting to dismiss the warning. |
| */ |
| public static final int SEVERITY_LEVEL_RECOMMENDATION = 300; |
| |
| /** |
| * Indicates a critical or urgent safety issue that should be addressed by the user. |
| * |
| * <p>This severity will be reflected in the UI through a red icon. |
| * |
| * <p>Issues with this severity will be dismissible by the user from the UI, and will trigger a |
| * confirmation dialog upon a user attempting to dismiss the warning. |
| */ |
| public static final int SEVERITY_LEVEL_CRITICAL_WARNING = 400; |
| |
| /** Indicates that the risk associated with the issue is related to a user's device safety. */ |
| public static final int ISSUE_CATEGORY_DEVICE = 100; |
| |
| /** Indicates that the risk associated with the issue is related to a user's account safety. */ |
| public static final int ISSUE_CATEGORY_ACCOUNT = 200; |
| |
| /** Indicates that the risk associated with the issue is related to a user's general safety. */ |
| public static final int ISSUE_CATEGORY_GENERAL = 300; |
| |
| @NonNull |
| public static final Parcelable.Creator<SafetySourceIssue> CREATOR = |
| new Parcelable.Creator<SafetySourceIssue>() { |
| @Override |
| public SafetySourceIssue createFromParcel(Parcel in) { |
| String id = in.readString(); |
| CharSequence title = |
| requireNonNull(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)); |
| CharSequence subtitle = |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| CharSequence summary = |
| requireNonNull(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)); |
| int severityLevel = in.readInt(); |
| int issueCategory = in.readInt(); |
| List<Action> actions = in.createTypedArrayList(Action.CREATOR); |
| PendingIntent onDismissPendingIntent = |
| PendingIntent.readPendingIntentOrNullFromParcel(in); |
| String issueTypeId = requireNonNull(in.readString()); |
| Builder builder = new Builder(id, title, summary, severityLevel, issueTypeId) |
| .setSubtitle(subtitle) |
| .setIssueCategory(issueCategory) |
| .setOnDismissPendingIntent(onDismissPendingIntent); |
| // TODO(b/224513050): Consider simplifying by adding a new API to the builder. |
| for (int i = 0; i < actions.size(); i++) { |
| builder.addAction(actions.get(i)); |
| } |
| return builder.build(); |
| } |
| |
| @Override |
| public SafetySourceIssue[] newArray(int size) { |
| return new SafetySourceIssue[size]; |
| } |
| }; |
| |
| @NonNull |
| private final String mId; |
| @NonNull |
| private final CharSequence mTitle; |
| @Nullable |
| private final CharSequence mSubtitle; |
| @NonNull |
| private final CharSequence mSummary; |
| @SeverityLevel |
| private final int mSeverityLevel; |
| private final List<Action> mActions; |
| @Nullable |
| private final PendingIntent mOnDismissPendingIntent; |
| @IssueCategory |
| private final int mIssueCategory; |
| @NonNull |
| private final String mIssueTypeId; |
| |
| private SafetySourceIssue(@NonNull String id, |
| @NonNull CharSequence title, |
| @Nullable CharSequence subtitle, |
| @NonNull CharSequence summary, |
| @SeverityLevel int severityLevel, |
| @IssueCategory int issueCategory, |
| @NonNull List<Action> actions, |
| @Nullable PendingIntent onDismissPendingIntent, |
| @NonNull String issueTypeId) { |
| this.mId = id; |
| this.mTitle = title; |
| this.mSubtitle = subtitle; |
| this.mSummary = summary; |
| this.mSeverityLevel = severityLevel; |
| this.mIssueCategory = issueCategory; |
| this.mActions = actions; |
| this.mOnDismissPendingIntent = onDismissPendingIntent; |
| this.mIssueTypeId = issueTypeId; |
| } |
| |
| /** |
| * Returns the identifier for this issue. |
| * |
| * <p>This id should uniquely identify the safety risk represented by this issue. Safety issues |
| * will be deduped by this id to be shown in the UI. |
| * |
| * <p>On multiple instances of providing the same issue to be represented in Safety Center, |
| * provide the same id across all instances. |
| */ |
| @NonNull |
| public String getId() { |
| return mId; |
| } |
| |
| /** Returns the localized title of the issue to be displayed in the UI. */ |
| @NonNull |
| public CharSequence getTitle() { |
| return mTitle; |
| } |
| |
| /** Returns the localized subtitle of the issue to be displayed in the UI. */ |
| @Nullable |
| public CharSequence getSubtitle() { |
| return mSubtitle; |
| } |
| |
| /** Returns the localized summary of the issue to be displayed in the UI. */ |
| @NonNull |
| public CharSequence getSummary() { |
| return mSummary; |
| } |
| |
| /** Returns the {@link SeverityLevel} of the issue. */ |
| @SeverityLevel |
| public int getSeverityLevel() { |
| return mSeverityLevel; |
| } |
| |
| /** |
| * Returns the category of the risk associated with the issue. |
| * |
| * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. |
| */ |
| @IssueCategory |
| public int getIssueCategory() { |
| return mIssueCategory; |
| } |
| |
| /** |
| * Returns a list of {@link Action}s representing actions supported in the UI for this issue. |
| * |
| * <p>Each issue must contain at least one action, in order to help the user resolve the issue. |
| * |
| * <p>In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most |
| * two actions supported from the UI. |
| */ |
| @NonNull |
| public List<Action> getActions() { |
| return new ArrayList<>(mActions); |
| } |
| |
| /** |
| * Returns the optional {@link PendingIntent} that will be invoked when an issue is dismissed. |
| * |
| * <p>When a safety issue is dismissed in Safety Center page, the issue is removed from view in |
| * Safety Center page. This method returns an additional optional action specified by the safety |
| * source that should be invoked on issue dismissal. |
| */ |
| @Nullable |
| public PendingIntent getOnDismissPendingIntent() { |
| return mOnDismissPendingIntent; |
| } |
| |
| /** |
| * Returns the identifier for the type of this issue. |
| * |
| * <p>The issue type should indicate the underlying basis for the issue, for e.g. a pending |
| * update or a disabled security feature. |
| * |
| * <p>The difference between this id and {@link #getId()} is that the issue type id is |
| * meant to be used for logging and should therefore contain no personally identifiable |
| * information (PII) (for e.g. account name). |
| * |
| * <p>On multiple instances of providing the same issue to be represented in Safety Center, |
| * provide the same issue type id across all instances. |
| */ |
| @NonNull |
| public String getIssueTypeId() { |
| return mIssueTypeId; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel dest, int flags) { |
| dest.writeString(mId); |
| TextUtils.writeToParcel(mTitle, dest, flags); |
| TextUtils.writeToParcel(mSubtitle, dest, flags); |
| TextUtils.writeToParcel(mSummary, dest, flags); |
| dest.writeInt(mSeverityLevel); |
| dest.writeInt(mIssueCategory); |
| dest.writeTypedList(mActions); |
| PendingIntent.writePendingIntentOrNullToParcel(mOnDismissPendingIntent, dest); |
| dest.writeString(mIssueTypeId); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (!(o instanceof SafetySourceIssue)) return false; |
| SafetySourceIssue that = (SafetySourceIssue) o; |
| return mSeverityLevel == that.mSeverityLevel |
| && TextUtils.equals(mId, that.mId) |
| && TextUtils.equals(mTitle, that.mTitle) |
| && TextUtils.equals(mSubtitle, that.mSubtitle) |
| && TextUtils.equals(mSummary, that.mSummary) |
| && mIssueCategory == that.mIssueCategory |
| && mActions.equals(that.mActions) |
| && Objects.equals(mOnDismissPendingIntent, that.mOnDismissPendingIntent) |
| && TextUtils.equals(mIssueTypeId, that.mIssueTypeId); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mId, mTitle, mSubtitle, mSummary, mSeverityLevel, mIssueCategory, |
| mActions, mOnDismissPendingIntent, mIssueTypeId); |
| } |
| |
| @Override |
| public String toString() { |
| return "SafetySourceIssue{" |
| + "mId=" |
| + mId |
| + "mTitle=" |
| + mTitle |
| + ", mSummary=" |
| + mSubtitle |
| + ", mSubtitle=" |
| + mSummary |
| + ", mSeverityLevel=" |
| + mSeverityLevel |
| + ", mIssueCategory=" |
| + mIssueCategory |
| + ", mActions=" |
| + mActions |
| + ", mOnDismissPendingIntent=" |
| + mOnDismissPendingIntent |
| + ", mIssueTypeId=" |
| + mIssueTypeId |
| + '}'; |
| } |
| |
| /** |
| * All possible severity levels for the safety source issue. |
| * |
| * <p>The severity level is meant to convey the severity of the individual issue. |
| * |
| * <p>The higher the severity level, the worse the safety level of the source and the higher |
| * the threat to the user. |
| * |
| * <p>The numerical values of the levels are not used directly, rather they are used to build |
| * a continuum of levels which support relative comparison. |
| * |
| * <p>The severity also determines how the issue is "dismissible" by the user, i.e. how |
| * the user can choose to ignore the issue and remove it from view in the Safety Center. |
| * |
| * @hide |
| */ |
| @IntDef(prefix = {"SEVERITY_LEVEL_"}, value = { |
| SEVERITY_LEVEL_INFORMATION, |
| SEVERITY_LEVEL_RECOMMENDATION, |
| SEVERITY_LEVEL_CRITICAL_WARNING |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SeverityLevel { |
| } |
| |
| /** |
| * All possible issue categories. |
| * |
| * <p>An issue's category represents a specific area of safety that the issue relates to. |
| * |
| * <p>An issue can only have one associated category. If the issue relates to multiple areas of |
| * safety, then choose the closest area or default to {@link #ISSUE_CATEGORY_GENERAL}. |
| * |
| * @hide |
| * @see Builder#setIssueCategory(int) |
| */ |
| @IntDef(prefix = {"ISSUE_CATEGORY_"}, value = { |
| ISSUE_CATEGORY_DEVICE, |
| ISSUE_CATEGORY_ACCOUNT, |
| ISSUE_CATEGORY_GENERAL, |
| |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface IssueCategory { |
| } |
| |
| /** |
| * Data for an action supported from a safety issue {@link SafetySourceIssue} in the Safety |
| * Center page. |
| * |
| * <p>The purpose of the action is to allow the user to address the safety issue, either by |
| * performing a fix suggested in the issue, or by navigating the user to the source of the issue |
| * where they can be exposed to details about the issue and further suggestions to resolve it. |
| * |
| * <p>The user will be allowed to invoke the action from the UI by clicking on a UI element and |
| * consequently resolve the issue. |
| * |
| * @hide |
| */ |
| @SystemApi |
| public static final class Action implements Parcelable { |
| |
| @NonNull |
| public static final Parcelable.Creator<Action> CREATOR = |
| new Parcelable.Creator<Action>() { |
| @Override |
| public Action createFromParcel(Parcel in) { |
| return new Builder( |
| in.readString(), |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in), |
| PendingIntent.readPendingIntentOrNullFromParcel(in)) |
| .setWillResolve(in.readBoolean()) |
| .setSuccessMessage( |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)) |
| .build(); |
| } |
| |
| @Override |
| public Action[] newArray(int size) { |
| return new Action[size]; |
| } |
| }; |
| |
| @NonNull |
| private final String mId; |
| @NonNull |
| private final CharSequence mLabel; |
| @NonNull |
| private final PendingIntent mPendingIntent; |
| private final boolean mWillResolve; |
| @Nullable |
| private final CharSequence mSuccessMessage; |
| |
| private Action( |
| String id, |
| @NonNull CharSequence label, |
| @NonNull PendingIntent pendingIntent, |
| boolean willResolve, |
| @Nullable CharSequence successMessage) { |
| mId = id; |
| mLabel = label; |
| mPendingIntent = pendingIntent; |
| mWillResolve = willResolve; |
| mSuccessMessage = successMessage; |
| } |
| |
| /** |
| * Returns the ID of the action, unique among actions in a given {@link SafetySourceIssue}. |
| */ |
| @NonNull |
| public String getId() { |
| return mId; |
| } |
| |
| /** |
| * Returns the localized label of the action to be displayed in the UI. |
| * |
| * <p>The label should indicate what action will be performed if when invoked. |
| */ |
| @NonNull |
| public CharSequence getLabel() { |
| return mLabel; |
| } |
| |
| /** |
| * Returns a {@link PendingIntent} to be fired when the action is clicked on. |
| * |
| * <p>The {@link PendingIntent} should perform the action referred to by |
| * {@link #getLabel()}. |
| */ |
| @NonNull |
| public PendingIntent getPendingIntent() { |
| return mPendingIntent; |
| } |
| |
| /** |
| * Returns whether invoking this action will fix or address the issue sufficiently for it |
| * to be considered resolved i.e. the issue will no longer need to be conveyed to the user |
| * in the UI. |
| */ |
| public boolean willResolve() { |
| return mWillResolve; |
| } |
| |
| /** |
| * Returns the optional localized message to be displayed in the UI when the action is |
| * invoked and completes successfully. |
| */ |
| @Nullable |
| public CharSequence getSuccessMessage() { |
| return mSuccessMessage; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel dest, int flags) { |
| dest.writeString(mId); |
| TextUtils.writeToParcel(mLabel, dest, flags); |
| mPendingIntent.writeToParcel(dest, flags); |
| dest.writeBoolean(mWillResolve); |
| TextUtils.writeToParcel(mSuccessMessage, dest, flags); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (!(o instanceof Action)) return false; |
| Action that = (Action) o; |
| return mId.equals(that.mId) |
| && TextUtils.equals(mLabel, that.mLabel) |
| && mPendingIntent.equals(that.mPendingIntent) |
| && mWillResolve == that.mWillResolve |
| && TextUtils.equals(mSuccessMessage, that.mSuccessMessage); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mId, mLabel, mPendingIntent, mWillResolve, mSuccessMessage); |
| } |
| |
| @Override |
| public String toString() { |
| return "Action{" |
| + "mId=" + mId |
| + ", mLabel=" + mLabel |
| + ", mPendingIntent=" + mPendingIntent |
| + ", mWillResolve=" + mWillResolve |
| + ", mSuccessMessage=" + mSuccessMessage |
| + '}'; |
| } |
| |
| /** Builder class for {@link Action}. */ |
| public static final class Builder { |
| @NonNull |
| private final String mId; |
| @NonNull |
| private final CharSequence mLabel; |
| @NonNull |
| private final PendingIntent mPendingIntent; |
| private boolean mWillResolve = false; |
| @Nullable |
| private CharSequence mSuccessMessage; |
| |
| /** Creates a {@link Builder} for an {@link Action}. */ |
| public Builder( |
| @NonNull String id, |
| @NonNull CharSequence label, |
| @NonNull PendingIntent pendingIntent) { |
| mId = requireNonNull(id); |
| mLabel = requireNonNull(label); |
| mPendingIntent = requireNonNull(pendingIntent); |
| } |
| |
| /** |
| * Sets whether the action will resolve the safety issue. Defaults to false. |
| * |
| * @see #willResolve() |
| */ |
| @SuppressLint("MissingGetterMatchingBuilder") |
| @NonNull |
| public Builder setWillResolve(boolean willResolve) { |
| mWillResolve = willResolve; |
| return this; |
| } |
| |
| /** |
| * Sets the optional localized message to be displayed in the UI when the action is |
| * invoked and completes successfully. |
| */ |
| @NonNull |
| public Builder setSuccessMessage(@Nullable CharSequence successMessage) { |
| mSuccessMessage = successMessage; |
| return this; |
| } |
| |
| /** Creates the {@link Action} defined by this {@link Builder}. */ |
| @NonNull |
| public Action build() { |
| return new Action(mId, mLabel, mPendingIntent, mWillResolve, mSuccessMessage); |
| } |
| } |
| } |
| |
| /** Builder class for {@link SafetySourceIssue}. */ |
| public static final class Builder { |
| @NonNull |
| private final String mId; |
| @NonNull |
| private final CharSequence mTitle; |
| @Nullable |
| private CharSequence mSubtitle; |
| @NonNull |
| private final CharSequence mSummary; |
| @SeverityLevel |
| private final int mSeverityLevel; |
| @IssueCategory |
| private int mIssueCategory = ISSUE_CATEGORY_GENERAL; |
| @Nullable |
| private PendingIntent mOnDismissPendingIntent; |
| @NonNull |
| private final List<Action> mActions = new ArrayList<>(); |
| @NonNull |
| private final String mIssueTypeId; |
| |
| /** Creates a {@link Builder} for a {@link SafetySourceIssue}. */ |
| public Builder( |
| @NonNull String id, |
| @NonNull CharSequence title, |
| @NonNull CharSequence summary, |
| @SeverityLevel int severityLevel, |
| @NonNull String issueTypeId) { |
| this.mId = id; |
| this.mTitle = requireNonNull(title); |
| this.mSummary = requireNonNull(summary); |
| this.mSeverityLevel = severityLevel; |
| this.mIssueTypeId = requireNonNull(issueTypeId); |
| } |
| |
| /** Sets the localized subtitle. */ |
| @NonNull |
| public Builder setSubtitle(@Nullable CharSequence subtitle) { |
| mSubtitle = subtitle; |
| return this; |
| } |
| |
| /** |
| * Sets the category of the risk associated with the issue. |
| * |
| * <p>The default category will be {@link #ISSUE_CATEGORY_GENERAL}. |
| */ |
| @NonNull |
| public Builder setIssueCategory(@IssueCategory int issueCategory) { |
| mIssueCategory = issueCategory; |
| return this; |
| } |
| |
| /** Adds data for an {@link Action} to be shown in UI. */ |
| @NonNull |
| public Builder addAction(@NonNull Action actionData) { |
| mActions.add(requireNonNull(actionData)); |
| return this; |
| } |
| |
| /** Clears data for all the {@link Action}s that were added to this {@link Builder}. */ |
| @NonNull |
| public Builder clearActions() { |
| mActions.clear(); |
| return this; |
| } |
| |
| /** |
| * Sets an optional {@link PendingIntent} to be invoked when an issue is dismissed from |
| * the UI. |
| * |
| * In particular, if the source would like to be notified of issue dismissals in Safety |
| * Center in order to be able to dismiss or ignore issues at the source, then set this |
| * field. |
| * |
| * @see #getOnDismissPendingIntent() |
| */ |
| @NonNull |
| public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) { |
| mOnDismissPendingIntent = onDismissPendingIntent; |
| return this; |
| } |
| |
| /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */ |
| @NonNull |
| public SafetySourceIssue build() { |
| checkArgument(!mActions.isEmpty(), |
| "Safety source issue must contain at least 1 action"); |
| checkArgument(mActions.size() <= 2, |
| "Safety source issue must not contain more than 2 actions"); |
| return new SafetySourceIssue(mId, mTitle, mSubtitle, mSummary, mSeverityLevel, |
| mIssueCategory, Collections.unmodifiableList(mActions), mOnDismissPendingIntent, |
| mIssueTypeId); |
| } |
| } |
| } |