同步處理資料

本指南適用於 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 將資料更新/插入至「健康資料同步」。更新/插入資料的含意是,只要 Health Connect 資料儲存庫中有 clientRecordId 值,且 clientRecordVersion 高於現有的值,那麼 Health Connect 中的所有現有資料都會遭到覆寫。但如果不是的話,則會以新資料的形式寫入更新/插入的資料。

    healthConnectClient.insertRecords(arrayListOf(record))
    

如要瞭解饋送資料的具體注意事項,請參閱「寫入資料」的最佳做法。

儲存健康資料同步 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
}

從「健康資料同步」提取資料

同步處理程序的第二階段,是將任何資料相關變更從「健康資料同步」提取至應用程式的資料儲存庫。資料相關變更可能包括更新和刪除行為。

取得變更權杖

如要取得清單,瞭解要從健康資料同步提取哪些變更,您的應用程式需要先追蹤「變更」權杖。您可以在要求「變更」時使用這些權杖,以便同時傳回資料變更清單,以及將於下次使用的全新「變更」權杖。

如要取得「變更」權杖,請呼叫 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,請使用 id 和源自其 metadatalastModifiedTime 更新/插入記錄。如果是 DeletionChange,請使用提供的 id刪除記錄。 如要執行這項操作,您必須先儲存記錄 id,詳情請參閱「儲存健康資料同步 ID」。

刪除「健康資料同步」中的資料

當使用者從您的應用程式刪除自己的資料時,請確保這些資料也會從「健康資料同步」中移除。您可以使用 deleteRecords 執行此操作。這會取得記錄類型,以及 idclientRecordId 值的清單,方便您批次處理多筆要刪除的資料。此外,您也可以使用替代的 deleteRecords,其中會納入 timeRangeFilter

穿戴式裝置的低延遲同步

如要將穿戴式健身裝置的資料同步至「健康資料同步」,並盡量減少延遲,請使用 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 提供特定時間戳記之後的記錄,並在應用程式資料儲存庫中插入或更新該記錄。
  • 要求提供變更權杖,並保留至下次需要時使用。

建議的變更管理策略

如果應用程式收到無效或過期的「變更」權杖,建議您根據邏輯適用情形,採用下列管理策略:

  • 讀取及簡化所有資料。這是最理想的策略。
    • 儲存上次從「健康資料同步」讀取資料時的時間戳記。
    • 在權杖過期時,從最近的時間戳記或過去 30 天重新讀取所有資料。接著請根據先前讀取的資料,使用 ID 簡化這些資料。
    • 在理想情況下,請實作用戶端 ID,因為必須使用用戶端 ID 才能更新資料。
  • 僅讀取上次讀取時間戳記後的資料。這會導致變更權杖到期前後的資料出現落差,但時間範圍會較短,可能需要數小時至數天。
    • 儲存上次從「健康資料同步」讀取資料時的時間戳記。
    • 在權杖到期時,讀取這個時間戳記後的所有資料。
  • 刪除再讀取過去 30 天的資料。這更貼近第一項整合作業的情況。
    • 刪除應用程式在過去 30 天內從「健康資料同步」讀取的所有資料。
    • 刪除完成後,再次讀取所有資料。
  • 讀取過去 30 天的資料,且不刪除重複的資料。這是最不理想的策略,且會導致使用者看到重複的資料。
    • 刪除應用程式在過去 30 天內從「健康資料同步」讀取的所有資料。
    • 允許重複的項目。

資料類型變更權杖

如果應用程式可獨立取用多種資料類型,請針對每種資料類型使用單獨的變更權杖。請只在同時取用或完全不取用多種資料類型時,搭配使用資料類型清單與 Changes Sync API。

前景讀取

應用程式只有在前景運作時,才能從健康資料同步讀取資料。從「健康資料同步」同步處理資料時,存取作業隨時可能中斷。舉例來說,應用程式從「健康資料同步」讀取大量資料時,必須能處理同步作業中途發生的干擾,並在下次開啟應用程式時繼續這項作業。

背景讀取

您可以要求應用程式在背景執行,並從「健康資料同步」讀取資料。如果您要求 Background Read 權限,使用者可以授予應用程式在背景讀取資料的權限。

匯入時間

如果應用程式無法在資料新增時收到通知,則應在以下兩個時間點檢查新資料:

  • 每當應用程式開始在前景運作時。請在這種情況下使用生命週期事件。
  • 應用程式持續在前景運作的期間。請定期檢查新資料。每當有新資料時,請通知使用者更新畫面,反映變更情形。