活跃数据和进行中的运动

Wear OS 的外形规格非常适合一些使用其他外形规格不太可取的情况,例如在运动期间。在此类情况下,您的应用可能需要频繁接收来自传感器的数据更新,或者您可能正积极帮助用户管理锻炼。健康服务提供了一些 API,可以帮助您更轻松地开发此类体验。

请参阅 GitHub 上的运动示例

添加依赖项

如需添加健康服务的依赖项,您必须将 Google Maven 代码库添加到项目中。如需了解相关信息,请参阅 Google 的 Maven 代码库

然后在您的模块级 build.gradle 文件中,添加以下依赖项:

Groovy

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

Kotlin

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

AndroidManifest.xml 文件中的 manifest 标记内添加以下内容,以便您的应用可以与健康服务交互。如需了解详情,请参阅软件包可见性

<queries>
    <package android:name="com.google.android.wearable.healthservices" />
</queries>

使用 MeasureClient

借助 MeasureClient API,您的应用可以注册回调,以便按照您需要的时长接收数据。这很适合应用处于使用中并且需要快速更新数据的情况。理想情况下,您在创建时应使用前台界面,以便让用户知晓。

检查功能

在注册数据更新之前,请检查设备是否可以提供您的应用所需的数据类型。提前检查功能,您便可以启用或停用某些功能,或修改应用的界面以弥补不可用的功能。

Kotlin


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

Java


HealthServicesClient healthClient = HealthServices.getClient(this /*context*/);
ListenableFuture<MeasureCapabilities> capabilitiesFuture =
        healthClient.getCapabilities();
Futures.addCallback(capabilitiesFuture,
        new FutureCallback<Capabilities>() {
            @Override
            public void onSuccess(@Nullable Capabilities result) {
                boolean supportsHeartRate = result
                        .supportedDataTypesMeasure()
                        .contains(DataType.HEART_RATE_BPM)
            }

            @Override
            public void onFailure(Throwable t) {
                // display an error
            }
        },
        ContextCompat.getMainExecutor(this /*context*/));

注册数据

您注册的每个回调都只针对一种数据类型。请注意,某些数据类型的可用性状态可能会发生变化。例如,当设备未正确佩戴到手腕上时,心率数据可能就无法使用。

请务必尽量减少注册回调的时间,因为回调会导致传感器的采样率提高,进而增加功耗。

val heartRateCallback = object : MeasureCallback {
    override fun onAvailabilityChanged(type: DataType, availability: Availability) {
        if (availability is DataTypeAvailability) {
            // Handle availability change.
        }
    }

    override fun onData(dataPoints: List<DataPoint>) {
        // Inspect data points.
    }
}
val healthClient = HealthServices.getClient(this /*context*/)

// Register the callback.
lifecycleScope.launch {
    healthClient.measureClient
        .registerCallback(DataType.HEART_RATE_BPM, heartRateCallback)
        .await()
}

// Unregister the callback.
lifecycleScope.launch {
    healthClient.measureClient.unregisterCallback(heartRateCallback).await()
}

使用 ExerciseClient

健康服务通过 ExerciseClient 为锻炼应用提供一流的支持。借助 ExerciseClient,您的应用可以控制运动的进行时间,添加运动目标,并获取有关运动状态或其他所需指标的更新信息。如需了解详情,请参阅健康服务支持的 ExerciseType 的完整列表。

应用结构

通过健康服务构建运动应用时,请使用以下应用结构。屏幕和导航组件应位于主 activity 中。使用前台服务管理锻炼状态、传感器数据、持续性活动和数据。使用 Room 存储数据,并使用 Work Manager 上传数据。

在准备锻炼时或在锻炼期间,activity 可能会因各种原因而停止。用户可能会切换到其他应用或返回到表盘。系统可能会在 activity 之上显示内容,或者屏幕可能会在闲置一段时间后关闭。请结合使用持续运行的 ForegroundServiceExerciseClient,确保整个锻炼正常进行。

通过使用 ForegroundService,您可以使用 Ongoing Activity API 在表盘上显示一个指示器,让用户能够快速返回锻炼中。

在请求位置数据时,使用 ForegroundService 至关重要。清单文件必须指定 foregroundServiceType="location 并指定适当的权限

建议让 activity 支持氛围模式。如需了解详情,请参阅让应用始终显示在 Wear 上

检查功能

对于每个 ExerciseType,相关指标和运动目标都会支持特定的数据类型。请在启动时检查这些功能,因为具体情况因设备而异。设备可能不支持某些运动类型,也可能不具备一些功能(例如自动暂停)。此外,设备的功能也可能会随着时间的推移而发生变化,例如在软件更新后发生变化。

您的应用应该在应用启动时查询设备功能,并存储和处理平台支持的运动、每种运动支持的功能(例如自动暂停)、每种运动支持的数据类型以及这其中每种数据类型所需的权限。

结合使用 ExerciseCapabilities.getExerciseTypeCapabilities() 与所需运动类型,了解您可以请求哪些类型的指标、可以配置哪些运动目标,以及该类型还可以使用哪些其他功能。

