blob: 1800efc5d5b972acc0fb482a789dd4e7e855fac5 [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.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.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 -- Add this line back when ready for API council review.
@RequiresApi(TIRAMISU)
public final class SafetySourceIssue implements Parcelable {
/**
* 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.
*
* @hide
*/
@IntDef(prefix = { "SEVERITY_LEVEL_" }, value = {
SEVERITY_LEVEL_INFORMATION,
SEVERITY_LEVEL_RECOMMENDATION,
SEVERITY_LEVEL_CRITICAL_WARNING
})
@Retention(RetentionPolicy.SOURCE)
public @interface SeverityLevel {}
/**
* Indicates an informational message. This severity will be reflected in the UI through a
* green icon.
*/
public static final int SEVERITY_LEVEL_INFORMATION = 200;
/**
* Indicates a medium-severity issue which the user is encouraged to act on. This severity will
* be reflected in the UI through a yellow icon.
*/
public static final int SEVERITY_LEVEL_RECOMMENDATION = 300;
/**
* Indicates a critical or urgent safety issue that should be addressed by the user. This
* severity will be reflected in the UI through a red icon.
*/
public static final int SEVERITY_LEVEL_CRITICAL_WARNING = 400;
/**
* 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 -- Add this line back when ready for API council review.
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) {
CharSequence label =
requireNonNull(
TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in));
PendingIntent clickPendingIntent =
requireNonNull(PendingIntent.readPendingIntentOrNullFromParcel(in));
boolean resolving = in.readBoolean();
return new Action(label, clickPendingIntent, resolving);
}
@Override
public Action[] newArray(int size) {
return new Action[size];
}
};
@NonNull
private final CharSequence mLabel;
@NonNull
private final PendingIntent mClickPendingIntent;
private final boolean mResolving;
private Action(@NonNull CharSequence label, @NonNull PendingIntent clickPendingIntent,
boolean resolving) {
this.mLabel = label;
this.mClickPendingIntent = clickPendingIntent;
this.mResolving = resolving;
}
/**
* 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 getClickPendingIntent() {
return mClickPendingIntent;
}
/**
* 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 isResolving() {
return mResolving;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
TextUtils.writeToParcel(mLabel, dest, flags);
mClickPendingIntent.writeToParcel(dest, flags);
dest.writeBoolean(mResolving);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Action)) return false;
Action that = (Action) o;
return mResolving == that.mResolving
&& TextUtils.equals(mLabel, that.mLabel)
&& mClickPendingIntent.equals(that.mClickPendingIntent);
}
@Override
public int hashCode() {
return Objects.hash(mLabel, mClickPendingIntent, mResolving);
}
@Override
public String toString() {
return "Action{"
+ "mLabel="
+ mLabel
+ ", mClickPendingIntent="
+ mClickPendingIntent
+ ", mResolving="
+ mResolving
+ '}';
}
/** Builder class for {@link Action}. */
public static final class Builder {
@NonNull
private final CharSequence mLabel;
@NonNull
private final PendingIntent mClickPendingIntent;
private boolean mResolving = false;
/** Creates a {@link Builder} for an {@link Action}. */
public Builder(@NonNull CharSequence label, @NonNull PendingIntent clickPendingIntent) {
this.mLabel = requireNonNull(label);
this.mClickPendingIntent = requireNonNull(clickPendingIntent);
}
/**
* Sets whether the action will resolve the safety issue.
*
* @see #isResolving()
*/
@NonNull
public Builder setResolving(boolean resolving) {
this.mResolving = resolving;
return this;
}
/** Creates the {@link Action} defined by this {@link Builder}. */
@NonNull
public Action build() {
return new Action(mLabel, mClickPendingIntent, mResolving);
}
}
}
@NonNull
public static final Parcelable.Creator<SafetySourceIssue> CREATOR =
new Parcelable.Creator<SafetySourceIssue>() {
@Override
public SafetySourceIssue createFromParcel(Parcel in) {
CharSequence title =
requireNonNull(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in));
CharSequence summary =
requireNonNull(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in));
int severityLevel = in.readInt();
List<Action> actions = new ArrayList<>();
in.readParcelableList(actions, Action.class.getClassLoader());
return new SafetySourceIssue(title, summary, severityLevel, actions);
}
@Override
public SafetySourceIssue[] newArray(int size) {
return new SafetySourceIssue[size];
}
};
@NonNull
private final CharSequence mTitle;
@NonNull
private final CharSequence mSummary;
private final @SeverityLevel int mSeverityLevel;
@NonNull
private final List<Action> mActions;
private SafetySourceIssue(@NonNull CharSequence title, @NonNull CharSequence summary,
@SeverityLevel int severityLevel, @NonNull List<Action> actions) {
this.mTitle = title;
this.mSummary = summary;
this.mSeverityLevel = severityLevel;
this.mActions = actions;
}
/** Returns the localized title of the issue to be displayed in the UI. */
@NonNull
public CharSequence getTitle() {
return mTitle;
}
/** 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 a list of {@link Action} instances 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);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
TextUtils.writeToParcel(mTitle, dest, flags);
TextUtils.writeToParcel(mSummary, dest, flags);
dest.writeInt(mSeverityLevel);
dest.writeParcelableList(mActions, flags);
}
@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(mTitle, that.mTitle)
&& TextUtils.equals(mSummary, that.mSummary)
&& mActions.equals(that.mActions);
}
@Override
public int hashCode() {
return Objects.hash(mTitle, mSummary, mSeverityLevel, mActions);
}
@Override
public String toString() {
return "SafetySourceIssue{"
+ "mTitle="
+ mTitle
+ ", mSummary="
+ mSummary
+ ", mSeverityLevel="
+ mSeverityLevel
+ ", mActions="
+ mActions
+ '}';
}
/** Builder class for {@link SafetySourceIssue}. */
public static final class Builder {
@NonNull
private final CharSequence mTitle;
@NonNull
private final CharSequence mSummary;
private @SeverityLevel final int mSeverityLevel;
@NonNull
private final List<Action> mActions = new ArrayList<>();
/** Creates a {@link Builder} for a {@link SafetySourceIssue}. */
public Builder(@NonNull CharSequence title, @NonNull CharSequence summary,
@SeverityLevel int severityLevel) {
this.mTitle = requireNonNull(title);
this.mSummary = requireNonNull(summary);
this.mSeverityLevel = severityLevel;
}
/** Adds data for an action to be shown in UI. */
@NonNull
public Builder addAction(@NonNull Action actionData) {
mActions.add(requireNonNull(actionData));
return this;
}
/** Clears data for all the actions that were added to this {@link Builder}. */
@NonNull
public Builder clearActions() {
mActions.clear();
return this;
}
/** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */
@NonNull
public SafetySourceIssue build() {
// TODO(b/207402324): Check with UX whether issues without any actions are permitted.
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(mTitle, mSummary, mSeverityLevel, mActions);
}
}
}