训练计划

本指南与健康数据共享版本 1.1.0-alpha11 兼容。

健康数据共享提供计划的锻炼数据类型,使训练应用能够写入训练计划,并使锻炼应用能够读取训练计划。系统可以读取记录的锻炼,以便进行个性化的表现分析,帮助用户实现训练目标。

功能的适用范围

如需确定用户的设备是否支持 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

如要向应用添加计划锻炼功能,请先请求 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 清单中声明。

// 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 请求这些权限。系统随即会显示“健康数据共享”权限界面。

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

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

训练计划与锻炼时段相关联。因此,用户必须授予使用与训练计划相关的每种记录类型的权限,才能充分利用健康数据共享的这项功能。

例如,如果某训练计划在用户进行一系列跑步活动期间测量其心率,那么开发者可能需要声明以下权限,并且用户需要授予这些权限,才能写入锻炼会话并读取结果以供日后评估:

  • 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 次。

  1. 用户在训练计划应用中定义了以下计划锻炼会话:
    1. 跑步的计划开始时间和结束时间
    2. 锻炼类型(跑步)
    3. 圈数(重复次数)
    4. 心率的表现目标值(介于 90 和 110 bpm 之间)
  2. 这些信息会分组为锻炼块和步数,并由训练计划应用作为 PlannedExerciseSessionRecord 写入健康数据共享。
  3. 用户执行计划的锻炼项目(跑步)。
  4. 与相应会话相关的锻炼数据会以以下任一方式记录:
    1. 在锻炼期间通过穿戴式设备。例如,心率。 相应数据会以活动记录类型的形式写入健康数据共享。在此示例中,值为 HeartRateRecord
    2. 由用户在会话结束后手动设置。例如,指示实际跑步的开始时间和结束时间。相应数据会作为 ExerciseSessionRecord 写入健康数据共享。
  5. 稍后,训练计划应用会从“健康数据共享”读取数据,以根据用户在计划的锻炼时段内设置的目标评估实际表现。

规划锻炼项目和设置目标

用户可以规划未来的锻炼并设定目标。将此数据作为计划的锻炼时段写入健康数据共享。

使用示例中所述的示例中,用户计划在两天后进行 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.
    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()

记录锻炼和活动数据

两天后,用户记录了实际的锻炼时段。将此数据作为锻炼时段写入健康数据共享。

在此示例中,用户的会话时长与计划时长完全一致。

以下代码段可能位于将锻炼会话记录到健康数据共享的应用的表单处理程序中。它也可能出现在能够检测和记录锻炼会话的穿戴式设备的数据提取和导出处理程序中。

此处的 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))

穿戴式设备还会记录跑步期间的心率。以下代码段可用于生成目标范围内的记录。

在真实应用中,此代码段的主要部分可能位于穿戴式设备的消息处理程序中,该处理程序会在收集到测量数据后将其写入健康数据共享。

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

锻炼时段

锻炼时段可涵盖跑步、打羽毛球等各种活动。

写入锻炼时段

下面展示了如何构建包含时段的插入请求:

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"
            ),
            // ... 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
    }
}

写入子类型数据

会话还可由可选的子类型数据组成,这类数据会利用更多信息使会话变得更丰富。

例如,锻炼时段可涵盖 ExerciseSegmentExerciseLapExerciseRoute 类:

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
)