ExerciseClient でエクササイズを記録する

ヘルスサービスは、ExerciseClient を通じてワークアウト アプリを最大限にサポートします。ExerciseClient により、アプリはエクササイズの進行状況の制御やエクササイズ目標の追加をしたり、エクササイズの状態やエクササイズ イベント、その他の必要な指標に関する最新情報を取得したりすることができます。詳細については、ヘルスサービスがサポートするエクササイズ タイプの一覧をご覧ください。

GitHub のエクササイズのサンプルをご覧ください。

依存関係を追加する

ヘルスサービスへの依存関係を追加するには、Google の Maven リポジトリをプロジェクトに追加する必要があります。詳細については、Google の Maven リポジトリをご覧ください。

次に、モジュール レベルの build.gradle ファイルで、次の依存関係を追加します。

Groovy

dependencies {
    implementation "androidx.health:health-services-client:1.1.0-alpha02"
}

Kotlin

dependencies {
    implementation("androidx.health:health-services-client:1.1.0-alpha02")
}

アプリの構造

ヘルスサービスでエクササイズ アプリを作成する場合は、次のアプリ構造を使用します。

ワークアウトの準備をするときや、ワークアウト中は、さまざまな理由でアクティビティが停止することがあります。ユーザーが別のアプリに切り替えたり、ウォッチフェイスに戻ったりする可能性があります。アクティビティの上に何かが表示される場合や、一定時間操作されず画面がオフになる場合があります。継続的に動作する ForegroundServiceExerciseClient を併用し、ワークアウト全体で正しく動作するようにします。

ForegroundService を使用すると、Ongoing Activity API を使用してウォッチフェイスにインジケーターを表示できるため、ユーザーはワークアウトにすばやく戻れます。

フォアグラウンド サービスで位置情報を適切にリクエストすることが重要です。マニフェスト ファイルで foregroundServiceType="location" を指定し、適切な権限を付与します。

prepareExercise() 呼び出しを含むワークアウト前アクティビティ、およびワークアウト アクティビティに AmbientLifecycleObserver を使用します。なお、背景モードのときは、ワークアウト中にディスプレイを更新しないでください。デバイス画面が背景モードの場合、ヘルスサービスは電力節約のためにワークアウト データをバッチ処理するので、表示される情報が最新ではない可能性があります。ワークアウト中は、ユーザーにとって意味のあるデータを表示し、最新情報または空白の画面を表示します。

機能を確認する

ExerciseType は、指標とエクササイズ目標について、特定のデータ型をサポートしています。これらの機能はデバイスによって異なる場合があるため、起動時に確認します。デバイスが特定のエクササイズ タイプをサポートしていない場合や、自動一時停止などの特定の機能をサポートしていない場合があります。また、デバイスの機能は、ソフトウェアの更新などによって、時間とともに変化する可能性があります。

アプリの起動時にデバイスの機能をクエリし、以下を保存して処理します。

  • プラットフォームがサポートするエクササイズ
  • 各エクササイズでサポートされている機能
  • 各エクササイズでサポートされているデータ型
  • 上記の各データ型に必要な権限

目的のエクササイズ タイプを指定して ExerciseCapabilities.getExerciseTypeCapabilities() を使用すると、そのタイプでリクエストできる指標、構成できるエクササイズ目標、利用できるその他の機能を確認できます。これを次の例に示します。

val healthClient = HealthServices.getClient(this /*context*/)
val exerciseClient = healthClient.exerciseClient
lifecycleScope.launch {
    val capabilities = exerciseClient.getCapabilitiesAsync().await()
    if (ExerciseType.RUNNING in capabilities.supportedExerciseTypes) {
        runningCapabilities =
            capabilities.getExerciseTypeCapabilities(ExerciseType.RUNNING)
    }
}

返される ExerciseTypeCapabilities 内で、supportedDataTypes は、データをリクエストできるデータ型をリストします。これはデバイスによって異なります。サポートされていない DataType をリクエストすると失敗することがあるため、リクエストしないように注意してください。

supportedGoals フィールドと supportedMilestones フィールドを使用して、作成するエクササイズ目標をエクササイズがサポートできるかどうか判断します。

ユーザーが自動一時停止を使用できるアプリの場合は、supportsAutoPauseAndResume を使用して、この機能がデバイスでサポートされていることを確認する必要があります。ExerciseClient は、デバイスでサポートされていないリクエストを拒否します。

次の例は、データ型 HEART_RATE_BPM、目標 STEPS_TOTAL、自動一時停止機能のサポートを確認しています。

