使用 ExerciseClient 記錄運動情形

健康照護服務能夠透過 ExerciseClient 為健身應用程式提供頂級支援功能。應用程式可以使用 ExerciseClient 控制運動進行的時間、新增運動目標,並接收最新的運動狀態、運動事件或其他必要指標。詳情請參閱健康照護服務支援的運動類型完整清單。

請參閱 GitHub 上的運動範例

新增依附元件

如要為健康照護服務新增依附元件,必須將 Google Maven 存放區新增至專案。詳情請參閱「Google 的 Maven 存放區」一文。

接下來,在模組層級的 build.gradle 檔案中新增以下依附元件:

Groovy

dependencies {
    implementation "androidx.health:health-services-client:1.1.0-alpha02"
}

Kotlin

dependencies {
    implementation("androidx.health:health-services-client:1.1.0-alpha02")
}

應用程式結構

如果想用健康照護服務建構運動應用程式,請採用以下應用程式結構:

準備健身和健身期間,活動可能會因各種原因而停止。使用者可能會切換到其他應用程式,或返回錶面。系統可能會顯示蓋住活動的內容,或是螢幕可能會在閒置一段時間後關閉。請用持續執行的 ForegroundService 搭配 ExerciseClient,確保應用程式可以在健身過程中保持正確運作。

只要使用 ForegroundService,就能運用 Ongoing Activity API 在錶面顯示指標,方便使用者快速回到健身活動。

請務必在前景服務中正確要求位置資料。在資訊清單檔案中指定 foregroundServiceType="location",以及合適的權限

請在包含 prepareExercise() 呼叫和健身活動的健身前活動中使用 AmbientLifecycleObserver。但如果健身期間使用的是微光模式,則請不要更新顯示畫面。這是因為裝置螢幕處於微光模式時,健康照護服務會批次處理運動資料以節省電力,因此顯示的可能不是最新資訊。請在健身期間顯示對使用者有意義的資料,建議顯示最新資訊,不然就將畫面留空。

檢查功能

每項 ExerciseType 都會支援適用於指標和運動目標的特定資料類型。由於每部裝置支援的功能可能有所差異,請在啟動時檢查這些功能。有些裝置可能不支援特定運動類型,或是不支援特定功能,例如自動暫停。另外,有些裝置的功能可能會隨時間變動,例如軟體更新後的異動。

在應用程式啟動時查詢裝置功能,並儲存及處理下列項目:

  • 平台支援的運動。
  • 每項運動支援的功能。
  • 每項運動支援的資料類型。
  • 每種資料類型的必要權限。

搭配所需運動類型使用 ExerciseCapabilities.getExerciseTypeCapabilities(),瞭解可以要求哪些指標、能設定什麼運動目標,以及該類型可用的其他功能。例如:

val healthClient = HealthServices.getClient(this /*context*/)
val exerciseClient = healthClient.exerciseClient
lifecycleScope.launch {
    val capabilities = exerciseClient.getCapabilitiesAsync().await()
    if (ExerciseType.RUNNING in capabilities.supportedExerciseTypes) {
        runningCapabilities =
            capabilities.getExerciseTypeCapabilities(ExerciseType.RUNNING)
    }
}

supportedDataTypes 會在回傳的 ExerciseTypeCapabilities 當中列出可要求取得資料的資料類型。各裝置的情形各有不同,請勿要求未受支援的 DataType,否則要求可能會失敗。

使用 supportedGoalssupportedMilestones 欄位,判斷運動能否符合您想建立的運動目標。

如果應用程式允許使用者使用自動暫停功能,您必須使用 supportsAutoPauseAndResume 檢查裝置是否支援此功能。ExerciseClient 會拒絕裝置不支援的要求。

以下範例會檢查是否支援 HEART_RATE_BPM 資料類型、STEPS_TOTAL 目標功能和自動暫停功能:

// Whether we can request heart rate metrics.
supportsHeartRate = DataType.HEART_RATE_BPM in runningCapabilities.supportedDataTypes

