blob: dc7d912366f2c79fee80edc8075a46cef42d3ca1 [file] [log] [blame]
/*
* 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);
}
}
}