Redirect to Safety Center
Any app can open Safety Center using the
android.content.Intent.ACTION_SAFETY_CENTER
action (string value
android.intent.action.SAFETY_CENTER
).
To open Safety Center, make a call from within an Activity
instance:
Intent openSafetyCenterIntent = new Intent(Intent.ACTION_SAFETY_CENTER);
startActivity(openSafetyCenterIntent);
Redirect to a specific issue
It's also possible to redirect to a specific Safety Center warning card using
specific intent extras. These extras aren't meant to be used by third parties so
they're part of SafetyCenterManager
, which is part of @SystemApi
. Only
system apps can access these extras.
Intent extras that redirect a specific warning card:
EXTRA_SAFETY_SOURCE_ID
- String value:
android.safetycenter.extra.SAFETY_SOURCE_ID
- String type: Specifies the ID of the safety source of the associated warning card
- Required for the redirection to the issue to work
- String value:
EXTRA_SAFETY_SOURCE_ISSUE_ID
- String value:
android.safetycenter.extra.SAFETY_SOURCE_ISSUE_ID
- String type: Specifies the warning card ID
- Required for the redirection to the issue to work
- String value:
EXTRA_SAFETY_SOURCE_USER_HANDLE
- String value:
android.safetycenter.extra.SAFETY_SOURCE_USER_HANDLE
UserHandle
type: SpecifiesUserHandle
for the associated warning card- Optional (default is current user)
- String value:
The code snippet below can be used from within an Activity
instance to open
the Safety Center screen to a specific issue:
UserHandle theUserHandleThisIssueCameFrom = …;
Intent openSafetyCenterIntent = new Intent(Intent.ACTION_SAFETY_CENTER)
.putExtra(SafetyCenterManager.EXTRA_SAFETY_SOURCE_ID, "TheSafetySourceIdThisIssueCameFrom")
.putExtra(SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID, "TheSafetySourceIssueIdToRedirectTo")
.putExtra(SafetyCenterManager.EXTRA_SAFETY_SOURCE_USER_HANDLE, theUserHandleThisIssueCameFrom);
startActivity(openSafetyCenterIntent);
Redirect to a specific subpage (Starting Android 14)
In Android 14 or above, the Safety Center page is split
into multiple subpages which represent the different SafetySourcesGroup
(in
Android 13, this is shown as collapsible entries).
It’s possible to redirect to a specific subpage by using this intent extra:
EXTRA_SAFETY_SOURCES_GROUP_ID
- String value:
android.safetycenter.extra.SAFETY_SOURCES_GROUP_ID
- String type: Specifies the ID of the
SafetySourcesGroup
- Required for the redirection to the subpage to work
- String value:
The code snippet below can be used from within an Activity
instance to open
the Safety Center screen to a specific subpage:
Intent openSafetyCenterIntent = new Intent(Intent.ACTION_SAFETY_CENTER)
.putExtra(SafetyCenterManager.EXTRA_SAFETY_SOURCES_GROUP_ID, "TheSafetySourcesGroupId");
startActivity(openSafetyCenterIntent);
Use the Safety Center source APIs
The Safety Center source APIs are available using
SafetyCenterManager
(which is a @SystemApi
). Code for the API surface is available in
Code
Search.
Implementation code of the APIs is available in Code
Search.
Permissions
The Safety Center source APIs are accessible only by allowlisted system apps using the permissions listed below. For additional information, see Privileged Permission Allowlisting.
READ_SAFETY_CENTER_STATUS
signature|privileged
- Used for the
SafetyCenterManager#isSafetyCenterEnabled()
API (not needed for Safety Center sources, they only need theSEND_SAFETY_CENTER_UPDATE
permission) - Used by system apps that check if the Safety Center is enabled
- Granted only to allowlisted system apps
SEND_SAFETY_CENTER_UPDATE
internal|privileged
- Used for the enabled API and the Safety Sources API
- Used by safety sources only
- Granted only to allowlisted system apps
These permissions are privileged and you can acquire them only by adding them to
the relevant file, for example, the
com.android.settings.xml
file for the Settings app, and to the app's AndroidManifest.xml
file. See
protectionLevel
for more information on the permission model.
Get the SafetyCenterManager
SafetyCenterManager
is a @SystemApi
class that's accessible from system apps
starting in Android 13. This call demonstrates how to
get SafetyCenterManager:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Must be on T or above to interact with Safety Center.
return;
}
SafetyCenterManager safetyCenterManager = context.getSystemService(SafetyCenterManager.class);
if (safetyCenterManager == null) {
// Should not be null on T.
return;
}
Check if Safety Center is enabled
This call checks whether Safety Center is enabled. The call requires either the
READ_SAFETY_CENTER_STATUS
or the SEND_SAFETY_CENTER_UPDATE
permission:
boolean isSafetyCenterEnabled = safetyCenterManager.isSafetyCenterEnabled();
if (isSafetyCenterEnabled) {
// …
} else {
// …
}
Provide data
Safety Center source data with the given String sourceId
is provided to Safety
Center with the SafetySourceData
object, which represents a UI entry and a
list of issues (warning cards). The UI entry and the warning cards can have
different severity levels specified in the SafetySourceData
class:
SEVERITY_LEVEL_UNSPECIFIED
- No severity specified
- Color: Gray or transparent (depending on the
SafetySourcesGroup
of the entry) - Used for dynamic data that poses as a static entry in the UI or to show an unspecified entry
- Must not be used for warning cards
SEVERITY_LEVEL_INFORMATION
- Basic information or minor suggestion
- Color: Green
SEVERITY_LEVEL_RECOMMENDATION
- Recommendation that the user should take action on this issue, as it could put them at risk
- Color: Yellow
SEVERITY_LEVEL_CRITICAL_WARNING
- Critical warning that the user must take action on this issue, as it presents a risk
- Color: Red
SafetySourceData
The SafetySourceData
object is composed of a UI entry, warning cards, and
invariants.
- Optional
SafetySourceStatus
instance (UI entry) - List of
SafetySourceIssue
instances (warning cards) - Optional
Bundle
extras (Starting 14) - Invariants:
- The
SafetySourceIssue
list must be composed of issues with unique identifiers. - The
SafetySourceIssue
instance must not be of greater importance thanSafetySourceStatus
if there is one (unlessSafetySourceStatus
isSEVERITY_LEVEL_UNSPECIFIED
, in which caseSEVERITY_LEVEL_INFORMATION
issues are allowed). - Additional requirements imposed by the API configuration must be met,
for example, if the source is issue-only, it must not provide a
SafetySourceStatus
instance.
- The
SafetySourceStatus
- Required
CharSequence
title - Required
CharSequence
summary - Required severity level
- Optional
PendingIntent
instance to redirect the user to the right page (default usesintentAction
from the config, if any) - Optional
IconAction
(shown as a side icon on the entry) composed of:- Required icon type, which must be one of the following types:
ICON_TYPE_GEAR
: Shown as a gear next to the UI entryICON_TYPE_INFO
: Shown as an information icon next to the UI entry
- Required
PendingIntent
to redirect the user to another page
- Required icon type, which must be one of the following types:
- Optional boolean
enabled
value that allows marking the UI entry as disabled, so it isn't clickable (default istrue
) - Invariants:
PendingIntent
instances must open anActivity
instance.- If the entry is disabled, it must be designated
SEVERITY_LEVEL_UNSPECIFIED
. - Additional requirements imposed by the API configuration.
SafetySourceIssue
- Required unique
String
identifier - Required
CharSequence
title - Optional
CharSequence
subtitle - Required
CharSequence
summary - Required severity level
- Optional issue category, which must be one of:
ISSUE_CATEGORY_DEVICE
: The issue affects the user's device.ISSUE_CATEGORY_ACCOUNT
: The issue affects the user's accounts.ISSUE_CATEGORY_GENERAL
: The issue affects the user's general safety. This is the default.ISSUE_CATEGORY_DATA
(Starting Android 14): The issue affects the user's data.ISSUE_CATEGORY_PASSWORDS
(Starting Android 14): The issue affects the user's passwords.ISSUE_CATEGORY_PERSONAL_SAFETY
(Starting Android 14): The issue affects the user's personal safety.
- List of
Action
elements that the user can take for this issue, eachAction
instance being composed of:- Required unique
String
identifier - Required
CharSequence
label - Required
PendingIntent
to redirect the user to another page or process the action directly from the Safety Center screen - Optional boolean to specify if this issue can be resolved directly from
the Safety Center screen (default is
false
) - Optional
CharSequence
success message, to be displayed to the user when the issue is successfully resolved directly from the Safety Center screen
- Required unique
- Optional
PendingIntent
that's called when the user dismisses the issue (default is nothing is called) - Required
String
issue type identifier; this is similar to the issue identifier but doesn't have to be unique and is used for logging - Optional
String
for the deduplication id, this allows posting the sameSafetySourceIssue
from different sources and only showing it once in the UI assuming they have the samededuplicationGroup
(Starting Android 14). If not specified, the issue is never deduplicated - Optional
CharSequence
for the attribution title, this is a text that shows where the warning card originated (Starting Android 14). If not specified uses the title of theSafetySourcesGroup
- Optional issue actionability (Starting Android 14),
which must be one of:
ISSUE_ACTIONABILITY_MANUAL
: The user needs to resolve this issue manually. This is the default.ISSUE_ACTIONABILITY_TIP
: This issue is just a tip and may not require any user input.ISSUE_ACTIONABILITY_AUTOMATIC
: This issue has already been actioned and may not require any user input.
- Optional notification behavior (Starting Android
14), which must be one of:
NOTIFICATION_BEHAVIOR_UNSPECIFIED
: Safety Center will decide whether a notification is needed for the warning card. This is the default.NOTIFICATION_BEHAVIOR_NEVER
: No notification is posted.NOTIFICATION_BEHAVIOR_DELAYED
: A notification is posted some time after the issue is first reported.NOTIFICATION_BEHAVIOR_IMMEDIATELY
: A notification is posted as soon as the issue is reported.
- Optional
Notification
, to show a custom notification with the warning card (Starting Android 14). If not specified, theNotification
is derived from the warning card. Composed of:- Required
CharSequence
title - Required
CharSequence
summary - List of
Action
elements that the user can take for this notification
- Required
- Invariants:
- The list of
Action
instances must be composed of actions with unique identifiers - The list of
Action
instances must contain either one or twoAction
elements. If the actionability is notISSUE_ACTIONABILITY_MANUAL
, having zeroAction
is allowed. - The OnDismiss
PendingIntent
must not open anActivity
instance - Additional requirements imposed by the API configuration
- The list of
Data is provided upon certain events to the Safety Center, so it's necessary to
specify what caused the source to provide SafetySourceData
with a
SafetyEvent
instance.
SafetyEvent
- Required type, which must be one of:
SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
: The state of the source has changed.SAFETY_EVENT_TYPE_REFRESH_REQUESTED
: Responding to a refresh/rescan signal from Safety Center; use this instead ofSAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
for Safety Center to be able track the refresh/rescan request.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED
: We resolvedSafetySourceIssue.Action
directly from the Safety Center screen; use this instead ofSAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
for Safety Center to be able track theSafetySourceIssue.Action
being resolved.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED
: We attempted to resolveSafetySourceIssue.Action
directly from the Safety Center screen, but failed to do so; use this instead ofSAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
for Safety Center to be able trackSafetySourceIssue.Action
having failed.SAFETY_EVENT_TYPE_DEVICE_LOCALE_CHANGED
: The language of the device has changed, so we're updating the text of the data provided; it's permitted to useSAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
for this.SAFETY_EVENT_TYPE_DEVICE_REBOOTED
: We're providing this data as part of an initial boot as the Safety Center data isn't persisted across reboots; it's permitted to useSAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
for this.
- Optional
String
identifier for the refresh broadcast ID. - Optional
String
identifier for theSafetySourceIssue
instance getting resolved. - Optional
String
identifier for theSafetySourceIssue.Action
instance getting resolved. - Invariants:
- The refresh broadcast ID must be provided if the type is
SAFETY_EVENT_TYPE_REFRESH_REQUESTED
- The issue and action IDs must be provided if the type is either
SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED
orSAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED
- The refresh broadcast ID must be provided if the type is
Below is an example of how a source might provide data to Safety Center (in this case it's providing an entry with a single warning card):
PendingIntent redirectToMyScreen =
PendingIntent.getActivity(
context, requestCode, redirectToMyScreenIntent, PendingIntent.FLAG_IMMUTABLE);
SafetySourceData safetySourceData =
new SafetySourceData.Builder()
.setStatus(
new SafetySourceStatus.Builder(
"title", "summary", SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION)
.setPendingIntent(redirectToMyScreen)
.build())
.addIssue(
new SafetySourceIssue.Builder(
"MyIssueId",
"title",
"summary",
SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION,
"MyIssueTypeId")
.setSubtitle("subtitle")
.setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE)
.addAction(
new SafetySourceIssue.Action.Builder(
"MyIssueActionId", "label", redirectToMyScreen)
.build())
.build())
.build();
SafetyEvent safetyEvent = new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build();
safetyCenterManager.setSafetySourceData("MySourceId", safetySourceData, safetyEvent);
Get last data provided
You can get the last data provided to Safety Center for a source owned by your
app. You can use this to surface something in your own UI, to check if the data
needs to be updated before performing an expensive operation, or to provide the
same SafetySourceData
instance to Safety Center with some changes or with a
new SafetyEvent
instance. It's also useful for testing.
Use this code to get the last data provided to Safety Center:
SafetySourceData lastDataProvided =
safetyCenterManager.getSafetySourceData("MySourceId");
Report an error
If you can't gather SafetySourceData
data, you can report the error to Safety
Center, which changes the entry to gray, clears the cached data, and provides a
message something like Couldn't check setting. You can also report an error if
an instance of SafetySourceIssue.Action
fails to resolve, in which case the
cached data isn't cleared and the UI entry isn't changed; but a message is
surfaced to the user to let them know that something went wrong.
You can provide the error using SafetySourceErrorDetails
, which is composed
of:
SafetySourceErrorDetails
: RequiredSafetyEvent
instance:
// An error has occurred in the background, need to clear the Safety Center data to avoid showing data that may not be valid anymore
SafetyEvent safetyEvent = new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build();
SafetySourceErrorDetails safetySourceErrorDetails = new SafetySourceErrorDetails(safetyEvent);
safetyCenterManager.reportSafetySourceError("MySourceId", safetySourceErrorDetails);
Respond to a refresh or rescan request
You can get a signal from the Safety Center to provide new data. Responding to a refresh or rescan request ensures that the user views the current status when opening Safety Center and when they tap the scan button.
This is done by receiving a broadcast with the following action:
ACTION_REFRESH_SAFETY_SOURCES
- String value:
android.safetycenter.action.REFRESH_SAFETY_SOURCES
- Triggered when Safety Center is sending a request to refresh the data of the safety source for a given app
- Protected intent that can be sent only by the system
- Sent to all safety sources in the configuration file as an explicit
intent and requires the
SEND_SAFETY_CENTER_UPDATE
permission
- String value:
The following extras are provided as part of this broadcast:
EXTRA_REFRESH_SAFETY_SOURCE_IDS
- String value:
android.safetycenter.extra.REFRESH_SAFETY_SOURCE_IDS
- String array type (
String[]
), represents the source IDs to refresh for the given app
- String value:
EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE
- String value:
android.safetycenter.extra.REFRESH_SAFETY_SOURCES_REQUEST_TYPE
- Integer type, represents a request type
@IntDef
- Must be one of:
EXTRA_REFRESH_REQUEST_TYPE_GET_DATA
: Requests the source to provide data relatively fast, typically when the user opens the pageEXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA
: Requests the source to provide data as fresh as possible, typically when the user presses the rescan button
- String value:
EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID
- String value:
android.safetycenter.extra.REFRESH_SAFETY_SOURCES_BROADCAST_ID
- String type, represents a unique identifier for the requested refresh
- String value:
To get a signal from the Safety Center, implement a
BroadcastReceiver
instance. The broadcast is sent with special BroadcastOptions
that allows the
receiver to start a foreground service.
BroadcastReceiver
responds to a refresh request:
public final class SafetySourceReceiver extends BroadcastReceiver {
// All the safety sources owned by this application.
private static final String[] ALL_SAFETY_SOURCES = new String[] {"MySourceId1", "…"};
@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Must be on T or above to interact with Safety Center.
return;
}
String action = intent.getAction();
if (!SafetyCenterManager.ACTION_REFRESH_SAFETY_SOURCES.equals(action)) {
return;
}
String refreshBroadcastId =
intent.getStringExtra(SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID);
if (refreshBroadcastId == null) {
// Should always be provided.
return;
}
String[] sourceIds =
intent.getStringArrayExtra(SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS);
if (sourceIds == null) {
sourceIds = ALL_SAFETY_SOURCES;
}
int requestType =
intent.getIntExtra(
SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE,
SafetyCenterManager.EXTRA_REFRESH_REQUEST_TYPE_GET_DATA);
SafetyCenterManager safetyCenterManager = context.getSystemService(SafetyCenterManager.class);
if (safetyCenterManager == null) {
// Should not be null on T.
return;
}
if (!safetyCenterManager.isSafetyCenterEnabled()) {
// Preferably, no Safety Source code should be run if Safety Center is disabled.
return;
}
SafetyEvent refreshSafetyEvent =
new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED)
.setRefreshBroadcastId(refreshBroadcastId)
.build();
for (String sourceId : sourceIds) {
SafetySourceData safetySourceData = getSafetySourceDataFor(sourceId, requestType);
// Set the data (or report an error with reportSafetySourceError, if something went wrong).
safetyCenterManager.setSafetySourceData(sourceId, safetySourceData, refreshSafetyEvent);
}
}
private SafetySourceData getSafetySourceDataFor(String sourceId, int requestType) {
switch (requestType) {
case SafetyCenterManager.EXTRA_REFRESH_REQUEST_TYPE_GET_DATA:
return getRefreshSafetySourceDataFor(sourceId);
case SafetyCenterManager.EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA:
return getRescanSafetySourceDataFor(sourceId);
default:
}
return getRefreshSafetySourceDataFor(sourceId);
}
// Data to provide when the user opens the page or on specific events.
private SafetySourceData getRefreshSafetySourceDataFor(String sourceId) {
// Get data for the source, if it's a fast operation it could potentially be executed in the
// receiver directly.
// Otherwise, it must start some kind of foreground service or expedited job.
return null;
}
// Data to provide when the user pressed the rescan button.
private SafetySourceData getRescanSafetySourceDataFor(String sourceId) {
// Could be implemented the same way as getRefreshSafetySourceDataFor, depending on the source's
// need.
// Otherwise, could potentially perform a longer task.
// In which case, it must start some kind of foreground service or expedited job.
return null;
}
}
The same instance of BroadcastReceiver
in the example above is declared in
AndroidManifest.xml
:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="…">
<application>
<!-- … -->
<receiver android:name=".SafetySourceReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.safetycenter.action.REFRESH_SAFETY_SOURCES"/>
</intent-filter>
</receiver>
<!-- … -->
</application>
</manifest>
Ideally, a Safety Center source is implemented in such a way that it calls
SafetyCenterManager
when its data changes. For system health reasons, we
recommend responding only to the rescan signal (when the user taps the scan
button), and not when the user opens the Safety Center. If this functionality is
required, the refreshOnPageOpenAllowed="true"
field in the configuration file
must be set for the source to receive the broadcast delivered in these cases.
Respond to the Safety Center when enabled or disabled
You can respond to when the Safety Center when enabled or disabled by using this intent action:
ACTION_SAFETY_CENTER_ENABLED_CHANGED
- String value:
android.safetycenter.action.SAFETY_CENTER_ENABLED_CHANGED
- Triggered when the Safety Center is either enabled or disabled while the device is running
- Not called on boot (use
ACTION_BOOT_COMPLETED
for that) - Protected intent that can be sent only by the system
- Sent to all safety sources in the configuration file as an explicit
intent, requires the
SEND_SAFETY_CENTER_UPDATE
permission - Sent as an implicit intent that requires the
READ_SAFETY_CENTER_STATUS
permission
- String value:
This intent action is useful to enable or disable features that are related to Safety Center on the device.
Implement resolving actions
A resolving action is a SafetySourceIssue.Action
instance that a user can
resolve directly from the Safety Center screen. The user taps an action button
and the PendingIntent
instance on SafetySourceIssue.Action
sent by the
safety source is triggered, which resolves the issue in the background and
notifies the Safety Center when it's done.
To implement resolving actions, the Safety Center source can use a service if
the operation is expected to take some time (PendingIntent.getService
) or a
broadcast receiver (PendingIntent.getBroadcast
).
Use this code to send a resolving issue to Safety Center:
Intent resolveIssueBroadcastIntent =
new Intent("my.package.name.MY_RESOLVING_ACTION").setClass(ResolveActionReceiver.class);
PendingIntent resolveIssue =
PendingIntent.getBroadcast(
context, requestCode, resolveIssueBroadcastIntent, PendingIntent.FLAG_IMMUTABLE);
SafetySourceData safetySourceData =
new SafetySourceData.Builder()
.setStatus(
new SafetySourceStatus.Builder(
"title", "summary", SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION)
.setPendingIntent(redirectToMyScreen)
.build())
.addIssue(
new SafetySourceIssue.Builder(
"MyIssueId",
"title",
"summary",
SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION,
"MyIssueTypeId")
.setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE)
.addAction(
new SafetySourceIssue.Action.Builder(
"MyIssueActionId", "label", resolveIssue)
.setWillResolve(true)
.build())
.build())
.build();
SafetyEvent safetyEvent = new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build();
safetyCenterManager.setSafetySourceData("MySourceId", safetySourceData, safetyEvent);
BroadcastReceiver
resolves the action:
public final class ResolveActionReceiver extends BroadcastReceiver {
private static final String MY_RESOLVING_ACTION = "my.package.name.MY_RESOLVING_ACTION";
@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Must be on T or above to interact with Safety Center.
return;
}
String action = intent.getAction();
if (!MY_RESOLVING_ACTION.equals(action)) {
return;
}
SafetyCenterManager safetyCenterManager = context.getSystemService(SafetyCenterManager.class);
if (safetyCenterManager == null) {
// Should not be null on T.
return;
}
if (!safetyCenterManager.isSafetyCenterEnabled()) {
// Preferably, no Safety Source code should be run if Safety Center is disabled.
return;
}
resolveTheIssue();
SafetyEvent resolveActionSafetyEvent =
new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED)
.setSafetySourceIssueId("MyIssueId")
.setSafetySourceIssueActionId("MyIssueActionId")
.build();
SafetySourceData dataWithoutTheIssue = …;
// Set the data (or report an error with reportSafetySourceError and
// SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED, if something went wrong).
safetyCenterManager.setSafetySourceData("MySourceId", dataWithoutTheIssue, resolveActionSafetyEvent);
}
private void resolveTheIssue() {
// Resolves the issue for the user. Given this a BroadcastReceiver, this should be a fast action.
// Otherwise, a foreground service and PendingIntent.getService should be used instead (or a job
// could be scheduled here, too).
}
}
The same instance of BroadcastReceiver
in the example above is declared in
AndroidManifest.xml
:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="…">
<application>
<!-- … -->
<receiver android:name=".ResolveActionReceiver"
android:exported="false">
<intent-filter>
<action android:name="my.package.name.MY_RESOLVING_ACTION"/>
</intent-filter>
</receiver>
<!-- … -->
</application>
</manifest>
Respond to issue dismissals
You can specify a PendingIntent
instance that can be triggered when a
SafetySourceIssue
instance is dismissed. The Safety Center handles these issue
dismissals:
- If a source pushes an issue, a user can dismiss it on the Safety Center screen by tapping the dismiss button (an X button on the warning card).
- When a user dismisses an issue, if the issue continues, it won't be surfaced in the UI again.
- Persistent dismissals on a disk remain during device reboots.
- If the Safety Center source stops providing an issue and then provides the issue again at a later time, the issue resurfaces. This is to allow situations where a user sees a warning, dismisses it, then takes action that should alleviate the problem but then the user does something again that causes a similar issue. At this point, the warning card should resurface.
- Yellow and red warning cards resurface every 180 days unless the user has dismissed them multiple times.
Additional behaviors shouldn't be needed by the source unless:
- The source tries to implement this behavior differently, for example, never resurface the issue.
- The source tries to use this as a callback, for example, to log the information.
Provide data for multiple users/profiles
The SafetyCenterManager
API can be used across users and profiles. For more
information, see Building Multiuser-Aware
Apps. The Context
object that provides SafetyCenterManager
is associated with a UserHandle
instance, so the returned SafetyCenterManager
instance interacts with the
Safety Center for that UserHandle
instance. By default, Context
is
associated with the running user, but it's possible to create an instance for
another user if the app holds the INTERACT_ACROSS_USERS
and
INTERACT_ACROSS_USERS_FULL
permissions. This example shows making a call
across users/profiles:
Context userContext = context.createContextAsUser(userHandle, 0);
SafetyCenterManager userSafetyCenterManager = userContext.getSystemService(SafetyCenterManager.class);
if (userSafetyCenterManager == null) {
// Should not be null on T.
return;
}
// Calls to userSafetyCenterManager will provide data for the given userHandle
Each user on the device can have multiple managed profiles. The Safety Center provides different data for each user, but merges the data of all the managed profiles associated with a given user.
When profile="all_profiles"
is set for the source in the configuration file,
the following occurs:
- There's a UI entry for the user (profile parent) and all of its associated
managed profiles (which use
titleForWork
instances). The refresh or rescan signal is sent for the profile parent and all the associated managed profiles. The associated receiver is started for each profile and can provide the associated data directly to
SafetyCenterManager
without having to make a cross-profile call unless the receiver or the app issingleUser
.The source is expected to provide data for the user and all its managed profiles. The data for each UI entry might be different depending on the profile.
Testing
you can access ShadowSafetyCenterManager
and use it in a Robolectric test.
private static final String MY_SOURCE_ID = "MySourceId";
private final MyClass myClass = …;
private final SafetyCenterManager safetyCenterManager = getApplicationContext().getSystemService(SafetyCenterManager.class);
@Test
public void whenRefreshingData_providesDataToSafetyCenterForMySourceId() {
shadowOf(safetyCenterManager).setSafetyCenterEnabled(true);
setupDataForMyClass(…);
myClass.refreshData();
SafetySourceData expectedSafetySourceData = …;
assertThat(safetyCenterManager.getSafetySourceData(MY_SOURCE_ID)).isEqualTo(expectedSafetySourceData);
SafetyEvent expectedSafetyEvent = …;
assertThat(shadowOf(safetyCenterManager).getLastSafetyEvent(MY_SOURCE_ID)).isEqualTo(expectedSafetyEvent);
}
You can write more end-to-end (E2E) tests, but that's out of the scope of this guide. For more information about writing these E2E tests, see CTS tests (CtsSafetyCenterTestCases)
Test and internal APIs
The internal APIs and test APIs are for internal use so they aren't described in detail in this guide. However, we might extend some internal APIs in the future to allow OEMs to build their own UI from and we'll update this guide to provide guidance on how to use them.
Permissions
MANAGE_SAFETY_CENTER
internal|installer|role
- Used for the internal Safety Center APIs
- Only granted to PermissionController and shell
Settings app
Safety Center redirection
By default, Safety Center is accessed through the Settings app with a new Security & privacy entry. If you use a different Settings app or if you've modified the Settings app, you might need to customize how Safety Center is accessed.
When Safety Center is enabled:
- Legacy Privacy entry is hidden code
- Legacy Security entry is hidden code
- New Security & privacy entry is added code
- New Security & privacy entry redirects to Safety Center code
android.settings.PRIVACY_SETTINGS
andandroid.settings.SECURITY_SETTINGS
intent actions are redirected to open Safety Center (code: security, privacy)
Advanced security and privacy pages
The Settings app contains additional settings under More security settings and More privacy settings titles, available from Safety Center:
Advanced security code
Advanced privacy code
Starting Android 14, the advanced security and advanced privacy settings page are merged under a single "More Security & Privacy" page with intent action
"com.android.settings.MORE_SECURITY_PRIVACY_SETTINGS"
Safety sources
Safety Center integrates with a specific set of safety sources provided by the Settings app:
- A lock screen safety source verifies that a lock screen is set up with a passcode (or other security), to ensure that the user's private information is kept safe from external access.
- A biometrics safety source (hidden by default) surfaces to integrate with a fingerprint or face sensor.
The source code for these Safety Center sources is accessible through Android code search. If the Settings app isn't modified (changes aren't made to the package name, source code or the source code that deals with a lock screen and biometrics), then this integration should work out of box. Otherwise, some modifications might be required such as changing the configuration file to change the package name of the Settings app and the sources that integrate with Safety Center, as well as the integration. For more information, see Update the configuration file and the integration settings.
About PendingIntent
If you rely on the existing Settings app Safety Center integration in Android 14 or above, the bug described below has been fixed. Reading this section is not necessary in this case.
When you're sure that the bug doesn't exist, set an XML boolean resource
configuration value in the Settings app
config_isSafetyCenterLockScreenPendingIntentFixed
to true
to turn off the
workaround within Safety Center.
PendingIntent workaround
This bug is caused by Settings using Intent
instance extras to determine which
fragment to open. Because Intent#equals
doesn't take the Intent
instance
extras into account, the PendingIntent
instance for the gear menu icon and the
entry are considered equal and navigate to the same UI (even though they're
intended to navigate to a different UI). This issue is fixed in a QPR release by
differentiating the PendingIntent
instances by request code. Alternatively,
this could be differentiated by using Intent#setId
.
Internal safety sources
Some Safety Center sources are internal and are implemented in the PermissionController system app inside the PermissionController module. These sources behave like regular Safety Center sources and receive no special treatment. Code for these sources is available through Android code search.
These are mainly privacy signals, for example:
- Accessibility
- Auto revoke unused apps
- Location access
- Notification listener
- Work policy info