Health Services 通过 ExerciseClient
为健身应用提供一流的支持。借助 ExerciseClient
,您的应用可以控制锻炼何时进行、添加锻炼目标,并获取有关锻炼状态、锻炼事件或所需其他指标的更新。有关更多信息,请参阅 Health Services 支持的锻炼类型完整列表。
请参阅 GitHub 上的 锻炼示例。
添加依赖项
要添加对 Health Services 的依赖项,您必须将 Google Maven 仓库添加到您的项目中。有关更多信息,请参阅Google 的 Maven 仓库。
然后,在您的模块级 build.gradle
文件中,添加以下依赖项
Groovy
dependencies { implementation "androidx.health:health-services-client:1.1.0-alpha05" }
Kotlin
dependencies { implementation("androidx.health:health-services-client:1.1.0-alpha05") }
应用结构
使用 Health Services 构建健身应用时,请使用以下应用结构
- 将屏幕和导航保留在主 Activity 中。
- 使用前台服务管理锻炼状态、传感器数据、持续进行的活动和数据。
- 使用 Room 存储数据,并使用 WorkManager 上传数据。
在准备锻炼和锻炼期间,您的 Activity 可能会因各种原因而停止。用户可能会切换到其他应用或返回表盘。系统可能会在您的 Activity 上方显示内容,或者屏幕可能会在一段时间不活动后关闭。结合 ExerciseClient
使用持续运行的 ForegroundService
有助于确保整个锻炼过程的正常运行。
使用 ForegroundService
可以让您使用 Ongoing Activity API 在表盘上显示指示器,让用户快速返回锻炼。
您必须在前台服务中适当地请求位置数据。在您的清单文件中,指定必要的前台服务类型和权限
<manifest ...> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application ...> <!-- If your app is designed only for devices that run Wear OS 4 or lower, use android:foregroundServiceType="location" instead. --> <service android:name=".MyExerciseSessionRecorder" android:foregroundServiceType="health|location"> </service> </application> </manifest>
将 AmbientLifecycleObserver
用于包含 prepareExercise()
调用的锻炼前 Activity 和锻炼 Activity。但是,在环境模式下,请勿在锻炼期间更新显示:这是因为当设备屏幕处于环境模式时,Health Services 会批量处理锻炼数据以节省电量,因此显示的信息可能不是最新的。在锻炼期间,显示对用户有意义的数据,可以显示最新信息,也可以显示空白屏幕。
检查功能
每个 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)
}
}
在返回的 ExerciseTypeCapabilities
中,supportedDataTypes
列出了您可以请求数据的数据类型。这因设备而异,因此请注意不要请求不支持的 DataType
,否则您的请求可能会失败。
使用 supportedGoals
和 supportedMilestones
字段来确定锻炼是否可以支持您想要创建的锻炼目标。
如果您的应用允许用户使用自动暂停,则必须使用 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)
管理锻炼生命周期
Health Services 在设备上的所有应用中最多支持同时进行一项锻炼。如果一项锻炼正在跟踪中,而另一个应用开始跟踪新的锻炼,则第一项锻炼会终止。
在开始锻炼之前,请执行以下操作
- 检查是否已经有锻炼正在跟踪中,并据此作出反应。例如,在覆盖之前的锻炼并开始跟踪新的锻炼之前,请求用户确认。
以下示例展示了如何使用 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
文件中为请求的数据类型指定适当的权限。 - 验证用户是否已授予必要的权限。有关更多信息,请参阅请求应用权限。如果尚未授予必要的权限,Health Services 将拒绝请求。
对于位置数据,请执行以下附加步骤
- 使用
isProviderEnabled(LocationManager.GPS_PROVIDER)
检查设备上是否启用了 GPS。如有必要,提示用户打开位置设置。 - 确保在整个锻炼过程中(包括准备呼叫),保持具有适当
foregroundServiceType
的ForegroundService
。
准备锻炼
某些传感器(如 GPS 或心率)可能需要短时间预热,或者用户可能希望在开始锻炼前查看其数据。可选的 prepareExerciseAsync()
方法允许这些传感器预热并接收数据,而无需启动锻炼计时器。activeDuration
不受此准备时间的影响。
在调用 prepareExerciseAsync()
之前,请检查以下事项
检查平台范围内的位置设置。用户在主“设置”菜单中控制此设置;它与应用级别的权限检查不同。
如果该设置已关闭,请通知用户他们已拒绝访问位置信息,如果您的应用需要位置信息,请提示他们启用。
确认您的应用具有身体传感器(API 级别 35 或更低)或心率(API 级别 36+)、活动识别和精细位置的运行时权限。对于缺失的权限,请提示用户授予运行时权限,提供足够的上下文。如果用户未授予特定权限,请从
prepareExerciseAsync()
调用中移除与该权限关联的数据类型。如果既未授予身体传感器(API 级别 36+ 上的心率)也未授予位置权限,请勿调用prepareExerciseAsync()
,因为 prepare 调用专门用于在开始锻炼之前获取稳定的心率或 GPS 定位。应用仍可以获取基于步数的距离、步速、速度和其他不需要这些权限的指标。
执行以下操作以确保您的 prepareExerciseAsync()
调用能够成功
- 将
AmbientLifecycleObserver
用于包含 prepare 调用的锻炼前 Activity。 - 从您的前台服务调用
prepareExerciseAsync()
。如果它不在服务中且与 Activity 生命周期绑定,则传感器准备可能会被不必要地终止。 - 如果用户离开锻炼前 Activity,调用
endExercise()
以关闭传感器并降低功耗。
以下示例展示了如何调用 prepareExerciseAsync()
val warmUpConfig = WarmUpConfig(
ExerciseType.RUNNING,
setOf(
DataType.HEART_RATE_BPM,
DataType.LOCATION
)
)
// Only necessary to call prepareExerciseAsync if body sensor (API level 35
// or lower), heart rate (API level 36+), 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()
}
您还可以为所有锻炼标记圈数。Health Services 提供一个 ExerciseLapSummary
,其中包含在圈数期间汇总的指标。
前面的示例展示了 isGpsEnabled
的使用,当请求位置数据时,它必须为 true。然而,使用 GPS 也可以协助其他指标。如果 ExerciseConfig
将距离指定为 DataType
,则默认使用步数来估计距离。通过可选地启用 GPS,可以使用位置信息来估计距离。
暂停、恢复和结束锻炼
您可以使用适当的方法暂停、恢复和结束锻炼,例如 pauseExerciseAsync()
或 endExerciseAsync()
。
使用 ExerciseUpdate
中的状态作为事实来源。当调用 pauseExerciseAsync()
返回时,锻炼不会被视为已暂停,而是当该状态反映在 ExerciseUpdate
消息中时才视为暂停。这在 UI 状态方面尤其重要。如果用户按下暂停,禁用暂停按钮并调用 Health Services 上的 pauseExerciseAsync()
。等待 Health Services 使用 ExerciseUpdate.exerciseStateInfo.state
达到暂停状态,然后将按钮切换为恢复。这是因为 Health Services 状态更新的传递可能比按钮按下需要更长的时间,因此如果您将所有 UI 更改绑定到按钮按下,UI 可能会与 Health Services 状态不同步。
在以下情况下请记住这一点
- 启用自动暂停: 锻炼可以在没有用户交互的情况下暂停或开始。
- 另一个应用开始锻炼: 您的锻炼可能会在没有用户交互的情况下终止。
如果您的应用的锻炼被另一个应用终止,您的应用必须优雅地处理终止
- 保存部分锻炼状态,以免用户进度被擦除。
- 移除“进行中的活动”图标,并向用户发送通知,告知他们锻炼已因其他应用接管而结束。
此外,还要处理在进行中锻炼期间权限被撤销的情况。这会使用 isEnded
状态发送,其 ExerciseEndReason
为 AUTO_END_PERMISSION_LOST
。以与终止情况类似的方式处理此情况:保存部分状态,移除“进行中的活动”图标,并向用户发送有关发生情况的通知。
以下示例展示了如何正确检查终止
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
}
...
}
...
}
管理活动时长
在锻炼期间,应用可以显示锻炼的活动时长。应用、Health Services 和设备微控制器单元 (MCU)(负责锻炼跟踪的低功耗处理器)都需要同步,具有相同的当前活动时长。为了帮助管理此问题,Health Services 会发送一个 ActiveDurationCheckpoint
,它提供一个锚点,应用可以从该锚点开始计时。
由于活动时长从 MCU 发送,并且可能需要少量时间才能到达应用,因此 ActiveDurationCheckpoint
包含两个属性
activeDuration
:锻炼已持续活动的时长time
:计算活动时长的时间
因此,在应用中,可以使用以下等式从 ActiveDurationCheckpoint
计算锻炼的活动时长
(now() - checkpoint.time) + checkpoint.activeDuration
这解释了活动时长在 MCU 上计算并到达应用之间的小时间差。这可用于在应用中播种一个计时器,并有助于确保应用的计时器与 Health Services 和 MCU 中的时间完全一致。
如果锻炼暂停,应用会等到计算时间超过 UI 当前显示的时间后才在 UI 中重新启动计时器。这是因为暂停信号到达 Health Services 和 MCU 会有轻微延迟。例如,如果应用在 t=10 秒暂停,Health Services 可能要到 t=10.2 秒才向应用传递 PAUSED
更新。
使用 ExerciseClient 中的数据
您的应用已注册的数据类型的指标在 ExerciseUpdate
消息中传递。
处理器仅在唤醒时或达到最大报告周期(例如每 150 秒)时才传递消息。不要依赖 ExerciseUpdate
频率来使用 activeDuration
推进计时器。有关如何实现独立计时器的示例,请参阅 GitHub 上的锻炼示例。
当用户开始锻炼时,ExerciseUpdate
消息可以频繁传递,例如每秒。当用户开始锻炼时,屏幕可能会关闭。然后 Health Services 可以更不频繁地传递数据,但仍以相同的频率采样,以避免唤醒主处理器。当用户查看屏幕时,正在批量处理的任何数据都会立即传递到您的应用。
控制批量处理速率
在某些情况下,您可能希望控制应用在屏幕关闭时接收某些数据类型的频率。BatchingMode
对象允许您的应用覆盖默认的批量处理行为,以更频繁地获取数据交付。
要配置批量处理速率,请完成以下步骤
检查设备是否支持特定的
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 }
指定
ExerciseConfig
对象应使用特定的BatchingMode
,如下面的代码片段所示。val config = ExerciseConfig( exerciseType = ExerciseType.WORKOUT, dataTypes = setOf( DataType.HEART_RATE_BPM, DataType.TOTAL_CALORIES ), // ... batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS) )
或者,您可以在锻炼期间动态配置
BatchingMode
,而不是让特定的批量处理行为在整个锻炼期间持续存在val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS) exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
要清除自定义的
BatchingMode
并返回默认行为,请将一个空集传递给exerciseClient.overrideBatchingModesForActiveExercise()
。
时间戳
每个数据点的时间点表示设备启动以来的持续时间。要将其转换为时间戳,请执行以下操作
val bootInstant =
Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())
此值可用于每个数据点的 getStartInstant()
或 getEndInstant()
。
数据准确性
某些数据类型可以与每个数据点关联精度信息。这在 accuracy
属性中表示。
HrAccuracy
和 LocationAccuracy
类可以分别用于 HEART_RATE_BPM
和 LOCATION
数据类型。如果存在,请使用 accuracy
属性来确定每个数据点对于您的应用是否具有足够的准确性。
存储和上传数据
使用 Room 存储从 Health Services 传递的数据。数据上传在锻炼结束时通过诸如 Work Manager 等机制进行。这确保了上传数据的网络调用被推迟到锻炼结束后,从而最大限度地降低了锻炼期间的功耗并简化了工作。
集成清单
在发布使用 Health Services ExerciseClient
的应用之前,请查阅以下清单,以确保您的用户体验避免一些常见问题。确认
- 您的应用在每次运行时都会检查锻炼类型的功能和设备的功能。这样,您可以检测特定设备或锻炼何时不支持您的应用所需的数据类型。
- 您请求并保持必要的权限,并在清单文件中指定这些权限。在调用
prepareExerciseAsync()
之前,您的应用会确认已授予运行时权限。 - 您的应用使用
getCurrentExerciseInfoAsync()
来处理以下情况- 已有锻炼正在跟踪中,您的应用覆盖了之前的锻炼。
- 另一个应用终止了您的锻炼。这可能发生在用户重新打开应用时,他们会收到一条消息,解释锻炼因另一个应用接管而停止。
- 如果您正在使用
LOCATION
数据- 您的应用在整个锻炼期间(包括准备调用)都保持一个具有相应
foregroundServiceType
的ForegroundService
。 - 使用
isProviderEnabled(LocationManager.GPS_PROVIDER)
检查设备上是否启用了 GPS,并在必要时提示用户打开位置设置。 - 对于要求苛刻的用例,当以低延迟接收位置数据非常重要时,请考虑集成融合位置提供程序 (FLP),并将其数据用作初始位置修复。当 Health Services 提供更稳定的位置信息时,请使用 Health Services 的信息而不是 FLP。
- 您的应用在整个锻炼期间(包括准备调用)都保持一个具有相应
- 如果您的应用需要上传数据,则任何上传数据的网络调用都会推迟到锻炼结束后。否则,在整个锻炼过程中,您的应用应尽可能少地进行任何必要的网络调用。
推荐给您
- 注意:如果 JavaScript 关闭,则显示链接文本
- 被动数据更新
- Wear OS 上的 Health Services
- 开始使用图块