訓練計畫

本指南適用於 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>

您有責任宣告所有要在裝置和應用程式中使用的適當權限。您也應在使用前檢查使用者是否已授予每項權限。

訓練計畫會連結至運動課程。因此,使用者必須授予使用訓練計畫相關記錄類型的權限,才能充分運用 Health Connect 這項功能。

舉例來說,如果訓練計畫會在一系列跑步期間測量使用者的心率,開發人員可能需要宣告下列權限,並由使用者授予權限,才能寫入運動時段並讀取結果,以利日後評估:

  • android.permission.health.READ_EXERCISE
  • android.permission.health.READ_EXERCISE_ROUTE
  • 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_ROUTE WRITE_EXERCISE_ROUTE
READ_HEART_RATE WRITE_HEART_RATE

預定運動時段記錄中包含的資訊

  • 工作階段的標題。
  • 預定運動區塊清單。
  • 工作階段的開始和結束時間。
  • 運動類型。
  • 活動的附註。
  • 中繼資料。
  • 已完成的運動時段 ID:系統會在與這個預定運動時段相關的運動時段完成後自動寫入。

計畫運動區塊記錄中包含的資訊

預定的運動區塊包含運動步驟清單,可支援重複執行不同組步驟 (例如連續五次做手臂彎舉、Burpee 和仰臥起坐的動作序列)。

計畫運動步驟記錄中包含的資訊

支援的匯總

這個資料類型不支援任何匯總。

使用範例

假設使用者計劃在兩天後進行 90 分鐘的跑步運動,這次跑步活動將繞湖跑三圈,目標心率為每分鐘 90 至 110 下。

  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 下。

在應用程式表單處理常式中,您可能會看到類似這類的程式碼片段,該應用程式會將預定的運動時段記錄到 Health Connect。也可以在整合點中找到,例如與提供訓練的服務整合。

// Ensure 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 = ExerciseSessionRecord.EXERCISE_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
)
val insertedPlannedExerciseSessions =
    healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord)).recordIdsList
val insertedPlannedExerciseSessionId = insertedPlannedExerciseSessions.first()

記錄運動和活動資料

兩天後,使用者記錄實際運動時段。將這項資料寫入 Health Connect 做為運動時段

在這個範例中,使用者的工作階段時間長度與預定時間長度完全一致。

以下程式碼片段可能會出現在表單處理常式中,用於記錄運動時段至 Health Connect。這項資訊也可能出現在資料擷取和匯出處理常式中,適用於可偵測及記錄運動時段的穿戴式裝置。

此處的 insertedPlannedExerciseSessionId 是從前一個範例重複使用。在實際應用程式中,系統會根據使用者從現有工作階段清單中選取的預定運動時段,決定 ID。

// Ensure 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,
)
val insertedExerciseSessions =
    healthConnectClient.insertRecords(listOf(exerciseSessionRecord))

可穿戴式裝置也會記錄跑步期間的心率。您可以使用下列程式碼片段,產生目標範圍內的記錄。

在實際應用程式中,這個程式碼片段的主要部分可能會出現在可穿戴式裝置訊息的處理常式中,該處理常式會在收集時將測量值寫入 Health Connect。

// Ensure 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,
)
val insertedHeartRateRecords = healthConnectClient.insertRecords(listOf(heartRateRecord))

評估成效目標

在使用者運動後的隔天,您可以擷取記錄的運動資料、檢查任何預定的運動目標,並評估其他資料類型,判斷是否已達到設定目標。

這類程式碼片段可能會出現在用於評估成效目標的定期工作,或是在載入練習清單並在應用程式中顯示成效目標通知時。

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