你的第一个 Health Connect 集成应用

1. 简介

e4a4985ad1cdae8b.png

什么是 Health Connect?

Health Connect 是一个面向 Android 应用开发者的健康数据平台。它提供了一个单一的、整合的界面,用于访问用户的健康和健身数据,并在所有设备上提供一致的功能行为。借助 Health Connect,用户可以在设备上拥有安全的健康和健身数据存储,并完全控制和透明地访问数据。

Health Connect 如何工作?

Health Connect 支持 50 多种常见的健康和健身数据类型和类别,包括活动、睡眠、营养、身体测量以及心率和血压等生命体征。

How Health Connect Works

在用户许可的情况下,开发者可以使用标准化的模式和 API 行为安全地读取和写入 Health Connect。用户可以完全控制其隐私设置,并进行细粒度的控制,随时查看哪些应用正在请求访问数据。Health Connect 中的数据存储在设备上并经过加密。用户还可以关闭访问权限或删除他们不希望在设备上保留的数据,以及在使用多个应用时选择优先使用一个数据源。

Health Connect 架构

architecture

以下是 Health Connect 的关键方面和架构组件的说明

  • 客户端应用:要与 Health Connect 集成,客户端应用会将其 SDK 链接到其健康和健身应用中。这提供了一个与 Health Connect API 交互的 API 表面。
  • 软件开发工具包 (SDK):SDK 使客户端应用能够与 Health Connect APK 通信。
  • Health Connect APK:这是实现 Health Connect 的 APK。它包含其权限管理和数据管理组件。Health Connect APK 直接在用户的设备上提供,使 Health Connect 成为以设备为中心而不是以帐户为中心的平台。
  • 权限管理:Health Connect 包含一个用户界面,应用通过该界面请求用户的显示数据权限。它还提供现有用户权限的列表。这允许用户管理他们已授予或拒绝给各种应用程序的访问权限。
  • 数据管理:Health Connect 提供一个用户界面,概述记录的数据,无论是用户的步数、骑行速度、心率还是其他受支持的数据类型。

你将构建什么

在这个 Codelab 中,你将构建一个与 Health Connect 集成的简单的健康和健身应用。你的应用将执行以下操作:

  • 获取并检查用户数据访问权限。
  • 将数据写入 Health Connect。
  • 从 Health Connect 读取聚合数据。

你将学到什么

  • 如何设置你的环境以支持 Health Connect 集成开发。
  • 如何获取权限和执行权限检查。
  • 如何将健康和健身数据贡献到 Health Connect 平台。
  • 如何利用设备上的数据存储。
  • 如何使用 Google 提供的开发者工具验证你的应用。

你需要什么

  • 最新稳定版本的 Android Studio
  • 安装了 Android SDK 版本 28 (Pie) 或更高版本的 Android 移动设备。

2. 设置

准备 Health Connect 应用

Health Connect 应用负责处理你的应用程序通过 Health Connect SDK 发送的所有请求。这些请求包括存储数据和管理其读写访问权限。

对 Health Connect 的访问取决于手机上安装的 Android 版本。以下部分概述了如何处理几个最近的 Android 版本。

Android 14

从 Android 14(API 级别 34)开始,Health Connect 是 Android 框架的一部分。由于此版本的 Health Connect 是一个框架模块,因此无需进行任何设置。

Android 13 及更低版本

在 Android 13(API 级别 33)和更低版本上,Health Connect 不是 Android 框架的一部分。因此,你需要从 Google Play 商店安装 Health Connect 应用。扫描二维码安装 Health Connect。

633ed0490a74595d.png

获取示例代码

首先从 GitHub 克隆源代码

git clone https://github.com/android/android-health-connect-codelab.git

示例目录包含此 Codelab 的startfinished代码。在 Android Studio 的**项目**视图中,你会找到两个模块

  • start:此项目的启动代码,你将对其进行修改以完成 Codelab。
  • finished:此 Codelab 的完整代码,用于检查你的工作。

浏览“start”代码