// Whether we can request heart rate metrics.
supportsHeartRate = DataType.HEART_RATE_BPM in runningCapabilities.supportedDataTypes

// Whether we can make a one-time goal for aggregate steps.
val stepGoals = runningCapabilities.supportedGoals[DataType.STEPS_TOTAL]
supportsStepGoals = 
    (stepGoals != null && ComparisonType.GREATER_THAN_OR_EQUAL in stepGoals)

// Whether auto-pause is supported.
val supportsAutoPause = runningCapabilities.supportsAutoPauseAndResume

エクササイズ状態の更新に登録する

エクササイズの更新はリスナーに配信されます。アプリは一度に 1 つのリスナーしか登録できません。次の例に示すように、ワークアウトを開始する前にリスナーをセットアップしてください。リスナーは、アプリが所有するエクササイズに関する更新のみを受け取ります。

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        val exerciseStateInfo = update.exerciseStateInfo
        val activeDuration = update.activeDurationCheckpoint
        val latestMetrics = update.latestMetrics
        val latestGoals = update.latestAchievedGoals
    }

    override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {
        // For ExerciseTypes that support laps, this is called when a lap is marked.
    }

    override fun onAvailabilityChanged(
        dataType: DataType<*, *>,
        availability: Availability
    ) {
        // Called when the availability of a particular DataType changes.
        when {
            availability is LocationAvailability -> // Relates to Location/GPS.
            availability is DataTypeAvailability -> // Relates to another DataType.
        }
    }
}
exerciseClient.setUpdateCallback(callback)

エクササイズの期間を管理する

ヘルスサービスは、デバイス上のすべてのアプリで一度に最大 1 つのエクササイズをサポートします。あるエクササイズのトラッキング中に、別のアプリが新しいエクササイズのトラッキングを開始した場合、最初のエクササイズは終了します。

エクササイズを開始する前に、次のようにしてください。

  • すでにトラッキング中のエクササイズがないかどうかを確認し、それに応じて対応します。たとえば、ユーザーに確認を求めてから、前のエクササイズをオーバーライドして新しいエクササイズのトラッキングを開始します。

次の例は、getCurrentExerciseInfoAsync を使用して既存のエクササイズを確認する方法を示しています。

lifecycleScope.launch {
    val exerciseInfo = exerciseClient.getCurrentExerciseInfoAsync().await()
    when (exerciseInfo.exerciseTrackedStatus) {
        OTHER_APP_IN_PROGRESS -> // Warn user before continuing, will stop the existing workout.
        OWNED_EXERCISE_IN_PROGRESS -> // This app has an existing workout.
        NO_EXERCISE_IN_PROGRESS -> // Start a fresh workout.
    }
}

権限

ExerciseClient を使用する場合は、必要な権限をアプリでリクエストして維持する必要があります。アプリで LOCATION データを使用する場合は、それについても適切な権限をリクエストし、維持します。

すべてのデータ型について、prepareExercise() または startExercise() を呼び出す前に、次のことを行います。

  • AndroidManifest.xml ファイルで、リクエストされたデータ型に適切な権限を指定します。
  • ユーザーが必要な権限を付与していることを確認します。詳細については、アプリの権限をリクエストするをご覧ください。必要な権限が付与されていない場合、ヘルスサービスはリクエストを拒否します。

位置情報の場合は、さらに次の手順を実施します。

  • isProviderEnabled(LocationManager.GPS_PROVIDER) を使用して、デバイスで GPS が有効になっていることを確認します。必要に応じて、位置情報の設定を開くようユーザーに促します。
  • 適切な foregroundServiceType を持つ ForegroundService がワークアウト全体を通して維持されることを確認します。

ワークアウトの準備をする

GPS や心拍数などのセンサーは、ウォームアップに時間がかかる場合があります。また、ユーザーがワークアウト開始前にデータを確認しようとする場合もあります。オプションの prepareExerciseAsync() メソッドを使用すると、ワークアウトのタイマーを開始せずにこれらのセンサーをウォームアップし、データを受け取ることができます。activeDuration は、この準備時間の影響を受けません。

