/*
 * Copyright (C) 2022 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 java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableSet;
import static java.util.Objects.requireNonNull;

import android.annotation.FlaggedApi;
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.app.compat.CompatChanges;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.permission.flags.Flags;
import android.safetycenter.config.SafetySourcesGroup;
import android.text.TextUtils;
import android.util.ArraySet;

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.List;
import java.util.Objects;
import java.util.Set;

/**
 * An issue in the Safety Center.
 *
 * <p>An issue represents an actionable matter on the device of elevated importance.
 *
 * <p>It contains localized messages to display to the user, explaining the underlying threat or
 * warning and suggested fixes, and contains actions that a user may take from the UI to resolve the
 * issue.
 *
 * <p>Issues are ephemeral and disappear when resolved by user action or dismissal.
 *
 * @hide
 */
@SystemApi
@RequiresApi(TIRAMISU)
public final class SafetyCenterIssue implements Parcelable {

    /** Indicates that this is low-severity, and informational. */
    public static final int ISSUE_SEVERITY_LEVEL_OK = 2100;

    /** Indicates that this issue describes a safety recommendation. */
    public static final int ISSUE_SEVERITY_LEVEL_RECOMMENDATION = 2200;

    /** Indicates that this issue describes a critical safety warning. */
    public static final int ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING = 2300;