Codelab 示例应用使用 Jetpack Compose 构建了基本的 UI,包含以下屏幕:

  • WelcomeScreen:这是应用的登录页面,根据 Health Connect 的可用性(已安装、未安装或不受支持)显示不同的消息。
  • PrivacyPolicyScreen:它解释了应用的权限使用情况,当用户点击 Health Connect 权限对话框中的**隐私政策**链接时会显示。
  • InputReadingsScreen:它演示了读取和写入简单的体重记录。
  • ExerciseSessionScreen:用户在此处插入和列出锻炼课程。点击记录后,它会将用户带到ExerciseSessionDetailScreen以显示与该课程相关的更多数据。
  • DifferentialChangesScreen:它演示了如何获取 Changes 令牌并从 Health Connect 获取新的更改。

HealthConnectManager存储与 Health Connect 交互的所有函数。在这个 Codelab 中,我们将逐步指导你完成基本功能。<!-- TODO:start构建中的字符串对应于此 Codelab 中的相应部分,其中提供了示例代码供你插入项目。

让我们从将 Health Connect 添加到项目开始!

添加 Health Connect 客户端 SDK

要开始使用 Health Connect SDK,你需要在build.gradle文件中添加一个依赖项。要查找 Health Connect 的最新版本,请查看Jetpack 库发行说明

dependencies {
    // Add a dependency of Health Connect SDK
    implementation "androidx.health.connect:connect-client:1.1.0-alpha10"
}

声明 Health Connect 可见性

要在应用中与Health Connect交互,请在AndroidManifest.xml中声明 Health Connect 包名称。

<!-- TODO: declare Health Connect visibility -->
<queries>
   <package android:name="com.google.android.apps.healthdata" />
</queries>

运行启动项目

设置完成后,运行start项目。此时,你应该会看到欢迎屏幕显示文本“Health Connect 已安装在此设备上”和一个菜单抽屉。我们将在后续部分添加与 Health Connect 交互的功能。

8f063e5b305189.png fd39f325f5c19e5d.png

3. 权限控制

Health Connect 建议开发者将权限请求限制为应用中使用的那些数据类型。广泛的权限请求会降低用户对应用的信任度,并可能降低用户信任度。如果权限被拒绝超过两次,你的应用将被锁定。因此,权限请求将不再出现。

出于本 Codelab 的目的,我们只需要以下权限:

  • 锻炼课程
  • 心率
  • 步数
  • 总卡路里消耗
  • 体重

声明权限

你的应用读取或写入的每种数据类型都需要使用AndroidManifest.xml中的权限进行声明。从版本1.0.0-alpha10开始,Health Connect 使用标准的 Android 权限声明格式。

要声明所需数据类型的权限,请使用<uses-permission>元素并为其分配相应的权限名称。将它们嵌套在<manifest>标签内。有关权限及其对应数据类型的完整列表,请参阅数据类型列表

<!-- TODO: Required to specify which Health Connect permissions the app can request -->
  <uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
  <uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
  <uses-permission android:name="android.permission.health.READ_STEPS"/>
  <uses-permission android:name="android.permission.health.WRITE_STEPS"/>
  <uses-permission android:name="android.permission.health.READ_EXERCISE"/>
  <uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
  <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
  <uses-permission android:name="android.permission.health.WRITE_TOTAL_CALORIES_BURNED"/>
  <uses-permission android:name="android.permission.health.READ_WEIGHT"/>
  <uses-permission android:name="android.permission.health.WRITE_WEIGHT"/>

AndroidManifest.xml中声明意图过滤器以处理解释你的应用如何使用这些权限的意图。你的应用需要处理此意图并显示一个隐私政策,说明如何使用和处理用户数据。一旦用户点击 Health Connect 权限对话框中的**隐私政策**链接,就会向应用发送此意图。

<!-- TODO: Add intent filter to handle permission rationale intent -->
<!-- Permission handling for Android 13 and before -->
<intent-filter>
  <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>

<!-- Permission handling for Android 14 and later -->
<intent-filter>
  <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/>
  <category android:name="android.intent.category.HEALTH_PERMISSIONS"/>
