Wear OSでExerciseClientを使用して自転車の走行情報を取得する

# はじめに
なんとなく思い立ったのでWear OSのアプリを作った。誰かの参考になれば良いので公開します。

### 環境
- Wear OS 3 （Wear OS by Google）
- SKAGEN Falster Gen6

### 要件
- ユーザーはある自転車のライドにおける総走行距離、総消費カロリー、最高速度を取得できる

### 完成品
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1092570/96d0d5f2-20c9-46b1-73c6-be48233da542.png" width=300/><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1092570/38772e64-6489-4aff-4ed5-0678789f86b8.png" width=300/>

# ExerciseClientの初期設定
### ExerciseTypeを初期化
```kotlin
val exerciseClient: ExerciseClient = HealthServices.getClient(context).exerciseClient
```


### 端末で該当のDataTypeをサポートしているか確認する
```kotlin
val necessaryDataTypes = setOf(
    DataType.SPEED_STATS,
    DataType.CALORIES_TOTAL,
    DataType.PACE_STATS,
    DataType.DISTANCE_TOTAL,
)

 // 引数にExerciseClientと↑のnecessaryDataTypesを渡す 
suspend fun isAllNecessaryDataTypesAvailable(
    exerciseClient: ExerciseClient,
    necessaryDataTypes: Set<DataType<*, *>>
): Boolean {
    val capabilities = exerciseClient.getCapabilitiesAsync().awaitWithException().getExerciseTypeCapabilities(ExerciseType.BIKING)
    return necessaryDataTypes.all { it in capabilities.supportedDataTypes }
}
```


### 端末で自転車の走行情報の計測が対応しているか確認する
```kotlin
suspend fun isBikingSupported(exerciseClient: ExerciseClient): Boolean {
    val capabilities = exerciseClient.getCapabilitiesAsync().awaitWithException()
    return ExerciseType.BIKING in capabilities.supportedExerciseTypes
}
```

### 端末で別のエクササイズが進行していないか確認する
```kotlin
suspend fun existsNoOtherExerciseInProgress(exerciseClient: ExerciseClient): Boolean {
    val exerciseInfo = exerciseClient.getCurrentExerciseInfoAsync().awaitWithException()
    return exerciseInfo.exerciseTrackedStatus == NO_EXERCISE_IN_PROGRESS
}
```
### 走行情報を取得するためのコールバックを設定する
```kotlin
private fun ExerciseClient.setExerciseUpdateCallback() {
        val callback = object : ExerciseUpdateCallback {
            override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
                val latestMetrics = update.latestMetrics
                latestMetrics.getData(DataType.DISTANCE_TOTAL)?.total?.let { distance ->
                    // こんな感じでそれぞれのDataType毎のデータを取得する
                }
            }

            override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {
                // no-op
            }

            override fun onRegistered() {
                // no-op
            }

            override fun onRegistrationFailed(throwable: Throwable) {
                // 省略
            }

            override fun onAvailabilityChanged(
                dataType: DataType<*, *>,
                availability: Availability
            ) {
                // 省略
            }
        }
        this.setUpdateCallback(callback)
    }
```

# アプリのPermission周りの設定
必要な権限は以下です。これらを適宜権限許諾リクエストをしてください。

- ForeGroundServiceを使用するための権限
- アプリからの通知送信権限
- ボディセンサーの使用権限
- [アクティビティ開始終了の検出権限](https://developer.android.com/develop/sensors-and-location/location/transitions)

詳しくは[こちら](https://qiita.com/nemo-855/items/1dc120b4ba0158dd8033)

# 動作確認のためのadbコマンド
次は動作確認です。自転車で走り回るのも良いですが、adbコマンドで走行情報を送信することもできます。以下のような項目を組み合わせて任意の走行情報を作ることができます。単位など詳しくは[こちら](https://developer.android.com/health-and-fitness/guides/health-services/simulated-data#custom-activity)になります。

- エクササイズの総時間
- 心拍数
- 平均速度
- 既定のルートを使用して位置データを出力するかどうか
- 最大標高変化率

それを踏まえて、走行速度10m/s、総時間3,600sの走行情報を作成すると、↓のようになります。

```shell
adb shell am broadcast \
-a "whs.synthetic.user.START_EXERCISE" \
--ei exercise_options_duration_secs 3600 \
--ef exercise_options_average_speed 10 \
com.google.android.wearable.healthservices
```

# 終わりに
今回の成果物は[こちら](https://github.com/nemo-855/VeloonWatch)のActivitySensorImpl辺りに載せてあります！興味ある方はこちらも合わせてどうぞ。

楽しいWear OS & サイクリングライフを！

# 参考文献
https://github.com/nemo-855/VeloonWatch

https://developer.android.com/health-and-fitness/guides/health-services/active-data

https://developer.android.com/health-and-fitness/guides/health-services/simulated-data#use_synthetic_data_on_wear_os_3

https://developer.android.com/reference/androidx/health/services/client/data/ExerciseUpdate