    /**
     * All possible severity levels for a {@link SafetyCenterIssue}.
     *
     * @hide
     * @see SafetyCenterIssue#getSeverityLevel()
     * @see Builder#setSeverityLevel(int)
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            prefix = "ISSUE_SEVERITY_LEVEL_",
            value = {
                ISSUE_SEVERITY_LEVEL_OK,
                ISSUE_SEVERITY_LEVEL_RECOMMENDATION,
                ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING,
            })
    public @interface IssueSeverityLevel {}

    @NonNull
    public static final Creator<SafetyCenterIssue> CREATOR =
            new Creator<SafetyCenterIssue>() {
                @Override
                public SafetyCenterIssue createFromParcel(Parcel in) {
                    SafetyCenterIssue.Builder builder;

                    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);

                    if (Flags.openSafetyCenterApis()) {
                        UserHandle user = in.readTypedObject(UserHandle.CREATOR);
                        Set<String> safetySourceIds = new ArraySet<>(in.createStringArrayList());
                        String issueTypeId = in.readString();
                        String safetySourceIssueId = in.readString();
                        if (user != null && issueTypeId != null && safetySourceIssueId != null) {
                            builder =
                                    new Builder(
                                            id,
                                            title,
                                            summary,
                                            user,
                                            safetySourceIds,
                                            issueTypeId,
                                            safetySourceIssueId);
                        } else {
                            builder = new Builder(id, title, summary, /* checkTargetSdk= */ false);
                        }
                    } else {
                        builder = new Builder(id, title, summary);
                    }

                    builder.setSubtitle(subtitle)
                            .setSeverityLevel(in.readInt())
                            .setDismissible(in.readBoolean())
                            .setShouldConfirmDismissal(in.readBoolean())
                            .setActions(in.createTypedArrayList(Action.CREATOR));
                    if (SdkLevel.isAtLeastU()) {
                        builder.setAttributionTitle(
                                TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in));
                        builder.setGroupId(in.readString());
                    }

                    return builder.build();
                }

                @Override
                public SafetyCenterIssue[] newArray(int size) {
                    return new SafetyCenterIssue[size];
                }
            };

    @NonNull private final String mId;
    @NonNull private final CharSequence mTitle;
    @Nullable private final CharSequence mSubtitle;
    @NonNull private final CharSequence mSummary;
    @IssueSeverityLevel private final int mSeverityLevel;
    private final boolean mDismissible;
    private final boolean mShouldConfirmDismissal;
    @NonNull private final List<Action> mActions;
    @Nullable private final CharSequence mAttributionTitle;
    @Nullable private final String mGroupId;
    @Nullable private final UserHandle mUser;
    @NonNull private final Set<String> mSafetySourceIds;
    @Nullable private final String mIssueTypeId;
    @Nullable private final String mSafetySourceIssueId;

    private SafetyCenterIssue(
            @NonNull String id,
            @NonNull CharSequence title,
            @Nullable CharSequence subtitle,
            @NonNull CharSequence summary,
            @IssueSeverityLevel int severityLevel,
            boolean isDismissible,
            boolean shouldConfirmDismissal,
            @NonNull List<Action> actions,
            @Nullable CharSequence attributionTitle,
            @Nullable String groupId,
            @Nullable UserHandle user,
            @NonNull Set<String> safetySourceIds,
            @Nullable String issueTypeId,
            @Nullable String safetySourceIssueId) {
        mId = id;
        mTitle = title;
        mSubtitle = subtitle;
        mSummary = summary;
        mSeverityLevel = severityLevel;
        mDismissible = isDismissible;
        mShouldConfirmDismissal = shouldConfirmDismissal;
        mActions = actions;
        mAttributionTitle = attributionTitle;
        mGroupId = groupId;
        mUser = user;
        mSafetySourceIds = safetySourceIds;
        mIssueTypeId = issueTypeId;
        mSafetySourceIssueId = safetySourceIssueId;
    }

    /**
     * Returns the encoded string ID which uniquely identifies this issue within the Safety Center
     * on the device for the current user across all profiles and accounts.
     */
    @NonNull
    public String getId() {
        return mId;
    }

    /** Returns the title that describes this issue. */
    @NonNull
    public CharSequence getTitle() {
        return mTitle;
    }

    /** Returns the subtitle of this issue, or {@code null} if it has none. */
    @Nullable
    public CharSequence getSubtitle() {
        return mSubtitle;
    }

    /** Returns the summary text that describes this issue. */
    @NonNull
    public CharSequence getSummary() {
        return mSummary;
    }

    /**
     * Returns the attribution title of this issue, or {@code null} if it has none.
     *
     * <p>This is displayed in the UI and helps to attribute issue cards to a particular source.
     *
     * <p>Note: On SDKs where Safety Center UI is fully migrated to the Settings app, the UI won't
     * rely on this data anymore.
     *
     * @throws UnsupportedOperationException if accessed from a version lower than {@link
     *     UPSIDE_DOWN_CAKE}
     */
    @Nullable
    @RequiresApi(UPSIDE_DOWN_CAKE)
    public CharSequence getAttributionTitle() {
        if (!SdkLevel.isAtLeastU()) {
            throw new UnsupportedOperationException(
                    "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
        }
        return mAttributionTitle;
    }

    /** Returns the {@link IssueSeverityLevel} of this issue. */
    @IssueSeverityLevel
    public int getSeverityLevel() {
        return mSeverityLevel;
    }

    /** Returns {@code true} if this issue can be dismissed. */
    public boolean isDismissible() {
        return mDismissible;
    }

    /** Returns {@code true} if this issue should have its dismissal confirmed. */
    public boolean shouldConfirmDismissal() {
        return mShouldConfirmDismissal;
    }

    /**
     * Returns the ordered list of {@link Action} objects that may be taken to resolve this issue.
     *
     * <p>An issue may have 0-2 actions. The first action will be considered the "Primary" action of
     * the issue.
     */
    @NonNull
    public List<Action> getActions() {
        return mActions;
    }

    /**
     * Returns the ID of the {@link SafetySourcesGroup} that this issue belongs to, or {@code null}
     * if it has none.
     *
     * <p>This ID is used for displaying the issue on its corresponding subpage in the Safety Center
     * UI.
     *
     * @throws UnsupportedOperationException if accessed from a version lower than {@link
     *     UPSIDE_DOWN_CAKE}
     */
    @Nullable
    @RequiresApi(UPSIDE_DOWN_CAKE)
    public String getGroupId() {
        if (!SdkLevel.isAtLeastU()) {
            throw new UnsupportedOperationException(
                    "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
        }
        return mGroupId;
    }

    /** Returns the user handle related to this issue. */
    @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    @Nullable
    public UserHandle getUser() {
        return mUser;
    }

    /** Returns the set of safety source IDs related to this issue. */
    @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    @NonNull
    public Set<String> getSafetySourceIds() {
        return mSafetySourceIds;
    }

    /** Returns the issue type ID of this issue. */
    @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    @Nullable
    public String getIssueTypeId() {
        return mIssueTypeId;
    }

    /**
     * Returns the safety source provided ID for this issue.
     *
     * <p>Unlike {@link SafetyCenterIssue#getId()}, this ID doesn't necessarily uniquely identify
     * this issue. It's just the raw ID that was provided by the safety source. A different safety
     * source could have used the same ID, or even the same safety source in a different user
     * profile could have used the same ID. See {@link SafetySourceIssue#getId()}.
     */
    @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    @Nullable
    public String getSafetySourceIssueId() {
        return mSafetySourceIssueId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof SafetyCenterIssue)) return false;
        SafetyCenterIssue that = (SafetyCenterIssue) o;
        return mSeverityLevel == that.mSeverityLevel
                && mDismissible == that.mDismissible
                && mShouldConfirmDismissal == that.mShouldConfirmDismissal
                && Objects.equals(mId, that.mId)
                && TextUtils.equals(mTitle, that.mTitle)
                && TextUtils.equals(mSubtitle, that.mSubtitle)
                && TextUtils.equals(mSummary, that.mSummary)
                && Objects.equals(mActions, that.mActions)
                && Objects.equals(mSafetySourceIds, that.mSafetySourceIds)
                && TextUtils.equals(mAttributionTitle, that.mAttributionTitle)
                && Objects.equals(mGroupId, that.mGroupId)
                && Objects.equals(mUser, that.mUser)
                && Objects.equals(mIssueTypeId, that.mIssueTypeId)
                && Objects.equals(mSafetySourceIssueId, that.mSafetySourceIssueId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                mId,
                mTitle,
                mSubtitle,
                mSummary,
                mSeverityLevel,
                mDismissible,
                mShouldConfirmDismissal,
                mActions,
                mSafetySourceIds,
                mAttributionTitle,
                mGroupId,
                mUser,
                mIssueTypeId,
                mSafetySourceIssueId);
    }

    @Override
    public String toString() {
        return "SafetyCenterIssue{"
                + "mId="
                + mId
                + ", mTitle="
                + mTitle
                + ", mSubtitle="
                + mSubtitle
                + ", mSummary="
                + mSummary
                + ", mSeverityLevel="
                + mSeverityLevel
                + ", mDismissible="
                + mDismissible
                + ", mConfirmDismissal="
                + mShouldConfirmDismissal
                + ", mActions="
                + mActions
                + ", mAttributionTitle="
                + mAttributionTitle
                + ", mGroupId="
                + mGroupId
                + (Flags.openSafetyCenterApis() ? ", mUser=" + mUser : "")
                + (Flags.openSafetyCenterApis() ? ", mSafetySourceIds=" + mSafetySourceIds : "")
                + (Flags.openSafetyCenterApis() ? ", mIssueTypeId=" + mIssueTypeId : "")
                + (Flags.openSafetyCenterApis()
                        ? ", mSafetySourceIssueId=" + mSafetySourceIssueId
                        : "")
                + '}';
    }

    @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);
        if (Flags.openSafetyCenterApis()) {
            dest.writeTypedObject(mUser, flags);
            dest.writeStringList(new ArrayList<>(mSafetySourceIds));
            dest.writeString(mIssueTypeId);
            dest.writeString(mSafetySourceIssueId);
        }
        dest.writeInt(mSeverityLevel);
        dest.writeBoolean(mDismissible);
        dest.writeBoolean(mShouldConfirmDismissal);
        dest.writeTypedList(mActions);
        if (SdkLevel.isAtLeastU()) {
            TextUtils.writeToParcel(mAttributionTitle, dest, flags);
            dest.writeString(mGroupId);
        }
    }

    /** Builder class for {@link SafetyCenterIssue}. */
    public static final class Builder {

        @NonNull private String mId;
        @NonNull private CharSequence mTitle;
        @NonNull private CharSequence mSummary;
        @Nullable private CharSequence mSubtitle;
        @IssueSeverityLevel private int mSeverityLevel = ISSUE_SEVERITY_LEVEL_OK;
        private boolean mDismissible = true;
        private boolean mShouldConfirmDismissal = true;
        private List<Action> mActions = new ArrayList<>();
        @Nullable private CharSequence mAttributionTitle;
        @Nullable private String mGroupId;
        @Nullable private UserHandle mUser;
        private Set<String> mSafetySourceIds = new ArraySet<>();
        @Nullable private String mIssueTypeId;
        @Nullable private String mSafetySourceIssueId;

        /**
         * Creates a {@link Builder} for a {@link SafetyCenterIssue}.
         *
         * @param id a unique encoded string ID, see {@link #getId()} for details
         * @param title a title that describes this issue
         * @param summary a summary of this issue
         * @throws UnsupportedOperationException If the target SDK version of the app is above or
         *     equal to Android C
         * @deprecated Use {@link #Builder(String, CharSequence, CharSequence, UserHandle, Set,
         *     String, String)} instead.
         */
        @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
        @Deprecated
        public Builder(
                @NonNull String id, @NonNull CharSequence title, @NonNull CharSequence summary) {
            this(id, title, summary, /* checkTargetSdk= */ true);
        }

        /**
         * Creates a {@link SafetyCenterIssue.Builder} for a {@link SafetyCenterIssue}.
         *
         * @param checkTargetSdk whether to check for the target SDK level
         * @throws UnsupportedOperationException If the target SDK version of the app is above or
         *     equal to Android C, when the {@code checkTargetSdk} argument is true
         */
        private Builder(
                @NonNull String id,
                @NonNull CharSequence title,
                @NonNull CharSequence summary,
                boolean checkTargetSdk) {
            if (checkTargetSdk
                    && Flags.openSafetyCenterApis()
                    && CompatChanges.isChangeEnabled(
                            SafetyCenterManager.RESTRICT_DEPRECATED_DATA_BUILDER_CONSTRUCTORS)) {
                throw new UnsupportedOperationException(
                        "Deprecated constructor no longer accessible.");
            }
            mId = requireNonNull(id);
            mTitle = requireNonNull(title);
            mSummary = requireNonNull(summary);
        }

        /**
         * Creates a {@link Builder} for a {@link SafetyCenterIssue}.
         *
         * @param id a unique encoded string ID, see {@link #getId()} for details
         * @param title a title that describes this issue
         * @param summary a summary of this issue
         * @param user the user handle for this issue
         * @param safetySourceIds at least one safety source ID that sent this issue
         * @param issueTypeId the issue type ID for this issue
         * @param safetySourceIssueId the ID for this issue as specified by the safety source
         */
        @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
        public Builder(
                @NonNull String id,
                @NonNull CharSequence title,
                @NonNull CharSequence summary,
                @NonNull UserHandle user,
                @NonNull Set<String> safetySourceIds,
                @NonNull String issueTypeId,
                @NonNull String safetySourceIssueId) {
            mId = requireNonNull(id);
            mTitle = requireNonNull(title);
            mSummary = requireNonNull(summary);
            mUser = requireNonNull(user);
            mSafetySourceIds = requireNonNull(safetySourceIds);
            if (mSafetySourceIds.isEmpty()) {
                throw new IllegalArgumentException(
                        "At least one safety source ID must be "
                                + "provided when open_safety_center_apis is enabled");
            }
            mIssueTypeId = requireNonNull(issueTypeId);
            mSafetySourceIssueId = requireNonNull(safetySourceIssueId);
        }

        /** Creates a {@link Builder} with the values from the given {@link SafetyCenterIssue}. */
        public Builder(@NonNull SafetyCenterIssue issue) {
            mId = issue.mId;
            mTitle = issue.mTitle;
            mSubtitle = issue.mSubtitle;
            mSummary = issue.mSummary;
            mSeverityLevel = issue.mSeverityLevel;
            mDismissible = issue.mDismissible;
            mShouldConfirmDismissal = issue.mShouldConfirmDismissal;
            mActions = new ArrayList<>(issue.mActions);
            mAttributionTitle = issue.mAttributionTitle;
            mGroupId = issue.mGroupId;
            mUser = issue.mUser;
            mSafetySourceIds = new ArraySet<>(issue.mSafetySourceIds);
            mIssueTypeId = issue.mIssueTypeId;
            mSafetySourceIssueId = issue.mSafetySourceIssueId;
        }

        /** Sets the ID for this issue. */
        @NonNull
        public Builder setId(@NonNull String id) {
            mId = requireNonNull(id);
            return this;
        }

        /** Sets the title for this issue. */
        @NonNull
        public Builder setTitle(@NonNull CharSequence title) {
            mTitle = requireNonNull(title);
            return this;
        }

        /** Sets or clears the optional subtitle for this issue. */
        @NonNull
        public Builder setSubtitle(@Nullable CharSequence subtitle) {
            mSubtitle = subtitle;
            return this;
        }

        /** Sets the summary for this issue. */
        @NonNull
        public Builder setSummary(@NonNull CharSequence summary) {
            mSummary = requireNonNull(summary);
            return this;
        }

        /**
         * Sets or clears the optional attribution title for this issue.
         *
         * <p>This is displayed in the UI and helps to attribute issue cards to a particular source.
         *
         * <p>Note: On SDKs where Safety Center UI is fully migrated to the Settings app, the UI
         * won't rely on this data anymore.
         *
         * @throws UnsupportedOperationException if accessed from a version lower than {@link
         *     UPSIDE_DOWN_CAKE}
         */
        @NonNull
        @RequiresApi(UPSIDE_DOWN_CAKE)
        public Builder setAttributionTitle(@Nullable CharSequence attributionTitle) {
            if (!SdkLevel.isAtLeastU()) {
                throw new UnsupportedOperationException(
                        "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
            }
            mAttributionTitle = attributionTitle;
            return this;
        }

        /**
         * Sets {@link IssueSeverityLevel} for this issue. Defaults to {@link
         * #ISSUE_SEVERITY_LEVEL_OK}.
         */
        @NonNull
        public Builder setSeverityLevel(@IssueSeverityLevel int severityLevel) {
            mSeverityLevel = validateIssueSeverityLevel(severityLevel);
            return this;
        }

        /** Sets whether this issue can be dismissed. Defaults to {@code true}. */
        @NonNull
        public Builder setDismissible(boolean dismissible) {
            mDismissible = dismissible;
            return this;
        }

        /**
         * Sets whether this issue should have its dismissal confirmed. Defaults to {@code true}.
         */
        @NonNull
        public Builder setShouldConfirmDismissal(boolean confirmDismissal) {
            mShouldConfirmDismissal = confirmDismissal;
            return this;
        }

        /**
         * Sets the list of potential actions to be taken to resolve this issue. Defaults to an
         * empty list.
         */
        @NonNull
        public Builder setActions(@NonNull List<Action> actions) {
            mActions = requireNonNull(actions);
            return this;
        }

        /**
         * Sets the ID of {@link SafetySourcesGroup} that this issue belongs to. Defaults to a
         * {@code null} value.
         *
         * <p>This ID is used for displaying the issue on its corresponding subpage in the Safety
         * Center UI.
         *
         * @throws UnsupportedOperationException if accessed from a version lower than {@link
         *     UPSIDE_DOWN_CAKE}
         */
        @NonNull
        @RequiresApi(UPSIDE_DOWN_CAKE)
        public Builder setGroupId(@Nullable String groupId) {
            if (!SdkLevel.isAtLeastU()) {
                throw new UnsupportedOperationException(
                        "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
            }
            mGroupId = groupId;
            return this;
        }

        /**
         * Sets the user handle for this issue.
         *
         * @param user the user handle for this issue
         */
        @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
        @NonNull
        public Builder setUser(@NonNull UserHandle user) {
            mUser = requireNonNull(user);
            return this;
        }

        /**
         * Sets the set of safety source IDs that sent this issue.
         *
         * @param safetySourceIds at least one safety source ID that sent this issue
         * @throws IllegalArgumentException if no safety source IDs are provided
         */
        @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
        @NonNull
        public Builder setSafetySourceIds(@NonNull Set<String> safetySourceIds) {
            mSafetySourceIds = requireNonNull(safetySourceIds);
            if (mSafetySourceIds.isEmpty()) {
                throw new IllegalArgumentException(
                        "At least one safety source ID must be "
                                + "provided when open_safety_center_apis is enabled");
            }
            return this;
        }

        /**
         * Sets the issue type ID for this issue.
         *
         * <p>The issue type ID comes directly from the {@link SafetySourceIssue#getIssueTypeId()}.
         *
         * @param issueTypeId the issue type ID for this issue
         */
        @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
        @NonNull
        public Builder setIssueTypeId(@NonNull String issueTypeId) {
            mIssueTypeId = requireNonNull(issueTypeId);
            return this;
        }

        /**
         * Sets the safety source issue ID for this issue.
         *
         * <p>See {@link SafetyCenterIssue#getSafetySourceIssueId()} for more info.
         *
         * @param safetySourceIssueId the safety source issue ID for this issue
         */
        @FlaggedApi(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
        @NonNull
        public Builder setSafetySourceIssueId(@NonNull String safetySourceIssueId) {
            mSafetySourceIssueId = requireNonNull(safetySourceIssueId);
            return this;
        }

        /** Creates the {@link SafetyCenterIssue} defined by this {@link Builder}. */
        @NonNull
        public SafetyCenterIssue build() {
            return new SafetyCenterIssue(
                    mId,
                    mTitle,
                    mSubtitle,
                    mSummary,
                    mSeverityLevel,
                    mDismissible,
                    mShouldConfirmDismissal,
                    unmodifiableList(new ArrayList<>(mActions)),
                    mAttributionTitle,
                    mGroupId,
                    mUser,
                    unmodifiableSet(new ArraySet<>(mSafetySourceIds)),
                    mIssueTypeId,
                    mSafetySourceIssueId);
        }
    }

    /**
     * An action that can be taken to resolve a given issue.
     *
     * <p>When a user initiates an {@link Action}, that action's associated {@link PendingIntent}
     * will be executed, and the {@code successMessage} will be displayed if present.
     */
    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())
                                        .setIsInFlight(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];
                    }
                };

        @NonNull private final String mId;
        @NonNull private final CharSequence mLabel;
        @NonNull private final PendingIntent mPendingIntent;
        private final boolean mWillResolve;
        private final boolean mInFlight;
        @Nullable private final CharSequence mSuccessMessage;
        @Nullable private final ConfirmationDialogDetails mConfirmationDialogDetails;

        private Action(
                @NonNull String id,
                @NonNull CharSequence label,
                @NonNull PendingIntent pendingIntent,
                boolean willResolve,
                boolean inFlight,
                @Nullable CharSequence successMessage,
                @Nullable ConfirmationDialogDetails confirmationDialogDetails) {
            mId = id;
            mLabel = label;
            mPendingIntent = pendingIntent;
            mWillResolve = willResolve;
            mInFlight = inFlight;
            mSuccessMessage = successMessage;
            mConfirmationDialogDetails = confirmationDialogDetails;
        }

        /** Returns the ID of this action. */
        @NonNull
        public String getId() {
            return mId;
        }

        /** Returns a label describing this {@link Action}. */
        @NonNull
        public CharSequence getLabel() {
            return mLabel;
        }

        /** Returns the {@link PendingIntent} to execute when this {@link Action} is taken. */
        @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 whether this action is currently being executed (i.e. the user clicked on a
         * button that triggered this action, and now the Safety Center is waiting for the action's
         * result).
         */
        public boolean isInFlight() {
            return mInFlight;
        }

        /**
         * Returns the success message to display after successfully completing this {@link Action}
         * or {@code null} if none should be displayed.
         */
        @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(
                        "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
            }
            return mConfirmationDialogDetails;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Action)) return false;
            Action action = (Action) o;
            return Objects.equals(mId, action.mId)
                    && TextUtils.equals(mLabel, action.mLabel)
                    && Objects.equals(mPendingIntent, action.mPendingIntent)
                    && mWillResolve == action.mWillResolve
                    && mInFlight == action.mInFlight
                    && TextUtils.equals(mSuccessMessage, action.mSuccessMessage)
                    && Objects.equals(
                            mConfirmationDialogDetails, action.mConfirmationDialogDetails);
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                    mId,
                    mLabel,
                    mSuccessMessage,
                    mWillResolve,
                    mInFlight,
                    mPendingIntent,
                    mConfirmationDialogDetails);
        }

        @Override
        public String toString() {
            return "Action{"
                    + "mId="
                    + mId
                    + ", mLabel="
                    + mLabel
                    + ", mPendingIntent="
                    + mPendingIntent
                    + ", mWillResolve="
                    + mWillResolve
                    + ", mInFlight="
                    + mInFlight
                    + ", mSuccessMessage="
                    + mSuccessMessage
                    + ", mConfirmationDialogDetails="
                    + 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);
            dest.writeBoolean(mInFlight);
            TextUtils.writeToParcel(mSuccessMessage, dest, flags);
            if (SdkLevel.isAtLeastU()) {
                dest.writeTypedObject(mConfirmationDialogDetails, flags);
            }
        }

        /** 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 String mId;
            @NonNull private CharSequence mLabel;
            @NonNull private PendingIntent mPendingIntent;
            private boolean mWillResolve;
            private boolean mInFlight;
            @Nullable private CharSequence mSuccessMessage;
            @Nullable private ConfirmationDialogDetails mConfirmationDialogDetails;

            /**
             * Creates a new {@link Builder} for an {@link Action}.
             *
             * @param id a unique ID for this action
             * @param label a label describing this action
             * @param pendingIntent a {@link PendingIntent} to be sent when this action is taken
             */
            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(
                            "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
                }
                requireNonNull(action);
                mId = action.mId;
                mLabel = action.mLabel;
                mPendingIntent = action.mPendingIntent;
                mWillResolve = action.mWillResolve;
                mInFlight = action.mInFlight;
                mSuccessMessage = action.mSuccessMessage;
                mConfirmationDialogDetails = action.mConfirmationDialogDetails;
            }

            /** Sets the ID of this {@link Action} */
            @NonNull
            public Builder setId(@NonNull String id) {
                mId = requireNonNull(id);
                return this;
            }

            /** Sets the label of this {@link Action}. */
            @NonNull
            public Builder setLabel(@NonNull CharSequence label) {
                mLabel = requireNonNull(label);
                return this;
            }

            /** Sets the {@link PendingIntent} to be sent when this {@link Action} is taken. */
            @NonNull
            public Builder setPendingIntent(@NonNull PendingIntent pendingIntent) {
                mPendingIntent = requireNonNull(pendingIntent);
                return this;
            }

            /**
             * Sets whether this action will resolve the issue when executed. Defaults to {@code
             * false}.
             *
             * @see #willResolve()
             */
            @SuppressLint("MissingGetterMatchingBuilder")
            @NonNull
            public Builder setWillResolve(boolean willResolve) {
                mWillResolve = willResolve;
                return this;
            }

            /**
             * Sets a boolean that indicates whether this action is currently being executed (i.e.
             * the user clicked on a button that triggered this action, and now the Safety Center is
             * waiting for the action's result). Defaults to {@code false}.
             *
             * @see #isInFlight()
             */
            @SuppressLint("MissingGetterMatchingBuilder")
            @NonNull
            public Builder setIsInFlight(boolean inFlight) {
                mInFlight = inFlight;
                return this;
            }

            /**
             * Sets or clears the optional success message to be displayed when this {@link Action}
             * completes.
             */
            @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(
                            "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
                }
                mConfirmationDialogDetails = confirmationDialogDetails;
                return this;
            }

            /** Creates the {@link Action} defined by this {@link Builder}. */
            @NonNull
            public Action build() {
                return new Action(
                        mId,
                        mLabel,
                        mPendingIntent,
                        mWillResolve,
                        mInFlight,
                        mSuccessMessage,
                        mConfirmationDialogDetails);
            }
        }
    }

    @IssueSeverityLevel
    private static int validateIssueSeverityLevel(int value) {
        switch (value) {
            case ISSUE_SEVERITY_LEVEL_OK:
            case ISSUE_SEVERITY_LEVEL_RECOMMENDATION:
            case ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING:
                return value;
            default:
        }
        throw new IllegalArgumentException(
                "Unexpected IssueSeverityLevel for SafetyCenterIssue: " + value);
    }
}
