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

本指南与 健康数据共享 1.1.0 或更高版本兼容。

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

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

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

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

您可以按照以下核心步骤,使用健康数据共享构建全面的睡眠监测体验:

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

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

准备工作

在实现睡眠功能之前:

主要概念

健康数据共享使用一些核心组件来表示睡眠数据。SleepSessionRecord 充当睡眠的中央记录,其中包含开始或结束时间以及睡眠阶段等详细信息。在会话期间,可以记录各种数据类型,例如 HeartRateRecord 或 OxygenSaturationRecord。

睡眠时段

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

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

应用可能会写入与时段关联的多种数据类型。

数据类型

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

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

开发注意事项

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

后台执行

睡眠监测应用通常会在屏幕关闭的情况下整夜运行。处于此状态时,您应使用:

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

在所有写入操作中保持会话 ID 一致,以确保连续性。

权限

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

对睡眠数据的访问受以下权限保护:

  • 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 清单中声明。

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 请求这些权限。系统会显示“健康数据共享”权限界面。
val permissions = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class)
    )

val requestPermissionsLauncher = rememberLauncherForActivityResult(
    contract = PermissionController.createRequestPermissionResultContract()
) { grantedPermissions ->
    if (grantedPermissions.containsAll(permissions)) {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") }
    } else {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") }
    }
}
由于用户可以随时授予或撤消权限,因此您的应用每次在使用权限之前都需要检查权限,并处理权限丢失的情况。

实现睡眠时段

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

如需将 HeartRateRecord 或 OxygenSaturationRecord 等数据类型与睡眠时段对应,请使用介于时段的 startTime 和 endTime 之间的时间戳记录这些数据类型。“健康数据共享”不会使用会话标识符将睡眠时段与精细数据相关联。相反,关联是通过重叠的时间间隔隐式建立的。读取睡眠数据时,您可以使用某个时段的时间范围来查询关联的数据类型,如读取睡眠数据中所示。

写入时段

虽然心率等精细数据可以在整个睡眠时段内记录,但 SleepSessionRecord 本身只能在时段结束后(例如用户醒来时)写入 Health Connect。该记录必须包含会话 startTime、endTime 以及会话期间记录的 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)发出第二个请求,并按睡眠时段的 startTime 和 endTime 进行过滤。

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

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 进行后台同步:使用 WorkManager 进行延迟写入。建议将间隔时间设为 15 分钟,以便在实时数据和电池效率之间取得平衡。
  • 批量写入请求:不要单独写入每个传感器事件。分块处理请求。健康数据共享每个写入请求最多可处理 1,000 条记录。
  • 确保会话 ID 稳定且唯一:为会话使用一致的标识符。如果某个练习被修改或更新,使用相同的 ID 可防止其被视为新的独立练习。
  • 对数据类型使用批处理:为了减少输入/输出开销并延长电池续航时间,请将数据点分组到单个 insertRecords 调用中,而不是单独写入每个数据点。
  • 避免写入重复数据:使用客户端 ID:创建记录时,请设置 metadata.clientRecordId。“健康数据共享”会使用此 ID 来识别唯一记录。如果您尝试写入一条 clientRecordId 已存在的记录,健康数据共享 会忽略重复记录或更新现有记录,而不是创建新记录。设置 metadata.clientRecordId 是防止在同步重试或应用重新安装期间出现重复项的最有效方法。
    val record = StepsRecord(
        count = 100,
        startTime = startTime,
        endTime = endTime,
        startZoneOffset = ZoneOffset.UTC,
        endZoneOffset = ZoneOffset.UTC,
        metadata = Metadata(
            // Use a unique ID from your own database
            clientRecordId = "daily_steps_2023_10_27_user_123"
        )
    )
  • 检查现有数据:在同步之前,查询相应时间范围,查看是否已存在来自您应用的记录。
  • 确保时间戳不重叠:验证新会话是否在上一个会话结束之前开始。重叠的锻炼时段可能会导致健身信息中心和摘要计算出现冲突。
  • 提供明确的权限请求理由:使用 Permission.createIntent 流程说明应用为何需要访问健康数据,例如:“为了监测您的血压趋势并提供数据洞见。”
  • 测试长时间运行的会话:监控持续数小时的会话期间的电池消耗情况,以验证批处理间隔和传感器使用情况不会耗尽设备电量。
  • 使时间戳与传感器速率保持一致:使记录时间戳与传感器的实际频率保持一致,以保持数据的高保真度。

测试

为验证数据正确性并确保优质的用户体验,请遵循以下测试策略,并参阅官方测试主要用例文档。

验证工具

质量核对清单

典型架构

睡眠监测实现通常包括:

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

问题排查

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

常见调试步骤

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