与安全中心互动

重定向到安全中心

任何应用都可以使用 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 的一部分,而 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 表面的代码可在代码搜索中找到。API 的实现代码可在代码搜索中找到。

权限

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

  • READ_SAFETY_CENTER_STATUS
    • signature|privileged
    • 用于 SafetyCenterManager#isSafetyCenterEnabled() API(安全中心来源不需要此权限,它们只需要 SEND_SAFETY_CENTER_UPDATE 权限)
    • 由检查安全中心是否已启用的系统应用使用
    • 仅授予允许列表中的系统应用
  • SEND_SAFETY_CENTER_UPDATE
    • internal|privileged
    • 用于启用的 API 和安全来源 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_STATUS 权限或 SEND_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(从版本 14 开始)
  • 不变量
    • SafetySourceIssue 列表必须由具有唯一标识符的问题组成。
    • 如果存在 SafetySourceStatus 实例,则 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 问题类型标识符;这类似于问题标识符,但不必是唯一的,并且用于日志记录
  • 可选的 String 重复数据删除 ID,这允许从不同来源发布相同的 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 配置施加的其他要求

数据会在特定事件发生时提供给安全中心,因此有必要指定是什么导致来源提供带有 SafetyEvent 实例的 SafetySourceData

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;对于安全中心能够跟踪 SafetySourceIssue.Action 是否已解决,请使用此类型而不是 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
    • SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED:我们尝试直接从安全中心屏幕解决 SafetySourceIssue.Action,但未能成功;对于安全中心能够跟踪 SafetySourceIssue.Action 是否解决失败,请使用此类型而不是 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
    • SAFETY_EVENT_TYPE_DEVICE_LOCALE_CHANGED:设备的语言已更改,因此我们正在更新所提供数据的文本;允许为此使用 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
    • SAFETY_EVENT_TYPE_DEVICE_REBOOTED:我们将此数据作为初始启动的一部分提供,因为安全中心数据不会跨重启持久存在;允许为此使用 SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED
  • 可选的刷新广播 ID 的 String 标识符。
  • 可选的正在解决的 SafetySourceIssue 实例的 String 标识符。
  • 可选的正在解决的 SafetySourceIssue.Action 实例的 String 标识符。
  • 不变量
    • 如果类型为 SAFETY_EVENT_TYPE_REFRESH_REQUESTED,则必须提供刷新广播 ID
    • 如果类型为 SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDEDSAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED,则必须提供问题和操作 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);

获取上次提供的数据

您可以获取上次为您的应用拥有的来源提供给安全中心的数据。您可以使用此数据在您自己的界面中显示某些内容,以检查数据是否需要在执行开销大的操作之前更新,或者提供相同的 SafetySourceData 实例给安全中心,其中包含一些更改或新的 SafetyEvent 实例。它也适用于测试。

使用以下代码获取上次提供给安全中心的数据

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

报告错误

如果您无法收集 SafetySourceData 数据,您可以向安全中心报告错误,这将条目更改为灰色,清除缓存的数据,并提供类似于无法检查设置的消息。如果 SafetySourceIssue.Action 实例无法解决,您也可以报告错误,在这种情况下,缓存的数据不会被清除,并且界面条目也不会更改;但会向用户显示一条消息,告知他们发生了错误。

您可以使用 SafetySourceErrorDetails 提供错误,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 权限
    • 作为隐式 intent 发送,需要 READ_SAFETY_CENTER_STATUS 权限

此 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>

响应问题解除

您可以指定一个 PendingIntent 实例,该实例可以在 SafetySourceIssue 实例被解除时触发。安全中心处理这些问题解除

  • 如果来源推送了一个问题,用户可以在安全中心屏幕上点按解除按钮(警告卡片上的 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 实例)都有一个 UI 条目。
  • 刷新或重新扫描信号会发送给用户资料父项及其所有关联的受管理的用户资料。关联的接收器会为每个用户资料启动,并且可以直接向 SafetyCenterManager 提供关联的数据,而无需进行跨用户资料调用,除非接收器或应用是 singleUser

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

测试

您可以访问 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 从中构建自己的 UI,并且我们将更新本指南以提供有关如何使用它们的指导。

权限

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

“设置”应用

安全中心重定向

默认情况下,安全中心通过“设置”应用访问,并带有一个新的 安全和隐私 条目。如果您使用不同的“设置”应用或修改了“设置”应用,您可能需要自定义安全中心的访问方式。

启用安全中心时

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

高级安全和隐私页面

“设置”应用在更多安全设置更多隐私设置标题下包含其他设置,可从安全中心访问

安全来源

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

  • 锁屏安全来源验证是否已使用密码(或其他安全措施)设置锁屏,以确保用户的私人信息免受外部访问。
  • 生物识别安全来源(默认情况下处于隐藏状态)表面与指纹或面部传感器集成。

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

关于 PendingIntent

如果您依赖 Android 14 或更高版本中现有的“设置”应用安全中心集成,则下面描述的错误已修复。在这种情况下,无需阅读本节。

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

PendingIntent 解决方法

此错误是由“设置”使用 Intent 实例 extra 来确定要打开哪个 fragment 引起的。由于 Intent#equals 未将 Intent 实例 extra 考虑在内,因此齿轮菜单图标和条目的 PendingIntent 实例被视为相等,并导航到同一 UI(即使它们旨在导航到不同的 UI)。此问题已在 QPR 版本中修复,方法是按请求代码区分 PendingIntent 实例。或者,可以使用 Intent#setId 来区分。

内部安全来源

一些安全中心来源是内部的,并在 PermissionController 模块内的 PermissionController 系统应用中实现。这些来源的行为类似于常规安全中心来源,并且没有受到特殊处理。这些来源的代码可通过 Android 代码搜索 获得。

这些主要是隐私信号,例如

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