</intent-filter>

现在重新打开应用以查看已声明的权限。从菜单抽屉中点击**设置**以进入 Health Connect 设置屏幕。然后,点击**应用权限**,你应该会看到列表中的**Health Connect Codelab**。点击**Health Connect Codelab**以显示该应用的读写访问权限的数据类型列表。

fbed69d871f92178.png 1b9c7764c1dbdfac.png

请求权限

除了直接将用户带到 Health Connect 设置以管理权限外,你还可以通过 Health Connect API 从你的应用请求权限。请注意,用户可以随时更改权限,因此请确保你的应用检查所需权限是否可用。在本 Codelab 项目中,我们在读取或写入数据之前检查并发送权限请求。

HealthConnectClient是 Health Connect API 的入口点。在HealthConnectManager.kt中,获取HealthConnectClient实例。

private val healthConnectClient by lazy { HealthConnectClient.getOrCreate(context) }

要在您的应用中启动请求权限对话框,首先需要构建一组所需数据类型的权限。您必须仅请求您实际使用的那些数据类型的权限。

例如,在**记录体重**屏幕中,您只需要授予体重的读写权限。我们在InputReadingsViewModel.kt中创建了一个权限集,如下代码所示。

  val permissions = setOf(
    HealthPermission.getReadPermission(WeightRecord::class),
    HealthPermission.getWritePermission(WeightRecord::class),
  )

然后,在启动权限请求之前,检查权限是否已授予。在HealthConnectManager.kt中,使用getGrantedPermissions检查所需数据类型的权限是否已授予。要启动权限请求,您必须使用PermissionController.createRequestPermissionResultContract()创建一个ActivityResultContract,当未授予所需权限时,应启动此Contract。

  suspend fun hasAllPermissions(permissions: Set<String>): Boolean {
    return healthConnectClient.permissionController.getGrantedPermissions().containsAll(permissions)
  }

  fun requestPermissionsActivityContract(): ActivityResultContract<Set<String>, Set<String>> {
    return PermissionController.createRequestPermissionResultContract()
  }

在Codelab示例应用中,如果您尚未向所需数据类型授予权限,则可能会看到屏幕上显示的**请求权限**按钮。单击**请求权限**以打开Health Connect权限对话框。允许所需的权限并返回Codelab应用。

a0eb27cea376e56f.png 4752973f6b0b8d56.png

4. 写入数据

让我们开始将记录写入Health Connect。要写入体重记录,请使用体重输入值创建一个WeightRecord对象。请注意,Health Connect SDK支持各种单位类。例如,使用Mass.kilograms(weightInput)以千克为单位设置用户的体重。

写入Health Connect的所有数据都应指定时区偏移信息。写入数据时指定时区偏移信息可在Health Connect中读取数据时提供时区信息。

创建体重记录后,使用healthConnectClient.insertRecords将数据写入Health Connect。

/**
* TODO: Writes [WeightRecord] to Health Connect.
*/
suspend fun writeWeightInput(weightInput: Double) {
   val time = ZonedDateTime.now().withNano(0)
   val weightRecord = WeightRecord(
       weight = Mass.kilograms(weightInput),
       time = time.toInstant(),
       zoneOffset = time.offset
   )
   val records = listOf(weightRecord)
   try {
      healthConnectClient.insertRecords(records)
      Toast.makeText(context, "Successfully insert records", Toast.LENGTH_SHORT).show()
   } catch (e: Exception) {
      Toast.makeText(context, e.message.toString(), Toast.LENGTH_SHORT).show()
   }
}

现在让我们运行应用。单击**记录体重**并以千克为单位输入新的体重记录。要验证体重记录是否已成功写入Health Connect,请在设置中打开Health Connect应用,然后转到**数据和访问 > 体测 > 体重 > 查看所有条目**。您应该会看到从Health Connect Codelab写入的新体重记录。

写入运动会话

会话是指用户执行活动的时间间隔。Health Connect中的运动会话可以包括从跑步到羽毛球的任何活动。会话允许用户测量基于时间段的性能。此数据记录在一段时间内测量的瞬时样本数组,例如活动期间的持续心率或位置样本。