// Whether we can make a one-time goal for aggregate steps.
val stepGoals = runningCapabilities.supportedGoals[DataType.STEPS_TOTAL]
supportsStepGoals = 
    (stepGoals != null && ComparisonType.GREATER_THAN_OR_EQUAL in stepGoals)

// Whether auto-pause is supported.
val supportsAutoPause = runningCapabilities.supportsAutoPauseAndResume

註冊運動狀態更新內容

運動更新內容會傳遞給事件監聽器。應用程式一次只能註冊一組事件監聽器。如以下範例所示,在開始健身之前設定事件監聽器。事件監聽器只能接收應用程式擁有的運動更新。

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        val exerciseStateInfo = update.exerciseStateInfo
        val activeDuration = update.activeDurationCheckpoint
        val latestMetrics = update.latestMetrics
        val latestGoals = update.latestAchievedGoals
    }

    override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {
        // For ExerciseTypes that support laps, this is called when a lap is marked.
    }

    override fun onAvailabilityChanged(
        dataType: DataType<*, *>,
        availability: Availability
    ) {
        // Called when the availability of a particular DataType changes.
        when {
            availability is LocationAvailability -> // Relates to Location/GPS.
            availability is DataTypeAvailability -> // Relates to another DataType.
        }
    }
}
exerciseClient.setUpdateCallback(callback)

管理運動生命週期

在同一裝置所有的應用程式中,健康照護服務一次最多只能支援一個運動。如果系統正在追蹤運動,且其他應用程式開始追蹤新的運動,則第一項運動會終止。

開始運動之前,請執行下列步驟:

  • 請先檢查系統是否正在追蹤某項運動,並按照情況做出回應。例如,覆寫先前運動並開始追蹤新運動前,請先要求使用者確認。

以下範例說明如何使用 getCurrentExerciseInfoAsync 檢查現有的運動:

lifecycleScope.launch {
    val exerciseInfo = exerciseClient.getCurrentExerciseInfoAsync().await()
    when (exerciseInfo.exerciseTrackedStatus) {
        OTHER_APP_IN_PROGRESS -> // Warn user before continuing, will stop the existing workout.
        OWNED_EXERCISE_IN_PROGRESS -> // This app has an existing workout.
        NO_EXERCISE_IN_PROGRESS -> // Start a fresh workout.
    }
}

權限

使用 ExerciseClient 時,請確認應用程式可以要求並維持必要的權限。如果您的應用程式使用 LOCATION 資料,請確認該應用程式也會要求並保留適當的權限。

所有資料類型在呼叫 prepareExercise()startExercise() 之前,都應執行以下操作:

  • AndroidManifest.xml 檔案中,為要求的資料類型指定適當的權限。
  • 驗證使用者是否已授予必要權限。詳情請參閱「要求應用程式權限」。如果使用者尚未授予必要權限,健康照護服務便會拒絕受理要求。

若是位置資料,請再另外進行以下操作:

準備健身

GPS 或心率等部分感應器可能需要短暫暖機時間,或使用者可能會想查看健身開始前的資料。在不啟動健身計時器的情況下,可視需要使用 prepareExerciseAsync() 方法讓感應器暖機並接收資料。這段準備時間不會影響 activeDuration

呼叫 prepareExerciseAsync() 之前,請先檢查下列事項:

  • 檢查平台的位置資訊設定。使用者可在主要「設定」選單中控制這項設定;這與應用程式層級權限檢查不同。

    如果這項設定處於關閉狀態,請通知使用者,他們已拒絕授予位置存取權。如果應用程式需要使用位置資訊,請提示使用者啟用這項設定。

  • 確認應用程式具備人體感應器、動作辨識以及精確位置的執行階段權限。如果缺少任何權限,請指示使用者授予執行階段權限,並提供合適的背景知識。如果使用者沒有授予特定權限,請從對 prepareExerciseAsync() 的呼叫中移除與該權限相關的資料類型。如果使用者並未授予人體感應器和位置的存取權,請不要呼叫 prepareExerciseAsync(),因為 Prepare 呼叫的用途是在開始運動前,取得穩定心率或 GPS 定位。應用程式仍能取得以步數計算的距離、配速、速度及其他不需透過這些權限取得的指標。

