使用 ExerciseClient 记录锻炼

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 可能会因各种原因而停止。用户可能会切换到其他应用或返回表盘。系统可能会在您的 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,否则您的请求可能会失败。

使用 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)

管理锻炼生命周期

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 将拒绝请求。

对于位置数据,请执行以下附加步骤

准备锻炼

某些传感器(如 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 状态发送,其 ExerciseEndReasonAUTO_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 对象允许您的应用覆盖默认的批量处理行为,以更频繁地获取数据交付。

要配置批量处理速率,请完成以下步骤

  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 属性中表示。

HrAccuracyLocationAccuracy 类可以分别用于 HEART_RATE_BPMLOCATION 数据类型。如果存在,请使用 accuracy 属性来确定每个数据点对于您的应用是否具有足够的准确性。

存储和上传数据

使用 Room 存储从 Health Services 传递的数据。数据上传在锻炼结束时通过诸如 Work Manager 等机制进行。这确保了上传数据的网络调用被推迟到锻炼结束后,从而最大限度地降低了锻炼期间的功耗并简化了工作。

集成清单

在发布使用 Health Services ExerciseClient 的应用之前,请查阅以下清单,以确保您的用户体验避免一些常见问题。确认

  • 您的应用在每次运行时都会检查锻炼类型的功能和设备的功能。这样,您可以检测特定设备或锻炼何时不支持您的应用所需的数据类型。
  • 您请求并保持必要的权限,并在清单文件中指定这些权限。在调用 prepareExerciseAsync() 之前,您的应用会确认已授予运行时权限。
  • 您的应用使用 getCurrentExerciseInfoAsync() 来处理以下情况
    • 已有锻炼正在跟踪中,您的应用覆盖了之前的锻炼。
    • 另一个应用终止了您的锻炼。这可能发生在用户重新打开应用时,他们会收到一条消息,解释锻炼因另一个应用接管而停止。
  • 如果您正在使用 LOCATION 数据
    • 您的应用在整个锻炼期间(包括准备调用)都保持一个具有相应 foregroundServiceTypeForegroundService
    • 使用 isProviderEnabled(LocationManager.GPS_PROVIDER) 检查设备上是否启用了 GPS,并在必要时提示用户打开位置设置。
    • 对于要求苛刻的用例,当以低延迟接收位置数据非常重要时,请考虑集成融合位置提供程序 (FLP),并将其数据用作初始位置修复。当 Health Services 提供更稳定的位置信息时,请使用 Health Services 的信息而不是 FLP。
  • 如果您的应用需要上传数据,则任何上传数据的网络调用都会推迟到锻炼结束后。否则,在整个锻炼过程中,您的应用应尽可能少地进行任何必要的网络调用。