/* * 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.config; import static android.os.Build.VERSION_CODES.TIRAMISU; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; import android.annotation.SystemApi; import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.RequiresApi; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** * Data class used to represent the initial configuration of a safety source. * * @hide */ @SystemApi @RequiresApi(TIRAMISU) public final class SafetySource implements Parcelable { /** * Static safety source. * *
A static safety source is a source completely defined in the Safety Center configuration. * The source is displayed with no icon and neither the description displayed nor the tap * behavior can be changed at runtime. A static safety source cannot have any issue associated * with it. */ public static final int SAFETY_SOURCE_TYPE_STATIC = 1; /** * Dynamic safety source. * *
The status, description, tap behavior, and related issues of a dynamic safety source can * be set at runtime by the package that owns the source. The source is displayed with an icon * reflecting the status when part of a collapsible safety sources group. */ public static final int SAFETY_SOURCE_TYPE_DYNAMIC = 2; /** * Issue-only safety source. * *
An issue-only safety source is not displayed as an entry in the Safety Center page. The
* package that owns an issue-only safety source can set the list of issues associated with the
* source at runtime.
*/
public static final int SAFETY_SOURCE_TYPE_ISSUE_ONLY = 3;
/**
* All possible safety source types.
*
* @hide
*/
@IntDef(
prefix = {"SAFETY_SOURCE_TYPE_"},
value = {
SAFETY_SOURCE_TYPE_STATIC,
SAFETY_SOURCE_TYPE_DYNAMIC,
SAFETY_SOURCE_TYPE_ISSUE_ONLY
})
@Retention(RetentionPolicy.SOURCE)
public @interface SafetySourceType {}
/** Profile property unspecified. */
public static final int PROFILE_NONE = 0;
/**
* Even when the active user has managed enabled profiles, a visible safety source will be
* displayed as a single entry for the primary profile. For dynamic sources, refresh requests
* will be sent to and set requests will be accepted from the primary profile only.
*/
public static final int PROFILE_PRIMARY = 1;
/**
* When the user has managed enabled profiles, a visible safety source will be displayed as
* multiple entries one for each enabled profile. For dynamic sources, refresh requests will be
* sent to and set requests will be accepted from all profiles.
*/
public static final int PROFILE_ALL = 2;
/**
* All possible profile configurations for a safety source.
*
* @hide
*/
@IntDef(
prefix = {"PROFILE_"},
value = {PROFILE_NONE, PROFILE_PRIMARY, PROFILE_ALL})
@Retention(RetentionPolicy.SOURCE)
public @interface Profile {}
/**
* The dynamic safety source will create an enabled entry in the Safety Center page until a set
* request is received.
*/
public static final int INITIAL_DISPLAY_STATE_ENABLED = 0;
/**
* The dynamic safety source will create a disabled entry in the Safety Center page until a set
* request is received.
*/
public static final int INITIAL_DISPLAY_STATE_DISABLED = 1;
/**
* The dynamic safety source will have no entry in the Safety Center page until a set request is
* received.
*/
public static final int INITIAL_DISPLAY_STATE_HIDDEN = 2;
/**
* All possible initial display states for a dynamic safety source.
*
* @hide
*/
@IntDef(
prefix = {"INITIAL_DISPLAY_STATE_"},
value = {
INITIAL_DISPLAY_STATE_ENABLED,
INITIAL_DISPLAY_STATE_DISABLED,
INITIAL_DISPLAY_STATE_HIDDEN
})
@Retention(RetentionPolicy.SOURCE)
public @interface InitialDisplayState {}
@NonNull
public static final Creator The id is unique among safety sources in a Safety Center configuration.
*/
@NonNull
public String getId() {
return mId;
}
/**
* Returns the package name of this safety source.
*
* This is the package that owns the source. The package will receive refresh requests and it
* can send set requests for the source.
*
* Throws an {@link UnsupportedOperationException} if the source is of type static.
*/
@NonNull
public String getPackageName() {
if (mType == SAFETY_SOURCE_TYPE_STATIC) {
throw new UnsupportedOperationException(
"getPackageName unsupported for static safety source");
}
return mPackageName;
}
/**
* Returns the resource id of the title of this safety source.
*
* The id refers to a string resource that is either accessible from any resource context or
* that is accessible from the same resource context that was used to load the Safety Center
* configuration. The id is {@link Resources#ID_NULL} when a title is not provided.
*
* Throws an {@link UnsupportedOperationException} if the source is of type issue-only.
*/
@StringRes
public int getTitleResId() {
if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
throw new UnsupportedOperationException(
"getTitleResId unsupported for issue-only safety source");
}
return mTitleResId;
}
/**
* Returns the resource id of the title for work of this safety source.
*
* The id refers to a string resource that is either accessible from any resource context or
* that is accessible from the same resource context that was used to load the Safety Center
* configuration. The id is {@link Resources#ID_NULL} when a title for work is not provided.
*
* Throws an {@link UnsupportedOperationException} if the source is of type issue-only or if
* the profile property of the source is set to {@link SafetySource#PROFILE_PRIMARY}.
*/
@StringRes
public int getTitleForWorkResId() {
if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
throw new UnsupportedOperationException(
"getTitleForWorkResId unsupported for issue-only safety source");
}
if (mProfile == PROFILE_PRIMARY) {
throw new UnsupportedOperationException(
"getTitleForWorkResId unsupported for primary profile safety source");
}
return mTitleForWorkResId;
}
/**
* Returns the resource id of the summary of this safety source.
*
* The id refers to a string resource that is either accessible from any resource context or
* that is accessible from the same resource context that was used to load the Safety Center
* configuration. The id is {@link Resources#ID_NULL} when a summary is not provided.
*
* Throws an {@link UnsupportedOperationException} if the source is of type issue-only.
*/
@StringRes
public int getSummaryResId() {
if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
throw new UnsupportedOperationException(
"getSummaryResId unsupported for issue-only safety source");
}
return mSummaryResId;
}
/**
* Returns the intent action of this safety source.
*
* An intent created from the intent action should resolve to a public activity. If the
* source is displayed as an entry in the Safety Center page, and if the action is set to {@code
* null} or if it does not resolve to an activity the source will be marked as disabled.
*
* Throws an {@link UnsupportedOperationException} if the source is of type issue-only.
*/
@Nullable
public String getIntentAction() {
if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
throw new UnsupportedOperationException(
"getIntentAction unsupported for issue-only safety source");
}
return mIntentAction;
}
/** Returns the profile property of this safety source. */
@Profile
public int getProfile() {
return mProfile;
}
/**
* Returns the initial display state of this safety source.
*
* Throws an {@link UnsupportedOperationException} if the source is of type static or
* issue-only.
*/
@InitialDisplayState
public int getInitialDisplayState() {
if (mType == SAFETY_SOURCE_TYPE_STATIC) {
throw new UnsupportedOperationException(
"getInitialDisplayState unsupported for static safety source");
}
if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
throw new UnsupportedOperationException(
"getInitialDisplayState unsupported for issue-only safety source");
}
return mInitialDisplayState;
}
/**
* Returns the maximum severity level of this safety source.
*
* The maximum severity level dictates the maximum severity level values that can be used in
* the source status or the source issues when setting the source data at runtime. A source can
* always send a status severity level of at least {@link
* android.safetycenter.SafetySourceData#SEVERITY_LEVEL_INFORMATION} even if the maximum
* severity level is set to a lower value.
*
* Throws an {@link UnsupportedOperationException} if the source is of type static.
*/
public int getMaxSeverityLevel() {
if (mType == SAFETY_SOURCE_TYPE_STATIC) {
throw new UnsupportedOperationException(
"getMaxSeverityLevel unsupported for static safety source");
}
return mMaxSeverityLevel;
}
/**
* Returns the resource id of the search terms of this safety source.
*
* The id refers to a string resource that is either accessible from any resource context or
* that is accessible from the same resource context that was used to load the Safety Center
* configuration. The id is {@link Resources#ID_NULL} when search terms are not provided.
*
* Throws an {@link UnsupportedOperationException} if the source is of type issue-only.
*/
@StringRes
public int getSearchTermsResId() {
if (mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
throw new UnsupportedOperationException(
"getSearchTermsResId unsupported for issue-only safety source");
}
return mSearchTermsResId;
}
/**
* Returns the logging allowed property of this safety source.
*
* Throws an {@link UnsupportedOperationException} if the source is of type static.
*/
public boolean isLoggingAllowed() {
if (mType == SAFETY_SOURCE_TYPE_STATIC) {
throw new UnsupportedOperationException(
"isLoggingAllowed unsupported for static safety source");
}
return mLoggingAllowed;
}
/**
* Returns the refresh on page open allowed property of this safety source.
*
* If set to {@code true}, a refresh request will be sent to the source when the Safety
* Center page is opened.
*
* Throws an {@link UnsupportedOperationException} if the source is of type static.
*/
public boolean isRefreshOnPageOpenAllowed() {
if (mType == SAFETY_SOURCE_TYPE_STATIC) {
throw new UnsupportedOperationException(
"isRefreshOnPageOpenAllowed unsupported for static safety source");
}
return mRefreshOnPageOpenAllowed;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SafetySource)) return false;
SafetySource that = (SafetySource) o;
return mType == that.mType
&& Objects.equals(mId, that.mId)
&& Objects.equals(mPackageName, that.mPackageName)
&& mTitleResId == that.mTitleResId
&& mTitleForWorkResId == that.mTitleForWorkResId
&& mSummaryResId == that.mSummaryResId
&& Objects.equals(mIntentAction, that.mIntentAction)
&& mProfile == that.mProfile
&& mInitialDisplayState == that.mInitialDisplayState
&& mMaxSeverityLevel == that.mMaxSeverityLevel
&& mSearchTermsResId == that.mSearchTermsResId
&& mLoggingAllowed == that.mLoggingAllowed
&& mRefreshOnPageOpenAllowed == that.mRefreshOnPageOpenAllowed;
}
@Override
public int hashCode() {
return Objects.hash(
mType,
mId,
mPackageName,
mTitleResId,
mTitleForWorkResId,
mSummaryResId,
mIntentAction,
mProfile,
mInitialDisplayState,
mMaxSeverityLevel,
mSearchTermsResId,
mLoggingAllowed,
mRefreshOnPageOpenAllowed);
}
@Override
public String toString() {
return "SafetySource{"
+ "mType="
+ mType
+ ", mId='"
+ mId
+ '\''
+ ", mPackageName='"
+ mPackageName
+ '\''
+ ", mTitleResId="
+ mTitleResId
+ ", mTitleForWorkResId="
+ mTitleForWorkResId
+ ", mSummaryResId="
+ mSummaryResId
+ ", mIntentAction='"
+ mIntentAction
+ '\''
+ ", mProfile="
+ mProfile
+ ", mInitialDisplayState="
+ mInitialDisplayState
+ ", mMaxSeverityLevel="
+ mMaxSeverityLevel
+ ", mSearchTermsResId="
+ mSearchTermsResId
+ ", mLoggingAllowed="
+ mLoggingAllowed
+ ", mRefreshOnPageOpenAllowed="
+ mRefreshOnPageOpenAllowed
+ '}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mType);
dest.writeString(mId);
dest.writeString(mPackageName);
dest.writeInt(mTitleResId);
dest.writeInt(mTitleForWorkResId);
dest.writeInt(mSummaryResId);
dest.writeString(mIntentAction);
dest.writeInt(mProfile);
dest.writeInt(mInitialDisplayState);
dest.writeInt(mMaxSeverityLevel);
dest.writeInt(mSearchTermsResId);
dest.writeBoolean(mLoggingAllowed);
dest.writeBoolean(mRefreshOnPageOpenAllowed);
}
/** Builder class for {@link SafetySource}. */
public static final class Builder {
@SafetySourceType private final int mType;
@Nullable private String mId;
@Nullable private String mPackageName;
@Nullable @StringRes private Integer mTitleResId;
@Nullable @StringRes private Integer mTitleForWorkResId;
@Nullable @StringRes private Integer mSummaryResId;
@Nullable private String mIntentAction;
@Nullable @Profile private Integer mProfile;
@Nullable @InitialDisplayState private Integer mInitialDisplayState;
@Nullable private Integer mMaxSeverityLevel;
@Nullable @StringRes private Integer mSearchTermsResId;
@Nullable private Boolean mLoggingAllowed;
@Nullable private Boolean mRefreshOnPageOpenAllowed;
/** Creates a {@link Builder} for a {@link SafetySource}. */
public Builder(@SafetySourceType int type) {
mType = type;
}
/**
* Sets the id of this safety source.
*
* The id must be unique among safety sources in a Safety Center configuration.
*/
@NonNull
public Builder setId(@Nullable String id) {
mId = id;
return this;
}
/**
* Sets the package name of this safety source.
*
* This is the package that owns the source. The package will receive refresh requests
* and it can send set requests for the source.
*
* The package name is required for sources of type dynamic and issue-only. The package
* name is prohibited for sources of type static.
*/
@NonNull
public Builder setPackageName(@Nullable String packageName) {
mPackageName = packageName;
return this;
}
/**
* Sets the resource id of the title of this safety source.
*
* The id must refer to a string resource that is either accessible from any resource
* context or that is accessible from the same resource context that was used to load the
* Safety Center config. The id defaults to {@link Resources#ID_NULL} when a title is not
* provided.
*
* The title is required for sources of type static and for sources of type dynamic that
* are not hidden and that do not provide search terms. The title is prohibited for sources
* of type issue-only.
*/
@NonNull
public Builder setTitleResId(@StringRes int titleResId) {
mTitleResId = titleResId;
return this;
}
/**
* Sets the resource id of the title for work of this safety source.
*
* The id must refer to a string resource that is either accessible from any resource
* context or that is accessible from the same resource context that was used to load the
* Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when a title
* for work is not provided.
*
* The title for work is required if the profile property of the source is set to {@link
* SafetySource#PROFILE_ALL} and either the source is of type static or the source is a
* source of type dynamic that is not hidden and that does not provide search terms. The
* title for work is prohibited for sources of type issue-only and if the profile property
* of the source is not set to {@link SafetySource#PROFILE_ALL}.
*/
@NonNull
public Builder setTitleForWorkResId(@StringRes int titleForWorkResId) {
mTitleForWorkResId = titleForWorkResId;
return this;
}
/**
* Sets the resource id of the summary of this safety source.
*
* The id must refer to a string resource that is either accessible from any resource
* context or that is accessible from the same resource context that was used to load the
* Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when a summary
* is not provided.
*
* The summary is required for sources of type dynamic that are not hidden. The summary
* is prohibited for sources of type issue-only.
*/
@NonNull
public Builder setSummaryResId(@StringRes int summaryResId) {
mSummaryResId = summaryResId;
return this;
}
/**
* Sets the intent action of this safety source.
*
* An intent created from the intent action should resolve to a public activity. If the
* source is displayed as an entry in the Safety Center page, and if the action is set to
* {@code null} or if it does not resolve to an activity the source will be marked as
* disabled.
*
* The intent action is required for sources of type static and for sources of type
* dynamic that are enabled. The intent action is prohibited for sources of type issue-only.
*/
@NonNull
public Builder setIntentAction(@Nullable String intentAction) {
mIntentAction = intentAction;
return this;
}
/**
* Sets the profile property of this safety source.
*
* The profile property is explicitly required for all source types.
*/
@NonNull
public Builder setProfile(@Profile int profile) {
mProfile = profile;
return this;
}
/**
* Sets the initial display state of this safety source.
*
* The initial display state is prohibited for sources of type static and issue-only.
*/
@NonNull
public Builder setInitialDisplayState(@InitialDisplayState int initialDisplayState) {
mInitialDisplayState = initialDisplayState;
return this;
}
/**
* Sets the maximum severity level of this safety source.
*
* The maximum severity level dictates the maximum severity level values that can be used
* in the source status or the source issues when setting the source data at runtime. A
* source can always send a status severity level of at least {@link
* android.safetycenter.SafetySourceData#SEVERITY_LEVEL_INFORMATION} even if the maximum
* severity level is set to a lower value.
*
* The maximum severity level is prohibited for sources of type static.
*/
@NonNull
public Builder setMaxSeverityLevel(int maxSeverityLevel) {
mMaxSeverityLevel = maxSeverityLevel;
return this;
}
/**
* Sets the resource id of the search terms of this safety source.
*
* The id must refer to a string resource that is either accessible from any resource
* context or that is accessible from the same resource context that was used to load the
* Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when search
* terms are not provided.
*
* The search terms are prohibited for sources of type issue-only.
*/
@NonNull
public Builder setSearchTermsResId(@StringRes int searchTermsResId) {
mSearchTermsResId = searchTermsResId;
return this;
}
/**
* Sets the logging allowed property of this safety source.
*
* The logging allowed property defaults to {@code true}.
*
* The logging allowed property is prohibited for sources of type static.
*/
@NonNull
public Builder setLoggingAllowed(boolean loggingAllowed) {
mLoggingAllowed = loggingAllowed;
return this;
}
/**
* Sets the refresh on page open allowed property of this safety source.
*
* If set to {@code true}, a refresh request will be sent to the source when the Safety
* Center page is opened. The refresh on page open allowed property defaults to {@code
* false}.
*
* The refresh on page open allowed property is prohibited for sources of type static.
*/
@NonNull
public Builder setRefreshOnPageOpenAllowed(boolean refreshOnPageOpenAllowed) {
mRefreshOnPageOpenAllowed = refreshOnPageOpenAllowed;
return this;
}
/**
* Creates the {@link SafetySource} defined by this {@link Builder}.
*
* Throws an {@link IllegalStateException} if any constraint on the safety source is
* violated.
*/
@NonNull
public SafetySource build() {
if (mType != SAFETY_SOURCE_TYPE_STATIC
&& mType != SAFETY_SOURCE_TYPE_DYNAMIC
&& mType != SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
throw new IllegalStateException("Unexpected type");
}
boolean isStatic = mType == SAFETY_SOURCE_TYPE_STATIC;
boolean isDynamic = mType == SAFETY_SOURCE_TYPE_DYNAMIC;
boolean isIssueOnly = mType == SAFETY_SOURCE_TYPE_ISSUE_ONLY;
BuilderUtils.validateAttribute(mId, "id", true, false);
BuilderUtils.validateAttribute(
mPackageName, "packageName", isDynamic || isIssueOnly, isStatic);
int initialDisplayState =
BuilderUtils.validateIntDef(
mInitialDisplayState,
"initialDisplayState",
false,
isStatic || isIssueOnly,
INITIAL_DISPLAY_STATE_ENABLED,
INITIAL_DISPLAY_STATE_ENABLED,
INITIAL_DISPLAY_STATE_DISABLED,
INITIAL_DISPLAY_STATE_HIDDEN);
boolean isEnabled = initialDisplayState == INITIAL_DISPLAY_STATE_ENABLED;
boolean isHidden = initialDisplayState == INITIAL_DISPLAY_STATE_HIDDEN;
boolean isDynamicNotHidden = isDynamic && !isHidden;
int profile =
BuilderUtils.validateIntDef(
mProfile,
"profile",
true,
false,
PROFILE_NONE,
PROFILE_PRIMARY,
PROFILE_ALL);
boolean hasWork = profile == PROFILE_ALL;
int searchTermsResId =
BuilderUtils.validateResId(
mSearchTermsResId, "searchTerms", false, isIssueOnly);
boolean isDynamicHiddenWithSearch =
isDynamic && isHidden && searchTermsResId != Resources.ID_NULL;
boolean titleRequired = isDynamicNotHidden || isDynamicHiddenWithSearch || isStatic;
int titleResId =
BuilderUtils.validateResId(mTitleResId, "title", titleRequired, isIssueOnly);
int titleForWorkResId =
BuilderUtils.validateResId(
mTitleForWorkResId,
"titleForWork",
hasWork && titleRequired,
!hasWork || isIssueOnly);
int summaryResId =
BuilderUtils.validateResId(
mSummaryResId, "summary", isDynamicNotHidden, isIssueOnly);
BuilderUtils.validateAttribute(
mIntentAction,
"intentAction",
(isDynamic && isEnabled) || isStatic,
isIssueOnly);
int maxSeverityLevel =
BuilderUtils.validateInteger(
mMaxSeverityLevel,
"maxSeverityLevel",
false,
isStatic,
Integer.MAX_VALUE);
boolean loggingAllowed =
BuilderUtils.validateBoolean(
mLoggingAllowed, "loggingAllowed", false, isStatic, true);
boolean refreshOnPageOpenAllowed =
BuilderUtils.validateBoolean(
mRefreshOnPageOpenAllowed,
"refreshOnPageOpenAllowed",
false,
isStatic,
false);
return new SafetySource(
mType,
mId,
mPackageName,
titleResId,
titleForWorkResId,
summaryResId,
mIntentAction,
profile,
initialDisplayState,
maxSeverityLevel,
searchTermsResId,
loggingAllowed,
refreshOnPageOpenAllowed);
}
}
}