請遵守下列規範,確保您可以成功呼叫 prepareExerciseAsync()

  • 若有內含 prepare 呼叫的健身前活動,請用 AmbientLifecycleObserver
  • 請從前景服務呼叫 prepareExerciseAsync()。如果該項目不在服務內,並和活動生命週期相關聯,系統可能會無端終止感應器的準備工作。
  • 呼叫 endExercise() 關閉感應器,並在使用者從健身前活動導覽到其他地方時降低耗電量。

以下範例說明了如何呼叫 prepareExerciseAsync()

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)
// Only necessary to call prepareExerciseAsync if body sensor or location
//permissions are given
exerciseClient.prepareExerciseAsync(warmUpConfig).await()

// Data and availability updates are delivered to the registered listener.

應用程式進入 PREPARING 狀態後,系統便會透過 onAvailabilityChanged()ExerciseUpdateCallback 中傳遞能否使用感應器的更新資訊。然後,系統即可向使用者顯示這項資訊,讓使用者決定是否要開始健身。

開始健身

當您想開始運動時,請建立 ExerciseConfig 設定運動類型、欲用來接收指標的資料類型,以及所有運動目標或里程碑。

運動目標中含有 DataType 及條件。運動目標是僅限單次的目標,符合條件後就會觸發 (例如使用者跑了特定距離)。同樣可設定運動里程碑。運動里程碑可能會多次觸發,例如,每次使用者跑到某個點超過其設定的距離時。

以下範例說明如何建立各類型的目標:

const val CALORIES_THRESHOLD = 250.0
const val DISTANCE_THRESHOLD = 1_000.0 // meters

suspend fun startExercise() {
    // Types for which we want to receive metrics.
    val dataTypes = setOf(
        DataType.HEART_RATE_BPM,
        DataType.CALORIES_TOTAL,
        DataType.DISTANCE
    )

    // Create a one-time goal.
    val calorieGoal = ExerciseGoal.createOneTimeGoal(
        DataTypeCondition(
            dataType = DataType.CALORIES_TOTAL,
            threshold = CALORIES_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        )
    )

    // Create a milestone goal. To make a milestone for every kilometer, set the initial
    // threshold to 1km and the period to 1km.
    val distanceGoal = ExerciseGoal.createMilestone(
        condition = DataTypeCondition(
            dataType = DataType.DISTANCE_TOTAL,
            threshold = DISTANCE_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        ),
        period = DISTANCE_THRESHOLD
    )

    val config = ExerciseConfig(
        exerciseType = ExerciseType.RUNNING,
        dataTypes = dataTypes,
        isAutoPauseAndResumeEnabled = false,
        isGpsEnabled = true,
        exerciseGoals = mutableListOf<ExerciseGoal<Double>>(calorieGoal, distanceGoal)
    )
    exerciseClient.startExerciseAsync(config).await()
}

您也可以為所有運動標記圈數。健康照護服務可提供 ExerciseLapSummary,以及在計圈期間匯總而成的指標。

在上文範例中,我們用了 isGpsEnabled,若要要求位置資料,此項目必須為 true。不過使用 GPS 也能輔助其他指標。如果 ExerciseConfig 將距離指定為 DataType,則根據預設會使用步驟估算距離。只要您選擇啟用 GPS,系統就能改用位置資訊估算距離。

暫停、繼續及結束健身

您可以使用合適的方法 (例如 pauseExerciseAsync()endExerciseAsync()) 暫停、繼續及結束健身。

