Android NDK开发详解Wear之活跃数据和进行中的运动


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

请参阅 GitHub 上的运动示例。

添加依赖项

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

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

Groovy

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

Kotlin

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

注意:此 API 是异步的,并且广泛依赖于 ListenableFuture。如需详细了解此概念,请参阅使用 ListenableFuture。

使用 MeasureClient

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

注意:MeasureClient 不适合用于跟踪锻炼。如需了解替代方案,请参阅使用 ExerciseClient 部分。

检查功能

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

以下示例展示了如何检查设备是否可以提供 HEART_RATE_BPM 数据类型:

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

注册数据

注意:在注册接收需要权限的数据之前,需要请求必要的权限。
您注册的每个回调都只针对一种数据类型。请注意,某些数据类型的可用性状态可能会发生变化。例如,当设备未正确佩戴到手腕上时,可能无法获得心率数据。

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

以下示例展示了如何注册和取消注册回调以接收 HEART_RATE_BPM 数据:

val heartRateCallback = object : MeasureCallback {
    override fun onAvailabilityChanged(dataType: DeltaDataType<*, *>, availability: Availability) {
        if (availability is DataTypeAvailability) {
            // Handle availability change.
        }
    }

    override fun onDataReceived(data: DataPointContainer) {
        // Inspect data points.
    }
}
val healthClient = HealthServices.getClient(this /*context*/)
val measureClient = healthClient.measureClient

// Register the callback.
measureClient.registerMeasureCallback(DataType.Companion.HEART_RATE_BPM, heartRateCallback)

// Unregister the callback.
awaitClose {
    runBlocking {
        measureClient.unregisterMeasureCallbackAsync(DataType.Companion.HEART_RATE_BPM, heartRateCallback)
    }
}

注意:Kotlin 开发者可以使用 callbackFlow 来利用协程和生命周期。如需查看示例,请参阅 GitHub 上的测量数据示例。

使用 ExerciseClient

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

应用结构

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

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

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

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

注意:如果应用遇到 AUTO_ENDED_PERMISSION_LOST 错误,原因可能是缺少具有适当位置信息权限的 ForegroundService。
我们建议您为包含 prepare 调用的锻炼前 activity 使用 AmbientModeSupport,但不要在锻炼期间显示指标。这是因为当设备屏幕处于氛围模式时,健康服务会批量处理锻炼数据以节省电量,因此显示的信息可能不是最新信息。

检查功能

在指标和运动目标方面,每个 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 字段是映射。键为 DataType 对象,值为与关联的 DataType 配合使用的 ComparisonType 对象集。使用这些字段来确定运动是否可以支持您想创建的运动目标。

如果您的应用允许用户使用自动暂停功能,您必须使用 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 文件中为请求的数据类型指定适当的权限。
验证用户是否已授予必要的权限。如需了解详情,请参阅请求应用权限。如果用户尚未授予必要的权限,健康服务会拒绝相关请求。
对于位置数据,还应额外执行以下步骤:

使用 isProviderEnabled(LocationManager.GPS_PROVIDER) 检查设备是否启用了 GPS。如有必要,提示用户打开位置信息设置。
确保在整个锻炼过程中保留具有相应 foregroundServiceType 的 ForegroundService。

准备锻炼

某些传感器(例如 GPS 或心率)可能需要短时间的预热,或者用户可能需要在开始锻炼前查看其数据。利用可选的 prepareExerciseAsync() 方法,无需启动锻炼计时器,就能支持接收数据并让传感器预热。activeDuration 不受此准备时间的影响。

在调用 prepareExerciseAsync() 之前,请检查以下内容:

请检查平台中的位置信息设置。如果此设置处于关闭状态,请通知用户应用未获得使用位置信息的许可,因此无法在应用中跟踪位置信息,并提示用户启用该设置。这是一项设备级设置,用户可以在“设置”主菜单中进行控制;该设置不同于应用级权限检查。
确认您的应用对身体传感器、运动状态识别和精确位置信息具有运行时权限。对于缺少的权限,请提示用户授予运行时权限并提供充分的上下文信息。如果用户未授予特定权限,请从 prepareExerciseAsync() 调用中移除与该权限关联的数据类型。如果身体传感器和位置信息权限均未授予,请勿调用 prepareExerciseAsync()。应用仍可获取基于步数的距离、步速、速度以及不需要这些权限的其他指标。
注意:您也可以使用 prepareExercise 的同步版本。
为了确保 prepareExerciseAsync() 调用成功,请执行以下操作:

