同步数据

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

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

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

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

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

将数据馈送到 Health Connect

同步过程的第一部分是将数据从应用的数据存储区馈送到健康数据共享数据存储区。

准备数据

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

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

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

向健康数据共享中写入数据

如需将数据馈送到健康数据共享,请按以下步骤操作:

  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向健康数据共享更新/插入数据。更新/插入数据是指,只要健康数据共享数据存储区中存在 clientRecordId 值且 clientRecordVersion 高于现有值,那么健康数据共享中的任何现有数据都会被覆盖。否则,更新/插入的数据会作为新数据写入。

    healthConnectClient.insertRecords(arrayListOf(record))

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

存储健康数据共享 ID

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

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

val response = healthConnectClient.insertRecords(listOf(record))
for (recordId in response.recordIdsList) {
    // Store recordId to your app's datastore
}

从健康数据共享中拉取数据

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

获取更改令牌

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

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

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

检查数据更改

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

  1. 使用令牌调用 getChanges 以获取更改列表。
  2. 检查各项更改的类型是否为 UpsertionChange 或 DeletionChange,然后执行必要的操作。
    • 对于 UpsertionChange,请仅进行并非来自调用应用的更改,从而确保不会重新导入数据。
  3. 将下一个更改令牌分配为新令牌。
  4. 重复步骤 1-3,直到没有剩余的更改为止。
  5. 存储下一个令牌,并将其预留以供日后导入。

suspend fun processChanges(context: Context, 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 中的 id 和 lastModifiedTime 以 更新/插入记录。 对于 DeletionChange,请使用提供的 id 删除 该记录。 这需要您存储记录 id,如 存储健康数据共享 ID 中所述。

从健康数据共享中删除数据

当用户从您的应用中删除自己的数据时,请确保这些数据也会从 Health Connect 中移除。请使用 deleteRecords 执行此操作。这需要使用记录类型以及 id 和 clientRecordId 值的列表,方便批量处理多种待删除的数据。此外,您还可以使用采用 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 回调接收数据。收到新数据后,系统会立即将其写入 Health Connect。

private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var healthConnectClient: HealthConnectClient? = null
private var bluetoothGatt: BluetoothGatt? = null

override fun onDeviceAppeared(address: String) {
    super.onDeviceAppeared(address)
    healthConnectClient = HealthConnectClient.getOrCreate(this)

    serviceScope.launch {
        val granted = healthConnectClient?.permissionController?.getGrantedPermissions()

        // 1. Check permissions ONCE when the device connects
        if (granted?.contains(HealthPermission.getWritePermission(HeartRateRecord::class)) ?: false) {
            // This is where you'd actually start the Bluetooth connection
            // bluetoothGatt = gattCallback.connect(...)
        }

        // 2. Do your initial database read
        readExerciseSessionAndRoute()
    }
}

private val gattCallback = object : BluetoothGattCallback() {
    override fun onCharacteristicChanged(
        gatt: BluetoothGatt,
        characteristic: BluetoothGattCharacteristic,
        value: ByteArray
    ) {
        super.onCharacteristicChanged(gatt, characteristic, value)

        // 3. ONLY process the incoming data here
        val rawData = value

        serviceScope.launch {
            // parseWearableData(rawData)
            // insertExerciseRoute() or writeToHealthConnect()
        }
    }
}

同步数据的最佳实践

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

令牌过期

由于闲置的更改令牌会在 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 的访问可能会随时中断。例如,从健康数据共享读取大量数据时,您的应用必须在同步过程中处理中断,并在下次打开应用时继续。

后台读取

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

数据导入时间

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

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