Interact with Safety Center

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
  • 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
  • EXTRA_SAFETY_SOURCE_USER_HANDLE
    • String value: android.safetycenter.extra.SAFETY_SOURCE_USER_HANDLE
    • UserHandle type: Specifies UserHandle for the associated warning card
    • Optional (default is current user)

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

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 the SEND_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 than SafetySourceStatus if there is one (unless SafetySourceStatus is SEVERITY_LEVEL_UNSPECIFIED, in which case SEVERITY_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.

SafetySourceStatus

  • Required CharSequence title
  • Required CharSequence summary
  • Required severity level
  • Optional PendingIntent instance to redirect the user to the right page (default uses intentAction 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 entry
      • ICON_TYPE_INFO: Shown as an information icon next to the UI entry
    • Required PendingIntent to redirect the user to another page
  • Optional boolean enabled value that allows marking the UI entry as disabled, so it isn't clickable (default is true)
  • Invariants:
    • PendingIntent instances must open an Activity 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, each Action 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
  • 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 same SafetySourceIssue from different sources and only showing it once in the UI assuming they have the same deduplicationGroup (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 the SafetySourcesGroup
  • 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, the Notification 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
  • Invariants:
    • The list of Action instances must be composed of actions with unique identifiers
    • The list of Action instances must contain either one or two Action elements. If the actionability is not ISSUE_ACTIONABILITY_MANUAL, having zero Action is allowed.
    • The OnDismiss PendingIntent must not open an Activity instance
    • Additional requirements imposed by the API configuration

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 of SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED for Safety Center to be able track the refresh/rescan request.
    • SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED: We resolved SafetySourceIssue.Action directly from the Safety Center screen; use this instead of SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED for Safety Center to be able track the SafetySourceIssue.Action being resolved.
    • SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED: We attempted to resolve SafetySourceIssue.Action directly from the Safety Center screen, but failed to do so; use this instead of SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED for Safety Center to be able track SafetySourceIssue.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 use SAFETY_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 use SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED for this.
  • Optional String identifier for the refresh broadcast ID.
  • Optional String identifier for the SafetySourceIssue instance getting resolved.
  • Optional String identifier for the SafetySourceIssue.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 or SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED

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: Required SafetyEvent 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

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
  • 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 page
      • EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA: Requests the source to provide data as fresh as possible, typically when the user presses the rescan button
  • 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

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

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 is singleUser.

  • 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 and android.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:

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