トレーニング計画

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

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

ヘルスコネクト アプリを使用する前に、ユーザーのデバイスでヘルスコネクトが利用可能であることを確認する必要があります。ヘルスコネクトは一部のデバイスにおいて、プリインストールされていない場合や、無効になっている場合があります。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 マニフェストで宣言されていることを確認します。

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

予定されているエクササイズ ブロックのレコードに含まれる情報

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

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

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