利用健康数据共享开发睡眠体验

如果您想在应用中打造睡眠跟踪体验,可以使用健康数据共享来执行以下操作:

  • 写入睡眠时段
  • 写入睡眠阶段数据
  • 写入睡眠数据,例如心率、血氧饱和度和呼吸频率
  • 读取其他应用的睡眠数据

本指南介绍了如何构建这些睡眠功能,涵盖数据类型、后台执行、权限、推荐的工作流程和最佳实践。

概览:构建全面的睡眠跟踪器

您可以按照以下核心步骤,使用“健康数据共享”打造全面的睡眠跟踪体验:

  • 根据健康权限正确实现权限。
  • 使用 SleepSessionRecord 录制会话。
  • 在会话期间持续写入睡眠阶段、心率和血氧饱和度等数据类型。
  • 妥善管理后台执行,以验证夜间是否持续捕获数据。
  • 读取会话数据,以便生成睡眠后总结和分析。

此工作流可实现与其他健康数据共享应用的互操作性,并验证用户控制的数据访问权限。

准备工作

在实现睡眠功能之前:

主要概念

健康数据共享使用几个核心组件来表示睡眠数据。SleepSessionRecord 可作为睡眠的中央记录,包含开始或结束时间以及睡眠阶段等详细信息。在会话期间,系统可以记录各种数据类型,例如 HeartRateRecordOxygenSaturationRecord

睡眠时段

睡眠数据由 SleepSessionRecord 表示。每条记录存储以下信息:

  • startTime
  • endTime
  • stagesSleepSessionRecord.Stage 的列表,包括深度睡眠、浅度睡眠、快速眼动睡眠和清醒睡眠。
  • 可选的会话元数据(标题、备注)

应用可以写入与会话关联的多种数据类型。

数据类型

睡眠时段内记录的常见数据类型包括:

每种数据类型都存储为单独的记录。

开发注意事项

睡眠跟踪应用通常需要长时间运行,并且在屏幕关闭时经常在后台运行。构建睡眠功能时,请务必考虑如何管理后台执行并请求睡眠数据所需的权限。

后台执行

睡眠跟踪应用通常会在夜间屏幕关闭的情况下运行。在这种状态下,您应使用:

  • 用于数据收集的前台服务
  • WorkManager 用于延迟写入或同步
  • 针对精细数据(例如心率)的常规记录写入的批处理策略

通过在所有写入操作中保持会话 ID 一致来维持连续性。

权限

您的应用必须先请求相关的健康数据共享权限,然后才能读取或写入睡眠数据。如需查看数据类型的完整列表,请参阅 健康数据共享数据类型。睡眠的常见权限包括睡眠会话和心率或血氧饱和度等指标。

对休眠的访问受以下权限保护:

  • android.permission.health.READ_SLEEP
  • android.permission.health.WRITE_SLEEP

如要向应用添加睡眠功能,请先请求 SleepSession 数据类型的权限。

您需要声明以下权限才能写入睡眠数据:

<application>
  <uses-permission
android:name="android.permission.health.WRITE_SLEEP" />
...
</application>

如需读取睡眠数据,您需要请求以下权限:

<application>
  <uses-permission
android:name="android.permission.health.READ_SLEEP" />
...
</application>

以下示例展示了如何为包含心率、血氧饱和度和呼吸频率数据的睡眠会话请求权限:

创建客户端实例后,应用需要向用户请求权限。用户必须能够随时授予或拒绝权限。

为此,请为所需的数据类型创建一组权限。 先确保此集中的权限已在 Android 清单中声明。

// Create a set of permissions for required data types
val PERMISSIONS =
    setOf(
  HealthPermission.getReadPermission(SleepSessionRecord::class),
  HealthPermission.getWritePermission(SleepSessionRecord::class),
  HealthPermission.getReadPermission(HeartRateRecord::class),
  HealthPermission.getWritePermission(HeartRateRecord::class),
  HealthPermission.getReadPermission(OxygenSaturationRecord::class),
  HealthPermission.getWritePermission(OxygenSaturationRecord::class),
  HealthPermission.getReadPermission(RespiratoryRateRecord::class),
  HealthPermission.getWritePermission(RespiratoryRateRecord::class)
)

使用 getGrantedPermissions 查看您的应用是否已获得所需的权限。如不具备,请使用 createRequestPermissionResultContract 请求这些权限。系统随即会显示“健康数据共享”权限界面。