以下示例演示如何写入运动会话。使用healthConnectClient.insertRecords插入与会话关联的多个数据记录。此示例中的插入请求包括带有ExerciseTypeExerciseSessionRecord,带有步数的StepsRecord,带有EnergyTotalCaloriesBurnedRecord以及一系列HeartRateRecord样本。

  /**
   * TODO: Writes an [ExerciseSessionRecord] to Health Connect.
   */
  suspend fun writeExerciseSession(start: ZonedDateTime, end: ZonedDateTime) {
    healthConnectClient.insertRecords(
      listOf(
        ExerciseSessionRecord(
          startTime = start.toInstant(),
          startZoneOffset = start.offset,
          endTime = end.toInstant(),
          endZoneOffset = end.offset,
          exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
          title = "My Run #${Random.nextInt(0, 60)}"
        ),
        StepsRecord(
          startTime = start.toInstant(),
          startZoneOffset = start.offset,
          endTime = end.toInstant(),
          endZoneOffset = end.offset,
          count = (1000 + 1000 * Random.nextInt(3)).toLong()
        ),
        TotalCaloriesBurnedRecord(
          startTime = start.toInstant(),
          startZoneOffset = start.offset,
          endTime = end.toInstant(),
          endZoneOffset = end.offset,
          energy = Energy.calories((140 + Random.nextInt(20)) * 0.01)
        )
      ) + buildHeartRateSeries(start, end)
    )
  }

  /**
   * TODO: Build [HeartRateRecord].
   */
  private fun buildHeartRateSeries(
    sessionStartTime: ZonedDateTime,
    sessionEndTime: ZonedDateTime,
  ): HeartRateRecord {
    val samples = mutableListOf<HeartRateRecord.Sample>()
    var time = sessionStartTime
    while (time.isBefore(sessionEndTime)) {
      samples.add(
        HeartRateRecord.Sample(
          time = time.toInstant(),
          beatsPerMinute = (80 + Random.nextInt(80)).toLong()
        )
      )
      time = time.plusSeconds(30)
    }
    return HeartRateRecord(
      startTime = sessionStartTime.toInstant(),
      startZoneOffset = sessionStartTime.offset,
      endTime = sessionEndTime.toInstant(),
      endZoneOffset = sessionEndTime.offset,
      samples = samples
    )
  }

5. 读取数据

既然您已经使用Codelab示例应用和Toolbox应用编写了体重和运动会话记录,那么让我们使用Health Connect API来读取这些记录。首先,创建一个ReadRecordsRequest,并指定要从中读取的记录类型和时间范围。ReadRecordsRequest还可以设置dataOriginFilter以指定要从中读取记录的源应用。

    /**
     * TODO: Reads in existing [WeightRecord]s.
     */
    suspend fun readWeightInputs(start: Instant, end: Instant): List<WeightRecord> {
        val request = ReadRecordsRequest(
            recordType = WeightRecord::class,
            timeRangeFilter = TimeRangeFilter.between(start, end)
        )
        val response = healthConnectClient.readRecords(request)
        return response.records
    }
  /**
   * TODO: Obtains a list of [ExerciseSessionRecord]s in a specified time frame.
   */
  suspend fun readExerciseSessions(start: Instant, end: Instant): List<ExerciseSessionRecord> {
    val request = ReadRecordsRequest(
      recordType = ExerciseSessionRecord::class,
      timeRangeFilter = TimeRangeFilter.between(start, end)
    )
    val response = healthConnectClient.readRecords(request)
    return response.records
  }

现在让我们运行应用,并检查您是否可以看到体重记录和运动会话的列表。

a08af54eef6bc832.png 3b0781389f1094a1.png

6. 在后台读取数据

声明权限

要在后台访问健康数据,请在您的AndroidManifest.xml文件中声明READ_HEALTH_DATA_IN_BACKGROUND权限。

<!-- TODO: Required to specify which Health Connect permissions the app can request -->
...
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND" />

