同步数据

本指南与健康数据共享版本 1.1.0-alpha12 兼容。

大多数与 Health Connect 集成的应用都有自己的数据存储区,以用作可信来源。Health Connect 提供了多种方法来让您的应用保持同步。

根据应用的架构,同步过程可能涉及以下部分或全部操作:

  • 将新数据或更新后数据从应用的数据存储区馈送至 Health Connect。
  • 将数据更改从 Health Connect 拉取到应用的数据存储区。
  • 从应用的数据存储区中删除数据时,一并从“健康数据共享”中删除这些数据。

无论是哪种情况,都要确保同步过程能使 Health Connect 与应用的数据存储区保持一致。

将数据馈送到 Health Connect

同步过程的第一部分是将数据从应用的数据存储区馈送到 Health Connect 数据存储区。

准备数据

应用的数据存储区中的记录通常包含以下详细信息:

  • 唯一键,例如 UUID
  • 版本或时间戳。

将数据同步到健康数据共享时,请仅识别并馈送自上次同步以来插入、更新或删除的数据。

向 Health Connect 中写入数据

如需将数据馈送到 Health Connect,请按以下步骤操作:

  1. 从应用的数据存储区获取新条目、更新条目或已删除条目的列表。
  2. 对于每个条目,创建相应数据类型的 Record 对象。例如,为与权重相关的数据创建 WeightRecord 对象。
  3. 使用每个 Record 指定一个 Metadata 对象。这包括 clientRecordId,它是应用数据存储区中的一个 ID,可用于唯一标识相应记录。您可以使用现有的唯一密钥。如果您的数据是版本化的,请同时提供与数据中使用的版本控制相一致的 clientRecordVersion。 如果未进行版本控制,您可以使用当前时间戳的 Long 值作为替代方案。

    val recordVersion = 0L
    // Specify as needed
    // The clientRecordId is an ID that you choose for your record. This
    // is often the same ID you use in your app's datastore.
    val clientRecordId = "<your-record-id>"
    
    val record = WeightRecord(
        metadata = Metadata.activelyRecorded(
            clientRecordId = clientRecordId,
            clientRecordVersion = recordVersion,
            device = Device(type = Device.TYPE_SCALE)
        ),
        weight = Mass.kilograms(62.0),
        time = Instant.now(),
        zoneOffset = ZoneOffset.UTC,
    )
    healthConnectClient.insertRecords(listOf()(record))
    
    
  4. 使用 insertRecords 向 Health Connect 更新/插入数据。更新/插入数据是指,只要 Health Connect 数据存储区中存在 clientRecordId 值且 clientRecordVersion 高于现有值,那么 Health Connect 中的任何现有数据都会被覆盖。否则,更新/插入的数据会作为新数据写入。

    healthConnectClient.insertRecords(arrayListOf(record))
    

如需了解馈送数据的实际注意事项,请查看写入数据的最佳实践。

存储 Health Connect ID

如果您的应用还会从“健康数据共享”读取数据,请在更新/插入记录后存储这些记录的“健康数据共享”id。从健康数据共享中拉取数据更改时,您需要使用此 id 来处理删除操作。

insertRecords 函数会返回包含 id 值列表的 InsertRecordsResponse。使用响应获取记录 ID 并存储它们。

val response = healthConnectClient.insertRecords(arrayListOf(record))

for (recordId in response.recordIdsList) {
    // Store recordId to your app's datastore
}

从 Health Connect 中拉取数据

同步过程的第二部分是将 Health Connect 中的所有数据更改拉取到应用的数据存储区。数据更改可能包括更新和删除。

获取更改令牌

如需获取要从 Health Connect 拉取的更改列表,您的应用需要跟踪更改令牌。您可以在请求更改时使用这些令牌,从而返回数据更改列表,以及返回新的更改令牌供下次使用。

要获取更改令牌,请调用 getChangesToken 并提供所需的数据类型。

val changesToken = healthConnectClient.getChangesToken(
    ChangesTokenRequest(recordTypes = setOf(WeightRecord::class))
)

检查数据更改

