您的第一个健康数据共享集成应用

1. 简介

e4a4985ad1cdae8b.png

健康数据共享是什么?

健康数据共享是一个面向 Android 应用开发者的健康数据平台。它提供了一个综合性界面,可用于访问用户的健康与健身数据,并确保在所有设备上提供一致的功能行为。借助健康数据共享,用户可在设备端安全地存储健康与健身数据,还可以全权控制和清楚了解相应访问权限。

健康数据共享如何运作?

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

健康数据共享的工作原理

获得用户权限后,开发者可以通过标准化架构和 API 行为,在健康数据共享中安全地读取和写入数据。用户可以全权控制其隐私设置,还可以随时使用精细的控制设置查看哪些应用在请求访问数据。健康数据共享中的数据会存储在设备端并进行加密。用户还可以在其设备上禁止应用访问数据或删除他们不想要的数据,还可以选择在使用多个应用时优先处理某个数据源。

健康数据共享架构

架构

下面介绍了健康数据共享的主要方面和架构组件:

  • 客户端应用 - 为了与健康数据共享集成,客户端应用会将相应 SDK 关联到其健康与健身应用。这样就有了与 Health Connect API 进行交互的 API Surface。
  • 软件开发套件 - 该 SDK 可让客户端应用与健康数据共享 APK 通信。
  • 健康数据共享 APK - 这是实现健康数据共享的 APK。它包含权限管理和数据管理组件。Health Connect APK 直接在用户设备上提供,从而使健康数据共享以设备为中心,而非以账号为中心。
  • 权限管理 - 健康数据共享包含一个界面,应用可通过该界面请求用户授予显示数据的权限。它还提供现有用户权限的列表。这样一来,用户便可以管理他们针对各种应用授予或拒绝的访问权限。
  • 数据管理 - 健康数据共享提供一个界面,其中显示了所记录数据的概览,包括用户的步数、骑车速度、心率或其他支持的数据类型。

构建内容

在此 Codelab 中,您将构建一个与健康数据共享集成的简单健康与健身应用。您的应用将执行以下操作:

  • 获取和检查用户的数据访问权限。
  • 将数据写入健康数据共享。
  • 从健康数据共享中读取汇总数据。

学习内容

  • 如何设置您的环境,以支持健康数据共享集成开发。
  • 如何获取权限和执行权限检查。
  • 如何为健康数据共享平台贡献健康与健身数据。
  • 如何从设备端数据存储功能中受益。
  • 如何使用 Google 提供的开发者工具验证您的应用。

所需条件

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

2. 准备工作

准备健康数据共享应用

健康数据共享应用负责处理您的应用通过健康数据共享 SDK 发送的所有请求。这些请求包括存储数据和管理数据读写访问权限。

对健康数据共享的访问权限取决于手机上安装的 Android 版本。以下部分概述了如何处理多个近期的 Android 版本。

Android 14

从 Android 14(API 级别 34)开始,健康数据共享是 Android 框架的一部分。由于此版本的健康数据共享是一个框架模块,因此无需进行设置。

Android 13 及更低版本

在 Android 13(API 级别 33)及更低版本中,健康数据共享不是 Android 框架的一部分。因此,您需要从 Google Play 商店安装健康数据共享应用。扫描下方二维码即可安装健康数据共享应用。

633ed0490a74595d.png

获取示例代码

示例目录中包含此 Codelab 的起始代码和完成后代码。在 Android Studio 的 Project 视图中,您将看到两个模块:

  • start - 该项目的起始代码;您将通过更改这些代码来完成此 Codelab。
  • finished:该 Codelab 的完成后代码;用于检查您的工作。

探索起始代码

此 Codelab 示例应用具有由 Jetpack Compose 构建的基本界面,其中包含以下屏幕:

  • WelcomeScreen:这是应用的着陆页,根据健康数据共享的可用性(已安装、未安装或不受支持)显示不同的消息。
  • PrivacyPolicyScreen:该页面说明了当用户点击“健康数据共享”权限对话框中的隐私权政策链接时所显示的应用的权限使用情况。
  • InputReadingsScreen:该页面演示了如何读取和写入简单的体重记录。
  • ExerciseSessionScreen:该页面可供用户插入和列出锻炼时段。点击记录后,用户会转到 ExerciseSessionDetailScreen,从而查看更多与相应时段相关的数据。
  • DifferentialChangesScreen:该页面演示了如何从健康数据共享获取更改令牌并获取新更改。

