| /* |
| * 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 android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; |
| |
| import static com.android.internal.util.Preconditions.checkArgument; |
| |
| import static java.util.Collections.unmodifiableList; |
| 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.annotation.TargetApi; |
| import android.app.PendingIntent; |
| import android.os.Build; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.text.TextUtils; |
| |
| import androidx.annotation.RequiresApi; |
| |
| import com.android.modules.utils.build.SdkLevel; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * 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 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. |
| * |
| * <p>This is the default. It is a generic value used when the category is not known or is not |
| * relevant. |
| */ |
| public static final int ISSUE_CATEGORY_GENERAL = 300; |
| |
| /** Indicates that the risk associated with the issue is related to a user's data. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int ISSUE_CATEGORY_DATA = 400; |
| |
| /** Indicates that the risk associated with the issue is related to a user's passwords. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int ISSUE_CATEGORY_PASSWORDS = 500; |
| |
| /** Indicates that the risk associated with the issue is related to a user's personal safety. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int ISSUE_CATEGORY_PERSONAL_SAFETY = 600; |
| |
| /** |
| * 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, |
| ISSUE_CATEGORY_DATA, |
| ISSUE_CATEGORY_PASSWORDS, |
| ISSUE_CATEGORY_PERSONAL_SAFETY |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @TargetApi(UPSIDE_DOWN_CAKE) |
| public @interface IssueCategory {} |
| |
| /** Value signifying that the source has not specified a particular notification behavior. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int NOTIFICATION_BEHAVIOR_UNSPECIFIED = 0; |
| |
| /** An issue which Safety Center should never notify the user about. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int NOTIFICATION_BEHAVIOR_NEVER = 100; |
| |
| /** |
| * An issue which Safety Center may notify the user about after a delay if it has not been |
| * resolved. Safety Center does not provide any guarantee about the duration of the delay. |
| */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int NOTIFICATION_BEHAVIOR_DELAYED = 200; |
| |
| /** An issue which Safety Center may notify the user about immediately. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int NOTIFICATION_BEHAVIOR_IMMEDIATELY = 300; |
| |
| /** |
| * All possible notification behaviors. |
| * |
| * <p>The notification behavior of a {@link SafetySourceIssue} determines if and when Safety |
| * Center should notify the user about it. |
| * |
| * @hide |
| * @see Builder#setNotificationBehavior(int) |
| */ |
| @IntDef( |
| prefix = {"NOTIFICATION_BEHAVIOR_"}, |
| value = { |
| NOTIFICATION_BEHAVIOR_UNSPECIFIED, |
| NOTIFICATION_BEHAVIOR_NEVER, |
| NOTIFICATION_BEHAVIOR_DELAYED, |
| NOTIFICATION_BEHAVIOR_IMMEDIATELY |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @TargetApi(UPSIDE_DOWN_CAKE) |
| public @interface NotificationBehavior {} |
| |
| /** |
| * An issue which requires manual user input to be resolved. |
| * |
| * <p>This is the default. |
| */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int ISSUE_ACTIONABILITY_MANUAL = 0; |
| |
| /** |
| * An issue which is just a "tip" and may not require any user input. |
| * |
| * <p>It is still possible to provide {@link Action}s to e.g. "learn more" about it or |
| * acknowledge it. |
| */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int ISSUE_ACTIONABILITY_TIP = 100; |
| |
| /** |
| * An issue which has already been actioned and may not require any user input. |
| * |
| * <p>It is still possible to provide {@link Action}s to e.g. "learn more" about it or |
| * acknowledge it. |
| */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final int ISSUE_ACTIONABILITY_AUTOMATIC = 200; |
| |
| /** |
| * All possible issue actionability. |
| * |
| * <p>An issue's actionability represent what action is expected from the user as a result of |
| * showing them this issue. |
| * |
| * <p>If the user needs to manually resolve it; this is typically achieved using an {@link |
| * Action} (e.g. by resolving the issue directly through the Safety Center screen, or by |
| * navigating to another page). |
| * |
| * <p>If the issue does not need to be resolved manually by the user, it is possible not to |
| * provide any {@link Action}. However, this may still be desirable to e.g. to "learn more" |
| * about it or acknowledge it. |
| * |
| * @hide |
| * @see Builder#setIssueActionability(int) |
| */ |
| @IntDef( |
| prefix = {"ISSUE_ACTIONABILITY_"}, |
| value = { |
| ISSUE_ACTIONABILITY_MANUAL, |
| ISSUE_ACTIONABILITY_TIP, |
| ISSUE_ACTIONABILITY_AUTOMATIC |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @TargetApi(UPSIDE_DOWN_CAKE) |
| public @interface IssueActionability {} |
| |
| @NonNull |
| public static final Creator<SafetySourceIssue> CREATOR = |
| new Creator<SafetySourceIssue>() { |
| @Override |
| public SafetySourceIssue createFromParcel(Parcel in) { |
| String id = in.readString(); |
| CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| CharSequence subtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| int severityLevel = in.readInt(); |
| int issueCategory = in.readInt(); |
| List<Action> actions = requireNonNull(in.createTypedArrayList(Action.CREATOR)); |
| PendingIntent onDismissPendingIntent = |
| in.readTypedObject(PendingIntent.CREATOR); |
| String issueTypeId = in.readString(); |
| Builder builder = |
| new Builder(id, title, summary, severityLevel, issueTypeId) |
| .setSubtitle(subtitle) |
| .setIssueCategory(issueCategory) |
| .setOnDismissPendingIntent(onDismissPendingIntent); |
| for (int i = 0; i < actions.size(); i++) { |
| builder.addAction(actions.get(i)); |
| } |
| if (SdkLevel.isAtLeastU()) { |
| builder.setCustomNotification(in.readTypedObject(Notification.CREATOR)); |
| builder.setNotificationBehavior(in.readInt()); |
| builder.setAttributionTitle( |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)); |
| builder.setDeduplicationId(in.readString()); |
| builder.setIssueActionability(in.readInt()); |
| } |
| 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; |
| @SafetySourceData.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; |
| @Nullable private final Notification mCustomNotification; |
| @NotificationBehavior private final int mNotificationBehavior; |
| @Nullable private final CharSequence mAttributionTitle; |
| @Nullable private final String mDeduplicationId; |
| @IssueActionability private final int mIssueActionability; |
| |
| private SafetySourceIssue( |
| @NonNull 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, |
| @Nullable Notification customNotification, |
| @NotificationBehavior int notificationBehavior, |
| @Nullable CharSequence attributionTitle, |
| @Nullable String deduplicationId, |
| @IssueActionability int issueActionability) { |
| 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; |
| this.mCustomNotification = customNotification; |
| this.mNotificationBehavior = notificationBehavior; |
| this.mAttributionTitle = attributionTitle; |
| this.mDeduplicationId = deduplicationId; |
| this.mIssueActionability = issueActionability; |
| } |
| |
| /** |
| * 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 localized attribution title of the issue to be displayed in the UI. |
| * |
| * <p>This is displayed in the UI and helps to attribute issue cards to a particular source. If |
| * this value is {@code null}, the title of the group that contains the Safety Source will be |
| * used. |
| */ |
| @Nullable |
| @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) |
| public CharSequence getAttributionTitle() { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| return mAttributionTitle; |
| } |
| |
| /** Returns the {@link SafetySourceData.SeverityLevel} of the issue. */ |
| @SafetySourceData.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 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. The action contained in the {@link |
| * PendingIntent} cannot start an activity. |
| */ |
| @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) |
| * (e.g. for 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; |
| } |
| |
| /** |
| * Returns the optional custom {@link Notification} for this issue which overrides the title, |
| * text and actions for any {@link android.app.Notification} generated for this {@link |
| * SafetySourceIssue}. |
| * |
| * <p>Safety Center may still generate a default notification from the other details of this |
| * issue when no custom notification has been set. See {@link #getNotificationBehavior()} for |
| * details |
| * |
| * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification |
| * @see #getNotificationBehavior() |
| */ |
| @Nullable |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Notification getCustomNotification() { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| return mCustomNotification; |
| } |
| |
| /** |
| * Returns the {@link NotificationBehavior} for this issue which determines if and when Safety |
| * Center will post a notification for this issue. |
| * |
| * <p>Any notification will be based on the {@link #getCustomNotification()} if set, or the |
| * other properties of this issue otherwise. |
| * |
| * <ul> |
| * <li>If {@link #NOTIFICATION_BEHAVIOR_IMMEDIATELY} then Safety Center will immediately |
| * create and post a notification |
| * <li>If {@link #NOTIFICATION_BEHAVIOR_DELAYED} then a notification will only be posted after |
| * a delay, if this issue has not been resolved. |
| * <li>If {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED} then a notification may or may not be |
| * posted, the exact behavior is defined by Safety Center. |
| * <li>If {@link #NOTIFICATION_BEHAVIOR_NEVER} Safety Center will never post a notification |
| * about this issue. Sources should specify this behavior when they wish to handle their |
| * own notifications. When this behavior is set sources should not set a custom |
| * notification. |
| * </ul> |
| * |
| * @see Builder#setNotificationBehavior(int) |
| */ |
| @NotificationBehavior |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public int getNotificationBehavior() { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| return mNotificationBehavior; |
| } |
| |
| /** |
| * Returns the identifier used to deduplicate this issue against other issues with the same |
| * deduplication identifiers. |
| * |
| * <p>Deduplication identifier will be used to identify duplicate issues. This identifier |
| * applies across all safety sources which are part of the same deduplication group. |
| * Deduplication groups can be set, for each source, in the SafetyCenter config. Therefore, two |
| * issues are considered duplicate if their sources are part of the same deduplication group and |
| * they have the same deduplication identifier. |
| * |
| * <p>Out of all issues that are found to be duplicates, only one will be shown in the UI (the |
| * one with the highest severity, or in case of same severities, the one placed highest in the |
| * config). |
| * |
| * <p>Expected usage implies different sources will coordinate to set the same deduplication |
| * identifiers on issues that they want to deduplicate. |
| * |
| * <p>This shouldn't be a default mechanism for deduplication of issues. Most of the time |
| * sources should coordinate or communicate to only send the issue from one of them. That would |
| * also allow sources to choose which one will be displaying the issue, instead of depending on |
| * severity and config order. This API should only be needed if for some reason this isn't |
| * possible, for example, when sources can't communicate with each other and/or send issues at |
| * different times and/or issues can be of different severities. |
| */ |
| @Nullable |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public String getDeduplicationId() { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| return mDeduplicationId; |
| } |
| |
| /** |
| * Returns the {@link IssueActionability} for this issue which determines what type of action is |
| * required from the user: |
| * |
| * <ul> |
| * <li>If {@link #ISSUE_ACTIONABILITY_MANUAL} then user input is required to resolve the issue |
| * <li>If {@link #ISSUE_ACTIONABILITY_TIP} then the user needs to review this issue as a tip |
| * to improve their overall safety, and possibly acknowledge it |
| * <li>If {@link #ISSUE_ACTIONABILITY_AUTOMATIC} then the user needs to review this issue as |
| * something that has been resolved on their behalf, and possibly acknowledge it |
| * </ul> |
| * |
| * @see Builder#setIssueActionability(int) |
| */ |
| @IssueActionability |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public int getIssueActionability() { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| return mIssueActionability; |
| } |
| |
| @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); |
| dest.writeTypedObject(mOnDismissPendingIntent, flags); |
| dest.writeString(mIssueTypeId); |
| if (SdkLevel.isAtLeastU()) { |
| dest.writeTypedObject(mCustomNotification, flags); |
| dest.writeInt(mNotificationBehavior); |
| TextUtils.writeToParcel(mAttributionTitle, dest, flags); |
| dest.writeString(mDeduplicationId); |
| dest.writeInt(mIssueActionability); |
| } |
| } |
| |
| @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) |
| && Objects.equals(mCustomNotification, that.mCustomNotification) |
| && mNotificationBehavior == that.mNotificationBehavior |
| && TextUtils.equals(mAttributionTitle, that.mAttributionTitle) |
| && TextUtils.equals(mDeduplicationId, that.mDeduplicationId) |
| && mIssueActionability == that.mIssueActionability; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash( |
| mId, |
| mTitle, |
| mSubtitle, |
| mSummary, |
| mSeverityLevel, |
| mIssueCategory, |
| mActions, |
| mOnDismissPendingIntent, |
| mIssueTypeId, |
| mCustomNotification, |
| mNotificationBehavior, |
| mAttributionTitle, |
| mDeduplicationId, |
| mIssueActionability); |
| } |
| |
| @Override |
| public String toString() { |
| return "SafetySourceIssue{" |
| + "mId=" |
| + mId |
| + "mTitle=" |
| + mTitle |
| + ", mSubtitle=" |
| + mSubtitle |
| + ", mSummary=" |
| + mSummary |
| + ", mSeverityLevel=" |
| + mSeverityLevel |
| + ", mIssueCategory=" |
| + mIssueCategory |
| + ", mActions=" |
| + mActions |
| + ", mOnDismissPendingIntent=" |
| + mOnDismissPendingIntent |
| + ", mIssueTypeId=" |
| + mIssueTypeId |
| + ", mCustomNotification=" |
| + mCustomNotification |
| + ", mNotificationBehavior=" |
| + mNotificationBehavior |
| + ", mAttributionTitle=" |
| + mAttributionTitle |
| + ", mDeduplicationId=" |
| + mDeduplicationId |
| + ", mIssueActionability=" |
| + mIssueActionability |
| + '}'; |
| } |
| |
| /** |
| * 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 detail 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. |
| */ |
| public static final class Action implements Parcelable { |
| |
| @NonNull |
| public static final Creator<Action> CREATOR = |
| new Creator<Action>() { |
| @Override |
| public Action createFromParcel(Parcel in) { |
| String id = in.readString(); |
| CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR); |
| Builder builder = |
| new Builder(id, label, pendingIntent) |
| .setWillResolve(in.readBoolean()) |
| .setSuccessMessage( |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel( |
| in)); |
| if (SdkLevel.isAtLeastU()) { |
| ConfirmationDialogDetails confirmationDialogDetails = |
| in.readTypedObject(ConfirmationDialogDetails.CREATOR); |
| builder.setConfirmationDialogDetails(confirmationDialogDetails); |
| } |
| return builder.build(); |
| } |
| |
| @Override |
| public Action[] newArray(int size) { |
| return new Action[size]; |
| } |
| }; |
| |
| private static void enforceUniqueActionIds( |
| @NonNull List<SafetySourceIssue.Action> actions, @NonNull String message) { |
| Set<String> actionIds = new HashSet<>(); |
| for (int i = 0; i < actions.size(); i++) { |
| SafetySourceIssue.Action action = actions.get(i); |
| |
| String actionId = action.getId(); |
| checkArgument(!actionIds.contains(actionId), message); |
| actionIds.add(actionId); |
| } |
| } |
| |
| @NonNull private final String mId; |
| @NonNull private final CharSequence mLabel; |
| @NonNull private final PendingIntent mPendingIntent; |
| private final boolean mWillResolve; |
| @Nullable private final CharSequence mSuccessMessage; |
| @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails; |
| |
| private Action( |
| @NonNull String id, |
| @NonNull CharSequence label, |
| @NonNull PendingIntent pendingIntent, |
| boolean willResolve, |
| @Nullable CharSequence successMessage, |
| @Nullable ConfirmationDialogDetails confirmationDialogDetails) { |
| mId = id; |
| mLabel = label; |
| mPendingIntent = pendingIntent; |
| mWillResolve = willResolve; |
| mSuccessMessage = successMessage; |
| mConfirmationDialogDetails = confirmationDialogDetails; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Returns the optional data to be displayed in the confirmation dialog prior to launching |
| * the {@link PendingIntent} when the action is clicked on. |
| */ |
| @Nullable |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public ConfirmationDialogDetails getConfirmationDialogDetails() { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| return mConfirmationDialogDetails; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel dest, int flags) { |
| dest.writeString(mId); |
| TextUtils.writeToParcel(mLabel, dest, flags); |
| dest.writeTypedObject(mPendingIntent, flags); |
| dest.writeBoolean(mWillResolve); |
| TextUtils.writeToParcel(mSuccessMessage, dest, flags); |
| if (SdkLevel.isAtLeastU()) { |
| dest.writeTypedObject(mConfirmationDialogDetails, 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) |
| && Objects.equals(mConfirmationDialogDetails, that.mConfirmationDialogDetails); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash( |
| mId, |
| mLabel, |
| mPendingIntent, |
| mWillResolve, |
| mSuccessMessage, |
| mConfirmationDialogDetails); |
| } |
| |
| @Override |
| public String toString() { |
| return "Action{" |
| + "mId=" |
| + mId |
| + ", mLabel=" |
| + mLabel |
| + ", mPendingIntent=" |
| + mPendingIntent |
| + ", mWillResolve=" |
| + mWillResolve |
| + ", mSuccessMessage=" |
| + mSuccessMessage |
| + ", mConfirmationDialogDetails=" |
| + mConfirmationDialogDetails |
| + '}'; |
| } |
| |
| /** Data for an action confirmation dialog to be shown before action is executed. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final class ConfirmationDialogDetails implements Parcelable { |
| |
| @NonNull |
| public static final Creator<ConfirmationDialogDetails> CREATOR = |
| new Creator<ConfirmationDialogDetails>() { |
| @Override |
| public ConfirmationDialogDetails createFromParcel(Parcel in) { |
| CharSequence title = |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| CharSequence text = |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| CharSequence acceptButtonText = |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| CharSequence denyButtonText = |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); |
| return new ConfirmationDialogDetails( |
| title, text, acceptButtonText, denyButtonText); |
| } |
| |
| @Override |
| public ConfirmationDialogDetails[] newArray(int size) { |
| return new ConfirmationDialogDetails[size]; |
| } |
| }; |
| |
| @NonNull private final CharSequence mTitle; |
| @NonNull private final CharSequence mText; |
| @NonNull private final CharSequence mAcceptButtonText; |
| @NonNull private final CharSequence mDenyButtonText; |
| |
| public ConfirmationDialogDetails( |
| @NonNull CharSequence title, |
| @NonNull CharSequence text, |
| @NonNull CharSequence acceptButtonText, |
| @NonNull CharSequence denyButtonText) { |
| mTitle = requireNonNull(title); |
| mText = requireNonNull(text); |
| mAcceptButtonText = requireNonNull(acceptButtonText); |
| mDenyButtonText = requireNonNull(denyButtonText); |
| } |
| |
| /** Returns the title of action confirmation dialog. */ |
| @NonNull |
| public CharSequence getTitle() { |
| return mTitle; |
| } |
| |
| /** Returns the text of action confirmation dialog. */ |
| @NonNull |
| public CharSequence getText() { |
| return mText; |
| } |
| |
| /** Returns the text of the button to accept action execution. */ |
| @NonNull |
| public CharSequence getAcceptButtonText() { |
| return mAcceptButtonText; |
| } |
| |
| /** Returns the text of the button to deny action execution. */ |
| @NonNull |
| public CharSequence getDenyButtonText() { |
| return mDenyButtonText; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel dest, int flags) { |
| TextUtils.writeToParcel(mTitle, dest, flags); |
| TextUtils.writeToParcel(mText, dest, flags); |
| TextUtils.writeToParcel(mAcceptButtonText, dest, flags); |
| TextUtils.writeToParcel(mDenyButtonText, dest, flags); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (!(o instanceof ConfirmationDialogDetails)) return false; |
| ConfirmationDialogDetails that = (ConfirmationDialogDetails) o; |
| return TextUtils.equals(mTitle, that.mTitle) |
| && TextUtils.equals(mText, that.mText) |
| && TextUtils.equals(mAcceptButtonText, that.mAcceptButtonText) |
| && TextUtils.equals(mDenyButtonText, that.mDenyButtonText); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mTitle, mText, mAcceptButtonText, mDenyButtonText); |
| } |
| |
| @Override |
| public String toString() { |
| return "ConfirmationDialogDetails{" |
| + "mTitle=" |
| + mTitle |
| + ", mText=" |
| + mText |
| + ", mAcceptButtonText=" |
| + mAcceptButtonText |
| + ", mDenyButtonText=" |
| + mDenyButtonText |
| + '}'; |
| } |
| } |
| |
| /** 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; |
| @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails; |
| |
| /** 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); |
| } |
| |
| /** Creates a {@link Builder} with the values from the given {@link Action}. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Builder(@NonNull Action action) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| requireNonNull(action); |
| mId = action.mId; |
| mLabel = action.mLabel; |
| mPendingIntent = action.mPendingIntent; |
| mWillResolve = action.mWillResolve; |
| mSuccessMessage = action.mSuccessMessage; |
| mConfirmationDialogDetails = action.mConfirmationDialogDetails; |
| } |
| |
| /** |
| * Sets whether the action will resolve the safety issue. Defaults to {@code false}. |
| * |
| * <p>Note: It is not allowed for resolvable actions to have a {@link PendingIntent} |
| * that launches activity. When extra confirmation is needed consider using {@link |
| * Builder#setConfirmationDialogDetails}. |
| * |
| * @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; |
| } |
| |
| /** |
| * Sets the optional data to be displayed in the confirmation dialog prior to launching |
| * the {@link PendingIntent} when the action is clicked on. |
| */ |
| @NonNull |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Builder setConfirmationDialogDetails( |
| @Nullable ConfirmationDialogDetails confirmationDialogDetails) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| mConfirmationDialogDetails = confirmationDialogDetails; |
| return this; |
| } |
| |
| /** Creates the {@link Action} defined by this {@link Builder}. */ |
| @NonNull |
| public Action build() { |
| if (SdkLevel.isAtLeastU()) { |
| boolean willResolveWithActivity = mWillResolve && mPendingIntent.isActivity(); |
| checkArgument( |
| !willResolveWithActivity, |
| "Launching activity from Action that should resolve the" |
| + " SafetySourceIssue is not allowed. Consider using setting a" |
| + " Confirmation if needed, and either set the willResolve to" |
| + " false or make PendingIntent to start a service/send a" |
| + " broadcast."); |
| } |
| return new Action( |
| mId, |
| mLabel, |
| mPendingIntent, |
| mWillResolve, |
| mSuccessMessage, |
| mConfirmationDialogDetails); |
| } |
| } |
| } |
| |
| /** |
| * Data for Safety Center to use when constructing a system {@link android.app.Notification} |
| * about a related {@link SafetySourceIssue}. |
| * |
| * <p>Safety Center can construct a default notification for any issue, but sources may use |
| * {@link Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification)} if |
| * they want to override the title, text or actions. |
| * |
| * @see #getCustomNotification() |
| * @see Builder#setCustomNotification(android.safetycenter.SafetySourceIssue.Notification) |
| * @see #getNotificationBehavior() |
| */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public static final class Notification implements Parcelable { |
| |
| @NonNull |
| public static final Creator<Notification> CREATOR = |
| new Creator<Notification>() { |
| @Override |
| public Notification createFromParcel(Parcel in) { |
| return new Builder( |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in), |
| TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)) |
| .addActions(in.createTypedArrayList(Action.CREATOR)) |
| .build(); |
| } |
| |
| @Override |
| public Notification[] newArray(int size) { |
| return new Notification[size]; |
| } |
| }; |
| |
| @NonNull private final CharSequence mTitle; |
| @NonNull private final CharSequence mText; |
| @NonNull private final List<Action> mActions; |
| |
| private Notification( |
| @NonNull CharSequence title, |
| @NonNull CharSequence text, |
| @NonNull List<Action> actions) { |
| mTitle = title; |
| mText = text; |
| mActions = actions; |
| } |
| |
| /** |
| * Custom title which will be used instead of {@link SafetySourceIssue#getTitle()} when |
| * building a {@link android.app.Notification} for this issue. |
| */ |
| @NonNull |
| public CharSequence getTitle() { |
| return mTitle; |
| } |
| |
| /** |
| * Custom text which will be used instead of {@link SafetySourceIssue#getSummary()} when |
| * building a {@link android.app.Notification} for this issue. |
| */ |
| @NonNull |
| public CharSequence getText() { |
| return mText; |
| } |
| |
| /** |
| * Custom list of {@link Action} instances which will be used instead of {@link |
| * SafetySourceIssue#getActions()} when building a {@link android.app.Notification} for this |
| * issue. |
| * |
| * <p>If this list is empty then the resulting {@link android.app.Notification} will have |
| * zero action buttons. |
| */ |
| @NonNull |
| public List<Action> getActions() { |
| return mActions; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel dest, int flags) { |
| TextUtils.writeToParcel(mTitle, dest, flags); |
| TextUtils.writeToParcel(mText, dest, flags); |
| dest.writeTypedList(mActions); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (!(o instanceof Notification)) return false; |
| Notification that = (Notification) o; |
| return TextUtils.equals(mTitle, that.mTitle) |
| && TextUtils.equals(mText, that.mText) |
| && mActions.equals(that.mActions); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mTitle, mText, mActions); |
| } |
| |
| @Override |
| public String toString() { |
| return "Notification{" |
| + "mTitle=" |
| + mTitle |
| + ", mText=" |
| + mText |
| + ", mActions=" |
| + mActions |
| + '}'; |
| } |
| |
| /** Builder for {@link SafetySourceIssue.Notification}. */ |
| public static final class Builder { |
| |
| @NonNull private final CharSequence mTitle; |
| @NonNull private final CharSequence mText; |
| @NonNull private final List<Action> mActions = new ArrayList<>(); |
| |
| public Builder(@NonNull CharSequence title, @NonNull CharSequence text) { |
| mTitle = requireNonNull(title); |
| mText = requireNonNull(text); |
| } |
| |
| /** Creates a {@link Builder} with the values from the given {@link Notification}. */ |
| public Builder(@NonNull Notification notification) { |
| requireNonNull(notification); |
| mTitle = notification.mTitle; |
| mText = notification.mText; |
| mActions.addAll(notification.mActions); |
| } |
| |
| /** Adds an {@link Action} to the custom {@link Notification}. */ |
| @NonNull |
| public Builder addAction(@NonNull Action action) { |
| mActions.add(requireNonNull(action)); |
| return this; |
| } |
| |
| /** Adds several {@link Action}s to the custom {@link Notification}. */ |
| @NonNull |
| public Builder addActions(@NonNull List<Action> actions) { |
| mActions.addAll(requireNonNull(actions)); |
| return this; |
| } |
| |
| /** Clears all the {@link Action}s that were added so far. */ |
| @NonNull |
| public Builder clearActions() { |
| mActions.clear(); |
| return this; |
| } |
| |
| /** Builds a {@link Notification} instance. */ |
| @NonNull |
| public Notification build() { |
| List<Action> actions = unmodifiableList(new ArrayList<>(mActions)); |
| Action.enforceUniqueActionIds( |
| actions, "Custom notification cannot have duplicate action ids"); |
| checkArgument( |
| actions.size() <= 2, |
| "Custom notification must not contain more than 2 actions"); |
| return new Notification(mTitle, mText, actions); |
| } |
| } |
| } |
| |
| /** Builder class for {@link SafetySourceIssue}. */ |
| public static final class Builder { |
| |
| @NonNull private final String mId; |
| @NonNull private final CharSequence mTitle; |
| @NonNull private final CharSequence mSummary; |
| @SafetySourceData.SeverityLevel private final int mSeverityLevel; |
| @NonNull private final String mIssueTypeId; |
| private final List<Action> mActions = new ArrayList<>(); |
| |
| @Nullable private CharSequence mSubtitle; |
| @IssueCategory private int mIssueCategory = ISSUE_CATEGORY_GENERAL; |
| @Nullable private PendingIntent mOnDismissPendingIntent; |
| @Nullable private CharSequence mAttributionTitle; |
| @Nullable private String mDeduplicationId; |
| |
| @Nullable private Notification mCustomNotification = null; |
| |
| @SuppressLint("NewApi") |
| @NotificationBehavior |
| private int mNotificationBehavior = NOTIFICATION_BEHAVIOR_UNSPECIFIED; |
| |
| @SuppressLint("NewApi") |
| @IssueActionability |
| private int mIssueActionability = ISSUE_ACTIONABILITY_MANUAL; |
| |
| /** Creates a {@link Builder} for a {@link SafetySourceIssue}. */ |
| public Builder( |
| @NonNull String id, |
| @NonNull CharSequence title, |
| @NonNull CharSequence summary, |
| @SafetySourceData.SeverityLevel int severityLevel, |
| @NonNull String issueTypeId) { |
| this.mId = requireNonNull(id); |
| this.mTitle = requireNonNull(title); |
| this.mSummary = requireNonNull(summary); |
| this.mSeverityLevel = validateSeverityLevel(severityLevel); |
| this.mIssueTypeId = requireNonNull(issueTypeId); |
| } |
| |
| /** Creates a {@link Builder} with the values from the given {@link SafetySourceIssue}. */ |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Builder(@NonNull SafetySourceIssue safetySourceIssue) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| requireNonNull(safetySourceIssue); |
| mId = safetySourceIssue.mId; |
| mTitle = safetySourceIssue.mTitle; |
| mSummary = safetySourceIssue.mSummary; |
| mSeverityLevel = safetySourceIssue.mSeverityLevel; |
| mIssueTypeId = safetySourceIssue.mIssueTypeId; |
| mActions.addAll(safetySourceIssue.mActions); |
| mSubtitle = safetySourceIssue.mSubtitle; |
| mIssueCategory = safetySourceIssue.mIssueCategory; |
| mOnDismissPendingIntent = safetySourceIssue.mOnDismissPendingIntent; |
| mAttributionTitle = safetySourceIssue.mAttributionTitle; |
| mDeduplicationId = safetySourceIssue.mDeduplicationId; |
| mCustomNotification = safetySourceIssue.mCustomNotification; |
| mNotificationBehavior = safetySourceIssue.mNotificationBehavior; |
| mIssueActionability = safetySourceIssue.mIssueActionability; |
| } |
| |
| /** Sets the localized subtitle. */ |
| @NonNull |
| public Builder setSubtitle(@Nullable CharSequence subtitle) { |
| mSubtitle = subtitle; |
| return this; |
| } |
| |
| /** |
| * Sets or clears the optional attribution title for this issue. |
| * |
| * <p>This is displayed in the UI and helps to attribute an issue to a particular source. If |
| * this value is {@code null}, the title of the group that contains the Safety Source will |
| * be used. |
| */ |
| @NonNull |
| @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) |
| public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| mAttributionTitle = attributionTitle; |
| 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 = validateIssueCategory(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. |
| * |
| * <p>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. The action contained in the {@link PendingIntent} must not start an activity. |
| * |
| * @see #getOnDismissPendingIntent() |
| */ |
| @NonNull |
| public Builder setOnDismissPendingIntent(@Nullable PendingIntent onDismissPendingIntent) { |
| checkArgument( |
| onDismissPendingIntent == null || !onDismissPendingIntent.isActivity(), |
| "Safety source issue on dismiss pending intent must not start an activity"); |
| mOnDismissPendingIntent = onDismissPendingIntent; |
| return this; |
| } |
| |
| /** |
| * Sets a custom {@link Notification} for this issue. |
| * |
| * <p>Using a custom {@link Notification} a source may specify a different {@link |
| * Notification#getTitle()}, {@link Notification#getText()} and {@link |
| * Notification#getActions()} for Safety Center to use when constructing a notification for |
| * this issue. |
| * |
| * <p>Safety Center may still generate a default notification from the other details of this |
| * issue when no custom notification has been set, depending on the issue's {@link |
| * #getNotificationBehavior()}. |
| * |
| * @see #getCustomNotification() |
| * @see #setNotificationBehavior(int) |
| */ |
| @NonNull |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Builder setCustomNotification(@Nullable Notification customNotification) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| mCustomNotification = customNotification; |
| return this; |
| } |
| |
| /** |
| * Sets the notification behavior of the issue. |
| * |
| * <p>Must be one of {@link #NOTIFICATION_BEHAVIOR_UNSPECIFIED}, {@link |
| * #NOTIFICATION_BEHAVIOR_NEVER}, {@link #NOTIFICATION_BEHAVIOR_DELAYED} or {@link |
| * #NOTIFICATION_BEHAVIOR_IMMEDIATELY}. See {@link #getNotificationBehavior()} for details |
| * of how Safety Center will interpret each of these. |
| * |
| * @see #getNotificationBehavior() |
| */ |
| @NonNull |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Builder setNotificationBehavior(@NotificationBehavior int notificationBehavior) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| mNotificationBehavior = validateNotificationBehavior(notificationBehavior); |
| return this; |
| } |
| |
| /** |
| * Sets the deduplication identifier for the issue. |
| * |
| * @see #getDeduplicationId() |
| */ |
| @NonNull |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Builder setDeduplicationId(@Nullable String deduplicationId) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| mDeduplicationId = deduplicationId; |
| return this; |
| } |
| |
| /** |
| * Sets the issue actionability of the issue. |
| * |
| * <p>Must be one of {@link #ISSUE_ACTIONABILITY_MANUAL} (default), {@link |
| * #ISSUE_ACTIONABILITY_TIP}, {@link #ISSUE_ACTIONABILITY_AUTOMATIC}. |
| * |
| * @see #getIssueActionability() |
| */ |
| @NonNull |
| @RequiresApi(UPSIDE_DOWN_CAKE) |
| public Builder setIssueActionability(@IssueActionability int issueActionability) { |
| if (!SdkLevel.isAtLeastU()) { |
| throw new UnsupportedOperationException(); |
| } |
| mIssueActionability = validateIssueActionability(issueActionability); |
| return this; |
| } |
| |
| /** Creates the {@link SafetySourceIssue} defined by this {@link Builder}. */ |
| @NonNull |
| public SafetySourceIssue build() { |
| List<SafetySourceIssue.Action> actions = unmodifiableList(new ArrayList<>(mActions)); |
| Action.enforceUniqueActionIds( |
| actions, "Safety source issue cannot have duplicate action ids"); |
| if (SdkLevel.isAtLeastU()) { |
| checkArgument( |
| mIssueActionability != ISSUE_ACTIONABILITY_MANUAL || !actions.isEmpty(), |
| "Actionable safety source issue must contain at least 1 action"); |
| } else { |
| checkArgument( |
| !actions.isEmpty(), "Safety source issue must contain at least 1 action"); |
| } |
| checkArgument( |
| actions.size() <= 2, |
| "Safety source issue must not contain more than 2 actions"); |
| return new SafetySourceIssue( |
| mId, |
| mTitle, |
| mSubtitle, |
| mSummary, |
| mSeverityLevel, |
| mIssueCategory, |
| actions, |
| mOnDismissPendingIntent, |
| mIssueTypeId, |
| mCustomNotification, |
| mNotificationBehavior, |
| mAttributionTitle, |
| mDeduplicationId, |
| mIssueActionability); |
| } |
| } |
| |
| @SafetySourceData.SeverityLevel |
| private static int validateSeverityLevel(int value) { |
| switch (value) { |
| case SafetySourceData.SEVERITY_LEVEL_INFORMATION: |
| case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: |
| case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: |
| return value; |
| case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: |
| throw new IllegalArgumentException( |
| "SeverityLevel for SafetySourceIssue must not be " |
| + "SEVERITY_LEVEL_UNSPECIFIED"); |
| default: |
| } |
| throw new IllegalArgumentException( |
| "Unexpected SeverityLevel for SafetySourceIssue: " + value); |
| } |
| |
| @IssueCategory |
| private static int validateIssueCategory(int value) { |
| switch (value) { |
| case ISSUE_CATEGORY_DEVICE: |
| case ISSUE_CATEGORY_ACCOUNT: |
| case ISSUE_CATEGORY_GENERAL: |
| return value; |
| default: |
| } |
| if (SdkLevel.isAtLeastU()) { |
| switch (value) { |
| case ISSUE_CATEGORY_DATA: |
| case ISSUE_CATEGORY_PASSWORDS: |
| case ISSUE_CATEGORY_PERSONAL_SAFETY: |
| return value; |
| default: |
| } |
| } |
| throw new IllegalArgumentException( |
| "Unexpected IssueCategory for SafetySourceIssue: " + value); |
| } |
| |
| @NotificationBehavior |
| private static int validateNotificationBehavior(int value) { |
| switch (value) { |
| case NOTIFICATION_BEHAVIOR_UNSPECIFIED: |
| case NOTIFICATION_BEHAVIOR_NEVER: |
| case NOTIFICATION_BEHAVIOR_DELAYED: |
| case NOTIFICATION_BEHAVIOR_IMMEDIATELY: |
| return value; |
| default: |
| } |
| throw new IllegalArgumentException( |
| "Unexpected NotificationBehavior for SafetySourceIssue: " + value); |
| } |
| |
| @IssueActionability |
| private static int validateIssueActionability(int value) { |
| switch (value) { |
| case ISSUE_ACTIONABILITY_MANUAL: |
| case ISSUE_ACTIONABILITY_TIP: |
| case ISSUE_ACTIONABILITY_AUTOMATIC: |
| return value; |
| default: |
| } |
| throw new IllegalArgumentException( |
| "Unexpected IssueActionability for SafetySourceIssue: " + value); |
| } |
| } |