与安全中心互动

重定向至安全中心

任何应用都可以使用 android.content.Intent.ACTION_SAFETY_CENTER 操作(字符串值 android.intent.action.SAFETY_CENTER)打开安全中心。

如需打开安全中心,请在 Activity 实例内进行调用:

Intent openSafetyCenterIntent = new Intent(Intent.ACTION_SAFETY_CENTER);

startActivity(openSafetyCenterIntent);

重定向至特定问题

您还可以使用特定的 intent extra 重定向到特定的安全中心警告卡片。这些 extra 不供第三方使用,因此它们属于 SafetyCenterManager@SystemApi 的一部分)。只有系统应用可以访问这些 extra。

重定向特定警告卡片的 intent extra:

  • EXTRA_SAFETY_SOURCE_ID
    • 字符串值:android.safetycenter.extra.SAFETY_SOURCE_ID
    • 字符串类型:指定相关警告卡片的安全性信息来源 ID
    • 对于重定向到相应问题是必需
  • EXTRA_SAFETY_SOURCE_ISSUE_ID
    • 字符串值:android.safetycenter.extra.SAFETY_SOURCE_ISSUE_ID
    • 字符串类型:指定警告卡片 ID
    • 对于重定向到相应问题是必需
  • EXTRA_SAFETY_SOURCE_USER_HANDLE
    • 字符串值:android.safetycenter.extra.SAFETY_SOURCE_USER_HANDLE
    • UserHandle 类型:为关联的警告卡片指定 UserHandle
    • 可选(默认为当前用户)

您可以在 Activity 实例中使用以下代码段,以使用安全中心打开某个特定问题:

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);

重定向到特定的子页面(从 Android 14 开始)

在 Android 14 或更高版本中,安全中心页面被拆分为多个表示不同 SafetySourcesGroup(在 Android 13 中,它们显示为可收起的条目)的子页面。

您可以使用此 intent extra 重定向到特定子页面:

  • EXTRA_SAFETY_SOURCES_GROUP_ID
    • 字符串值:android.safetycenter.extra.SAFETY_SOURCES_GROUP_ID
    • 字符串类型:指定 SafetySourcesGroup 的 ID
    • 对于重定向到要相应子页面是必需

您可以在 Activity 实例中使用以下代码段,以使用安全中心打开某个特定子页面:

Intent openSafetyCenterIntent = new Intent(Intent.ACTION_SAFETY_CENTER)
.putExtra(SafetyCenterManager.EXTRA_SAFETY_SOURCES_GROUP_ID, "TheSafetySourcesGroupId");

startActivity(openSafetyCenterIntent);

使用安全中心信息来源 API

安全中心信息来源 API 是使用 SafetyCenterManager(即 @SystemApi)提供的。API Surface 的代码可在代码搜索中找到。可在代码搜索中找到这些 API 的实现代码。

权限

只有列入许可名单的系统应用可以使用下面列出的权限,才能访问安全中心信息来源 API。如需了解详情,请参阅特许权限许可名单

  • READ_SAFETY_CENTER_STATUS
    • signature|privileged
    • 用于 SafetyCenterManager#isSafetyCenterEnabled() API(安全中心信息来源不需要,而只需 SEND_SAFETY_CENTER_UPDATE 权限)
    • 供检查安全中心是否已启用的系统应用使用
    • 仅授予列入许可名单的系统应用
  • SEND_SAFETY_CENTER_UPDATE
    • internal|privileged
    • 用于已启用的 API 和 Safety Source API
    • 仅供安全性信息来源使用
    • 仅授予列入许可名单的系统应用

这些权限是特权应用,您只能通过将这些权限添加到相关文件(例如“设置”应用的 com.android.settings.xml 文件)来获取和应用的 AndroidManifest.xml 文件。如需详细了解权限模型,请参阅 protectionLevel

获取 SafetyCenterManager

SafetyCenterManager 是一个 @SystemApi 类,从 Android 13 开始,系统应用可以访问该类。此调用演示了如何获取 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;
}