针对包含 prepare 调用的锻炼前 activity 使用 AmbientModeSupport。
从前台服务调用 prepareExerciseAsync()。如果它不在某项服务中并且与 activity 生命周期相关联,那么传感器准备可能会被不必要地终止。
在用户离开锻炼前 activity 时,调用 endExercise() 关闭传感器并降低功耗。
以下示例展示了如何调用 prepareExerciseAsync():

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)

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 消息中反映出暂停状态时认为锻炼已暂停。尤其是在涉及界面状态时,更是必须考虑这一点。如果用户按暂停按钮,请停用暂停按钮,并对健康服务调用 pauseExerciseAsync()。使用 ExerciseUpdate.exerciseStateInfo.state 等待健康服务达到“已暂停”状态,然后将按钮切换到“恢复”状态。这是因为传送健康服务状态更新所需的时间可能比按下按钮的时间略长,如果您将所有界面更改都与按下按钮相关联,界面可能会与健康服务状态不同步。

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

已启用自动暂停功能:锻炼可能会在无用户互动的情况下暂停或开始。
其他应用开始锻炼:系统可能会在无用户互动的情况下终止您的锻炼。
如果您的应用的锻炼被其他应用终止,您的应用必须能够妥善处理这种终止情况:

保存部分完成锻炼状态,以免清除用户的进度。
移除持续性活动图标,并向用户发送通知,告知他们锻炼已被其他应用结束。
此外,请处理运动进行期间权限被撤消的情况。这种情况会通过 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
        }
        ...
    }
    ...
}

管理进行时长

在锻炼期间,应用可以显示锻炼的进行时长。应用、健康服务和设备 MCU(负责跟踪运动的低功耗处理器)都需要与同样的当前进行时长保持同步。为了便于对此进行管理,健康服务会发送 ActiveDurationCheckpoint 来提供定位点,应用可从定位点启动自己的计时器。

由于进行时长是从 MCU 发送的,可能需要一些时间才能到达应用,因此 ActiveDurationCheckpoint 包含两个属性:

activeDuration:运动已进行的时长
time:计算进行时长的时间
因此,在应用中,可以使用以下公式根据 ActiveDurationCheckpoint 计算的运动的进行时长:

(now() - checkpoint.time) + checkpoint.activeDuration

这种做法考虑到了从 MCU 上计算进行时长到计算结果到达应用之间的细微增量。这可用于在应用中植入精密计时器,并有助于确保应用的计时器与健康服务和 MCU 中的时间完全一致。

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

处理来自 ExerciseClient 的数据

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

处理器仅在处于唤醒状态或已达到最大报告期(例如每 150 秒)时传送消息。请勿依赖 ExerciseUpdate 频率让具有 activeDuration 的精密计时器前进。如需查看有关如何实现独立的精密计时器的示例,请参阅 GitHub 上的运动示例。

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

控制批处理过程中的数据传送频率
在某些情况下,您可能希望控制屏幕关闭时应用接收特定数据类型的频率。借助 BatchingMode 对象,您的应用可以替换默认的批处理行为,以更频繁地传送数据。

注意:健康服务的默认批处理行为适用于大多数锻炼场景。由于对电池性能有影响,请仅在绝对必要的情况下使用 BatchingMode。博文中列出了一些 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,如以下代码段所示。

注意:虽然您可以照常添加其他 DataType 对象,但其传送频率不会与默认批处理行为保持一致。

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 属性表示此信息。

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

存储和上传数据

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

目前没有任何推荐文档页面。

请尝试登录您的 Google 账号。

本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2023-08-07。

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五一编程

程序之路有我与你同行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值