val healthClient = HealthServices.getClient(this /*context*/)
val exerciseClient = healthClient.exerciseClient
lifecycleScope.launch {
    val capabilities = exerciseClient.capabilities.await()
    if (ExerciseType.RUNNING in capabilities.supportedExerciseTypes) {
        runningCapabilities =
            capabilities.getExerciseTypeCapabilities(ExerciseType.RUNNING)
    }
}

在返回的 ExerciseTypeCapabilities 内,supportedExerciseTypes 会列出您可以请求数据的 DataType。这一结果因设备而异,因此请注意不要请求不受支持的 DataType,否则您的请求可能会失败。

supportedGoalssupportedMilestones 字段是映射,其中键为 DataType,值为一组与关联的 DataType 配合使用的 ComparisonType。使用这些字段来确定运动是否可以支持您想创建的运动目标。

如果您的应用允许用户使用自动暂停或圈数功能,就必须检查设备是否支持相应功能。请分别使用 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]
supportsStepGoals =
    (stepGoals != null && ComparisonType.GREATER_THAN_OR_EQUAL in stepGoals)

// Whether auto-pause is supported
val supportsAutoPause = runningCapabilities.supportsAutoPauseAndResume

// Whether laps is supported
val supportsLaps = runningCapabilities.supportsLaps

注册运动状态更新

运动更新会传送给监听器。您的应用一次只能注册一个监听器。请在开始锻炼前设置监听器。监听器只会接收应用拥有的运动的更新信息。

val listener = object : ExerciseUpdateListener {
    override fun onExerciseUpdate(update: ExerciseUpdate) {
        // Process the latest information about the exercise.
        exerciseStatus = update.state // e.g. ACTIVE, USER_PAUSED, etc.
        activeDuration = update.activeDuration // Duration
        latestMetrics = update.latestMetrics // Map<DataType, List<DataPoint>>
        latestAggregateMetrics = update.latestAggregateMetrics // Map<DataType, AggregateDataPoint>
        latestGoals = update.latestAchievedGoals // Set<AchievedExerciseGoal>
        latestMilestones = update.latestMilestoneMarkerSummaries // Set<MilestoneMarkerSummary>

    }

    override fun onLapSummary(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
        }
    }
}
val exerciseClient = HealthServices.getClient(this /*context*/).exerciseClient

// Register the listener.
lifecycleScope.launch {
    exerciseClient.setUpdateListener(listener).await()
}

管理运动生命周期

在设备上的所有应用中,健康服务一次支持最多一项运动。如果某个应用开始一项运动,会导致其他应用中的任何当前运动被终止。您的应用应在开始运动之前检查是否存在其他正在进行的运动,并相应地做出应对。例如,在开始之前请求用户确认。

在用户启动您的应用时,请检查您的应用中是否已有正在通过健康服务进行的锻炼,如果有,那么此时恢复锻炼才合理。由于健康服务不会在您的应用关闭时自动停止锻炼,因此可能有一个属于您的应用的锻炼已经在进行了。如果有一个正在进行的运动属于当前应用,那么应用应转换到锻炼追踪屏幕,并通过监听器继续处理运动更新。

