訓練計畫

「健康資料同步」提供預定運動資料類型,讓訓練應用程式寫入訓練計畫,運動應用程式則可讀取訓練計畫。系統會讀取記錄的運動,進行個人化表現分析,協助使用者達成訓練目標。

確認「健康資料同步」適用情形

嘗試使用健康資料同步前,應用程式應先確認使用者的裝置是否支援健康資料同步。部分裝置可能未預先安裝「健康資料同步」或已停用這項服務。 您可以使用 HealthConnectClient.getSdkStatus() 方法檢查可用性。

如何查看「健康資料同步」是否適用

fun checkHealthConnectAvailability(context: Context) {
    val providerPackageName = "com.google.android.apps.healthdata" // Or get from HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
    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 商店安裝或更新健康資料同步。

功能適用情況

如要判斷使用者的裝置是否支援「健康資料同步」的訓練計畫,請檢查用戶端是否提供 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 資訊清單中聲明該組權限。

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 要求這些權限。系統會顯示「健康資料同步」權限畫面。
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_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:完成與這項預定運動訓練相關的運動訓練後,系統會自動填寫這個 ID。

預定運動區塊記錄包含的資訊

預先規劃的運動區塊包含運動步驟清單,可支援重複執行不同群組的步驟 (例如連續做五次二頭彎舉、波比跳和仰臥起坐)。

計畫運動步驟記錄包含的資訊

支援的匯總

這個資料類型不支援任何匯總。

使用範例

假設使用者打算在兩天後跑步 90 分鐘,這次跑步活動將繞湖三圈,目標心率介於 90 到 110 bpm 之間。

  1. 使用者在訓練計畫應用程式中定義的預定運動課程如下:
    1. 預計的跑步開始和結束時間
    2. 運動類型 (跑步)
    3. 趟數 (重複次數)
    4. 心率的目標範圍 (90 到 110 bpm)
  2. 這項資訊會依運動區間和步數分組,並由訓練計畫應用程式以 PlannedExerciseSessionRecord 形式寫入「健康資料同步」。
  3. 使用者執行預先規劃的訓練 (跑步)。
  4. 系統會透過下列方式記錄與運動時段相關的運動資料:
    1. 由穿戴式裝置在活動期間提供,例如心率。這類資料會以活動的記錄類型寫入「健康資料同步」,在本例中為 HeartRateRecord
    2. 使用者在活動結束後手動輸入,例如指出實際跑步的開始和結束時間。這類資料會以 ExerciseSessionRecord 形式寫入「健康資料同步」。
  5. 稍後,訓練計畫應用程式會從「健康資料同步」讀取資料,根據使用者在規劃的運動時段中設定的目標,評估實際表現。

規劃運動並設定目標

使用者可能會規劃未來的運動行程並設定目標。請將這項資訊以預定運動活動的形式寫入「健康資料同步」。

如「範例用法」一節所述,使用者計畫在兩天後進行 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(
        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(
        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(
        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
    }
}

寫入子類型資料

時段也可以包含選填的子類型資料,運用額外資訊提供更豐富的時段資料。

舉例來說,運動時段可包含 ExerciseSegmentExerciseLapExerciseRoute 類別:

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()
)