// Create the permissions launcher
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()

val requestPermissions = registerForActivityResult(requestPermissionActivityContract) { granted ->
  if (granted.containsAll(PERMISSIONS)) {
    // Permissions successfully granted
  } else {
    // Lack of required permissions
  }
}

suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient) {
  val granted = healthConnectClient.permissionController.getGrantedPermissions()
  if (granted.containsAll(PERMISSIONS)) {
    // Permissions already granted; proceed with inserting or reading data
  } else {
    requestPermissions.launch(PERMISSIONS)
  }
}

由于用户可以随时授予或撤消权限,因此您的应用需要在每次使用权限之前检查权限,并处理权限丢失的情况。

实现睡眠时段

本部分介绍了记录睡眠数据的推荐工作流程。

如需将 HeartRateRecordOxygenSaturationRecord 等数据类型与睡眠时段对齐,请使用介于相应时段的 startTimeendTime 之间的时间戳记录这些数据。Health Connect 不会使用会话标识符将睡眠时段与精细数据相关联。相反,关联是通过重叠的时间区间隐式实现的。读取睡眠数据时,您可以使用会话的时间范围查询关联的数据类型,如读取睡眠数据中所述。

写入会话

虽然可以在整个睡眠时段内记录心率等精细数据,但 SleepSessionRecord 本身只能在睡眠时段结束后(例如用户醒来时)写入健康数据共享。记录必须包含会话 startTimeendTime 以及会话期间记录的 SleepSessionRecord.Stage 对象列表,因为 SleepSessionRecord 要求 endTime 位于 startTime 之后。

如需写入睡眠时段,请执行以下操作:

  1. 生成唯一的客户记录 ID。
  2. 当用户醒来或睡眠跟踪停止时,收集所有睡眠阶段并构建 SleepSessionRecord
  3. 使用 insertRecords 插入记录。

示例:

val clientRecordId = UUID.randomUUID().toString()
val sessionStartTime = LocalDateTime.of(2023, 10, 30, 22, 0).toInstant(ZoneOffset.UTC)
val sessionEndTime = LocalDateTime.of(2023, 10, 31, 7, 0).toInstant(ZoneOffset.UTC)

val stages = mutableListOf<SleepSessionRecord.Stage>()
// Add recorded stages, for example:
stages.add(SleepSessionRecord.Stage(
    startTime = sessionStartTime.plusSeconds(3600),
    endTime = sessionStartTime.plusSeconds(7200),
    stage = SleepSessionRecord.STAGE_TYPE_LIGHT)
)
stages.add(SleepSessionRecord.Stage(
    startTime = sessionStartTime.plusSeconds(7200),
    endTime = sessionStartTime.plusSeconds(10800),
    stage = SleepSessionRecord.STAGE_TYPE_DEEP)
)
// ... other stages

val session = SleepSessionRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    stages = stages,
    metadata = Metadata(clientRecordId = clientRecordId)
)

healthConnectClient.insertRecords(listOf(session))

读取睡眠数据

应用可以读取睡眠时段及其关联的数据,以总结活动、提供健康数据分析或将数据与外部服务器同步。例如,您可以读取某个 SleepSessionRecord,然后查询在同一时间间隔内发生的 HeartRateRecord

读取具有关联数据的会话

您可以使用 ReadRecordsRequest 读取睡眠时段,其中 SleepSessionRecord 为记录类型,并按时间范围进行过滤。如需读取指定会话的相关数据,请再次请求所选数据类型(例如 HeartRateRecord),并按睡眠时段的 startTimeendTime 进行过滤。

以下示例展示了如何读取给定时间范围内的睡眠时段以及关联的心率数据:

suspend fun readSleepSessionsWithAssociatedData(
    healthConnectClient: HealthConnectClient,
    startTime: Instant,
    endTime: Instant
) {
    val response = healthConnectClient.readRecords(
        ReadRecordsRequest(
            recordType = SleepSessionRecord::class,
            timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
        )
    )

    for (sleepRecord in response.records) {
        // Process each session
        val stages = sleepRecord.stages
        val notes = sleepRecord.notes

        // To read specific granular data (like heart rate) that occurred during
        // this session, use the session's startTime and endTime to filter
        // the request for that data type.
        val hrResponse = healthConnectClient.readRecords(
            ReadRecordsRequest(
                recordType = HeartRateRecord::class,
                timeRangeFilter = TimeRangeFilter.between(
                    sleepRecord.startTime,
                    sleepRecord.endTime
                )
            )
        )
        for (heartRateRecord in hrResponse.records) {
            for (sample in heartRateRecord.samples) {
                val bpm = sample.beatsPerMinute
            }
        }
    }
}