prepareExerciseAsync() を呼び出す前に、次の点を確認してください。

  • プラットフォーム全体の位置情報の設定を確認します。ユーザーはメインの [設定] メニューでこの設定を管理します。これはアプリレベルの権限確認とは異なります

    この設定がオフになっている場合は、位置情報へのアクセスが拒否されていることをユーザーに通知します。アプリで位置情報が必要な場合は、オンにするようユーザーに促します。

  • ボディセンサー、操作の認識、精細な位置情報に関する実行時の権限がアプリに付与されていることを確認します。権限がない場合は、適切な説明を添えて、実行時の権限を付与するようユーザーに促します。ユーザーが特定の権限を付与しない場合は、その権限に関連付けられているデータ型を prepareExerciseAsync() の呼び出しから削除します。ボディセンサーの権限も位置情報の権限も付与されていない場合は、prepareExerciseAsync() を呼び出さないでください。prepare 呼び出しは、エクササイズを開始する前に安定した心拍数や GPS 修正情報を取得するためのものです。それでもアプリは、歩数ベースの距離、ペース、速度や、これらの権限を必要としないその他の指標を取得できます。

以下を実施して、prepareExerciseAsync() の呼び出しが成功するようにします。

  • prepare 呼び出しを含むワークアウト前のアクティビティに AmbientLifecycleObserver を使用します。
  • フォアグラウンド サービスから prepareExerciseAsync() を呼び出します。サービス内になく、アクティビティのライフサイクルに関連付けられている場合、センサーの準備が不必要に停止される可能性があります。
  • ユーザーがワークアウト前のアクティビティから移動した場合は、endExercise() を呼び出してセンサーをオフにし、電力使用量を減らします。

次の例は、prepareExerciseAsync() を呼び出す方法を示しています。

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)
// Only necessary to call prepareExerciseAsync if body sensor or location
//permissions are given
exerciseClient.prepareExerciseAsync(warmUpConfig).await()

// Data and availability updates are delivered to the registered listener.

アプリが PREPARING 状態になると、センサーの可用性の更新が、ExerciseUpdateCallbackonAvailabilityChanged() を通じて配信されます。この情報はユーザーに提示され、ユーザーはワークアウトを開始するかどうかを決めることができます。

ワークアウトを開始する

エクササイズを開始するには、ExerciseConfig を作成し、エクササイズ タイプ、指標を受け取るデータ型、エクササイズの目標やマイルストーンを構成します。

エクササイズ目標は、DataType と条件から構成されます。エクササイズ目標は 1 回限りの目標であり、ユーザーが特定の距離を走ったときなど、条件が満たされたときにトリガーされます。エクササイズ マイルストーンを設定することもできます。エクササイズ マイルストーンは、ユーザーが設定した距離を超えてある点まで走るたびにトリガーするなど、複数回トリガーできます。

次のサンプルは、タイプごとに 1 つの目標を作成する方法を示しています。

const val CALORIES_THRESHOLD = 250.0
const val DISTANCE_THRESHOLD = 1_000.0 // meters

suspend fun startExercise() {
    // Types for which we want to receive metrics.
    val dataTypes = setOf(
        DataType.HEART_RATE_BPM,
        DataType.CALORIES_TOTAL,
        DataType.DISTANCE
    )

    // Create a one-time goal.
    val calorieGoal = ExerciseGoal.createOneTimeGoal(
        DataTypeCondition(
            dataType = DataType.CALORIES_TOTAL,
            threshold = CALORIES_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        )
    )

    // Create a milestone goal. To make a milestone for every kilometer, set the initial
    // threshold to 1km and the period to 1km.
    val distanceGoal = ExerciseGoal.createMilestone(
        condition = DataTypeCondition(
            dataType = DataType.DISTANCE_TOTAL,
            threshold = DISTANCE_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        ),
        period = DISTANCE_THRESHOLD
    )

    val config = ExerciseConfig(
        exerciseType = ExerciseType.RUNNING,
        dataTypes = dataTypes,
        isAutoPauseAndResumeEnabled = false,
        isGpsEnabled = true,
        exerciseGoals = mutableListOf<ExerciseGoal<Double>>(calorieGoal, distanceGoal)
    )
    exerciseClient.startExerciseAsync(config).await()
}

すべてのエクササイズでラップをマークすることもできます。ヘルスサービスは、ラップ期間に集計された指標を含む ExerciseLapSummary を提供します。

前の例で isGpsEnabled を使用していますが、これは位置情報をリクエストするときに true にする必要があります。ただし、GPS を使用することが他の指標に役立つ場合もあります。ExerciseConfig で距離が DataType として指定されている場合、距離の推定にはデフォルトで歩数が使用されます。必要に応じて GPS を有効にすると、代わりに位置情報を使用して距離を推定できます。

ワークアウトの一時停止、再開、終了

ワークアウトは、pauseExerciseAsync()endExerciseAsync() などの適切なメソッドを使用して、一時停止、再開、終了できます。