检查安全中心是否已启用

此调用会检查安全中心是否已启用。此调用需要 READ_SAFETY_CENTER_STATUSSEND_SAFETY_CENTER_UPDATE 权限:

boolean isSafetyCenterEnabled = safetyCenterManager.isSafetyCenterEnabled();
if (isSafetyCenterEnabled) {
  // …
} else {
  // …
}

提供数据

具有给定 String sourceId 的安全中心信息来源数据会通过 SafetySourceData 对象提供给安全中心,该对象代表界面条目和问题列表(警告卡片)。界面条目和警告卡片可以在 SafetySourceData 类中指定不同的严重级别:

  • SEVERITY_LEVEL_UNSPECIFIED
    • 未指定严重程度。
    • 颜色:灰色或透明(具体取决于条目的 SafetySourcesGroup
    • 用于呈现在界面中呈现为静态条目的动态数据,或显示未指定的条目
    • 不得用于警告卡片
  • SEVERITY_LEVEL_INFORMATION
    • 基本信息或次要建议
    • 颜色:绿色
  • SEVERITY_LEVEL_RECOMMENDATION
    • 建议用户应针对此问题采取措施,因为这可能会使用户面临风险
    • 颜色:黄色
  • SEVERITY_LEVEL_CRITICAL_WARNING
    • 严重警告,用户必须针对此问题采取措施,因为它存在风险
    • 颜色:红色

SafetySourceData

SafetySourceData 对象由界面条目、警告卡片和不变量组成。

  • 可选的 SafetySourceStatus 实例(界面条目)
  • SafetySourceIssue 实例列表(警告卡片)
  • 可选的 Bundle extra(从 Android 14 开始)
  • 不变量:
    • SafetySourceIssue 列表必须由唯一标识符的问题组成。
    • 如果存在 SafetySourceIssue 实例,则其优先级不得高于 SafetySourceStatus(除非 SafetySourceStatusSEVERITY_LEVEL_UNSPECIFIED,在这种情况下允许 SEVERITY_LEVEL_INFORMATION 问题)。
    • 必须满足 API 配置施加的额外要求,例如,如果来源仅存在问题,则不得提供 SafetySourceStatus 实例。

SafetySourceStatus

  • 必需的 CharSequence 标题
  • 必需的 CharSequence 摘要
  • 必需的严重级别
  • 可选的 PendingIntent 实例,用于将用户重定向到正确页面(如果有的话,默认使用配置中的 intentAction
  • 可选 IconAction(在条目上显示为侧边图标),其中包含:
    • 必需的图标类型,它必须是以下类型之一:
      • ICON_TYPE_GEAR:显示为界面条目旁边的齿轮图标
      • ICON_TYPE_INFO:显示为信息条目旁边的界面条目
    • 必须提供 PendingIntent 才能将用户重定向到其他页面
  • 可选的布尔值 enabled,用于将界面条目标记为已停用,因此不可点击(默认为 true
  • 不变量:
    • PendingIntent 实例必须打开一个 Activity 实例。
    • 如果该条目已停用,则必须指定 SEVERITY_LEVEL_UNSPECIFIED
    • API 配置施加的额外要求。

SafetySourceIssue

  • 必需的唯一 String 标识符
  • 必需的 CharSequence 标题
  • 可选的 CharSequence 副标题
  • 必需的 CharSequence 摘要
  • 必需的严重级别
  • 可选的问题类别,它必须为以下类别之一:
    • ISSUE_CATEGORY_DEVICE:该问题会影响用户的设备。
    • ISSUE_CATEGORY_ACCOUNT:该问题会影响用户的账号。
    • ISSUE_CATEGORY_GENERAL:此问题会影响用户的基本安全。 这是默认值。
    • ISSUE_CATEGORY_DATA(从 Android 14 开始):该问题会影响用户的数据。
    • ISSUE_CATEGORY_PASSWORDS(从 Android 14 开始):该问题会影响用户的密码。
    • ISSUE_CATEGORY_PERSONAL_SAFETY(从 Android 14 开始):该问题会影响用户的个人安全。
  • 用户可以为该问题采取的 Action 元素列表,其中每个 Action 实例都由以下部分组成:
    • 必需的唯一 String 标识符
    • 必需的 CharSequence 标签
    • 必需的 PendingIntent,以便将用户重定向到其他网页或直接从安全中心屏幕处理操作
    • 可选的布尔值,用于指定是否可以直接从安全中心屏幕解决此问题(默认值为 false
    • 可选的 CharSequence 成功消息,在成功通过安全中心屏幕成功解决问题后向用户显示的消息
  • 可选的 PendingIntent(在用户关闭问题时调用,默认为不调用)
  • 必需的 String 问题类型标识符;这与问题标识符类似,但不必具有唯一性,用于记录
  • 针对重复信息删除 ID 的可选 String。使用它,可以发布来自不同来源的同一 SafetySourceIssue,并且只在界面中显示一次,前提是假设它们具有相同的 deduplicationGroup(从 Android 14 开始)。如果未指定,则永远不会对问题进行重复信息删除操作
  • 针对来源标题的可选 CharSequence,这是显示警告卡片来源的文本(从 Android 14 开始)。如果未指定,则使用 SafetySourcesGroup 的标题
  • 可选的问题可操作性(从 Android 14 开始),它必须是下列项之一:
    • ISSUE_ACTIONABILITY_MANUAL:用户需要手动解决此问题。这是默认值。
    • ISSUE_ACTIONABILITY_TIP:此问题只是一个小提示,不需要用户输入。
    • ISSUE_ACTIONABILITY_AUTOMATIC:此问题已处理,不需要用户输入。
  • 可选的通知行为(从 Android 14 开始),它必须是下列项之一:
    • NOTIFICATION_BEHAVIOR_UNSPECIFIED:安全中心将决定是否需要针对警告卡片发送通知。这是默认值。
    • NOTIFICATION_BEHAVIOR_NEVER:不发布任何通知。
    • NOTIFICATION_BEHAVIOR_DELAYED:在首次报告问题后的某个时间发布通知。
    • NOTIFICATION_BEHAVIOR_IMMEDIATELY:报告问题后立即发布通知。
  • 可选的 Notification,用于显示包含警告卡片的自定义通知(从 Android 14 开始)。如果未指定,则 Notification 来自警告卡片。组成元素:
    • 必需的 CharSequence 标题
    • 必需的 CharSequence 摘要
    • 用户可以针对此通知接受的 Action 元素列表
  • 不变量:
    • Action 实例列表必须由具有唯一标识符的操作组成
    • Action 实例列表必须包含一个或两个 Action 元素。如果可操作性不是 ISSUE_ACTIONABILITY_MANUAL,则不允许任何 Action
    • OnDismiss PendingIntent 不得打开 Activity 实例
    • API 配置施加的额外要求

系统会在某些事件发生时向安全中心提供数据,因此您必须注明来源,以便向 SafetySourceData 提供 SafetyEvent 实例。

SafetyEvent

  • 必需的类型,必须为以下类型之一:
    • SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED:来源的状态已更改。
    • SAFETY_EVENT_TYPE_REFRESH_REQUESTED:响应安全中心的刷新/重新扫描信号;请使用此指标(而非 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED),以便安全中心能够跟踪刷新/重新扫描请求。
    • SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED:我们直接从安全中心屏幕解析了 SafetySourceIssue.Action;使用它(而不是 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED)来启用安全中心,以便跟踪正在解析的 SafetySourceIssue.Action
    • SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED:我们尝试直接从安全中心屏幕解析 SafetySourceIssue.Action,但未能成功;使用此 API 代替 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED,以便安全中心跟踪已经失败的 SafetySourceIssue.Action
    • SAFETY_EVENT_TYPE_DEVICE_LOCALE_CHANGED:设备的语言已发生变化,因此我们要更新所提供的数据的文本;因此可以使用 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED 执行此操作。
    • SAFETY_EVENT_TYPE_DEVICE_REBOOTED:我们仅在首次启动时提供这些数据,因为安全中心数据不会在重新启动后保留;为此,可以使用 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
  • 可选的 String 标识符,用于刷新广播 ID。
  • 可选的 String 标识符,用于解析 SafetySourceIssue 实例。
  • 可选的 String 标识符,用于解析的 SafetySourceIssue.Action 实例。
  • 不变量:
    • 如果类型为 SAFETY_EVENT_TYPE_REFRESH_REQUESTED,则必须提供刷新广播 ID
    • 如果类型为 SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDEDSAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED,则必须提供问题 ID 和操作 ID

以下示例说明了来源如何向安全中心提供数据(在本例中,它提供了一个条目,其中包含一个警告卡片):

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);

获取提供的最新数据

您可以获取为应用拥有的来源提供给安全中心的最新数据。您可以使用它在您自己的 UI 中显示某些内容,在执行昂贵的操作之前检查数据是否需要更新,或者通过一些更改或使用新的 SafetyEvent 实例向安全中心提供相同的 SafetySourceData 实例。它对于测试也很有用。

使用此代码获取向安全中心提供的最新数据:

SafetySourceData lastDataProvided = safetyCenterManager.getSafetySourceData("MySourceId");

报告错误

如果您无法收集 SafetySourceData 数据,可以向安全中心报告错误,安全中心会将条目更改为灰色,清除缓存的数据,并提供类似“Couldn't check setting”(无法检查设置)之类的消息。如果 SafetySourceIssue.Action 的实例无法解析,在这种情况下,清除缓存的数据不会被清除且界面条目不会更改,您也可以报告错误;但向用户显示一条消息,让他们知道出了问题。

您可以使用 SafetySourceErrorDetails 提供错误消息,它由以下内容组成:

  • SafetySourceErrorDetails必需SafetyEvent 实例:
// 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);

响应刷新或重新扫描请求

您收到的安全中心会提供信号,让您提供新数据。响应刷新或重新扫描请求可确保用户在打开安全中心时以及点按扫描按钮时查看当前状态。

这可以通过接收具有以下操作的广播来完成:

  • ACTION_REFRESH_SAFETY_SOURCES
    • 字符串值:android.safetycenter.action.REFRESH_SAFETY_SOURCES
    • 当安全中心发送刷新给定应用的安全性信息来源的数据的请求时触发
    • 只能由系统发送的受保护 intent
    • 作为显式 intent 发送到配置文件中的所有安全性信息来源,并需要 SEND_SAFETY_CENTER_UPDATE 权限

此广播中包含以下 extra:

  • EXTRA_REFRESH_SAFETY_SOURCE_IDS
    • 字符串值:android.safetycenter.extra.REFRESH_SAFETY_SOURCE_IDS
    • 字符串数组类型 (String[]) 表示要刷新给定应用的来源 ID
  • EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE

    • 字符串值:android.safetycenter.extra.REFRESH_SAFETY_SOURCES_REQUEST_TYPE
    • 整数类型,表示请求类型 @IntDef
    • 必须是以下值之一:
      • EXTRA_REFRESH_REQUEST_TYPE_GET_DATA:请求来源相对较快地提供数据,通常是在用户打开页面时提供
      • EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA:请求来源尽可能提供最新数据,通常是在用户按下重新扫描按钮时提供
  • EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID

    • 字符串值:android.safetycenter.extra.REFRESH_SAFETY_SOURCES_BROADCAST_ID
    • 字符串类型,表示请求刷新的唯一标识符

如需从安全中心获取信号,请实现 BroadcastReceiver 实例。广播带有特殊 BroadcastOptions,允许接收器启动前台服务。

BroadcastReceiver 响应刷新请求:

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;
  }
}

上例中的同一 BroadcastReceiver 实例在 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>

理想情况下,安全中心实现的方式是在其数据发生更改时调用 SafetyCenterManager。出于系统运行状况方面的原因,我们建议只响应重新扫描信号(当用户点按扫描按钮时),而不是在用户打开安全中心时做出响应。如果需要此功能,必须设置配置文件中 refreshOnPageOpenAllowed="true" 字段,以便来源接收在这些情况下发送的广播。

在启用或停用时响应安全中心

您可以使用以下 intent 操作,在安全中心启用或停用时做出响应:

  • ACTION_SAFETY_CENTER_ENABLED_CHANGED
    • 字符串值:android.safetycenter.action.SAFETY_CENTER_ENABLED_CHANGED
    • 当设备正在运行时,安全中心启用或停用时会触发
    • 启动时未调用(为此,请使用 ACTION_BOOT_COMPLETED
    • 只能由系统发送的受保护 intent
    • 作为显式 intent 发送到配置文件中的所有安全性信息来源,需要 SEND_SAFETY_CENTER_UPDATE 权限
    • 作为需要 READ_SAFETY_CENTER_STATUS 权限的隐式 intent 发送

此 intent 操作对于启用或停用设备上的安全中心相关功能非常有用。

实现解析操作

解析操作是一个 SafetySourceIssue.Action 实例,用户可以直接从安全中心屏幕解析该实例。用户点按某个操作按钮,系统会触发由安全性信息来源发送的 SafetySourceIssue.Action 中的 PendingIntent 实例,这会在后台解决问题并在完成时通知安全中心。

若要实现解析操作,而相应操作预计需要一些时间 (PendingIntent.getService) 或需要广播接收器 (PendingIntent.getBroadcast),安全中心信息来源可以使用一项服务。

请使用以下代码向安全中心发送已解决的问题:

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 将解析以下操作:

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).
  }
}