最佳做法

请遵循以下准则,以提高数据可靠性和用户体验:

  • 写入频率
    • 主动跟踪(前台):对于主动睡眠跟踪,在数据可用时或以最长 15 分钟的间隔写入数据。
    • 后台同步:使用 WorkManager 进行延迟写入。建议将间隔时间设为 15 分钟,以在实时数据和电池效率之间取得平衡。
    • 批处理:不要单独写入每个传感器事件。将请求分块。健康数据共享每次写入请求最多可处理 1,000 条记录。
  • 保持会话 ID 的稳定性和唯一性:为会话使用一致的标识符。如果修改或更新了某个睡眠时段,使用同一 ID 可防止系统将其视为新的单独睡眠时段。
  • 对数据类型使用批处理:为了减少输入/输出开销并节省电池电量,请将数据点分组到单个 insertRecords 调用中,而不是单独写入每个点。
  • 避免写入重复数据:使用客户端 ID 创建记录时,请设置 metadata.clientRecordId。健康数据共享使用此属性来标识唯一记录。如果您尝试写入具有已存在的 clientRecordId 的记录,健康数据共享会忽略重复记录或更新现有记录,而不是创建新记录。设置 metadata.clientRecordId 是防止在同步重试或应用重新安装期间出现重复项的最有效方法。

    val record = RespiratoryRateRecord(
        rate = 16.0,
        time = time,
        zoneOffset = ZoneOffset.UTC,
        metadata = Metadata(
            // Use a unique ID from your own database
            clientRecordId = "respiratory_rate_20231030_1"
        )
    )
    
  • 检查现有数据:在同步之前,查询时间范围,看看您的应用中是否已存在记录。

  • 确保时间戳不重叠:验证新会话是否在上一会话结束之前开始。重叠的会话可能会导致健身信息中心和总结计算出现冲突。

  • 提供清晰的权限使用理由:使用 Permission.createIntent 流程说明您的应用为何需要访问健康数据,例如“分析您的睡眠模式”。

  • 测试长时间运行的会话:监控持续数小时的会话期间的电池消耗情况,以验证您的批处理间隔和传感器使用情况不会耗尽设备电量。

  • 使时间戳与传感器速率保持一致:将记录时间戳与传感器的实际频率相匹配,以保持数据的高保真度。

测试

如需验证数据正确性并确保提供优质的用户体验,请遵循以下测试策略,并参阅官方测试热门使用情形文档。

验证工具

质量核对清单

典型架构

睡眠跟踪实现通常包括:

组件 管理
会话控制器 会话状态
计时器
批处理逻辑
数据类型控制器
数据收集
代码库层(封装了健康数据共享操作): 插入会话
插入数据类型
插入睡眠阶段
读取会话摘要
界面层(显示): 时长
实时数据类型
睡眠阶段可视化图表

问题排查

问题 可能的原因 分辨率
缺少数据类型(例如,心率) 缺少写入权限或时间过滤条件不正确。 检查您是否已请求并获得用户授予的特定数据类型权限。验证您的 ReadRecordsRequest 是否使用了与会话匹配的 TimeRangeFilter。请参阅权限
会话写入失败 时间戳重叠。 健康数据共享可能会拒绝与同一应用中的现有数据重叠的记录。请验证新会话的 startTime 是否在上一会话的 endTime 之后。
睡眠期间未记录任何传感器数据 前台服务被终止或处于非活动状态。 如需在屏幕关闭时夜间收集传感器数据,您可以使用带有 foregroundServiceType="health"前台服务
显示重复记录 缺少 clientRecordId 在每条记录的 Metadata 中分配唯一的 clientRecordId。这样一来,如果同步重试期间写入了两次相同的数据,健康数据共享便可以执行去重操作。请参阅最佳实践

常见调试步骤

  • 检查权限状态:在尝试执行读取或写入操作之前,请务必先调用 getPermissionStatus()。用户可以随时在系统设置中撤消权限。
  • 验证执行模式:如果您的应用未在后台收集数据,请验证您是否已在 AndroidManifest.xml 中声明正确的权限,以及用户是否未将应用置于“电池用量限制”模式。