本指南与健康数据共享版本 1.1.0-alpha12 或更高版本兼容。
健康数据共享 提供计划锻炼数据类型,使训练应用能够写入训练计划,并使锻炼应用能够读取训练计划。系统可以回读记录的锻炼(锻炼),以便进行个性化表现分析,帮助用户实现训练目标。
查看“健康数据共享”的适用范围
在尝试使用健康数据共享之前,您的应用应验证用户的设备上是否可以使用健康数据共享。健康数据共享可能未预安装在所有设备上,或者可能已被停用。您可以使用 HealthConnectClient.getSdkStatus() 方法检查可用性。
如何查看“健康数据共享”的适用范围
fun checkHealthConnectAvailability(context: Context) { val providerPackageName = "com.google.android.apps.healthdata" val availabilityStatus = HealthConnectClient.getSdkStatus(context, providerPackageName) if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) { // Health Connect is not available. Guide the user to install/enable it. // For example, show a dialog. return // early return as there is no viable integration } if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) { // Health Connect is available but requires an update. // Optionally redirect to package installer to find a provider, for example: val uriString = "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding" context.startActivity( Intent(Intent.ACTION_VIEW).apply { setPackage("com.android.vending") data = Uri.parse(uriString) putExtra("overlay", true) putExtra("callerId", context.packageName) } ) return } // Health Connect is available, obtain a HealthConnectClient instance val healthConnectClient = HealthConnectClient.getOrCreate(context) // Issue operations with healthConnectClient }
根据 getSdkStatus() 返回的状态,您可以引导用户在必要时从 Google Play 商店安装或更新“健康数据共享”。
功能的适用范围
如需确定用户的设备是否支持 Health Connect 上的训练计划,请检查客户端上是否提供FEATURE_PLANNED_EXERCISE:
if (healthConnectClient
.features
.getFeatureStatus(
HealthConnectFeatures.FEATURE_PLANNED_EXERCISE
) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE) {
// Feature is available
} else {
// Feature isn't available
}
所需权限
对计划锻炼的访问权限受以下权限保护:
android.permission.health.READ_PLANNED_EXERCISEandroid.permission.health.WRITE_PLANNED_EXERCISE
如要向应用添加预设锻炼功能,请先请求 PlannedExerciseSession 数据类型的权限。
您需要声明以下权限才能写入计划的锻炼:
<application>
<uses-permission
android:name="android.permission.health.WRITE_PLANNED_EXERCISE" />
...
</application>
如需读取锻炼计划,您需要请求以下权限:
<application>
<uses-permission
android:name="android.permission.health.READ_PLANNED_EXERCISE" />
...
</application>
向用户请求权限
创建客户端实例后,您的应用需要向用户请求权限。必须允许用户随时授予或拒绝权限。 为此,请为所需的数据类型创建一个权限集。 先确保此集中的权限已在 Android 清单中声明。
val permissions = setOf( HealthPermission.getReadPermission(HeartRateRecord::class), HealthPermission.getWritePermission(HeartRateRecord::class), HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class), HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class), HealthPermission.getReadPermission(ExerciseSessionRecord::class), HealthPermission.getWritePermission(ExerciseSessionRecord::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.") } } }
相关权限
训练计划与锻炼时段相关联。因此,用户必须授予使用与训练计划相关的每种记录类型的权限,才能充分利用健康数据共享的这项功能。
例如,如果某个训练计划需要测量用户在一系列跑步过程中的心率,开发者可能需要声明以下权限,并由用户授予这些权限,以便写入锻炼时段并读取结果以供日后评估:
android.permission.health.READ_EXERCISEandroid.permission.health.READ_EXERCISE_ROUTESandroid.permission.health.READ_HEART_RATEandroid.permission.health.WRITE_EXERCISEandroid.permission.health.WRITE_EXERCISE_ROUTEandroid.permission.health.WRITE_HEART_RATE
不过,创建训练计划并根据计划评估表现的应用通常与使用训练计划并写入实际锻炼数据的应用不同。根据应用类型,可能不需要所有读写权限。例如,对于每种类型的应用,您可能只需要以下权限:
| 训练计划应用 | 锻炼应用 |
|---|---|
WRITE_PLANNED_EXERCISE |
READ_PLANNED_EXERCISE |
READ_EXERCISE |
WRITE_EXERCISE |
READ_EXERCISE_ROUTES |
WRITE_EXERCISE_ROUTE |
READ_HEART_RATE |
WRITE_HEART_RATE |
计划锻炼时段记录中包含的信息
- 会议标题。
- 计划的锻炼块列表。
- 会话的开始时间和结束时间。
- 锻炼类型。
- 活动的备注。
- metadata参数
- 已完成的锻炼活动 ID - 在与此计划锻炼活动相关的锻炼活动完成后,系统会自动写入此 ID。
计划锻炼模块记录中包含的信息
计划锻炼块包含一系列锻炼步骤,用于支持重复执行不同的步骤组(例如,连续五次执行一系列弯举、波比跳和卷腹)。
- 屏蔽设置的说明。
- 计划的锻炼步骤列表。
- 重复次数。
计划锻炼步骤记录中包含的信息
支持的聚合
此数据类型不支持任何聚合。
用法示例
假设用户计划在两天后进行 90 分钟的跑步。这次跑步将围绕湖泊跑三圈,目标心率在 90 到 110 bpm 之间。
- 用户在训练计划应用中定义了包含以下内容的计划锻炼课程:
- 计划的运行开始和结束时间
- 锻炼类型(跑步)
- 圈数(重复次数)
- 心率性能目标(90 到 110 bpm 之间)
- 这些信息会分组到锻炼块和步骤中,并由训练计划应用作为
PlannedExerciseSessionRecord写入健康数据共享。 - 用户执行计划的练习(跑步)。
- 与练习相关的数据会通过以下任一方式记录:
- 在练习期间由穿戴式设备收集。例如心率。
这些数据会作为活动的记录类型写入健康数据共享。在此示例中为
HeartRateRecord。 - 由用户在会话结束后手动更新。例如,指示实际跑步的开始和结束。此数据会作为
ExerciseSessionRecord写入 Health Connect。
- 在练习期间由穿戴式设备收集。例如心率。
这些数据会作为活动的记录类型写入健康数据共享。在此示例中为
- 稍后,训练计划应用会从健康数据共享读取数据,以根据用户在计划的锻炼时段中设定的目标评估实际表现。
规划练习并设定目标
用户可以规划未来的锻炼并设定目标。将此数据作为计划锻炼时段写入健康数据共享。
在用法示例中描述的示例中,用户计划在两天后跑步 90 分钟。这次跑步将围绕湖泊跑三圈,目标心率在 90 到 110 bpm 之间。
在将计划的锻炼课程记录到“健康数据共享”的应用的表单处理程序中,可能会找到如下代码段。它也可能出现在集成的数据注入点,例如提供训练的服务。
// Verify the user has granted all necessary permissions for this task val grantedPermissions = healthConnectClient.permissionController.getGrantedPermissions() if (!grantedPermissions.contains( HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class))) { // The user hasn't granted the app permission to write planned exercise session data. Log.w("HealthConnect", "Write permission for PlannedExerciseSessionRecord not granted.") return } val plannedExerciseSessionRecord = PlannedExerciseSessionRecord( startTime = startTime, endTime = endTime, exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, blocks = listOf( PlannedExerciseBlock( repetitions = 1, steps = listOf( PlannedExerciseStep( exerciseType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING, exercisePhase = PlannedExerciseStep.EXERCISE_PHASE_ACTIVE, completionGoal = ExerciseCompletionGoal.RepetitionsGoal(repetitions = 3), performanceTargets = listOf( ExercisePerformanceTarget.HeartRateTarget( minHeartRate = 90.0, maxHeartRate = 110.0 ) ) ), ), description = "Three laps around the lake" ) ), title = "Run at lake", notes = null, metadata = Metadata.activelyRecorded( device = Device(type = Device.Companion.TYPE_PHONE), ), startZoneOffset = null, endZoneOffset = null, ) try { // Attempt to insert the record val response = healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord)) // If execution reaches here, the insert succeeded. // Safely extract the ID using firstOrNull() val insertedPlannedExerciseSessionId = response.recordIdsList.firstOrNull() if (insertedPlannedExerciseSessionId != null) { Log.d("HealthConnect", "Successfully inserted planned exercise session ID: $insertedPlannedExerciseSessionId") } else { Log.w("HealthConnect", "Insertion succeeded but no record IDs were returned.") } } catch (e: Exception) { // Handle API failures, database errors, or system issues safely without crashing Log.e("HealthConnect", "Failed to insert planned exercise session record", e) }
记录锻炼和活动数据
两天后,用户记录了实际锻炼。将此数据作为锻炼时段写入 Health Connect。
在此示例中,用户的会话时长与计划时长完全一致。
以下代码段可能位于将锻炼时段记录到健康数据共享的应用的表单处理程序中。此外,在能够检测和记录锻炼时段的可穿戴设备的数据注入和导出处理程序中,也可能会发现此权限。
此处的 insertedPlannedExerciseSessionId 是从上一个示例中重复使用的。在实际应用中,ID 将由用户从现有练习列表中选择计划的练习来确定。
// Verify the user has granted all necessary permissions for this task val grantedPermissions = healthConnectClient.permissionController.getGrantedPermissions() if (!grantedPermissions.contains( HealthPermission.getWritePermission(ExerciseSessionRecord::class))) { // The user doesn't granted the app permission to write exercise session data. return } val sessionDuration = Duration.ofMinutes(90) val sessionEndTime = Instant.now() val sessionStartTime = sessionEndTime.minus(sessionDuration) val exerciseSessionRecord = ExerciseSessionRecord( startTime = sessionStartTime, startZoneOffset = ZoneOffset.UTC, endTime = sessionEndTime, endZoneOffset = ZoneOffset.UTC, exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, segments = listOf( ExerciseSegment( startTime = sessionStartTime, endTime = sessionEndTime, repetitions = 3, segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING ) ), title = "Run at lake", plannedExerciseSessionId = insertedPlannedExerciseSessionId, metadata = Metadata.activelyRecorded( device = Device(type = Device.Companion.TYPE_PHONE) ) ) val insertedExerciseSessions = healthConnectClient.insertRecords(listOf(exerciseSessionRecord))
可穿戴设备还会记录他们跑步期间的心率。以下代码段可用于在目标范围内生成记录。
在实际应用中,此代码段的主要部分可能位于来自穿戴式设备的消息的处理程序中,该处理程序会在收集测量数据后将其写入健康数据共享。
// Verify the user has granted all necessary permissions for this task val grantedPermissions = healthConnectClient.permissionController.getGrantedPermissions() if (!grantedPermissions.contains( HealthPermission.getWritePermission(HeartRateRecord::class))) { // The user doesn't granted the app permission to write heart rate record data. return } val samples = mutableListOf<HeartRateRecord.Sample>() var currentTime = sessionStartTime while (currentTime.isBefore(sessionEndTime)) { val bpm = Random.nextInt(21) + 90 val heartRateRecord = HeartRateRecord.Sample( time = currentTime, beatsPerMinute = bpm.toLong(), ) samples.add(heartRateRecord) currentTime = currentTime.plusSeconds(180) } val heartRateRecord = HeartRateRecord( startTime = sessionStartTime, startZoneOffset = ZoneOffset.UTC, endTime = sessionEndTime, endZoneOffset = ZoneOffset.UTC, samples = samples, metadata = Metadata.activelyRecorded( device = Device(type = Device.Companion.TYPE_WATCH) ) ) val insertedHeartRateRecords = healthConnectClient.insertRecords(listOf(heartRateRecord))
评估效果目标
在用户锻炼后的第二天,您可以检索记录的锻炼、检查是否有任何计划的锻炼目标,并评估其他数据类型,以确定是否达到了设定的目标。
此类代码段可能会出现在定期作业中,用于评估效果目标;也可能会出现在应用中,用于加载运动列表并显示有关效果目标的通知。
// Verify the user has granted all necessary permissions for this task val grantedPermissions = healthConnectClient.permissionController.getGrantedPermissions() if (!grantedPermissions.containsAll( listOf( HealthPermission.getReadPermission(ExerciseSessionRecord::class), HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class), HealthPermission.getReadPermission(HeartRateRecord::class) ) ) ) { // The user doesn't granted the app permission to read exercise session record data. return } val searchDuration = Duration.ofDays(1) val searchEndTime = Instant.now() val searchStartTime = searchEndTime.minus(searchDuration) val response = healthConnectClient.readRecords( ReadRecordsRequest<ExerciseSessionRecord>( timeRangeFilter = TimeRangeFilter.between(searchStartTime, searchEndTime) ) ) for (exerciseRecord in response.records) { val plannedExerciseRecordId = exerciseRecord.plannedExerciseSessionId val plannedExerciseRecord = if (plannedExerciseRecordId == null) null else healthConnectClient.readRecord( PlannedExerciseSessionRecord::class, plannedExerciseRecordId ).record if (plannedExerciseRecord != null) { val aggregateRequest = AggregateRequest( metrics = setOf(HeartRateRecord.BPM_AVG), timeRangeFilter = TimeRangeFilter.between( exerciseRecord.startTime, exerciseRecord.endTime ), ) val aggregationResult = healthConnectClient.aggregate(aggregateRequest) val maxBpm = aggregationResult[HeartRateRecord.BPM_MAX] val minBpm = aggregationResult[HeartRateRecord.BPM_MIN] if (maxBpm != null && minBpm != null) { plannedExerciseRecord.blocks.forEach { block -> block.steps.forEach { step -> step.performanceTargets.forEach { target -> when (target) { is ExercisePerformanceTarget.HeartRateTarget -> { val minTarget = target.minHeartRate val maxTarget = target.maxHeartRate if( minBpm >= minTarget && maxBpm <= maxTarget ) { // Success! } } // Handle more target types } } } } } } } }
锻炼时段
锻炼时段可涵盖跑步、打羽毛球等各种活动。
写入锻炼时段
下面展示了如何构建包含时段的插入请求:
suspend fun writeExerciseSession(healthConnectClient: HealthConnectClient) {
healthConnectClient.insertRecords(
listOf(
ExerciseSessionRecord(
startTime = START_TIME,
startZoneOffset = START_ZONE_OFFSET,
endTime = END_TIME,
endZoneOffset = END_ZONE_OFFSET,
exerciseType = ExerciseSessionRecord.ExerciseType.RUNNING,
title = "My Run",
metadata = Metadata.manualEntry()
),
// ... other records
)
)
}
读取锻炼时段
下面的示例展示了如何读取锻炼时段:
suspend fun readExerciseSessions(
healthConnectClient: HealthConnectClient,
startTime: Instant,
endTime: Instant
) {
val response =
healthConnectClient.readRecords(
ReadRecordsRequest(
ExerciseSessionRecord::class,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
)
for (exerciseRecord in response.records) {
// Process each exercise record
// Optionally pull in with other data sources of the same time range.
val distanceRecord =
healthConnectClient
.readRecords(
ReadRecordsRequest(
DistanceRecord::class,
timeRangeFilter =
TimeRangeFilter.between(
exerciseRecord.startTime,
exerciseRecord.endTime
)
)
)
.records
}
}
写入子类型数据
时段还可由可选的子类型数据组成,这类数据会利用更多信息使时段变得更丰富。
例如,锻炼时段可涵盖 ExerciseSegment、ExerciseLap 和 ExerciseRoute 类:
val segments = listOf(
ExerciseSegment(
startTime = Instant.parse("2022-01-02T10:10:10Z"),
endTime = Instant.parse("2022-01-02T10:10:13Z"),
segmentType = ActivitySegmentType.BENCH_PRESS,
repetitions = 373
)
)
val laps = listOf(
ExerciseLap(
startTime = Instant.parse("2022-01-02T10:10:10Z"),
endTime = Instant.parse("2022-01-02T10:10:13Z"),
length = 0.meters
)
)
ExerciseSessionRecord(
exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_CALISTHENICS,
startTime = Instant.parse("2022-01-02T10:10:10Z"),
endTime = Instant.parse("2022-01-02T10:10:13Z"),
startZoneOffset = ZoneOffset.UTC,
endZoneOffset = ZoneOffset.UTC,
segments = segments,
laps = laps,
route = route,
metadata = Metadata.manualEntry()
)