lifecycleScope.launch {
    val exerciseInfo = exerciseClient.currentExerciseInfo.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 调用的锻炼前 activity 使用 AmbientModeSupport
  • 从前台服务调用 prepareExercise()。如果它不在某项服务中并且与 activity 生命周期相关联,那么传感器准备可能会被不必要地终止。
  • 在用户离开锻炼前 activity 时,调用 endExercise() 关闭传感器以节省电量。

进入 PREPARING 状态后,系统将通过 onAvailabilityChanged()ExerciseUpdateListener 中传送传感器可用性更新。此信息随后会提供给用户,以便用户决定是否开始锻炼。

val dataTypes = setOf(
    DataType.HEART_RATE_BPM,
    DataType.LOCATION
)
val warmUpConfig = WarmUpConfig.Builder()
    .setDataTypes(dataTypes)
    .setExerciseType(ExerciseType.RUNNING)
    .build()
exerciseClient.prepareExercise(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.LOCATION

    )
    // Types for which we want to receive aggregate metrics.
    val aggregateDataTypes = setOf(
        DataType.DISTANCE,
        // "Total" here refers not to the aggregation but to basal + activity.
        DataType.TOTAL_CALORIES
    )

    // Create a one-time goal.
    val calorieGoal = ExerciseGoal.createOneTimeGoal(
        condition = DataTypeCondition(
            dataType = DataType.TOTAL_CALORIES,
            threshold = Value.ofDouble(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,
            threshold = Value.ofDouble(DISTANCE_THRESHOLD),
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        ),
        period = Value.ofDouble(DISTANCE_THRESHOLD)
    )
    val config = ExerciseConfig.builder()
        .setExerciseType(ExerciseType.RUNNING)
        .setDataTypes(dataTypes)
        .setAggregateDataTypes(aggregateDataTypes)
        .setExerciseGoals(listOf(calorieGoal, distanceGoal))
        .setShouldEnableAutoPauseAndResume(false)
        // Required for GPS for LOCATION data type, optional for some other types.
        .setShouldEnableGps(true)
        .build()
    HealthServices.getClient(this /*context*/)
        .exerciseClient
        .startExercise(config)
        .await()
}

对于某些运动类型,您还可以标记圈数。健康服务提供了一个 ExerciseLapSummary,其中包含运动期间的汇总指标。

上面的示例展示了 setShouldEnableGps 的用法,在请求位置数据时,其值必须为 true。不过,GPS 可用于帮助获取其他指标。 如果 ExerciseConfig 将距离指定为 DataType,那么在默认情况下将使用步数来估算距离。而选择启用 GPS 后,就可以改为使用位置信息来估算距离。

暂停、恢复和结束锻炼

您可以使用 pauseExercise()endExercise() 等合适的方法暂停、恢复和结束锻炼。

使用 ExerciseUpdate 中的状态作为可信来源。系统不会在 pauseExercise() 调用返回时认为锻炼暂停,而是在 ExerciseUpdate 消息中反映出暂停状态时认为锻炼暂停。尤其是在涉及界面状态时,更是必须考虑这一点。如果用户按暂停按钮,您的应用应停用暂停按钮,并对健康服务调用 pauseExercise()。应用应等待健康服务通过 ExerciseUpdate.ExerciseState 达到“已暂停”状态后,再将按钮切换到“恢复”状态。这是因为传送健康服务状态更新所需的时间可能比按下按钮的时间略长,如果您将所有界面更改都与按下按钮相关联,可能会与健康服务状态不同步。

在以下情况下请务必注意这一点:

  • 已启用自动暂停功能:锻炼可能会在无用户互动的情况下暂停或开始。
  • 其他应用开始锻炼:系统可能会在无用户互动的情况下终止您的锻炼。

如果您的应用的锻炼被其他应用终止,您的应用必须能够妥善处理这种可能出现的终止情况。如果被终止,您的应用应该做到以下几点:

  • 保存部分完成锻炼状态,以免清除用户的进度
  • 移除持续性活动图标,并向用户发送通知,告知他们锻炼已被其他应用结束。

您的应用还应该能够处理运动进行期间权限被撤消的情况。这种情况会通过 isEnded 状态 AUTO_ENDED_PERMISSIONS_LOST 得知。处理这种情况的方式应当与锻炼被终止的情况类似。应用应保存部分完成状态,移除持续性活动图标,并向用户发送相关通知。

请参阅以下示例,了解如何正确检查终止情况:

val listener = object : ExerciseUpdateListener {
    override fun onExerciseUpdate(update: ExerciseUpdate) {
        if (update.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 中的时间完全一致。

如果运动暂停,应用应等到计算出的时间超过界面当前显示的时间之后再重启界面中的计时器。其原因在于,暂停信号到达健康服务和 MCU 时可能会略有延迟。例如,如果应用在 t=10 秒时暂停,健康服务可能直到 t=10.2 秒才将 PAUSED 更新传送给应用。

处理来自 ExerciseClient 的数据

您的应用已注册的数据类型的指标在 ExerciseUpdate 消息中传送。

以下项目符号列表说明了 ExerciseClient 如何传送数据:

  • 汇总数据和非汇总数据被划分到两个不同的属性:latestAggregateMetricslatestMetrics
  • 在任何 ExerciseUpdate 中,汇总指标都包含每个 DataType 的最新值。
  • 相反,非汇总指标则列出了每个 DataType 的数据点。这表示自上次传送数据以来采集的所有样本。
  • ExerciseClient 可以批量传送数据,仅在处理器处于唤醒状态或在达到最大报告期(例如每 150 秒)时传送数据。

当用户开始锻炼时,ExerciseUpdate 消息可能会频繁传送,例如每秒传送一次。随着用户开始锻炼,屏幕可能会关闭。然后,健康服务可能会传送数据,采样也会以相同的频率进行,以避免唤醒主处理器。当用户查看屏幕时,批处理过程中的所有数据就会立即传送到应用。

汇总指标

汇总指标有两种不同的形式:CumulativeDataPointStatisticalDataPoint

例如,DataType.DISTANCE 在汇总时表示为 CumulativeDataPoint,提供总距离。DataType.HEART_RATE_BPM 表示为 StatisticalDataPoint,提供最小值、最大值和平均值。

DataTypes 类提供了 isCumulativeDataTypeisStatisticalDataType 等辅助方法,帮助确定如何转换每个 AggregateDataPoint

时间戳

每个数据点的时间点表示自设备启动以来的时长。如需将其转换为时间戳,请使用以下代码:

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

然后,可将此值与每个数据点的 getStartInstant()getEndInstant() 一起使用。

数据准确性

某些数据类型可能具有与每个数据点关联的准确性信息。我们使用 accuracy 属性表示此信息。

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

存储和上传数据

请使用 Room 持久保存从健康服务传送的数据。数据上传应在运动结束时使用 Work Manager 等机制完成。这样可以确保用于上传数据的网络调用被推迟到运动结束后,从而最大限度地减少运动期间的功耗并简化服务需要完成的工作。