トレーニング計画

このガイドは、Health Connect バージョン 1.1.0-alpha11 に対応しています。

ヘルスコネクトには、トレーニング アプリがトレーニング計画を作成し、ワークアウト アプリがトレーニング計画の読み取りを行えるようにする計画されたエクササイズのデータ型が用意されています。記録されたエクササイズ(ワークアウト)を読み戻して、パーソナライズされたパフォーマンス分析を行い、ユーザーがトレーニング目標を達成できるようにします。

機能の提供状況

ユーザーのデバイスがヘルスコネクトのトレーニング プランをサポートしているかどうかを確認するには、クライアントで 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

以下の権限は、アプリの Google Play Console とアプリのマニフェストで宣言します。

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

デバイスとアプリで使用する適切な権限をすべて宣言する責任はデベロッパーにあります。また、使用する前に、各権限がユーザーによって付与されていることを確認する必要があります。

トレーニング プランはエクササイズ セッションにリンクされています。そのため、ヘルスコネクトのこの機能を最大限に活用するには、ユーザーがトレーニング プランに関連する各レコードタイプの使用を許可する必要があります。

たとえば、トレーニング プランで連続したランニング中にユーザーの心拍数を測定する場合は、エクササイズ セッションを書き込んで結果を読み取り、後で評価するために、デベロッパーが次の権限を申告し、ユーザーに付与する必要があります。

  • 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 - この計画されたエクササイズ セッションに関連するエクササイズ セッションが完了すると、自動的に書き込まれます。

計画されたエクササイズ ブロック レコードに含まれる情報

計画されたエクササイズ ブロックには、エクササイズのステップのリストが含まれており、さまざまなステップのグループの反復をサポートします(たとえば、腕立て伏せ、バーピー、腹筋を連続して 5 回行うなど)。

計画したエクササイズのステップ レコードに含まれる情報

サポートされている集計

このデータ型では、サポートされている集計はありません。

使用例

ユーザーが 2 日後の 90 分間のランニングを計画しているとします。このランニングでは、湖を 3 周し、心拍数を 90 ~ 110 bpm に保ちます。

  1. トレーニング計画アプリで、ユーザーが次のような計画されたエクササイズ セッションを定義します。
    1. 実行の開始日と終了日
    2. エクササイズの種類(ランニング)
    3. 周回数(繰り返し)
    4. 心拍数のパフォーマンス目標(90 ~ 110 bpm)
  2. この情報はエクササイズ ブロックとステップにグループ化され、トレーニング プラン アプリによって PlannedExerciseSessionRecord としてヘルスコネクトに書き込まれます。
  3. ユーザーが計画したセッションを実行します(実行中)。
  4. セッションに関連するエクササイズ データは、次のいずれかの方法で記録されます。
    1. セッション中にウェアラブルによって測定されます。たとえば、心拍数などです。このデータは、アクティビティのレコードタイプとしてヘルスコネクトに書き込まれます。この場合は HeartRateRecord です。
    2. セッション後にユーザーが手動で削除します。たとえば、実際の実行の開始と終了を示すなどです。このデータは、ExerciseSessionRecord としてヘルスコネクトに書き込まれます。
  5. 後で、トレーニング プラン アプリがヘルスコネクトからデータを読み取り、計画されたエクササイズ セッションでユーザーが設定した目標と実際のパフォーマンスを評価します。

エクササイズを計画して目標を設定する

ユーザーは、今後のエクササイズを計画し、目標を設定できます。これをヘルスコネクトに計画されたエクササイズ セッションとして書き込みます。

使用例で説明した例では、ユーザーは 2 日後の 90 分間のランニングを計画しています。このランニングでは、湖を 3 周し、心拍数を 90 ~ 110 bpm に保ちます。

このようなスニペットは、予定したエクササイズ セッションをヘルスコネクトに記録するアプリのフォーム ハンドラで見つかります。トレーニングを提供するサービスなど、統合のインジェスト ポイントでも確認できます。

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

エクササイズとアクティビティのデータを記録する

2 日後、ユーザーが実際のエクササイズ セッションを記録します。これをヘルスコネクトにエクササイズ セッションとして書き込みます。

この例では、ユーザーのセッション時間は予定された時間と完全に一致しています。

次のスニペットは、エクササイズ セッションをヘルスコネクトに記録するアプリのフォーム ハンドラで使用できます。また、エクササイズ セッションを検出して記録できるウェアラブルのデータ取り込みハンドラとエクスポート ハンドラにも存在する可能性があります。

ここでの 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))

ウェアラブルは、ランニング中の心拍数も記録します。次のスニペットを使用すると、ターゲット範囲内のレコードを生成できます。

実際のアプリでは、このスニペットの主な部分は、ウェアラブルからのメッセージのハンドラに存在する可能性があります。このハンドラは、収集時に測定値をヘルスコネクトに書き込みます。

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