/* * 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. * *
An issue represents an actionable matter relating to a particular safety source. * *
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. * *
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. * *
An issue's category represents a specific area of safety that the issue relates to. * *
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. * *
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. * *
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. * *
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. * *
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. * *
An issue's actionability represent what action is expected from the user as a result of * showing them this issue. * *
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). * *
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 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.
     *
     *  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.
     *
     *  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.
     *
     *  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.
     *
     *  Each issue must contain at least one action, in order to help the user resolve the issue.
     *
     *  In Android {@link android.os.Build.VERSION_CODES#TIRAMISU}, each issue can contain at most
     * two actions supported from the UI.
     */
    @NonNull
    public List 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.
     *
     *  The issue type should indicate the underlying basis for the issue, for e.g. a pending
     * update or a disabled security feature.
     *
     *  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).
     *
     *  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}.
     *
     *  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.
     *
     *  Any notification will be based on the {@link #getCustomNotification()} if set, or the
     * other properties of this issue otherwise.
     *
     *  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.
     *
     *  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).
     *
     *  Expected usage implies different sources will coordinate to set the same deduplication
     * identifiers on issues that they want to deduplicate.
     *
     *  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:
     *
     *  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.
     *
     *  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 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.
         *
         *  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 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}.
     *
     *  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 If this list is empty then the resulting {@link android.app.Notification} will have
         * zero action buttons.
         */
        @NonNull
        public List 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.
         *
         *  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.
         *
         *  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.
         *
         *  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.
         *
         *  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.
         *
         *  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.
         *
         *  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
     *   
*
     * @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.
     *
     *
     *   
*
     * @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.
     *
     *