HealthConnectManager 会存储与健康数据共享交互的所有功能。在此 Codelab 中,我们将逐步引导您实现基本功能。start build 中的 <!-- TODO: 字符串在此 Codelab 中有对应的部分,您可以将其中提供的示例代码插入该项目中。

首先,将健康数据共享添加到项目中!

添加健康数据共享客户端 SDK

如需开始使用健康数据共享 SDK,您需要向 build.gradle 文件添加一个依赖项。如需查找最新版本的健康数据共享,请查看 Jetpack 库版本

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

声明健康数据共享可见性

如需在应用内与健康数据共享互动,请在 AndroidManifest.xml 中声明健康数据共享软件包名称:

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

运行起始项目

全部设置完毕后,运行 start 项目。此时,您应该能够看到显示“Health Connect is installed on this device”这段文字的欢迎屏幕,以及一个菜单抽屉式导航栏。我们将在接下来的几个部分添加与健康数据共享互动的功能。

d54773774e4dc9f.png 462cd7b6cf553ad.png

3. 权限控制

健康数据共享建议开发者仅针对应用中要用的数据类型发出权限请求。一揽子权限请求会降低用户对应用的信心,并可能降低用户信任度。如果权限遭拒超过两次,您的应用将被锁定,因此,不再显示权限请求。

在此 Codelab 中,我们只需要与下列数据相关的权限:

  • 锻炼时段
  • 心率
  • 步数
  • 消耗的总卡路里
  • 体重

声明权限

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

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

<!-- TODO: declare Health Connect permissions -->
  <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 中声明一个 intent 过滤器,以处理可说明应用如何使用这些权限的 intent。您的应用需要处理此 intent,并显示一份隐私权政策,说明如何使用和处理用户数据。用户点按健康数据共享权限对话框中的隐私权政策链接后,系统会向应用发送此 intent。

<!-- TODO: Add intent filter to handle permission rationale intent -->
<intent-filter>
    <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>

现在,重新打开该应用,查看声明的权限。点击菜单抽屉式导航栏中的设置前往健康数据共享设置屏幕。然后,点击应用权限,您应在列表中看到 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) }

如需在您的应用内启动请求权限对话框,请先为所需的数据类型构建一组权限。您必须请求对仅使用的数据类型的权限。

例如,在“Record weight”屏幕中,您只需授予对 Record weight 的读写权限。我们已在 InputReadingsViewModel.kt 中创建权限集,如以下代码所示。

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

然后,在启动权限请求之前,检查用户是否已授予相应权限。在 HealthConnectManager.kt 中,使用 getGrantedPermissions 检查用户是否已授予对所需数据类型的权限。如需启动权限请求,您必须使用 PermissionController.createRequestPermissionResultContract() 创建 ActivityResultContract,如果未获得所需权限,则需要启动它。

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

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

在该 Codelab 示例应用中,如果您未针对所需的数据类型授予权限,您可能会在屏幕上看到 Request permissions 按钮。点击请求权限以打开 Health Connect 权限对话框。授予所需的权限,并返回 Codelab 应用。

626eedcec23659ce.png 6df6cf0e5c4a1a9e.png

4. 写入数据

让我们开始向 Health Connect 中写入记录。如需写入 Weight 记录,请创建带有体重输入值的 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()
   }
}

现在,我们运行这个应用。点击 Record weight,并以公斤为单位输入新的体重记录。如需验证体重记录是否已成功写入健康数据共享,请在“设置”中打开健康数据共享应用,然后依次前往数据和访问权限 -> 身体测量数据 -> 体重 -> 查看所有条目。您应该会看到从 Health Connect Codelab 写入的新体重记录。

写入锻炼时段

时段是指用户执行活动的时间间隔。“健康数据共享”中的锻炼时段包括跑步、打羽毛球,等等。通过时段,用户可以衡量基于时间的表现。此数据记录一段时间内测量的一系列瞬时样本,例如活动期间的连续心率或位置样本。

以下示例展示了如何写入锻炼时段。使用 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. Health Connect Toolbox

Health Connect Toolbox 简介

Health Connect Toolbox 是一款配套的开发者工具,可帮助您测试应用与 Health Connect 的集成。它可以直接在 Health Connect 中轻松读取和写入数据,支持您测试应用的 CRUD 操作。

在此 Codelab 中,我们将使用 Health Connect Toolbox 测试您刚刚实现的读写功能。

