トレーニング計画

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

ヘルスコネクトを利用できるか確認する

ヘルスコネクト アプリを使用する前に、ユーザーのデバイスでヘルスコネクトが利用可能であることを確認する必要があります。ヘルスコネクトは一部のデバイスにおいて、プリインストールされていない場合や、無効になっている場合があります。HealthConnectClient.getSdkStatus() メソッドを使用して、利用可能かどうかを確認できます。

ヘルスコネクトを利用できるか確認する方法

fun checkHealthConnectAvailability(context: Context) {
    val providerPackageName = "com.google.android.apps.healthdata" // Or get from HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
    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 ストアからヘルスコネクトをインストールまたは更新するようユーザーに案内できます。

機能の提供状況

ユーザーのデバイスがヘルスコネクトのトレーニング プランに対応しているかどうかを判断するには、クライアントで 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 マニフェストで最初に宣言されていることを確認してください。

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

予定されたエクササイズ ブロックの記録に含まれる情報

予定されたエクササイズ ブロックには、さまざまなステップのグループの繰り返しをサポートするエクササイズ ステップのリストが含まれています(たとえば、アームカール、バーピー、クランチのシーケンスを 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 に設定します。

このようなスニペットは、予定されているエクササイズ セッションをヘルスコネクトに記録するアプリのフォーム ハンドラで見つかることがあります。また、トレーニングを提供するサービスなど、統合の取り込みポイントで検出されることもあります。

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

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

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

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

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

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