/* * 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.Manifest.permission.MANAGE_SAFETY_CENTER; import static android.Manifest.permission.READ_SAFETY_CENTER_STATUS; import static android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE; import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION; import static android.os.Build.VERSION_CODES.TIRAMISU; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static java.util.Objects.requireNonNull; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.RemoteException; import android.safetycenter.config.SafetyCenterConfig; import android.util.ArrayMap; import androidx.annotation.RequiresApi; import com.android.internal.annotations.GuardedBy; import com.android.modules.utils.build.SdkLevel; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; /** * Interface for communicating with the Safety Center, which consolidates UI for security and * privacy features on the device. * *
These APIs are intended to be used by the following clients: * *
This broadcast will inform receivers about changes to {@link * SafetyCenterManager#isSafetyCenterEnabled()}, should they want to check the new value and * enable/disable components accordingly. * *
This broadcast is sent explicitly to safety sources by targeting intents to a specified * set of packages in the {@link SafetyCenterConfig}. The receiving components must hold the * {@link android.Manifest.permission#SEND_SAFETY_CENTER_UPDATE} permission, and can use a * manifest-registered receiver to be woken up by Safety Center. * *
This broadcast is also sent implicitly system-wide. The receiving components must hold the * {@link android.Manifest.permission#READ_SAFETY_CENTER_STATUS} permission. * *
This broadcast is not sent out if the device does not support Safety Center. * *
This is a protected intent that can only be sent by the system. */ @SdkConstant(BROADCAST_INTENT_ACTION) public static final String ACTION_SAFETY_CENTER_ENABLED_CHANGED = "android.safetycenter.action.SAFETY_CENTER_ENABLED_CHANGED"; /** * Broadcast Action: A broadcast sent by the system to indicate that {@link SafetyCenterManager} * is requesting data from safety sources regarding their safety state. * *
This broadcast is sent when a user triggers a data refresh from the Safety Center UI or * when Safety Center detects that its stored safety information is stale and needs to be * updated. * *
This broadcast is sent explicitly to safety sources by targeting intents to a specified * set of packages provided by the safety sources in the {@link SafetyCenterConfig}. The * receiving components must hold the {@link * android.Manifest.permission#SEND_SAFETY_CENTER_UPDATE} permission, and can use a * manifest-registered receiver to be woken up by Safety Center. * *
On receiving this broadcast, safety sources should determine their safety state according * to the parameters specified in the intent extras (see below) and set {@link SafetySourceData} * using {@link #setSafetySourceData}, along with a {@link SafetyEvent} with {@link * SafetyEvent#getType()} set to {@link SafetyEvent#SAFETY_EVENT_TYPE_REFRESH_REQUESTED} and * {@link SafetyEvent#getRefreshBroadcastId()} set to the value of broadcast intent extra {@link * #EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID}. If the safety source is unable to provide data, * it can set a {@code null} {@link SafetySourceData}, which will clear any existing {@link * SafetySourceData} stored by Safety Center, and Safety Center will fall back to any * placeholder data specified in {@link SafetyCenterConfig}. * *
This is a protected intent that can only be sent by the system. * *
Includes the following extras: * *
When this extra field is not specified in the intent, it is assumed that Safety Center is * requesting data from all safety sources supported by the component receiving the broadcast. */ public static final String EXTRA_REFRESH_SAFETY_SOURCE_IDS = "android.safetycenter.extra.REFRESH_SAFETY_SOURCE_IDS"; /** * Used as an {@code int} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to * specify the type of data request from Safety Center. * *
Possible values are {@link #EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA} and {@link * #EXTRA_REFRESH_REQUEST_TYPE_GET_DATA} */ public static final String EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE = "android.safetycenter.extra.REFRESH_SAFETY_SOURCES_REQUEST_TYPE"; /** * Used as a {@code String} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to * specify a string identifier for the broadcast. */ public static final String EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID = "android.safetycenter.extra.REFRESH_SAFETY_SOURCES_BROADCAST_ID"; /** * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to * specify an issue ID to redirect to, if applicable. * *
This extra must be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ID} as an issue ID * does not uniquely identify a {@link SafetySourceIssue}. Otherwise, no redirection will occur. */ public static final String EXTRA_SAFETY_SOURCE_ISSUE_ID = "android.safetycenter.extra.SAFETY_SOURCE_ISSUE_ID"; /** * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to * specify a source ID for the {@link SafetySourceIssue} to redirect to, if applicable. * *
This extra must be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ISSUE_ID}. * Otherwise, no redirection will occur. */ public static final String EXTRA_SAFETY_SOURCE_ID = "android.safetycenter.extra.SAFETY_SOURCE_ID"; /** * Used as a {@link android.os.UserHandle} extra field in {@link Intent#ACTION_SAFETY_CENTER} * intents to specify a user for a given {@link SafetySourceIssue} to redirect to, if * applicable. * *
This extra can be used if the same issue ID is created for multiple users (e.g. to * disambiguate personal profile vs. managed profiles issues). * *
This extra can be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ISSUE_ID} and * {@link #EXTRA_SAFETY_SOURCE_ID}. Otherwise, the device's primary user will be used. */ public static final String EXTRA_SAFETY_SOURCE_USER_HANDLE = "android.safetycenter.extra.SAFETY_SOURCE_USER_HANDLE"; /** * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to * specify the ID for a group of safety sources. If applicable, this will redirect to the * group's corresponding subpage in the UI. */ @RequiresApi(UPSIDE_DOWN_CAKE) public static final String EXTRA_SAFETY_SOURCES_GROUP_ID = "android.safetycenter.extra.SAFETY_SOURCES_GROUP_ID"; /** * Used as an int value for {@link #EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE} to indicate that * the safety source should fetch fresh data relating to their safety state upon receiving a * broadcast with intent action {@link #ACTION_REFRESH_SAFETY_SOURCES} and provide it to Safety * Center. * *
The term "fresh" here means that the sources should ensure that the safety data is * accurate as possible at the time of providing it to Safety Center, even if it involves * performing an expensive and/or slow process. */ public static final int EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA = 0; /** * Used as an int value for {@link #EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE} to indicate that * upon receiving a broadcast with intent action {@link #ACTION_REFRESH_SAFETY_SOURCES}, the * safety source should provide data relating to their safety state to Safety Center. * *
If the source already has its safety data cached, it may provide it without triggering a
* process to fetch state which may be expensive and/or slow.
*/
public static final int EXTRA_REFRESH_REQUEST_TYPE_GET_DATA = 1;
/**
* All possible types of data refresh requests in broadcasts with intent action {@link
* #ACTION_REFRESH_SAFETY_SOURCES}.
*
* @hide
*/
@IntDef(
prefix = {"EXTRA_REFRESH_REQUEST_TYPE_"},
value = {
EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA,
EXTRA_REFRESH_REQUEST_TYPE_GET_DATA,
})
@Retention(RetentionPolicy.SOURCE)
public @interface RefreshRequestType {}
/** Indicates that the Safety Center UI has been opened by the user. */
public static final int REFRESH_REASON_PAGE_OPEN = 100;
/** Indicates that the rescan button in the Safety Center UI has been clicked on by the user. */
public static final int REFRESH_REASON_RESCAN_BUTTON_CLICK = 200;
/** Indicates that the device was rebooted. */
public static final int REFRESH_REASON_DEVICE_REBOOT = 300;
/** Indicates that the device locale was changed. */
public static final int REFRESH_REASON_DEVICE_LOCALE_CHANGE = 400;
/** Indicates that the Safety Center feature was enabled. */
public static final int REFRESH_REASON_SAFETY_CENTER_ENABLED = 500;
/** Indicates a generic reason for Safety Center refresh. */
public static final int REFRESH_REASON_OTHER = 600;
/** Indicates a periodic background refresh. */
@RequiresApi(UPSIDE_DOWN_CAKE)
public static final int REFRESH_REASON_PERIODIC = 700;
/**
* The reason for requesting a refresh of {@link SafetySourceData} from safety sources.
*
* @hide
*/
@IntDef(
prefix = {"REFRESH_REASON_"},
value = {
REFRESH_REASON_PAGE_OPEN,
REFRESH_REASON_RESCAN_BUTTON_CLICK,
REFRESH_REASON_DEVICE_REBOOT,
REFRESH_REASON_DEVICE_LOCALE_CHANGE,
REFRESH_REASON_SAFETY_CENTER_ENABLED,
REFRESH_REASON_OTHER,
REFRESH_REASON_PERIODIC
})
@Retention(RetentionPolicy.SOURCE)
@TargetApi(UPSIDE_DOWN_CAKE)
public @interface RefreshReason {}
/** Listener for changes to {@link SafetyCenterData}. */
public interface OnSafetyCenterDataChangedListener {
/**
* Called when {@link SafetyCenterData} tracked by the manager changes.
*
* @param data the updated data
*/
void onSafetyCenterDataChanged(@NonNull SafetyCenterData data);
/**
* Called when the Safety Center should display an error related to changes in its data.
*
* @param errorDetails details of an error that should be displayed to the user
*/
default void onError(@NonNull SafetyCenterErrorDetails errorDetails) {}
}
private final Object mListenersLock = new Object();
@GuardedBy("mListenersLock")
private final Map If this returns {@code false}, all the other methods in this class will no-op and/or
* return default values.
*/
@RequiresPermission(anyOf = {READ_SAFETY_CENTER_STATUS, SEND_SAFETY_CENTER_UPDATE})
public boolean isSafetyCenterEnabled() {
try {
return mService.isSafetyCenterEnabled();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Set the latest {@link SafetySourceData} for a safety source, to be displayed in Safety Center
* UI.
*
* Each {@code safetySourceId} uniquely identifies the {@link SafetySourceData} for the
* calling user.
*
* This call will rewrite any existing {@link SafetySourceData} already set for the given
* {@code safetySourceId} for the calling user.
*
* @param safetySourceId the unique identifier for a safety source in the calling user
* @param safetySourceData the latest safety data for the safety source in the calling user. If
* a safety source does not have any data to set, it can set its {@link SafetySourceData} to
* {@code null}, in which case Safety Center will fall back to any placeholder data
* specified in the safety source xml configuration.
* @param safetyEvent the event that triggered the safety source to set safety data
*/
@RequiresPermission(SEND_SAFETY_CENTER_UPDATE)
public void setSafetySourceData(
@NonNull String safetySourceId,
@Nullable SafetySourceData safetySourceData,
@NonNull SafetyEvent safetyEvent) {
requireNonNull(safetySourceId, "safetySourceId cannot be null");
requireNonNull(safetyEvent, "safetyEvent cannot be null");
try {
mService.setSafetySourceData(
safetySourceId,
safetySourceData,
safetyEvent,
mContext.getPackageName(),
mContext.getUser().getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the latest {@link SafetySourceData} set through {@link #setSafetySourceData} for the
* given {@code safetySourceId} and calling user.
*
* Returns {@code null} if there never was any data sent for the given {@code safetySourceId}
* and user.
*/
@RequiresPermission(SEND_SAFETY_CENTER_UPDATE)
@Nullable
public SafetySourceData getSafetySourceData(@NonNull String safetySourceId) {
requireNonNull(safetySourceId, "safetySourceId cannot be null");
try {
return mService.getSafetySourceData(
safetySourceId, mContext.getPackageName(), mContext.getUser().getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Notifies the Safety Center of an error related to a given safety source.
*
* Safety sources should use this API to notify Safety Center when Safety Center requested or
* expected them to perform an action or provide data, but they were unable to do so.
*
* @param safetySourceId the id of the safety source that provided the issue
* @param safetySourceErrorDetails details of the error that occurred
*/
@RequiresPermission(SEND_SAFETY_CENTER_UPDATE)
public void reportSafetySourceError(
@NonNull String safetySourceId,
@NonNull SafetySourceErrorDetails safetySourceErrorDetails) {
requireNonNull(safetySourceId, "safetySourceId cannot be null");
requireNonNull(safetySourceErrorDetails, "safetySourceErrorDetails cannot be null");
try {
mService.reportSafetySourceError(
safetySourceId,
safetySourceErrorDetails,
mContext.getPackageName(),
mContext.getUser().getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Requests safety sources to set their latest {@link SafetySourceData} for Safety Center.
*
* This API sends a broadcast to all safety sources with action {@link
* #ACTION_REFRESH_SAFETY_SOURCES}. See {@link #ACTION_REFRESH_SAFETY_SOURCES} for details on
* how safety sources should respond to receiving these broadcasts.
*
* @param refreshReason the reason for the refresh
*/
@RequiresPermission(MANAGE_SAFETY_CENTER)
public void refreshSafetySources(@RefreshReason int refreshReason) {
try {
mService.refreshSafetySources(refreshReason, mContext.getUser().getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Requests a specific subset of safety sources to set their latest {@link SafetySourceData} for
* Safety Center.
*
* This API sends a broadcast to safety sources with action {@link
* #ACTION_REFRESH_SAFETY_SOURCES} and {@link #EXTRA_REFRESH_SAFETY_SOURCE_IDS} to specify the
* IDs of safety sources being requested for data by Safety Center.
*
* This API is an overload of {@link #refreshSafetySources(int)} and is used to request data
* from safety sources that are part of a subpage in the Safety Center UI.
*
* @see #refreshSafetySources(int)
* @param refreshReason the reason for the refresh
* @param safetySourceIds list of IDs for the safety sources being refreshed
* @throws UnsupportedOperationException if accessed from a version lower than {@link
* UPSIDE_DOWN_CAKE}
*/
@RequiresPermission(MANAGE_SAFETY_CENTER)
@RequiresApi(UPSIDE_DOWN_CAKE)
public void refreshSafetySources(
@RefreshReason int refreshReason, @NonNull List Note: This API serves to facilitate CTS testing and should not be used for other purposes.
*/
@RequiresPermission(MANAGE_SAFETY_CENTER)
public void clearAllSafetySourceDataForTests() {
try {
mService.clearAllSafetySourceDataForTests();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Overrides the {@link SafetyCenterConfig} for testing.
*
* When set, the overridden {@link SafetyCenterConfig} will be used instead of the {@link
* SafetyCenterConfig} parsed from the XML file to read configured safety sources.
*
* Note: This API serves to facilitate CTS testing and should not be used to configure safety
* sources dynamically for production. Once used for testing, the override should be cleared.
*
* @see #clearSafetyCenterConfigForTests()
*/
@RequiresPermission(MANAGE_SAFETY_CENTER)
public void setSafetyCenterConfigForTests(@NonNull SafetyCenterConfig safetyCenterConfig) {
requireNonNull(safetyCenterConfig, "safetyCenterConfig cannot be null");
try {
mService.setSafetyCenterConfigForTests(safetyCenterConfig);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Clears the override of the {@link SafetyCenterConfig} set for testing.
*
* Once cleared, the {@link SafetyCenterConfig} parsed from the XML file will be used to read
* configured safety sources.
*
* Note: This API serves to facilitate CTS testing and should not be used for other purposes.
*
* @see #setSafetyCenterConfigForTests(SafetyCenterConfig)
*/
@RequiresPermission(MANAGE_SAFETY_CENTER)
public void clearSafetyCenterConfigForTests() {
try {
mService.clearSafetyCenterConfigForTests();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private static final class ListenerDelegate extends IOnSafetyCenterDataChangedListener.Stub {
@NonNull private final Executor mExecutor;
@NonNull private final OnSafetyCenterDataChangedListener mOriginalListener;
private volatile boolean mRemoved = false;
private ListenerDelegate(
@NonNull Executor executor,
@NonNull OnSafetyCenterDataChangedListener originalListener) {
mExecutor = executor;
mOriginalListener = originalListener;
}
@Override
public void onSafetyCenterDataChanged(@NonNull SafetyCenterData safetyCenterData) {
requireNonNull(safetyCenterData, "safetyCenterData cannot be null");
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(
() -> {
if (mRemoved) {
return;
}
// The remove call could still complete at this point, but we're ok
// with this raciness on separate threads. If the listener is removed on
// the same thread as `mExecutor`; the above check should ensure that
// the listener won't be called after the remove call.
mOriginalListener.onSafetyCenterDataChanged(safetyCenterData);
});
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onError(@NonNull SafetyCenterErrorDetails safetyCenterErrorDetails) {
requireNonNull(safetyCenterErrorDetails, "safetyCenterErrorDetails cannot be null");
final long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(
() -> {
if (mRemoved) {
return;
}
// The remove call could still complete at this point, but we're ok
// with this raciness on separate threads. If the listener is removed on
// the same thread as `mExecutor`; the above check should ensure that
// the listener won't be called after the remove call.
mOriginalListener.onError(safetyCenterErrorDetails);
});
} finally {
Binder.restoreCallingIdentity(identity);
}
}
public void markAsRemoved() {
mRemoved = true;
}
}
}