检查功能可用性

因为用户可能并不总是拥有最新版本的Health Connect,所以最好首先验证功能可用性。在HealthConnectManager.kt中,我们使用getFeatureStatus方法来实现此目的。

fun isFeatureAvailable(feature: Int): Boolean{
    return healthConnectClient
      .features
      .getFeatureStatus(feature) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE
  }

ExerciseSessionViewModel.kt中的后台读取功能使用FEATURE_READ_HEALTH_DATA_IN_BACKGROUND常量进行验证。

backgroundReadAvailable.value = healthConnectManager.isFeatureAvailable(
      HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND
    )

请求权限

验证后台读取功能可用后,您可以通过单击**运动会话**屏幕上的**请求后台读取**来请求PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND权限。

用户会看到以下提示

18796e03ae3941b1.png

用户也可以通过在系统设置中导航到**Health Connect > 应用权限 > Health Connect Codelab > 附加访问**来授予后台读取访问权限。

958081eb9b1fecb3.png

在后台读取数据

利用WorkManager来安排后台任务。点击**在后台读取步数**按钮后,应用将在10秒延迟后启动ReadStepWorker。此工作器将从Health Connect检索过去24小时的总步数。随后,Logcat中会出现包含此信息的类似日志条目。

There are 4000 steps in Health Connect in the last 24 hours. 

7. 读取差异数据

Health Connect差异更改API有助于跟踪从特定时间点开始的一组数据类型的更改。例如,您想知道用户是否已在您的应用之外更新或删除了任何现有记录,以便您可以相应地更新您的数据库。

使用Health Connect读取数据仅限于在前景中运行的应用程序。此限制是为了进一步加强用户隐私。它会通知并向用户保证Health Connect无权后台读取其数据,并且数据仅在前景中读取和访问。当应用处于前景时,差异更改API允许开发人员通过部署更改令牌来检索对Health Connect所做的更改。

HealthConnectManager.kt中有两个函数getChangesToken()getChanges()。我们将向这些函数添加差异更改API以获取数据更改。

初始更改令牌设置

只有当您的应用使用更改令牌请求它们时,才会从Health Connect检索数据更改。更改令牌表示将从中获取差异数据的提交历史记录中的点。

要获取更改令牌,请发送一个ChangesTokenRequest,其中包含要跟踪其数据更改的一组数据类型。保留令牌,并在您想要从Health Connect检索任何更新时使用它。

  /**
   * TODO: Obtains a Changes token for the specified record types.
   */
  suspend fun getChangesToken(): String {
    return healthConnectClient.getChangesToken(
      ChangesTokenRequest(
        setOf(
          ExerciseSessionRecord::class
        )
      )
    )
  }

使用更改令牌更新数据

当您想要获取自上次应用与Health Connect同步以来的更改时,请使用您之前获得的更改令牌,并使用该令牌发送getChanges调用。ChangesResponse返回从Health Connect观察到的更改列表,例如UpsertionChangeDeletionChange

  /**
   * TODO: Retrieve changes from a Changes token.
   */
  suspend fun getChanges(token: String): Flow<ChangesMessage> = flow {
    var nextChangesToken = token
    do {
      val response = healthConnectClient.getChanges(nextChangesToken)
      if (response.changesTokenExpired) {
        throw IOException("Changes token has expired")
      }
      emit(ChangesMessage.ChangeList(response.changes))
      nextChangesToken = response.nextChangesToken
    } while (response.hasMore)
    emit(ChangesMessage.NoMoreChanges(nextChangesToken))
  }

现在让我们运行应用并转到**更改**屏幕。首先,启用**跟踪更改**以获取更改令牌。然后从Toolbox或Codelab应用插入体重或运动会话。返回**更改**屏幕并选择**获取新的更改**。您现在应该会看到插入更改。

f3aded8ae5487e9c.png 437d69e3e000ce81.png

8. 聚合数据

Health Connect还通过聚合API提供聚合数据。以下示例演示如何从Health Connect获取累积数据和统计数据。

