训练计划

本指南与 Health Connect 版本 1.1.0-alpha11 兼容。

Health Connect 提供一个计划锻炼数据类型,使训练应用能够写入训练计划,并使锻炼应用能够读取训练计划。可以回读已记录的锻炼(运动),进行个性化表现分析,帮助用户实现训练目标。

功能可用性

要确定用户的设备是否支持 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_EXERCISE
  • android.permission.health.WRITE_PLANNED_EXERCISE

在 Play 管理中心和您的应用清单中声明这些权限。

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

您有责任声明您打算在设备和应用中使用的所有适当权限。您还应在使用前检查用户是否已授予每项权限。

向用户请求权限

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

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

// Create a set of permissions for required data types
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 请求这些权限。这将显示 Health Connect 权限屏幕。

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

由于用户可以随时授予或撤销权限,因此您的应用需要定期检查已授予的权限,并处理权限丢失的情况。

训练计划与运动会话相关联。因此,用户必须授予使用与训练计划相关的每种记录类型的权限,才能充分利用 Health Connect 的此功能。

例如,如果训练计划在一系列跑步期间测量用户的心率,则开发人员可能需要声明以下权限,并且用户需要授予这些权限,才能写入运动会话并读取结果以供后续评估:

  • android.permission.health.READ_EXERCISE
  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.READ_HEART_RATE
  • android.permission.health.WRITE_EXERCISE
  • android.permission.health.WRITE_EXERCISE_ROUTE
  • android.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

计划锻炼会话记录中包含的信息

  • 会话标题。
  • 一个计划锻炼块列表。
  • 会话的开始和结束时间。
  • 锻炼类型。
  • 活动备注。
  • 元数据。
  • 已完成的锻炼会话 ID — 在与此计划锻炼会话相关的锻炼会话完成后,此 ID 会自动写入。

计划锻炼块记录中包含的信息

一个计划锻炼块包含一个运动步骤列表,以支持不同组步骤的重复(例如,连续进行五次手臂弯举、波比跳和仰卧起坐)。

计划锻炼步骤记录中包含的信息

支持的聚合

此数据类型不支持任何聚合。

使用示例

假设用户计划两天后进行一次 90 分钟的跑步。这次跑步将围绕湖泊进行三圈,目标心率介于 90 到 110 bpm 之间。

  1. 用户在训练计划应用中定义了一个包含以下内容的计划锻炼会话:
    1. 跑步的计划开始和结束时间
    2. 运动类型(跑步)
    3. 圈数(重复次数)
    4. 心率表现目标(介于 90 和 110 bpm 之间)
  2. 此信息被分组为锻炼块和步骤,并由训练计划应用作为 PlannedExerciseSessionRecord 写入 Health Connect。
  3. 用户执行计划的会话(跑步)。
  4. 与会话相关的锻炼数据记录方式为:
    1. 由可穿戴设备在会话期间记录。例如,心率。此数据将作为活动记录类型写入 Health Connect。在此情况下,为 HeartRateRecord
    2. 用户在会话结束后手动记录。例如,指示实际跑步的开始和结束。此数据将作为 ExerciseSessionRecord 写入 Health Connect。
  5. 稍后,训练计划应用会从 Health Connect 读取数据,以评估实际表现是否符合用户在计划锻炼会话中设定的目标。

规划锻炼并设置目标

用户可以规划未来的锻炼并设置目标。将其作为计划锻炼会话写入 Health Connect。

使用示例中描述的示例中,用户计划两天后进行一次 90 分钟的跑步。这次跑步将围绕湖泊进行三圈,目标心率介于 90 到 110 bpm 之间。

这样的代码片段可能会出现在将计划锻炼会话记录到 Health Connect 的应用表单处理程序中。它也可能出现在集成摄入点中,例如与提供训练服务的集成。

// 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.
    return
}

val plannedDuration = Duration.ofMinutes(90)
val plannedStartDate = LocalDate.now().plusDays(2)

val plannedExerciseSessionRecord = PlannedExerciseSessionRecord(
    startDate = plannedStartDate,
    duration = plannedDuration,
    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.manualEntry(
      device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedPlannedExerciseSessions =
    healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord)).recordIdsList
val insertedPlannedExerciseSessionId = insertedPlannedExerciseSessions.first()

记录锻炼和活动数据

两天后,用户记录实际锻炼会话。将其作为锻炼会话写入 Health Connect。

在此示例中,用户的会话持续时间与计划持续时间完全匹配。

以下代码片段可能会出现在将锻炼会话记录到 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.manualEntry(
      device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedExerciseSessions =
    healthConnectClient.insertRecords(listOf(exerciseSessionRecord))

可穿戴设备也会在整个跑步过程中记录心率。以下代码片段可用于生成目标范围内的记录。

在实际应用中,此代码片段的主要部分可能会出现在可穿戴设备消息的处理程序中,该处理程序会在收集到数据后将测量值写入 Health Connect。

// 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.autoRecorded(
      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
                            }
                        }
                    }
                }
            }
        }
    }
}