/* * 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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"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() {
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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(
"Method not supported on versions lower than UPSIDE_DOWN_CAKE");
}
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.
*
*