使用healthConnectClient.aggregate发送AggregateRequest。在聚合请求中,指定一组聚合指标和您想要获取的时间范围。例如,ExerciseSessionRecord.EXERCISE_DURATION_TOTALStepsRecord.COUNT_TOTAL提供累积数据,而WeightRecord.WEIGHT_AVGHeartRateRecord.BPM_MAXHeartRateRecord.BPM_MIN提供统计数据。

    /**
     * TODO: Returns the weekly average of [WeightRecord]s.
     */
    suspend fun computeWeeklyAverage(start: Instant, end: Instant): Mass? {
        val request = AggregateRequest(
            metrics = setOf(WeightRecord.WEIGHT_AVG),
            timeRangeFilter = TimeRangeFilter.between(start, end)
        )
        val response = healthConnectClient.aggregate(request)
        return response[WeightRecord.WEIGHT_AVG]
    }

此示例演示如何获取特定运动会话的相关聚合数据。首先,使用healthConnectClient.readRecorduid读取记录。然后,使用运动会话的startTimeendTime作为时间范围,以及dataOrigin作为过滤器来读取相关的聚合。

  /**
   * TODO: Reads aggregated data and raw data for selected data types, for a given [ExerciseSessionRecord].
   */
  suspend fun readAssociatedSessionData(
      uid: String,
  ): ExerciseSessionData {
    val exerciseSession = healthConnectClient.readRecord(ExerciseSessionRecord::class, uid)
    // Use the start time and end time from the session, for reading raw and aggregate data.
    val timeRangeFilter = TimeRangeFilter.between(
      startTime = exerciseSession.record.startTime,
      endTime = exerciseSession.record.endTime
    )
    val aggregateDataTypes = setOf(
      ExerciseSessionRecord.EXERCISE_DURATION_TOTAL,
      StepsRecord.COUNT_TOTAL,
      TotalCaloriesBurnedRecord.ENERGY_TOTAL,
      HeartRateRecord.BPM_AVG,
      HeartRateRecord.BPM_MAX,
      HeartRateRecord.BPM_MIN,
    )
    // Limit the data read to just the application that wrote the session. This may or may not
    // be desirable depending on the use case: In some cases, it may be useful to combine with
    // data written by other apps.
    val dataOriginFilter = setOf(exerciseSession.record.metadata.dataOrigin)
    val aggregateRequest = AggregateRequest(
      metrics = aggregateDataTypes,
      timeRangeFilter = timeRangeFilter,
      dataOriginFilter = dataOriginFilter
    )
    val aggregateData = healthConnectClient.aggregate(aggregateRequest)
    val heartRateData = readData<HeartRateRecord>(timeRangeFilter, dataOriginFilter)

    return ExerciseSessionData(
      uid = uid,
      totalActiveTime = aggregateData[ExerciseSessionRecord.EXERCISE_DURATION_TOTAL],
      totalSteps = aggregateData[StepsRecord.COUNT_TOTAL],
      totalEnergyBurned = aggregateData[TotalCaloriesBurnedRecord.ENERGY_TOTAL],
      minHeartRate = aggregateData[HeartRateRecord.BPM_MIN],
      maxHeartRate = aggregateData[HeartRateRecord.BPM_MAX],
      avgHeartRate = aggregateData[HeartRateRecord.BPM_AVG],
      heartRateSeries = heartRateData,
    )
  }

现在让我们运行应用,并检查您是否可以在**记录体重**屏幕上看到平均体重。您还可以通过打开**运动会话**屏幕并选择其中一个运动会话记录来查看运动会话的详细信息。

af1fe646159d6a60.png

9. 恭喜

恭喜您成功构建了第一个集成Health Connect的健康和健身应用。

该应用可以声明权限并在应用中使用的各种数据类型上请求用户权限。它还可以读取和写入Health Connect数据存储中的数据。您还学习了如何使用Health Connect Toolbox通过在Health Connect数据存储中创建模拟数据来支持您的应用开发。

您现在了解了使您的健康和健身应用成为Health Connect生态系统一部分所需的关键步骤。

进一步阅读