Android-ヘルスコネクトを使って単位時間ごとのデータを取得する

## この記事はどんな内容？
- Android(Kotlin)で[ヘルスコネクト](https://developer.android.com/guide/health-and-fitness/health-connect?hl=ja)を使ってヘルスケアデータ(歩数)を取得します

## この記事でやることの詳細
- ヘルスコネクトを介して、2種類の方法でGoogleFit経由で取得している歩数データを取得します
    - 基本的なデータのとり方
    - 単位時間(今回は1日)ごとのデータのとり方


## 基本的なデータのとり方
[HealthConnectClient.readRecords()](https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#readRecords(androidx.health.connect.client.request.ReadRecordsRequest))を使用して、取得したいデータ型と期間だけを指定してデータを取得します。

```kotlin
        val startTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
        val endTime  = LocalDateTime.of(2024, 1, 6, 0, 0, 0)

        val response  = client.readRecords(
            ReadRecordsRequest(
                StepsRecord::class,
                timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
            )
        )
        var step = 0L
        for (data in response.records){
            step += data.count
        }
```

この形式だと返ってくるデータ(ReadRecordsResponse)の取得単位がまちまちです(1分単位だったり10分以上間隔があいたり)

## 単位時間ごとのデータのとり方
[HealthConnectClient.aggregateGroupByDuration()](https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest))を使用して、取得したいデータ型と期間、データのを指定してデータを取得します。
特定アプリで取得したデータだけを返す設定も追加しています

```kotlin
        val startTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
        val endTime  = LocalDateTime.of(2024, 1, 6, 0, 0, 0)

        val response  = client.aggregateGroupByDuration(
            AggregateGroupByDurationRequest(
                metrics = setOf(StepsRecord.COUNT_TOTAL),
                timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
                timeRangeSlicer = Duration.ofDays(1),
                // Note: Google Fitで取得したデータだけとるようにする
                dataOriginFilter = setOf(DataOrigin("com.google.android.apps.fitness"))
            )
        )
        for (data in response){
            step += data.result[StepsRecord.COUNT_TOTAL] ?: 0L
        }
        
```

1日ごとの5つのデータ、5日分のデータが返ってきます


## 今回のサンプルコード
```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val  HEALTH_CONNECT_PERMISSIONS = setOf(
        HealthPermission.getReadPermission(StepsRecord::class)
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onStart() {
        super.onStart()
        val healthConnectClient: HealthConnectClient = getHealthConnectClient() ?: return

        displayHealthConnectSDKStatus(onOKClick = {
            lifecycleScope.launch {
                requestPermission(healthConnectClient)
            }
        })
    }

    private fun displayHealthConnectSDKStatus(onOKClick: () -> Unit){
        val status = getHealthConnectSDKStatus()
        var text = "";

        when (status){
            SDK_AVAILABLE  -> {
                text = "API利用可能です"
            }
            SDK_UNAVAILABLE ->{
                text = "SDKを利用できません"
            }
            SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED ->{
                text = "プロバイダーがインストールされていません"
            }
        }

        AlertDialog.Builder(this)
            .setTitle("API利用可否")
            .setTitle(text)
            .setPositiveButton("OK", DialogInterface.OnClickListener { _, _ ->
                onOKClick()
            })
            .show()
    }

    private fun getHealthConnectSDKStatus(): Int{
        // Note: ProviderNameは無くてもいい
        // AndroidManifestに入れたヘルスコネクトパッケージ名を入れている
        val providerName = "com.google.android.apps.healthdata"
        return HealthConnectClient.getSdkStatus(this, providerName)
    }

    private fun getHealthConnectClient():  HealthConnectClient?{
        if  (getHealthConnectSDKStatus() == SDK_AVAILABLE){
            return HealthConnectClient.getOrCreate(this)
        }
        return null
    }

    private fun onPermissionAcceptedAction(){
        lifecycleScope.launch{
            val healthConnectClient: HealthConnectClient = getHealthConnectClient() ?: return@launch
            val step = readStepRecord(healthConnectClient)
            Toast.makeText(this@MainActivity,  "歩数は"+step+"歩", Toast.LENGTH_LONG).show()
        }
    }

    //  NOTE: registerForActivityResultを使う場合はライフサイクルがSTARTED前に定義しないといけないため、プロパティとして定義する
    private val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()
    private val requestPermissions = registerForActivityResult(requestPermissionActivityContract) {granted ->
        if(granted.containsAll(HEALTH_CONNECT_PERMISSIONS)){
            Toast.makeText(this,  "全ての権限があります", Toast.LENGTH_LONG).show()
            onPermissionAcceptedAction()
        }else{
            Toast.makeText(this,  "権限が許可されませんでした", Toast.LENGTH_LONG).show()
        }
    }

    private suspend fun requestPermission(client: HealthConnectClient){
        val granted = client.permissionController.getGrantedPermissions()

        if (granted.containsAll(HEALTH_CONNECT_PERMISSIONS)){
            Log.d("MyLog", "権限は全て付与")
            onPermissionAcceptedAction()
        }else{
            Log.d("MyLog", "権限はありません")
            requestPermissions.launch(HEALTH_CONNECT_PERMISSIONS)
        }
    }


    private suspend fun readStep(client: HealthConnectClient, startTime: LocalDateTime, endTime: LocalDateTime): Long{
        var step = 0L;
        try {
            val response  = client.readRecords(
                ReadRecordsRequest(StepsRecord::class,
                    timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
                    dataOriginFilter = emptySet()))
            for (data in response.records){
                step += data.count
            }
        }catch (e: Exception){
            Log.d("MyLog", "歩数取得に失敗")
        }
        return step
    }

    // NOTE:  https://developer.android.com/guide/health-and-fitness/health-connect/common-workflows/aggregate-data?hl=ja
    private suspend  fun readDurationStep(client: HealthConnectClient, startTime: LocalDateTime, endTime: LocalDateTime): Long{
        var step = 0L;
        try {
            val response  = client.aggregateGroupByDuration(
                AggregateGroupByDurationRequest(
                    metrics = setOf(StepsRecord.COUNT_TOTAL),
                    timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
                    timeRangeSlicer = Duration.ofDays(1),
                    // Note: Google Fitで取得したデータだけとるようにする
                    dataOriginFilter = setOf(DataOrigin("com.google.android.apps.fitness"))
                )
            )
            for (data in response){
                step += data.result[StepsRecord.COUNT_TOTAL] ?: 0L
            }

        }catch (e: Exception){
            Log.d("MyLog", "歩数取得に失敗")
        }
        return step
    }

    private suspend fun readStepRecord(client: HealthConnectClient): Long{
        val startTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
        val endTime  = LocalDateTime.of(2024, 1, 6, 0, 0, 0)

        // return readStep(client, startTime, endTime)
        return readDurationStep(client, startTime, endTime)
    }
}
```