From 28c1690b80ff2f5f3233b3a3b0a7ec68b2a485c9 Mon Sep 17 00:00:00 2001 From: Louis Lagrange Date: Tue, 6 Aug 2024 13:05:54 +0200 Subject: [PATCH] feat: add getChanges differential API --- .../healthconnect/HealthConnectManager.kt | 47 +++++++++++++++ .../healthconnect/HealthConnectModule.kt | 5 ++ .../records/ReactHealthRecord.kt | 20 +++++++ .../healthconnect/utils/HealthConnectUtils.kt | 59 +++++++++++++++++++ android/src/oldarch/HealthConnectSpec.kt | 3 + src/NativeHealthConnect.ts | 5 ++ src/index.tsx | 8 +++ src/types/changes.types.ts | 15 +++++ src/types/index.ts | 1 + 9 files changed, 163 insertions(+) create mode 100644 src/types/changes.types.ts diff --git a/android/src/main/java/dev/matinzd/healthconnect/HealthConnectManager.kt b/android/src/main/java/dev/matinzd/healthconnect/HealthConnectManager.kt index a298e59..8440535 100644 --- a/android/src/main/java/dev/matinzd/healthconnect/HealthConnectManager.kt +++ b/android/src/main/java/dev/matinzd/healthconnect/HealthConnectManager.kt @@ -2,14 +2,19 @@ package dev.matinzd.healthconnect import android.content.Intent import androidx.health.connect.client.HealthConnectClient +import androidx.health.connect.client.changes.DeletionChange +import androidx.health.connect.client.changes.UpsertionChange import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableNativeArray +import com.facebook.react.bridge.WritableNativeMap import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate import dev.matinzd.healthconnect.permissions.PermissionUtils import dev.matinzd.healthconnect.records.ReactHealthRecord import dev.matinzd.healthconnect.utils.ClientNotInitialized +import dev.matinzd.healthconnect.utils.convertChangesTokenRequestOptionsFromJS import dev.matinzd.healthconnect.utils.getTimeRangeFilter import dev.matinzd.healthconnect.utils.reactRecordTypeToClassMap import dev.matinzd.healthconnect.utils.rejectWithException @@ -143,6 +148,48 @@ class HealthConnectManager(private val applicationContext: ReactApplicationConte } } + fun getChanges(options: ReadableMap, promise: Promise) { + throwUnlessClientIsAvailable(promise) { + coroutineScope.launch { + try { + val changesToken = + options.getString("changesToken") ?: healthConnectClient.getChangesToken(convertChangesTokenRequestOptionsFromJS(options)) + val changesResponse = healthConnectClient.getChanges(changesToken) + + promise.resolve(WritableNativeMap().apply { + val upsertionChanges = WritableNativeArray() + val deletionChanges = WritableNativeArray() + + for (change in changesResponse.changes) { + when (change) { + is UpsertionChange -> { + upsertionChanges.pushMap(WritableNativeMap().apply { + val record = ReactHealthRecord.parseRecord(change.record) + putMap("record", record) + }) + } + + is DeletionChange -> { + deletionChanges.pushMap(WritableNativeMap().apply { + putString("recordId", change.recordId) + }) + } + } + } + + putArray("upsertionChanges", upsertionChanges) + putArray("deletionChanges", deletionChanges) + putString("nextChangesToken", changesResponse.nextChangesToken) + putBoolean("hasMore", changesResponse.hasMore) + putBoolean("changesTokenExpired", changesResponse.changesTokenExpired) + }) + } catch (e: Exception) { + promise.rejectWithException(e) + } + } + } + } + fun deleteRecordsByUuids( recordType: String, recordIdsList: ReadableArray, diff --git a/android/src/main/java/dev/matinzd/healthconnect/HealthConnectModule.kt b/android/src/main/java/dev/matinzd/healthconnect/HealthConnectModule.kt index 56d423c..ea871a8 100644 --- a/android/src/main/java/dev/matinzd/healthconnect/HealthConnectModule.kt +++ b/android/src/main/java/dev/matinzd/healthconnect/HealthConnectModule.kt @@ -70,6 +70,11 @@ class HealthConnectModule internal constructor(context: ReactApplicationContext) return manager.aggregateRecord(record, promise) } + @ReactMethod + override fun getChanges(options: ReadableMap, promise: Promise) { + return manager.getChanges(options, promise) + } + @ReactMethod override fun deleteRecordsByUuids( recordType: String, diff --git a/android/src/main/java/dev/matinzd/healthconnect/records/ReactHealthRecord.kt b/android/src/main/java/dev/matinzd/healthconnect/records/ReactHealthRecord.kt index f4e421e..d3c6b55 100644 --- a/android/src/main/java/dev/matinzd/healthconnect/records/ReactHealthRecord.kt +++ b/android/src/main/java/dev/matinzd/healthconnect/records/ReactHealthRecord.kt @@ -13,6 +13,8 @@ import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap import dev.matinzd.healthconnect.utils.InvalidRecordType import dev.matinzd.healthconnect.utils.convertReactRequestOptionsFromJS +import dev.matinzd.healthconnect.utils.healthConnectClassToReactClassMap +import dev.matinzd.healthconnect.utils.reactClassToReactTypeMap import dev.matinzd.healthconnect.utils.reactRecordTypeToClassMap import dev.matinzd.healthconnect.utils.reactRecordTypeToReactClassMap import kotlin.reflect.KClass @@ -28,6 +30,15 @@ class ReactHealthRecord { return reactClass?.newInstance() as ReactHealthRecordImpl } + private fun createReactHealthRecordInstance(recordClass: Class): ReactHealthRecordImpl { + if (!healthConnectClassToReactClassMap.containsKey(recordClass)) { + throw InvalidRecordType() + } + + val reactClass = healthConnectClassToReactClassMap[recordClass] + return reactClass?.newInstance() as ReactHealthRecordImpl + } + fun getRecordByType(recordType: String): KClass { if (!reactRecordTypeToClassMap.containsKey(recordType)) { throw InvalidRecordType() @@ -85,5 +96,14 @@ class ReactHealthRecord { val recordClass = createReactHealthRecordInstance(recordType) return recordClass.parseRecord(response.record) } + + fun parseRecord( + record: Record + ): WritableNativeMap { + val reactRecordClass = createReactHealthRecordInstance(record.javaClass) + val reactRecord = reactRecordClass.parseRecord(record) + reactRecord.putString("recordType", reactClassToReactTypeMap[reactRecordClass.javaClass]) + return reactRecord + } } } diff --git a/android/src/main/java/dev/matinzd/healthconnect/utils/HealthConnectUtils.kt b/android/src/main/java/dev/matinzd/healthconnect/utils/HealthConnectUtils.kt index 29a976f..e75698b 100644 --- a/android/src/main/java/dev/matinzd/healthconnect/utils/HealthConnectUtils.kt +++ b/android/src/main/java/dev/matinzd/healthconnect/utils/HealthConnectUtils.kt @@ -3,6 +3,7 @@ package dev.matinzd.healthconnect.utils import androidx.health.connect.client.records.* import androidx.health.connect.client.records.metadata.DataOrigin import androidx.health.connect.client.records.metadata.Metadata +import androidx.health.connect.client.request.ChangesTokenRequest import androidx.health.connect.client.request.ReadRecordsRequest import androidx.health.connect.client.time.TimeRangeFilter import androidx.health.connect.client.units.* @@ -47,6 +48,14 @@ fun convertJsToDataOriginSet(readableArray: ReadableArray?): Set { return readableArray.toArrayList().mapNotNull { DataOrigin(it.toString()) }.toSet() } +fun convertJsToRecordTypeSet(readableArray: ReadableArray?): Set> { + if (readableArray == null) { + return emptySet() + } + + return readableArray.toArrayList().mapNotNull { reactRecordTypeToClassMap[it.toString()] }.toSet() +} + fun ReadableArray.toMapList(): List { val list = mutableListOf() for (i in 0 until size()) { @@ -205,6 +214,49 @@ val reactRecordTypeToReactClassMap: Map; + getChanges(request: { + changesToken?: string; + recordTypes?: string[]; + dataOriginFilters?: string[]; + }): Promise<{}>; deleteRecordsByUuids( recordType: string, recordIdsList: string[], diff --git a/src/index.tsx b/src/index.tsx index f95d3de..ff9e589 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,6 +9,8 @@ import type { ReadRecordsOptions, RecordResult, RecordType, + GetChangesRequest, + GetChangesResults, } from './types'; import type { TimeRangeFilter } from './types/base.types'; @@ -147,6 +149,12 @@ export function aggregateRecord( return HealthConnect.aggregateRecord(request); } +export function getChanges( + request: GetChangesRequest +): Promise { + return HealthConnect.getChanges(request); +} + export function deleteRecordsByUuids( recordType: RecordType, recordIdsList: string[], diff --git a/src/types/changes.types.ts b/src/types/changes.types.ts new file mode 100644 index 0000000..108a470 --- /dev/null +++ b/src/types/changes.types.ts @@ -0,0 +1,15 @@ +import type { RecordType, HealthConnectRecord } from './records.types'; + +export interface GetChangesRequest { + changesToken?: string; + recordTypes?: RecordType[]; + dataOriginFilter?: string[]; +} + +export interface GetChangesResults { + upsertionChanges: Array<{ record: HealthConnectRecord }>; + deletionChanges: Array<{ recordId: string }>; + nextChangesToken: string; + changesTokenExpired: boolean; + hasMore: boolean; +} diff --git a/src/types/index.ts b/src/types/index.ts index 250bb17..e053d68 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,3 +8,4 @@ export interface Permission { export * from './records.types'; export * from './results.types'; export * from './aggregate.types'; +export * from './changes.types';