活動資料與運動

Stay organized with collections Save and categorize content based on your preferences.

Wear OS 板型規格非常適合在運動期間這類情境下使用,其他板型規格則較不適任。在這些情境下,您的應用程式可能需要頻繁更新來自感應器的資料,或是您需要積極協助使用者管理健身活動。健康照護服務提供的 API 可幫助您用更輕鬆的方式開發這類使用體驗。

請參閱 GitHub 上的 Exercise 範例

新增依附元件

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

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

Groovy

dependencies {
    implementation "androidx.health:health-services-client:1.0.0-beta01"
}

Kotlin

dependencies {
    implementation("androidx.health:health-services-client:1.0.0-beta01")
}

使用 MeasureClient

透過使用 MeasureClient API,應用程式即可藉由註冊回呼按照您的需求接收資料。當應用程式在使用中,且必須快速更新資料時,才適合使用這個方法。建議您使用前景 UI 建立此內容,以便使用者知道他們使用了這項功能。

檢查功能

在註冊資料更新前,請確認裝置可以提供應用程式所需的資料類型。預先檢查功能可讓您啟用或停用特定功能,或修改應用程式的使用者介面,以彌補無法使用的功能。

val healthClient = HealthServices.getClient(this /*context*/)
val measureClient = healthClient.measureClient
lifecycleScope.launch {
    val capabilities = measureClient.getCapabilitiesAsync().await()
    supportsHeartRate = DataType.HEART_RATE_BPM in capabilities.supportedDataTypesMeasure
}

註冊資料

註冊回呼時只能使用單一種資料類型。請注意,某些資料類型可能會因狀態不同而影響取得能力。舉例來說,當使用者並未在手腕上正確裝備裝置時,便可能無法取得心率資料。

您應儘可能減少註冊回呼所花費的時間,因為回呼會提高感應器的取樣率,並因此消耗更多電力。

val heartRateCallback = object : MeasureCallback {
    override fun onAvailabilityChanged(dataType: DeltaDataType<*, *>, availability: Availability) {
        if (availability is DataTypeAvailability) {
            // Handle availability change.
        }
    }

    override fun onDataReceived(data: DataPointContainer) {
        // Inspect data points.
    }
}
val healthClient = HealthServices.getClient(this /*context*/)

// Register the callback.
measureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, heartRateCallback)

// Unregister the callback.
awaitClose {
    runBlocking {
        measureClient.unregisterMeasureCallbackAsync(DataType.HEART_RATE_BPM, heartRateCallback)
    }
}

使用 ExerciseClient

健康照護服務能夠藉由 ExerciseClient 為健身應用程式提供一流的支援功能。透過 ExerciseClient,應用程式可以控制運動進行的時間、新增運動目標,並能接收最新的運動狀態或其他必要的指標。詳情請參閱健康照護服務支援的所有 ExerciseType 清單:

應用程式結構

如果想用健康照護服務建構運動應用程式,請採用以下應用程式結構。螢幕和導覽應在主要活動內。使用前景服務管理健身狀態、感應器資料、進行中的活動及資料。使用 Room 儲存資料,並用 Work Manager 上傳資料。

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

藉由使用 ForegroundService,您就能用 Ongoing Activity API 在錶面顯示指標,讓使用者得以快速重新回到健身運動上。

當要求取得位置資料時,請務必使用 ForegroundService。資訊清單檔案內必須指定 foregroundServiceType="location,並指定合適的權限

建議您讓活動支援微光模式。詳情請參閱「在 Wear 上讓應用程式持續顯示」。

檢查功能

每項 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)
    }
}

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

supportedGoalssupportedMilestones 欄位是對應,其中的鍵是 DataType,而值是一組 ComparisonType (您可以和對應的 DataType 一起使用)。藉由使用這些內容,您就能決定某項運動能不能支援您打算建立的運動目標。

如果您的應用程式可讓使用者使用自動暫停或計圈功能,則應用程式必須檢查裝置是否可以支援這些功能。這兩項功能分別可以使用 supportsAutoPauseAndResumesupportsLapsExerciseClient 則會拒絕處理裝置不支援的要求。

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

管理運動生命週期

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

開始運動之前,應用程式應執行下列事項:

  1. 在開始追蹤新運動之前,請先檢查系統是否正在追蹤某個運動,並按照情況進行回應。例如,在覆寫先前的運動並開始追蹤新的運動之前,要求使用者進行確認。
  2. 檢查其他應用程式是否終止了您的運動,並按照情況進行回應。例如,說明其他應用程式已接管追蹤,且您的運動已停止。
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() 之前,請先:

  • 請看 DataType 參照文件,瞭解需要取得哪些權限。
  • AndroidManifest.xml 當中指定權限,並檢查使用者是否已授予必要的權限。詳情請參閱「要求應用程式權限」。如果使用者尚未授予必要權限,健康照護服務便會拒絕受理要求。

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

準備健身

部分如 GPS 或心率等感應器可能需要先暖機一段時間,或是使用者可能會想查看開始健身之前的資料。您可以選擇使用 prepareExercise() 方法,即可在不啟動健身計時器的情況下讓感應器暖機和接收資料。這樣做可讓 activeDuration 不受影響。

在呼叫 prepareExercise() 之前,應用程式應該檢查以下幾個項目:

  • 應用程式應檢查平台的位置資訊設定。如果位置資訊設定為關閉狀態,應通知使用者瞭解應用程式無法追蹤該地區,並指示使用者啟用設定。請注意,這是裝置端的設定檢查 (由使用者在主要「設定」選單中設定),並非應用程式層級權限檢查。
  • 確認應用程式擁有人體感應器、動作辨識以及精確位置的執行階段權限。如果缺少任何權限,請指示使用者授予執行階段權限,並提供合適的背景知識。如果使用者並未授予特定權限,您應從 prepare 呼叫中移除和該項權限相關的資料類型。如果使用者並未授予人體感應器和位置的存取權,請勿呼叫 prepare。應用程式仍然可以取得以步數計算的距離、配速、速度及其他不需透過這些權限取得的指標。

請遵守下列規範,確保您可以成功呼叫 prepare:

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

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

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)

exerciseClient.prepareExerciseAsync(warmUpConfig).await()

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

開始健身

當您想開始運動時,請建立 ExerciseConfig 設定運動類型、欲用來接收指標的資料類型,以及所有運動目標或里程碑。運動目標中含有 DataType 及條件。運動目標是僅限單次的目標,符合條件後就會觸發 (例如使用者跑了 5 公里)。使用者也可以設定里程碑,里程碑能夠多次觸發,例如使用者每次跑 1 公里都可以觸發一次。以下範例顯示的每個類型都有一個目標。

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 變更都連結到按下按鈕的操作上,可能會導致資料和健康照護服務狀態無法保持同步。

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

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

當其他應用程式終止了您的應用程式時,您的應用程式必須妥善處理可能發生的終止行為。應用程式在終止時應該:

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

應用程式也應該要處理權限在運動中途遭到撤銷的情形。撤銷權限會透過 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 訊息,例如每秒傳送一次。當使用者啟動健身時,可能會關閉螢幕,此時健康照護服務便可採用相同的取樣頻率但減少傳送資料次數,藉此避免喚醒主要處理器。如果使用者查看螢幕,所有正在批次處理中的資料都會立即傳送到應用程式內。

時間戳記

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

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

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

資料準確性

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

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

儲存及上傳資料

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