請用 ExerciseUpdate 的狀態做為可靠資料來源。當呼叫 pauseExerciseAsync() 回傳內容時,系統不會判定健身已經暫停,而是到這個狀態反映在 ExerciseUpdate 訊息時才會判定暫停。這在 UI 狀態方面格外重要。當使用者按下暫停時,應該停用暫停按鈕,並在健康照護服務上呼叫 pauseExerciseAsync()。等待健康照護服務使用 ExerciseUpdate.exerciseStateInfo.state 達到暫停狀態,然後切換為繼續按鈕。這是由於健康照護服務狀態更新內容傳遞的時間可能會比按下按鈕的時間稍長,如果將所有 UI 變更都連結到按下按鈕的操作,可能會導致 UI 和健康照護服務狀態無法保持同步。

當遇到下列情況,請特別留意以上這一點:

  • 已啟用自動暫停:健身可能會在使用者未進行互動的情況下便暫停或開始。
  • 其他應用程式開始健身:您的健身可能會在使用者未進行互動的情況下就遭到終止。

當其他應用程式終止了您的應用程式時,您的應用程式必須妥善處理終止行為。

  • 儲存部分健身狀態,以便保留使用者的進度。
  • 移除「進行中的活動」圖示,並向使用者傳送通知,讓使用者知道其他應用程式結束了健身行程。

另外還要處理權限在運動中途遭到撤銷的情形。撤銷權限會使用 isEnded 狀態傳送,AUTO_END_PERMISSION_LOSTExerciseEndReason。處理這種情況的方式與終止案例類似:儲存部分狀態,移除「持續進行的活動」圖示,並向使用者傳送事件通知。

以下範例說明如何正確檢查終止狀況:

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        if (update.exerciseStateInfo.state.isEnded) {
            // Workout has either been ended by the user, or otherwise terminated
        }
        ...
    }
    ...
}

管理進行時間

在運動期間,應用程式可顯示目前的健身進行時間。應用程式、健康照護服務,以及裝置微型控制單元 (MCU) (負責處理運動追蹤的低功率處理器) 都必須同步處理目前的進行時間。健康照護服務會傳送 ActiveDurationCheckpoint,提供應用程式可以開始計時的錨點,以便協助管理進行時間。

由於 MCU 將會傳送進行時間,並可能需要一點時間才能傳入應用程式,因此 ActiveDurationCheckpoint 內有兩種屬性:

  • activeDuration︰運動進行的時間長短
  • time︰計算上列進行時間的時間點

因此,您可以用下列方程式,在應用程式內透過 ActiveDurationCheckpoint 計算目前運動的進行時間:

(now() - checkpoint.time) + checkpoint.activeDuration

從 MCU 計算進行時間到傳入應用程式之間有段小差距,您可以利用這段差距在應用程式中設定計時器種子,如此一來,即可確保應用程式計時器和健康照護服務及 MCU 內的時間完全一致。

當暫停運動時,應用程式會等到計算時間超過 UI 目前顯示的時間之後,才能在 UI 重新啟動計時器。原因是暫停信號會延遲一段時間後才會到達健康照護服務和 MCU。舉例來說,如果應用程式在 t=10 秒時暫停,健康照護服務可能要到 t=10.2 秒才能把 PAUSED 更新內容傳送到應用程式。

使用 ExerciseClient 的資料

系統會用 ExerciseUpdate 訊息傳送您的應用程式所註冊的資料類型指標。

只有在喚醒或達到最大報表統計期 (例如每 150 秒) 時,處理器才會傳送訊息。請勿依靠 ExerciseUpdate 的頻率使用 activeDuration 推進計時器。如果想查看實作獨立計時器的範例,請看 GitHub 的 Exercise 範例

當使用者啟動健身時,系統可能會頻繁傳送 ExerciseUpdate 訊息,例如每秒傳送一次。當使用者啟動健身時,可能會關閉螢幕,此時健康照護服務便可採用相同的取樣頻率但減少傳送資料次數,藉此避免喚醒主要處理器。如果使用者查看螢幕,所有正在批次處理的資料都會立即傳送到應用程式。

控管批次處理率