现在,您已经获得了更改令牌,可以用它来获取所有更改了。我们建议您创建一个循环来获取所有更改,从而检查是否有可用数据更改。具体步骤如下:

  1. 使用令牌调用 getChanges 以获取更改列表。
  2. 检查各项更改的类型是否为 UpsertionChangeDeletionChange,然后执行必要的操作。
    • 对于 UpsertionChange,请仅进行并非来自调用应用的更改,从而确保不会重新导入数据。
  3. 将下一个更改令牌分配为新令牌。
  4. 重复步骤 1-3,直到没有剩余的更改为止
  5. 存储下一个令牌,并将其预留以供日后导入。
suspend fun processChanges(token: String): String {
    var nextChangesToken = token
    do {
        val response = healthConnectClient.getChanges(nextChangesToken)
        response.changes.forEach { change ->
            when (change) {
                is UpsertionChange ->
                    if (change.record.metadata.dataOrigin.packageName != context.packageName) {
                        processUpsertionChange(change)
                    }
                is DeletionChange -> processDeletionChange(change)
            }
        }
        nextChangesToken = response.nextChangesToken
    } while (response.hasMore)
    // Return and store the changes token for use next time.
    return nextChangesToken
}

如需了解有关拉取数据的实际注意事项,请参阅同步数据的最佳实践。

处理数据更改

反映对应用数据存储区所做的更改。对于 UpsertionChange,请使用 metadata 中的 idlastModifiedTime更新/插入记录。对于 DeletionChange,请使用提供的 id 删除该记录。这要求您已存储记录 id,如存储健康数据共享 ID 中所述。

从 Health Connect 中删除数据

当用户从您的应用中删除自己的数据时,请确保这些数据也会从 Health Connect 中移除。请使用 deleteRecords 执行此操作。这需要使用记录类型以及 idclientRecordId 值的列表,方便批量处理多种待删除的数据。此外,还提供了一种接受 timeRangeFilter 的替代 deleteRecords

从穿戴式设备进行低延迟同步

如需以低延迟将数据从穿戴式健身设备同步到“健康数据共享”,请使用 CompanionDeviceService。此方法适用于支持 BLE GATT 通知或指示且以 Android 8.0(API 级别 26)或更高版本为目标平台的设备。CompanionDeviceService 允许您的应用从穿戴式设备接收数据并将其写入健康数据共享,即使该应用尚未运行也是如此。如需详细了解 BLE 最佳实践,请参阅蓝牙低功耗概览

关联设备

首先,您的应用必须引导用户完成一次性流程,以使用 CompanionDeviceManager 将穿戴式设备与应用相关联。这样一来,您的应用便可获得与设备互动的必要权限。如需了解详情,请参阅配套设备配对

在清单中声明服务

接下来,在应用的清单文件中声明 CompanionDeviceService。将以下内容添加到 AndroidManifest.xml 中:

<manifest ...>
   <application ...>
       <service
           android:name=".MyWearableService"
           android:exported="true"
           android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">
           <intent-filter>
               <action android:name="android.companion.CompanionDeviceService" />
           </intent-filter>
       </service>
   </application>
</manifest>

创建 CompanionDeviceService

最后,创建一个扩展 CompanionDeviceService 的类。此服务负责处理与穿戴式设备的连接,并通过 BLE GATT 回调接收数据。当收到新数据时,系统会立即将其写入健康数据共享。

import android.companion.CompanionDeviceService
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.StepsRecord
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

class MyWearableService : CompanionDeviceService() {

   // A coroutine scope for handling suspend functions like writing to Health Connect
   private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
   private var healthConnectClient: HealthConnectClient? = null
   private var bluetoothGatt: BluetoothGatt? = null

   // This is called by the system when your wearable connects
   override fun onDeviceAppeared(address: String) {
       super.onDeviceAppeared(address)
       healthConnectClient = HealthConnectClient.getOrCreate(this)

       serviceScope.launch {
           // Check which permissions have been granted before subscribing to data from the wearable.
           // A service cannot request permissions, so your app must have already requested
           // and been granted them from an Activity.
           val granted = healthConnectClient?.permissionController?.getGrantedPermissions()

           // ... set up your GATT connection here ...

           // Once connected, subscribe to notifications for the data types you have
           // permission to write.
           if (granted?.contains(HealthPermission.getWritePermission(HeartRateRecord::class)) == true) {
               // subscribeToHeartRate(bluetoothGatt)
           }
       }
   }