上例中的同一 BroadcastReceiver 实例在 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>

响应问题被关闭的情况

您可以指定可在 SafetySourceIssue 实例关闭时触发的 PendingIntent 实例。安全中心会处理以下问题被关闭的情况:

  • 如果有来源推送问题,用户可以通过点按“关闭”按钮(警告卡片上的 X 按钮),在安全中心屏幕中关闭问题。
  • 当用户关闭问题后,如果问题继续出现,则不会再在界面中显示。
  • 在设备重新启动期间,磁盘上的永久性关闭行为仍会保留。
  • 如果安全中心信息来源停止提供问题,稍后又再次提供问题,该问题便会重新出现。这是为了让用户看到警告、关闭警告,然后执行一些操作来缓解问题,但随后用户执行了某项操作以引发类似问题。此时,警告卡片应重新显示。
  • 黄色和红色警告卡片每 180 天重新显示一次,除非用户将其多次关闭。

此来源不应需要其他行为,除非:

  • 来源会尝试以不同方式实现此行为,例如,永远不会再次出现此问题。
  • 该来源会尝试用它作为回调,例如记录信息。

为多个用户/个人资料提供数据

SafetyCenterManager API 可跨用户和个人资料使用。如需了解详情,请参阅构建可感知多用户的应用。提供 SafetyCenterManagerContext 对象会与 UserHandle 实例关联,因此返回的 SafetyCenterManager 实例会与该 UserHandle 实例的安全中心进行互动。默认情况下,Context 与正在运行的用户相关联,但如果应用拥有 INTERACT_ACROSS_USERSINTERACT_ACROSS_USERS_FULL 权限,则可以为其他用户创建实例。以下示例展示了如何跨用户/个人资料进行调用:

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