在某些情況下,您可能想要控制應用程式在螢幕關閉時接收特定資料類型的頻率。BatchingMode 物件可讓應用程式覆寫預設批次處理行為,提高資料傳送頻率。

如要設定批次處理率,請完成下列步驟:

  1. 確認裝置是否支援特定的 BatchingMode 定義:

    // Confirm BatchingMode support to control heart rate stream to phone.
    suspend fun supportsHrWorkoutCompanionMode(): Boolean {
        val capabilities = exerciseClient.getCapabilities()
        return BatchingMode.HEART_RATE_5_SECONDS in
                capabilities.supportedBatchingModeOverrides
    }
    
  2. 指定 ExerciseConfig 物件應使用特定的 BatchingMode,如以下程式碼片段所示。

    val config = ExerciseConfig(
        exerciseType = ExerciseType.WORKOUT,
        dataTypes = setOf(
            DataType.HEART_RATE_BPM,
            DataType.TOTAL_CALORIES
        ),
        // ...
        batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    )
    
  3. 您可以視需要在健身期間動態設定 BatchingMode,而不是讓整個健身期間的特定批次處理行為保持不變:

    val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
    
  4. 如要清除自訂 BatchingMode 並返回到預設行為,請將空白組合傳入 exerciseClient.overrideBatchingModesForActiveExercise()

時間戳記

每個資料點的時間點,都代表自裝置啟動以來的時間長度。如要將這些時間點轉換為時間戳記,請按照以下方式操作:

val bootInstant =
    Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())

接著,可以將這個值與每個資料點的 getStartInstant()getEndInstant() 搭配使用。

資料準確性

部分資料類型可能含有與各個資料點相關的準確性資訊。accuracy 屬性便代表這項準確性。

系統可以分別為 HEART_RATE_BPMLOCATION 資料類型產生 HrAccuracyLocationAccuracy 類別。當出現時,請用 accuracy 屬性判斷各個資料點的準確性是否可滿足應用程式需求。

儲存及上傳資料

請用 Room 保存健康照護服務傳送的資料。在運動結束後,系統會使用如 Work Manager 這類的機制上傳資料。這樣做能夠確保把上傳資料的網路呼叫延後到運動結束為止,從而讓運動期間的耗電量降到最低,同時簡化工作。

整合檢查清單

發布使用健康照護服務 ExerciseClient 的應用程式前,請先參考下列檢查清單,確保使用者體驗可避免某些常見問題。請確認下列事項:

  • 應用程式會在每次執行時檢查運動類型的功能和裝置功能。如此一來,您就可以偵測特定的裝置或運動不支援應用程式需要的其中一種資料類型。
  • 您要求並維護必要的權限,並在資訊清單檔案中指定這些權限。呼叫 prepareExerciseAsync() 前,應用程式會確認已授予執行階段權限。
  • 您的應用程式會使用 getCurrentExerciseInfoAsync() 處理下列情況
    • 系統已追蹤運動,您的應用程式會覆寫先前的運動。
    • 其他應用程式已終止您的運動。當使用者重新開啟應用程式時,使用者可能會遇到系統顯示訊息,說明該運動因另一個應用程式接管而停止。
  • 如果您使用 LOCATION 資料:
    • 應用程式會在運動期間 (包括 prepare 呼叫) 維護 ForegroundService,以及對應的 foregroundServiceType
    • 使用 isProviderEnabled(LocationManager.GPS_PROVIDER) 檢查裝置是否已啟用 GPS,並提示使用者在必要時開啟位置資訊設定。
    • 若是需要要求的用途,因為接收延遲時間短的位置資料很重要,請考慮整合整合式位置預測提供工具 (FLP),並使用其中的資料做為初始位置修正。當健康照護服務提供較穩定的位置資訊時,請使用該資訊,而非 FLP。
  • 如果應用程式需要上傳資料,任何用來上傳資料的網路呼叫都會延後,直到運動結束為止。否則,在運動期間,應用程式會盡量發出任何必要的網路呼叫。