1. 簡介
什麼是健康資料同步?
健康資料同步是 Android 應用程式開發人員專用的健康資料平台,可透過單一整合式介面存取使用者的健康與健身資料,並且在所有裝置上保持一致的功能行為。使用者可透過健康資料同步將健康與健身資料安全地儲存在裝置上,並且完全控管及瞭解資料存取權。
健康資料同步的運作方式為何?
健康資料同步支援超過 50 種常見的健康與健身資料類型和類別,包括活動、睡眠、營養、身體測量資料,以及心率和血壓等生命徵象。
開發人員可以透過使用者權限,使用標準化結構定義和 API 行為在健康資料同步中安全地讀取及寫入資料。使用者可透過精細的控制項完全掌控自己的隱私權設定,以便隨時查看哪些應用程式要求存取資料。健康資料同步中的資料會儲存在裝置上並進行加密。使用者也能夠關閉存取權,或是刪除不想保存在裝置上的資料,並可在使用多個應用程式時,選擇要優先處理哪一項資料來源。
健康資料同步架構
以下將說明健康資料同步的重要面向和架構元件:
- 用戶端應用程式:如要與「健康資料同步」整合,用戶端應用程式必須將 SDK 連結至其健康與健身應用程式。這麼做可提供用來與 Health Connect API 互動的 API 介面。
- 軟體開發套件:這個 SDK 可讓用戶端應用程式與健康資料同步APK 進行通訊。
- 健康資料同步 APK:這是實作「健康資料同步」的 APK,當中包含權限管理和資料管理元件。系統會直接在使用者的裝置上提供這個 APK,讓健康資料同步以裝置為中心,而非以帳戶為中心。
- 權限管理:健康資料同步提供使用者介面,如果應用程式要顯示資料,可透過該介面請使用者授予權限。此外,健康資料同步也提供現有的使用者權限清單,讓使用者能管理先前授予或拒絕的各種應用程式的存取權。
- 資料管理:健康資料同步提供的使用者介面可列出記錄資料總覽,無論是使用者的步數、自行車速度、心率或任何其他支援的資料類型,都能一目瞭然。
建構項目
在這個程式碼研究室中,您將建構一個與健康資料同步整合的簡易健康與健身應用程式,該應用程式會執行以下作業:
- 取得及檢查使用者的資料存取權。
- 將資料寫入 Health Connect。
- 讀取健康資料同步的匯總資料。
課程內容
- 如何設定支援健康資料同步整合開發作業的環境。
- 如何取得及檢查權限。
- 如何將健康與健身資料提供給健康資料同步平台。
- 如何活用裝置端儲存空間。
- 如何使用 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 商店安裝健康資料同步應用程式。掃描下方 QR code 即可安裝「健康資料同步」。
取得範例程式碼
範例目錄包含本程式碼研究室的 start 和 finished 程式碼。在 Android Studio 的「Project」檢視畫面中,您會看到兩個模組:
start
:本專案的範例程式碼。您需要加以修改,才能完成這個程式碼研究室。finished
:本程式碼研究室完成後的程式碼,可用來檢查您的成果。
探索「範例」程式碼
本程式碼研究室的範例應用程式中有透過 Jetpack Compose 建立的基本 UI,包含下列畫面:
WelcomeScreen
:這是應用程式的到達網頁,會依據健康資料同步的適用情形 (已安裝、未安裝或不支援) 顯示不同的訊息。PrivacyPolicyScreen
:說明應用程式的權限用途,也就是使用者在健康資料同步權限對話方塊中點選「隱私權政策」連結時顯示的內容。InputReadingsScreen
:説明如何讀取及寫入簡單的體重記錄。ExerciseSessionScreen
:使用者可以在這裡插入及列出運動時段。使用者點選記錄後,系統會將使用者導向ExerciseSessionDetailScreen
,以便顯示與該時段相關的更多資料。DifferentialChangesScreen
:說明如何取得「變更」權杖與健康資料同步新的變更內容。
HealthConnectManager
會儲存與健康資料同步互動的所有函式。在本程式碼研究室中,我們會逐步引導您完成常用功能。start
版本中的 <!-- TODO:
字串在本程式碼研究室中有對應章節,提供了可供您插入專案的範例程式碼。
首先,請將健康資料同步新增至專案!
新增健康資料同步用戶端 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 專案
設定完成後,請執行 start
專案。此時,您應該會看到顯示「Health Connect is installed on this device」文字訊息的歡迎畫面,以及選單導覽匣。我們將在後續章節中,新增要與健康資料同步互動的功能。
3. 權限控制
健康資料同步建議開發人員只針對應用程式會使用的資料類型提出權限要求。大量權限要求會減少使用者對應用程式的信心,降低使用者的信任。如果使用者拒絕授予權限超過兩次,應用程式會遭到鎖定,因此系統不會再顯示權限要求。
以本程式碼研究室為例,我們只需要下列權限:
- 運動時段
- 心跳速率
- 操作步驟
- 卡路里燃燒總量
- 體重
宣告權限
應用程式讀取或寫入的每個資料類型,都需要在 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
中宣告意圖篩選器,以處理用來說明應用程式使用權限方式的意圖。應用程式必須處理此意圖,並顯示隱私權政策,說明使用者資料的使用方式和處理方式。當使用者輕觸健康資料同步權限對話方塊中的「隱私權政策」連結時,系統就會將此意圖傳送至應用程式。
<!-- TODO: Add intent filter to handle permission rationale intent -->
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
現在,請重新開啟應用程式,查看已宣告的權限。按一下選單導覽匣中的「Settings」,前往健康資料同步的「Settings」畫面。接著,按一下「App permissions」,清單中應會顯示「Health Connect Codelab」。按一下「Health Connect Codelab」,系統會列出該應用程式可讀取及寫入的資料類型。
要求權限
除了直接將使用者直接導向健康資料同步設定來管理權限外,您也可以透過 Health Connect API 從應用程式要求權限。請注意,使用者可以隨時變更權限,因此請確保應用程式會檢查是否可存取必要權限。在本程式碼研究室專案中,我們會先檢查並傳送權限要求,再讀取或寫入資料。
HealthConnectClient
是 Health Connect API 的進入點。請在 HealthConnectManager.kt
中,取得 HealthConnectClient
例項。
private val healthConnectClient by lazy { HealthConnectClient.getOrCreate(context) }
如要在應用程式內啟動要求權限對話方塊,請先為所需資料類型建構一組權限。您必須只針對要使用的資料類型要求權限。
舉例來說,在「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()
}
如果未授予必要資料類型的存取權,在程式碼研究室的範例應用程式中,畫面上可能會顯示「Request permissions」按鈕。按一下「Request permissions」,開啟健康資料同步權限對話方塊。請授予必要權限,然後回到程式碼研究室應用程式。
4. 寫入資料
現在開始將記錄寫入 Health Connect。如要寫入體重記錄,請建立含有體重輸入值的 WeightRecord
物件。請注意,健康資料同步 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」,並輸入新的體重記錄 (以公斤為單位)。如要驗證體重記錄是否已成功寫入健康資料同步,請開啟「健康資料同步」應用程式的「設定」,然後依序前往「資料和存取權」>「身體測量資料」>「體重」>「查看所有資料」。您應該會看到從健康資料同步程式碼研究室寫入的新體重記錄。
寫入運動時段
時段是指一段時間間隔,使用者會在這段時間執行特定活動。健康資料同步中的運動時段可以包含從跑步到打羽毛球等任何活動。使用者可利用時段,依據時間來評估成效。這項資料會記錄在一段時間內測量的一系列即時樣本,例如特定活動期間的連續心率或位置樣本。
以下範例說明如何寫入運動時段。請使用 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. 健康資料同步 Toolbox
健康資料同步 Toolbox 簡介
健康資料同步 Toolbox 是隨附的開發人員工具,有助於測試應用程式與健康資料同步的整合情形。這項工具可直接在健康資料同步中讀取及寫入資料,方便您測試應用程式的 CRUD 作業。
在這個程式碼研究室中,我們會使用健康資料同步 Toolbox 測試剛才實作的讀取和寫入功能。
設定健康資料同步 Toolbox
請解壓縮 ZIP 資料夾,取得 APK 檔案。接著,如要在已連結的裝置上安裝 Toolbox APK,請使用 adb
。前往 APK 所在的資料夾,然後執行下列指令:
$ adb install HealthConnectToolbox-{Version Number}.apk
首次開啟健康資料同步 Toolbox 應用程式時,系統會將您導向「應用程式」>「特殊應用程式存取權」>「顯示在其他應用程式上層」下的權限設定。這項權限可讓健康資料同步 Toolbox 在其他應用程式上層顯示疊加層,因此您不必離開正在開發的應用程式,即可測試讀取和寫入資料。
如要管理測試用的讀取和寫入權限,可以從 Toolbox 應用程式的主畫面開啟健康資料同步應用程式,或直接前往權限流程。
讀取及寫入健康記錄
健康資料同步 Toolbox 支援讀取及寫入所有「健康資料同步」資料類型。在程式碼研究室的上一節中,您已成功將體重和運動時段記錄寫入 Health Connect。請確認您是否能讀取健康資料同步 Toolbox 中的資料。
必須先取得使用者的許可,才能在健康資料同步中讀寫資料。健康資料同步 Toolbox 也是如此。首先,請接受健康資料同步 Toolbox 的權限要求。接著,按一下疊加選單中的搜尋圖示 開啟對話方塊,選取資料類型 (例如體重),然後按一下「READ HEALTH RECORD」。您應該會在程式碼研究室的範例應用程式中,看到剛剛寫入健康資料同步的記錄。
如要將記錄插入健康資料同步,請按一下疊加選單中的編輯圖示 開啟對話方塊,然後選取資料類型。從 Toolbox 插入體重記錄。在下一堂課中,我們會說明如何透過 Health Connect API 讀取記錄,以及在應用程式中顯示資料。
6. 讀取資料
您已使用程式碼研究室範例應用程式和 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
}
現在執行應用程式,看看是否顯示體重記錄和運動時段清單。
7. 讀取差異化資料
健康資料同步的 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 或程式碼研究室應用程式插入體重或運動時段。返回「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. 恭喜
恭喜,您已成功建構第一個健康資料同步整合式健康與健身應用程式。
該應用程式可以宣告權限,以及針對應用程式中使用的資料類型要求使用者授權,還可以從健康資料同步資料儲存空間讀取和寫入資料。此外,您也學到了如何在健康資料同步資料儲存空間中建立模擬資料,進而使用健康資料同步 Toolbox 支援應用程式的開發作業。
您現已瞭解,如想讓健康與健身應用程式成為健康資料同步生態系統一部分,必須採取哪些重要步驟。