1. 简介
健康数据共享是什么?
健康数据共享是一个面向 Android 应用开发者的健康数据平台。它提供了一个综合性界面,可用于访问用户的健康与健身数据,并确保在所有设备上提供一致的功能行为。借助健康数据共享,用户可在设备端安全地存储健康与健身数据,还可以全面控制和清楚了解相应访问权限。
健康数据共享如何运作?
健康数据共享支持 50 多种常见的健康与健身数据类型和类别,包括:活动数据、睡眠数据、营养数据、身体测量数据,以及心率和血压等生命体征数据。
获得用户权限后,开发者可以通过标准化架构和 API 行为,在健康数据共享中安全地读取和写入数据。用户可以全权控制其隐私设置,还可以随时使用精细的控制设置查看哪些应用在请求访问数据。健康数据共享中的数据会存储在设备端并进行加密。用户还可以在其设备上禁止应用访问数据或删除他们不想要的数据,还可以选择在使用多个应用时优先处理某个数据源。
健康数据共享架构
下面介绍了健康数据共享的主要方面和架构组件:
- 客户端应用 - 为了与健康数据共享集成,客户端应用会将相应 SDK 关联到其健康与健身应用。这样就有了与 Health Connect API 进行交互的 API Surface。
- 软件开发套件 - 该 SDK 可让客户端应用与健康数据共享 APK 通信。
- 健康数据共享 APK - 这是实现健康数据共享的 APK。它包含权限管理和数据管理组件。健康数据共享 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 商店安装健康数据共享应用。扫描下方二维码即可安装健康数据共享应用。
获取示例代码
首先,从 GitHub 克隆源代码:
git clone https://github.com/android/android-health-connect-codelab.git
示例目录中包含此 Codelab 的 start
和 finished
代码。在 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.1.0-alpha10"
}
声明健康数据共享可见性
如需在应用内与 Health Connect
互动,请在 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”这段文字的欢迎屏幕,以及一个菜单抽屉式导航栏。我们将在接下来的几个部分添加与健康数据共享互动的功能。
3. 权限控制
健康数据共享建议开发者仅针对应用中要用的数据类型发出权限请求。一揽子权限请求会降低用户对应用的信心,并可能降低用户信任度。如果权限遭拒超过两次,您的应用将被锁定,因此,不再显示权限请求。
在此 Codelab 中,我们只需要与下列数据相关的权限:
- 锻炼时段
- 心率
- 步数
- 消耗的总卡路里
- 体重
声明权限
应用读取或写入的每种数据类型都需要使用 AndroidManifest.xml
中的权限进行声明。从版本 1.0.0-alpha10
开始,健康数据共享会使用标准 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
中声明一个 intent 过滤器,以处理可说明应用如何使用这些权限的 intent。您的应用需要处理此 intent,并显示一份隐私权政策,说明如何使用和处理用户数据。用户点按健康数据共享权限对话框中的隐私权政策链接后,系统会向应用发送此 intent。
<!-- 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 Codelab。点击 Health Connect Codelab,以显示针对该应用的读取和写入权限的数据类型列表。
请求权限
除了直接将用户引导至健康数据共享设置页面来管理权限外,您还可以通过 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 按钮。点击请求权限以打开健康数据共享权限对话框。授予所需的权限,并返回 Codelab 应用。
4. 写入数据
让我们开始向健康数据共享中写入记录。如需写入 Weight 记录,请创建带有体重输入值的 WeightRecord
对象。请注意,Health Connect SDK 支持各种单位类。例如,可以使用 Mass.kilograms(weightInput)
将用户体重设置为以公斤为单位。
写入健康数据共享的所有数据都应指定时区偏移量信息。通过在写入数据时指定时区偏移量信息,可以在读取健康数据共享中的数据时提供时区信息。
创建体重记录后,使用 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
插入与时段关联的多条数据记录。此示例中的插入请求包括带有 ExerciseType
的 ExerciseSessionRecord
、带有步数的 StepsRecord
、带有 Energy
的 TotalCaloriesBurnedRecord
和一系列 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
}
现在,我们运行这个应用,看看您能否看到一系列体重记录和锻炼时段。
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" />
查看功能的适用范围
由于用户不一定总是使用最新版本的健康数据共享,因此最好先验证功能的适用范围。在 HealthConnectManager.kt
中,我们使用 getFeatureStatus
方法来实现此目的。
fun isFeatureAvailable(feature: Int): Boolean{
return healthConnectClient
.features
.getFeatureStatus(feature) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE
}
使用 FEATURE_READ_HEALTH_DATA_IN_BACKGROUND
常量验证 ExerciseSessionViewModel.kt
中的后台读取功能:
backgroundReadAvailable.value = healthConnectManager.isFeatureAvailable(
HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND
)
请求权限
验证后台读取功能是否可用后,您可以在锻炼时段屏幕上点击 Request Background Read(请求后台读取),请求 PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND 权限。
用户会看到以下提示:
用户还可以通过在系统设置中前往健康数据共享 > 应用权限 > Health Connect Codelab(健康数据共享 Codelab)> 其他访问权限,来授予后台读取访问权限:
在后台读取数据
使用 WorkManager
安排后台任务。点按 Read Steps In Background(在后台读取步数)按钮后,应用将在 10 秒延迟后启动 ReadStepWorker
。此 worker 将从健康数据共享检索过去 24 小时的总步数。随后,Logcat 中将显示类似以下内容的日志条目,详细说明此信息:
There are 4000 steps in Health Connect in the last 24 hours.
7. 读取差分数据
Health Connect Differential Changes API 有助于跟踪一组数据类型在特定时间点的更改。例如,如果您想了解用户是否已更新或删除应用外的任何现有记录,以便相应地更新数据库,就可以使用该 API。
只有在前台运行的应用才能通过健康数据共享读取数据。此限制旨在进一步加强用户隐私保护。它会通知用户并使其确信健康数据共享对他们的数据没有后台读取访问权限,只能在前台读取和访问数据。当应用在前台运行时,借助 Differential Changes API,开发者可以通过部署更改令牌来检索对健康数据共享所做的更改。
HealthConnectManager.kt
中有两个函数:getChangesToken()
和 getChanges()
。我们将向这些函数添加 Differential Changes API,以获取数据更改。
初始更改令牌设置
只有当您的应用使用更改令牌请求数据更改时,系统才会从健康数据共享中检索这些更改。该更改令牌表示提交历史记录中将要发生差分数据获取操作的点。
若要获取更改令牌,请发送 ChangesTokenRequest
,其中含有您要跟踪数据更改的一组数据类型。保存令牌,以便在想要从健康数据共享检索任何更新时使用。
/**
* TODO: Obtains a Changes token for the specified record types.
*/
suspend fun getChangesToken(): String {
return healthConnectClient.getChangesToken(
ChangesTokenRequest(
setOf(
ExerciseSessionRecord::class
)
)
)
}
包含更改令牌的数据更新
如果您想获取上次您的应用与健康数据共享同步时所做的更改,请使用之前获得的更改令牌并发送包含该令牌的 getChanges
调用。ChangesResponse
会返回健康数据共享中观察到的变化(例如 UpsertionChange
和 DeletionChange
)。
/**
* 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。现在,您应该可以看到插入的更改。
8. 汇总数据
健康数据共享还通过汇总 API 提供汇总数据。以下示例展示了如何从健康数据共享获取累计的统计数据。
使用 healthConnectClient.aggregate
发送 AggregateRequest
。在汇总请求中,指定一组汇总指标以及您希望获取的时间范围。例如,ExerciseSessionRecord.EXERCISE_DURATION_TOTAL
和 StepsRecord.COUNT_TOTAL
提供累计数据,而 WeightRecord.WEIGHT_AVG
、HeartRateRecord.BPM_MAX
和 HeartRateRecord.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]
}
以下示例展示了如何获取特定锻炼时段的关联汇总数据。首先,使用带有 uid
的 healthConnectClient.readRecord
读取一条记录。然后,将锻炼时段的 startTime
和 endTime
作为时间范围,将 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 屏幕,然后选择某个锻炼时段记录。
9. 恭喜
恭喜,您已成功构建您的第一个健康数据共享集成式健康与健身应用。
该应用可以声明对应用中所用数据类型的权限以及请求用户权限,还可以从健康数据共享数据存储中读取和写入数据。您还学习了如何使用 Health Connect Toolbox 在健康数据共享数据存储中创建模拟数据,从而为应用开发提供支持。
现在,您已了解让您的健康与健身应用加入健康数据共享生态系统所需的主要步骤。