ExerciseUpdate の状態を信頼できる情報源として使用します。ワークアウトは、pauseExerciseAsync() の呼び出しが返されたときに一時停止したとみなされるのではなく、その状態が ExerciseUpdate メッセージに反映されたときに一時停止したとみなされます。UI の状態に関しては、これが特に重要です。ユーザーが一時停止を押した場合、一時停止ボタンを無効にし、ヘルスサービスで pauseExerciseAsync() を呼び出します。ヘルスサービスが ExerciseUpdate.exerciseStateInfo.state を使用して一時停止状態になるまで待ってから、ボタンを再開に切り替えます。これは、ヘルスサービスの状態の更新がボタンの押下より時間がかかる場合があることから、すべての UI 変更をボタンの押下に関連付けると、UI がヘルスサービスの状態と同期しなくなる可能性があるためです。

次の場合は、この点に注意してください。

  • 自動一時停止が有効になっている: ワークアウトは、ユーザーによる操作なしで一時停止または開始することがあります。
  • 別のアプリがワークアウトを開始する: ワークアウトは、ユーザーによる操作なしで終了することがあります。

アプリのワークアウトが別のアプリによって終了される場合、アプリはその終了を適切に処理する必要があります。

  • ユーザーの進捗状況が消去されないように、部分的なワークアウト状態を保存する。
  • 進行中のアクティビティのアイコンを削除し、ワークアウトが別のアプリによって終了されたことを知らせる通知をユーザーに送信する。

進行中のエクササイズの間に権限が取り消されるケースも処理します。これは、AUTO_END_PERMISSION_LOSTExerciseEndReason により、isEnded 状態を使用して送信されます。このケースは終了の場合と同様に処理します。部分的な状態を保存し、進行中のアクティビティのアイコンを削除して、状況を知らせる通知をユーザーに送信します。

次の例は、終了を正しく確認する方法を示しています。

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        if (update.exerciseStateInfo.state.isEnded) {
            // Workout has either been ended by the user, or otherwise terminated
        }
        ...
    }
    ...
}

アクティブ期間を管理する

エクササイズ中、アプリでワークアウトのアクティブ期間を表示できます。アプリ、ヘルスサービス、デバイスのマイクロ コントローラ ユニット(MCU、エクササイズの測定を担当する低消費電力プロセッサ)はすべて、現在の同じアクティブ期間で同期している必要があります。これに対処するために、ヘルスサービスは、アンカー ポイントを提供する ActiveDurationCheckpoint を送信します。アプリはこのアンカー ポイントからタイマーを開始できます。

アクティブ期間は MCU から送信され、アプリに到達するまでに少し時間がかかることがあるため、ActiveDurationCheckpoint には次の 2 つのプロパティがあります。

  • activeDuration: エクササイズがアクティブだった期間
  • time: 上記のアクティブ期間を計算した時刻

したがって、アプリでは、次の式を使用して ActiveDurationCheckpoint からエクササイズのアクティブ期間を算出できます。

(now() - checkpoint.time) + checkpoint.activeDuration

これで、MCU で計算されるアクティブ期間からアプリに到達するまでの間のわずかな差分が考慮されます。これはアプリのクロノメーターのシードに使用でき、アプリのタイマーがヘルスサービスと MCU の時刻に完全に一致するようになります。

エクササイズが一時停止された場合、アプリは、算出した時間が UI に現在表示されている時間を超えるまで、UI のタイマーの再開を待機します。これは、一時停止シグナルがわずかに遅れてヘルスサービスと MCU に到達するためです。たとえば、アプリが t=10 秒で一時停止した場合、ヘルスサービスは t=10.2 秒までアプリに PAUSED の更新を配信しない可能性があります。

ExerciseClient のデータを使用する

アプリが登録したデータ型の指標は ExerciseUpdate メッセージで配信されます。

プロセッサは、起動中、または最大レポート対象期間(150 秒ごとなど)に達した場合にのみメッセージを配信します。activeDuration でクロノメーターを進めるために ExerciseUpdate の頻度に依存しないでください。独立したクロノメーターの実装例については、GitHub のエクササイズのサンプルをご覧ください。

ユーザーがワークアウトを開始すると、ExerciseUpdate メッセージが頻繁に配信されることがあります(1 秒ごとなど)。ユーザーがワークアウトを開始した際、画面がオフになることがあります。ヘルスサービスは、メイン プロセッサが復帰しないように、同じ頻度でサンプリングされたデータをより少ない頻度で配信できます。ユーザーが画面を見たとき、バッチ処理中のデータがすぐにアプリに配信されます。

