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

Kotlin

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

应用结构

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

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

使用 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 使用以下公式计算运动的活动时长:

(now() - 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。
  • 如果您的应用需要数据上传,任何上传数据的网络调用都将被延迟到运动结束。否则,在整个运动过程中,您的应用会尽量减少必要的网络调用。