   // The core of your low-latency pipeline is the BLE callback
   private val gattCallback = object : BluetoothGattCallback() {
       override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
           super.onCharacteristicChanged(gatt, characteristic, value)

           // 1. Instantly receive the data
           val rawData = value

           // 2. Parse the data from the wearable
           val healthData = parseWearableData(rawData) // Your custom parsing logic

           // 3. Immediately process it. For simplicity, this example writes
           //    directly to Health Connect. A real-world app might write to its
           //    own datastore first and then sync with Health Connect.
           serviceScope.launch {
               writeToHealthConnect(healthData)
           }
       }
   }

   private suspend fun writeToHealthConnect(healthData: HealthData) {
       val records = prepareHealthConnectRecords(healthData) // Convert to Health Connect records
       try {
           healthConnectClient?.insertRecords(records)
       } catch (e: Exception) {
           // Handle exceptions
       }
   }

   // This is called by the system when your wearable disconnects
   override fun onDeviceDisappeared(address: String) {
       super.onDeviceDisappeared(address)
       // Clean up your GATT connection and other resources
       bluetoothGatt?.close()
   }
}

同步数据的最佳实践

以下因素会影响同步过程。

令牌过期

由于闲置的更改令牌会在 30 天内过期,因此您必须遵守同步策略,以避免在此类情况下丢失信息。您的策略可以采用以下方法:

  • 在应用的数据存储区中搜索最近用过的记录,记录中还应具有来自 Health Connect 的 id
  • 向 Health Connect 请求以特定时间戳开头的记录,然后在应用的数据存储区中插入或更新这些记录。
  • 请求一个更改令牌以备下次需要时使用。

建议的更改管理策略

如果应用获取的更改令牌无效或已过期,我们建议您根据该令牌在应用逻辑中的使用方式,采取以下管理策略:

  • 读取所有数据并对其进行去重处理。这是最理想的策略。
    • 存储上次从 Health Connect 读取数据的时间戳。
    • 在令牌过期后,重新读取自最近时间戳以来或过去 30 天内的所有数据。然后,使用标识符对照之前读取的数据对其进行去重处理。
    • 最好实现客户端 ID,因为在更新数据时需要用到该 ID。
  • 只读取自上次读取时间戳以来的数据。这会导致在更改令牌过期前后出现一些数据差异,不过这段时间较短,可能是几个小时到几天。
    • 存储上次从 Health Connect 读取数据的时间戳。
    • 在令牌过期后,读取从这个时间戳开始的所有数据。
  • 删除过去 30 天内的数据,然后再读取该数据。此操作更适合在首次集成时执行。
    • 删除该应用在过去 30 天内从 Health Connect 读取的所有数据。
    • 删除后,重新读取所有这些数据。
  • 读取过去 30 天内的数据,但不进行去重处理。这是下下之策,会导致向用户显示重复数据。
    • 删除该应用在过去 30 天内从 Health Connect 读取的所有数据。
    • 允许条目重复。

数据类型更改令牌

如果您的应用可以独立使用多种类型的数据,则针对每种数据类型使用单独的更改令牌。仅当这些类型的数据被一起使用或不被使用时,才借助 Changes Sync API 来利用多种数据类型。

前台读取

应用只有在前台运行时才能读取来自 Health Connect 的数据。同步 Health Connect 中的数据时,对 Health Connect 的访问可能会随时中断。例如,从 Health Connect 读取大量数据时,您的应用必须在同步过程中处理中断,并在下次打开应用时继续。

后台读取

您可以请求让应用在后台运行并从“健康数据共享”读取数据。如果您请求 Background Read 权限,用户可以向您的应用授予在后台读取数据的权限。

数据导入时间

由于应用无法在有新数据时收到通知,因此需在以下两个时间点检查是否有新数据:

  • 每当应用在前台进入活动状态时。在这种情况下,请使用生命周期事件。
  • 应用持续在前台运行时。在这种情况下,应定期检查。在有新数据可用时通知用户,让对方更新界面以反映更改。