バッチ処理頻度を制御する

状況によっては、画面がオフのときにアプリが特定のデータ型を受信する頻度を制御する必要があります。BatchingMode オブジェクトを使用すると、アプリでデフォルトのバッチ処理動作をオーバーライドして、データ配信の頻度を上げられます。

バッチ処理頻度を設定する手順は次のとおりです。

  1. 特定の BatchingMode 定義がデバイスでサポートされているかどうかを確認します。

    // Confirm BatchingMode support to control heart rate stream to phone.
    suspend fun supportsHrWorkoutCompanionMode(): Boolean {
        val capabilities = exerciseClient.getCapabilities()
        return BatchingMode.HEART_RATE_5_SECONDS in
                capabilities.supportedBatchingModeOverrides
    }
    
  2. 次のコード スニペットに示すように、ExerciseConfig オブジェクトが特定の BatchingMode を使用するよう指定します。

    val config = ExerciseConfig(
        exerciseType = ExerciseType.WORKOUT,
        dataTypes = setOf(
            DataType.HEART_RATE_BPM,
            DataType.TOTAL_CALORIES
        ),
        // ...
        batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    )
    
  3. オプションで、ワークアウト期間中に特定のバッチ処理動作を継続する代わりに、ワークアウト中に BatchingMode を動的に設定できます。

    val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
    
  4. カスタマイズした BatchingMode を消去してデフォルトの動作に戻すには、exerciseClient.overrideBatchingModesForActiveExercise() に空のセットを渡します。

タイムスタンプ

各データポイントの現在時点は、デバイスが起動してからの経過時間を表します。これをタイムスタンプに変換する手順は次のとおりです。

val bootInstant =
    Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())

この値は、各データポイントの getStartInstant() または getEndInstant() で使用できます。

データの精度

データ型によっては、各データポイントに精度情報が関連付けられていることがあります。これは accuracy プロパティで表されます。

HrAccuracy クラスと LocationAccuracy クラスは、それぞれ HEART_RATE_BPM データ型と LOCATION データ型について入力されることがあります。存在する場合は accuracy プロパティを使用して、各データポイントがアプリにとって十分な精度であるかどうかを判断します。

データの保存とアップロード

ヘルスサービスから配信されたデータを永続化するには、Room を使用します。データのアップロードは、ワーク マネージャーなどのメカニズムを使用して、エクササイズの最後に行われます。これにより、データをアップロードするネットワーク呼び出しはエクササイズが終了するまで延期されるため、エクササイズ中の消費電力が最小限に抑えられ、処理が簡素化されます。

導入チェックリスト

ヘルスサービスの ExerciseClient を使用するアプリを公開する前に、次のチェックリストを参照して、ユーザー エクスペリエンスで一般的な問題が回避されることを確認してください。次の事項を確認します。

  • アプリは、アプリが実行されるたびにエクササイズ タイプの機能を確認し、デバイスの機能もチェックします。これにより、アプリが必要とするデータ型をサポートしていないデバイスやエクササイズを検出できます。
  • 必要な権限をリクエストして維持し、マニフェスト ファイルでそれらを指定します。アプリは prepareExerciseAsync() を呼び出す前に、実行時の権限が付与されていることを確認します。
  • アプリは getCurrentExerciseInfoAsync() を使用して、以下のケースを処理します。
    • エクササイズがすでにトラッキングされており、アプリが以前のエクササイズをオーバーライドする。
    • 別のアプリがエクササイズを終了しました。ユーザーがアプリを再度開くと、別のアプリが引き継いでエクササイズが停止したことを説明するメッセージが表示されます。
  • LOCATION データを使用している場合:
    • エクササイズ(準備の呼び出しを含む)の間、アプリは ForegroundService を対応する foregroundServiceType で維持します。
    • isProviderEnabled(LocationManager.GPS_PROVIDER) を使用してデバイスで GPS が有効になっていることを確認します。必要に応じて位置情報の設定を開くようユーザーに促します。
    • 低レイテンシの位置情報データの受信が非常に重要な厳しいユースケースでは、Fused Location Provider(FLP)を統合し、そのデータを初期位置情報として使用することを検討してください。ヘルスサービスからより安定した位置情報を利用できる場合は、FLP の代わりにその位置情報を使用します。
  • アプリでデータのアップロードが必要な場合、データをアップロードするためのネットワーク呼び出しは、エクササイズが終了するまで延期されます。そうしないと、エクササイズ全体を通して、アプリは必要なネットワーク呼び出しを控えめに実行します。