设备上的每位用户均可拥有多份受管理资料。安全中心会为每位用户提供不同的数据,但会合并与指定用户关联的所有受管理资料的数据。

为配置文件中的来源设置 profile="all_profiles" 后,会发生以下情况:

  • 会存在一个用户(个人资料父账号)及其所有关联的受管理资料(使用 titleForWork 实例)的界面条目。
  • 系统会针对个人资料父账号以及所有关联的受管理资料发送刷新或重新扫描信号。关联的接收器会针对每个配置文件启动,并且可以直接向 SafetyCenterManager 提供关联的数据,而无需进行跨资料调用,除非接收器或应用是 singleUser

  • 来源应为用户及其所有受管理资料提供数据。每个界面条目的数据可能会因个人资料而异。

测试

您可以访问 ShadowSafetyCenterManager,并在 Robolectric 测试中使用它。

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);
}

您可以编写更多端到端 (E2E) 测试,但这不在本指南的讨论范围内。如需详细了解如何编写这些 E2E 测试,请参阅 CTS 测试 (CtsSafetyCenterTestCases)

测试和内部 API

内部 API 和测试 API 供内部使用,因此本指南不做详细说明。不过,我们可能会在未来扩展一些内部 API,以允许 OEM 构建他们自己的界面。我们会更新本指南,就如何使用这些 API 提供指导。

