本指南適用於 Health Connect 1.1.0-alpha12 以上版本。
Health Connect 提供預定運動資料類型,讓訓練應用程式寫入訓練計畫,並讓健身應用程式讀取訓練計畫。系統可讀取記錄的運動 (健身) 資料,進行個人化成效分析,協助使用者達成訓練目標。
查看健康資料同步的適用情形
嘗試使用健康資料同步前,應用程式應先確認使用者裝置上是否支援健康資料同步。部分裝置可能未預先安裝健康資料同步,或可能已停用。您可以使用 HealthConnectClient.getSdkStatus() 方法檢查是否可用。
如何查看健康資料同步的適用情形
fun checkHealthConnectAvailability(context: Context) { val providerPackageName = "com.google.android.apps.healthdata" 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 商店安裝或更新健康資料同步。
功能適用情況
如要判斷使用者裝置是否支援 Health Connect 訓練計畫,請檢查用戶端是否提供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_EXERCISEandroid.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 要求這些權限。系統接著會顯示 Health Connect 權限畫面。
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_EXERCISEandroid.permission.health.READ_EXERCISE_ROUTESandroid.permission.health.READ_HEART_RATEandroid.permission.health.WRITE_EXERCISEandroid.permission.health.WRITE_EXERCISE_ROUTEandroid.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:與這項運動計畫相關的運動課程完成後,系統會自動寫入這個 ID。
預定運動時段記錄包含的資訊
預定運動區塊包含運動步驟清單,可重複執行不同組步驟 (例如連續五次進行手臂彎舉、波比跳和仰臥起坐)。
- 區塊說明。
- 預定運動步驟清單。
- 重複次數。
預定運動步驟記錄包含的資訊
支援的匯總
這個資料類型不支援任何匯總。
使用範例
假設使用者打算兩天後跑步 90 分鐘,這項跑步活動會繞湖跑三圈,目標心率介於 90 到 110 bpm 之間。
- 使用者在訓練計畫應用程式中定義的預定運動課程,包含下列項目:
- 預定開始和結束時間
- 運動類型 (跑步)
- 趟數 (重複次數)
- 心率表現目標 (介於 90 至 110 bpm)
- 這項資訊會分組為運動區塊和步驟,並由訓練計畫應用程式以
PlannedExerciseSessionRecord的形式寫入健康資料同步。 - 使用者執行預定課程 (跑步)。
- 與課程相關的運動資料會透過下列方式記錄:
- 在課程期間由穿戴式裝置測量。例如心率。這項資料會以活動記錄類型的形式寫入健康資料同步。在本例中為
HeartRateRecord。 - 由使用者在工作階段結束後手動更新。例如,指出實際執行的開始和結束時間。這項資料會以
ExerciseSessionRecord的形式寫入 Health Connect。
- 在課程期間由穿戴式裝置測量。例如心率。這項資料會以活動記錄類型的形式寫入健康資料同步。在本例中為
- 之後,訓練計畫應用程式會從健康資料同步讀取資料,根據使用者在計畫運動課程中設定的目標,評估實際效能。
規劃演練並設定目標
使用者可以規劃日後的運動並設定目標。將這項資料以預定運動時段的形式寫入健康資料同步。
在「使用範例」一節所述的範例中,使用者計畫在兩天後跑步 90 分鐘。這次跑步會繞湖三圈,目標心率介於 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.activelyRecorded( 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) }
記錄運動和活動資料
兩天後,使用者記錄實際運動。將這項資料以運動時段的形式寫入 Health Connect。
在本例中,使用者的工作階段持續時間與預定時間長度完全一致。
下列程式碼片段可能位於應用程式的表單處理常式中,該應用程式會將運動工作階段記錄到健康資料同步。此外,如果穿戴式裝置能偵測及記錄運動課程,則資料擷取和匯出處理常式中也可能會有這類資料。
這裡的 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.activelyRecorded( 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.activelyRecorded( 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()
)