本指南適用於「健康資料同步」1.1.0-alpha11 版。
健康資料同步提供「預定運動」資料類型,讓訓練應用程式可寫入訓練計畫,運動應用程式則可讀取訓練計畫。系統會讀取記錄的運動 (訓練) 資料,進行個人化運動成效分析,協助使用者達成訓練目標。
功能適用情況
如要判斷使用者的裝置是否支援「健康資料同步」的訓練計畫,請檢查用戶端是否提供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
要求這些權限。系統接著會顯示 Health Connect 權限畫面。
// 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)
}
}
由於使用者可以隨時授予或撤銷權限,應用程式需要定期檢查獲得的權限,並處理權限遺失的情況。
相關權限
訓練計畫會連結至運動訓練。因此,使用者必須授予使用與訓練計畫相關的各項記錄類型權限,才能充分運用這項 Health Connect 功能。
舉例來說,如果訓練計畫會在一連串跑步活動中測量使用者的心率,開發人員可能需要聲明下列權限,且使用者必須授予這些權限,才能寫入運動活動並讀取結果,以供日後評估:
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:完成與這項預定運動訓練工作階段相關的運動訓練工作階段後,系統會自動填寫這項資訊。
預定運動區塊記錄包含的資訊
預先規劃的運動區塊包含運動步驟清單,可支援重複執行不同群組的步驟 (例如連續五次進行一系列的屈臂彎舉、波比跳和仰臥起坐)。
- 區塊說明。
- 預計運動步驟清單。
- 重複次數。
預定運動步驟記錄包含的資訊
支援的匯總
這個資料類型不支援任何匯總。
使用範例
假設使用者打算在兩天後跑步 90 分鐘,這次跑步活動將繞湖三圈,目標心率介於 90 到 110 bpm 之間。
- 使用者在訓練計畫應用程式中定義的預定運動課程如下:
- 跑步的預計開始和結束時間
- 運動類型 (跑步)
- 趟數 (重複次數)
- 心率的目標範圍 (90 到 110 bpm)
- 這些資訊會分組為運動區塊和步驟,並由訓練計畫應用程式以
PlannedExerciseSessionRecord
形式寫入「健康資料同步」。 - 使用者執行預先規劃的訓練 (跑步)。
- 系統會透過下列方式記錄與運動相關的資料:
- 在運動期間使用穿戴式裝置。例如心率。
這項資料會以活動的記錄類型寫入「健康資料同步」。在這種情況下,
HeartRateRecord
。 - 使用者在工作階段結束後手動操作。例如,指出實際跑步的開始和結束時間。這項資料會以
ExerciseSessionRecord
形式寫入健康資料同步。
- 在運動期間使用穿戴式裝置。例如心率。
這項資料會以活動的記錄類型寫入「健康資料同步」。在這種情況下,
- 稍後,訓練計畫應用程式會從「健康資料同步」讀取資料,根據使用者在規劃的運動時段中設定的目標,評估實際表現。
規劃運動並設定目標
使用者可以規劃未來的運動並設定目標。將這項資料寫入「健康資料同步」,做為預定運動時段。
在「範例用法」一節所述的範例中,使用者打算在兩天後跑步 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.
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()
記錄運動和活動資料
兩天後,使用者記錄實際的運動時段。將這項資料寫入「健康資料同步」,做為運動時段。
在本例中,使用者工作階段時間長度與預計時間長度完全一致。
如果應用程式會將運動工作階段記錄到「健康資料同步」,您可能會在表單處理常式中看到下列程式碼片段。如果穿戴式裝置可以偵測及記錄運動時數,您也可能會在資料擷取和匯出處理常式中找到這項資訊。
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"
),
// ... 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
)