使用 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-alpha04"
}

Kotlin

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

应用结构

使用以下应用结构构建使用 Health Services 的锻炼应用

准备锻炼 和锻炼期间,您的活动可能会因各种原因停止。用户可能会切换到其他应用或返回到表盘。系统可能会在您的活动顶部显示某些内容,或者在一段时间不活动后屏幕可能会关闭。结合使用持续运行的 ForegroundServiceExerciseClient,以帮助确保整个锻炼过程的正确操作。

使用 ForegroundService 可以使用 正在进行的活动 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()调用。但是,在环境模式下进行锻炼期间不要更新显示内容:这是因为当设备屏幕处于环境模式时,健康服务会批量处理锻炼数据以节省电量,因此显示的信息可能不是最新的。在锻炼期间,显示对用户有意义的数据,可以显示最新信息或空白屏幕。

检查功能

每个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)

管理锻炼生命周期

健康服务最多支持设备上所有应用同时进行一项锻炼。如果正在跟踪一项锻炼,并且另一个应用开始跟踪新的锻炼,则第一项锻炼将终止。

在开始锻炼之前,请执行以下操作

  • 检查是否已在跟踪锻炼,并相应地做出反应。例如,在覆盖之前的锻炼并开始跟踪新的锻炼之前,请询问用户确认。

以下示例显示了如何使用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(),因为准备调用专门用于在开始锻炼之前获取稳定的心率或 GPS 定位。应用仍然可以获取基于步数的距离、配速、速度和其他不需要这些权限的指标。

请执行以下操作以确保对prepareExerciseAsync()的调用可以成功

  • 对包含准备调用的热身活动使用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状态发送的,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
        }
        ...
    }
    ...
}

管理活动时长

在锻炼期间,应用可以显示锻炼的活动时长。应用、健康服务和设备微控制器单元 (MCU)(负责锻炼跟踪的低功耗处理器)都需要同步,并具有相同的当前活动时长。为了帮助管理这一点,健康服务会发送一个ActiveDurationCheckpoint,该检查点提供一个锚点,应用可以从此锚点开始其计时器。

由于活动时长是从 MCU 发送的,并且可能需要少量时间才能到达应用,因此ActiveDurationCheckpoint包含两个属性

  • activeDuration:锻炼的激活时长
  • time:计算激活时长的时间点

因此,在应用中,可以利用ActiveDurationCheckpoint通过以下公式计算锻炼的激活时长:

(现在时间 - checkpoint.time) + checkpoint.activeDuration

这可以解决激活时长在 MCU 上计算并在应用中接收之间的小时间差。这可以用来初始化应用中的计时器,并确保应用的计时器与健康服务和 MCU 中的时间完美同步。

如果锻炼暂停,应用会等待 UI 中的计时器重新开始,直到计算出的时间超过 UI 当前显示的时间。这是因为暂停信号到达健康服务和 MCU 会有轻微的延迟。例如,如果应用在 t=10 秒时暂停,健康服务可能直到 t=10.2 秒才会将PAUSED更新传递到应用。

使用 ExerciseClient 的数据

应用已注册的数据类型的指标会通过ExerciseUpdate消息传递。

处理器仅在唤醒或达到最大报告周期(例如每 150 秒)时才传递消息。不要依赖ExerciseUpdate的频率来使用activeDuration推进计时器。请参阅 GitHub 上的锻炼示例,了解如何实现独立计时器的示例。

当用户开始锻炼时,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属性中表示。

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

存储和上传数据

使用Room持久化从健康服务传递的数据。数据上传在锻炼结束时使用诸如Work Manager之类的机制进行。这确保了上传数据的网络调用被延迟到锻炼结束后,从而最大限度地减少锻炼期间的功耗并简化工作。

集成检查清单

在发布使用健康服务的ExerciseClient的应用之前,请参考以下检查清单,以确保您的用户体验避免一些常见问题。确认:

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