权限

  • MANAGE_SAFETY_CENTER
    • internal|installer|role
    • 用于内部安全中心 API
    • 仅授予 PermissionController 和 Shell

“设置”应用

安全中心重定向

默认情况下,您可以通过“设置”应用访问新的安全和隐私条目来进入安全中心。如果您使用的是其他“设置”应用,或者已修改“设置”应用,则可能需要自定义安全中心的访问方式。

启用安全中心后:

  • 旧版隐私条目已隐藏代码
  • 旧版安全条目已隐藏代码
  • 新增了安全和隐私条目代码
  • 新的安全和隐私条目重定向到安全中心代码
  • android.settings.PRIVACY_SETTINGSandroid.settings.SECURITY_SETTINGS intent 操作重定向到打开的安全中心(代码:安全隐私

高级安全和隐私设置页面

“设置”应用包含在更多安全设置更多隐私设置标题下提供的其他设置(可在安全中心内找到):

安全性信息来源

安全中心与“设置”应用提供的一组特定安全性信息来源集成:

  • 锁定屏幕安全性信息来源会验证系统是否设置了密码(或其他安全机制),以确保用户的私密信息免遭外部访问。
  • 生物识别安全性来源(默认隐藏)可与指纹或人脸传感器集成。

您可以通过 Android 代码搜索访问这些安全中心信息来源的源代码。 如果未修改“设置”应用(未更改软件包名称、源代码或处理锁定屏幕和生物识别技术的源代码),则此集成应该支持开箱即用。否则,可能需要进行一些修改,例如更改配置文件以更改“设置”应用的软件包名称、与安全中心集成的来源以及集成。如需了解详情,请参阅更新配置文件集成设置

PendingIntent 简介

如果您依赖于 Android 14 或更高版本中现有的“设置”应用安全中心集成,以下 bug 已得到修复。在这种情况下,无需阅读此部分。

如果您确定 bug 不存在,请在“设置”应用中将 XML 布尔值资源配置值 config_isSafetyCenterLockScreenPendingIntentFixed 设置为 true,以关闭安全中心内的解决方法。

PendingIntent 解决方法

此错误是由“设置”使用 Intent 实例 extra 确定要打开的 fragment 引起的。由于 Intent#equals 不会考虑 Intent 实例 extra,因此齿轮图标和条目的 PendingIntent 实例会被视为相等,并会导航到同一界面(即使是但它们旨在导航到其他界面)。通过按请求代码区分 PendingIntent 实例,此问题在 QPR 版本中已得到修复。或者,也可以通过使用 Intent#setId 进行区分。

内部安全性信息来源

某些安全中心信息来源是内部来源,并在 PermissionController 模块内的 PermissionController 系统应用程序中实现。这些来源的行为与常规安全中心源一样,不会受到任何特殊处理。您可以通过 Android 代码搜索获得这些来源的代码。

这些来源主要是隐私保护信号,例如:

  • 无障碍
  • 自动撤消未使用的应用
  • 位置信息访问权限
  • 通知侦听器
  • 工作政策信息