设置 Health Connect Toolbox

解压缩该 ZIP 文件以获取 APK 文件。然后,使用 adb 在已连接的设备上安装 Toolbox APK。遍历到 APK 所在的文件夹,然后运行以下命令:

$ adb install HealthConnectToolbox-{Version Number}.apk

首次打开 Health Connect Toolbox 应用时,系统会带您转到应用 > 特殊应用权限 > 显示在其他应用的上层下的权限设置。借助此权限,Health Connect Toolbox 可在其他应用上方显示叠加层,让您无需离开正在开发的应用即可测试数据读写。

如需管理测试的读写权限,您可以从 Toolbox 应用的主屏幕打开 Health Connect 应用,也可以直接进入权限流程。

c3e6fd40b03b408a.png

读取和写入健康记录

健康数据共享 Toolbox 支持读取和写入所有类型的健康数据共享数据。在此 Codelab 的上次会话中,您已成功向 Health Connect 写入了体重和锻炼时段记录。我们来看看您是否可以从 Health Connect Toolbox 中读取数据。

在 Health Connect 中读写数据之前,必须先从用户处获取权限;对于 Health Connect Toolbox 也是如此。首先,接受健康数据共享 Toolbox 的权限请求。接下来,点击叠加层菜单中的搜索图标 1f407c55884bb8c3.png 打开对话框,选择数据类型(例如“体重”)并点击读取健康记录。您应该会在 Codelab 示例应用中看到您刚才写入健康数据共享的记录。

如需将记录插入到健康数据共享中,请点击叠加层菜单中的修改图标 10c524823c596aea.png 打开对话框,然后选择相应数据类型。从 Toolbox 插入一条体重记录。在下一部分,我们将介绍如何通过 Health Connect API 读取记录,并在您的应用中显示数据。

bde87b4026896f99.png

6. 读取数据

现在,您已使用 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

7. 读取差分数据

Health Connect Differential Changes API 有助于跟踪一组数据类型在特定时间点的更改。例如,如果您想了解用户是否已更新或删除应用外的任何现有记录,以便相应地更新数据库,就可以使用该 API。

只有在前台运行的应用才能通过 Health Connect 读取数据。此限制旨在进一步加强用户隐私保护。它会通知用户并使其确信 Health Connect 对他们的数据没有后台读取访问权限,只能在前台读取和访问数据。当应用在前台运行时,借助 Differential Changes API,开发者可以通过部署更改令牌来检索对 Health Connect 所做的更改。

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

初始更改令牌设置

只有当您的应用使用更改令牌请求数据更改时,系统才会从 Health Connect 中检索这些更改。该更改令牌表示提交历史记录中将要发生差分数据获取操作的点。

若要获取更改令牌,请发送 ChangesTokenRequest,其中含有您要跟踪数据更改的一组数据类型。保存令牌,以便在想要从健康数据共享检索任何更新时使用。

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

包含更改令牌的数据更新

如果您想获取上次您的应用与健康数据共享同步时所做的更改,请使用之前获得的更改令牌并发送包含该令牌的 getChanges 调用。ChangesResponse 会返回健康数据共享中观察到的变化(例如 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))
  }

现在,运行应用,并转到Changes屏幕。首先,点击启用 Track changes,以获取更改令牌。然后,插入来自 Toolbox 或 Codelab 应用的体重或锻炼时段。返回 Changes 屏幕,然后选择 Get new changes。现在,您应该可以看到插入的更改。

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]
    }

以下示例展示了如何获取特定锻炼时段的关联汇总数据。首先,使用带有 uidhealthConnectClient.readRecord 读取一条记录。然后,将锻炼时段的 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,
    )
  }

现在,我们运行这个应用,看看您能否在 Record weight 屏幕上看到平均体重。此外,如要查看某个锻炼时段的详细数据,请打开 Exercise sessions 屏幕,然后选择某个锻炼时段记录。

af1fe646159d6a60.png

9. 恭喜

恭喜,您已成功构建您的第一个 Health Connect 集成式健康与健身应用。

该应用可以声明对应用中所用数据类型的权限以及请求用户权限,还可以从 Health Connect 数据存储中读取和写入数据。您还学习了如何使用 Health Connect Toolbox 在 Health Connect 数据存储中创建模拟数据,从而为应用开发提供支持。

现在,您已了解让您的健康与健身应用加入 Health Connect 生态系统所需的主要步骤。

深入阅读