Skip to content

Commit

Permalink
feat: add getChanges differential API (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
Minishlink authored Aug 8, 2024
1 parent d1df454 commit 01e0d83
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +30,15 @@ class ReactHealthRecord {
return reactClass?.newInstance() as ReactHealthRecordImpl<T>
}

private fun <T : Record> createReactHealthRecordInstance(recordClass: Class<out Record>): ReactHealthRecordImpl<T> {
if (!healthConnectClassToReactClassMap.containsKey(recordClass)) {
throw InvalidRecordType()
}

val reactClass = healthConnectClassToReactClassMap[recordClass]
return reactClass?.newInstance() as ReactHealthRecordImpl<T>
}

fun getRecordByType(recordType: String): KClass<out Record> {
if (!reactRecordTypeToClassMap.containsKey(recordType)) {
throw InvalidRecordType()
Expand Down Expand Up @@ -85,5 +96,14 @@ class ReactHealthRecord {
val recordClass = createReactHealthRecordInstance<Record>(recordType)
return recordClass.parseRecord(response.record)
}

fun parseRecord(
record: Record
): WritableNativeMap {
val reactRecordClass = createReactHealthRecordInstance<Record>(record.javaClass)
val reactRecord = reactRecordClass.parseRecord(record)
reactRecord.putString("recordType", reactClassToReactTypeMap[reactRecordClass.javaClass])
return reactRecord
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -47,6 +48,14 @@ fun convertJsToDataOriginSet(readableArray: ReadableArray?): Set<DataOrigin> {
return readableArray.toArrayList().mapNotNull { DataOrigin(it.toString()) }.toSet()
}

fun convertJsToRecordTypeSet(readableArray: ReadableArray?): Set<KClass<out Record>> {
if (readableArray == null) {
return emptySet()
}

return readableArray.toArrayList().mapNotNull { reactRecordTypeToClassMap[it.toString()] }.toSet()
}

fun ReadableArray.toMapList(): List<ReadableMap> {
val list = mutableListOf<ReadableMap>()
for (i in 0 until size()) {
Expand Down Expand Up @@ -205,6 +214,49 @@ val reactRecordTypeToReactClassMap: Map<String, Class<out ReactHealthRecordImpl<
"MenstruationPeriod" to ReactMenstruationPeriodRecord::class.java
)

val reactClassToReactTypeMap = reactRecordTypeToReactClassMap.entries.associateBy({ it.value }) { it.key }

val healthConnectClassToReactClassMap = mapOf(
ActiveCaloriesBurnedRecord::class.java to ReactActiveCaloriesBurnedRecord::class.java,
BasalBodyTemperatureRecord::class.java to ReactBasalBodyTemperatureRecord::class.java,
BasalMetabolicRateRecord::class.java to ReactBasalMetabolicRateRecord::class.java,
BloodGlucoseRecord::class.java to ReactBloodGlucoseRecord::class.java,
BloodPressureRecord::class.java to ReactBloodPressureRecord::class.java,
BodyFatRecord::class.java to ReactBodyFatRecord::class.java,
BodyTemperatureRecord::class.java to ReactBodyTemperatureRecord::class.java,
BodyWaterMassRecord::class.java to ReactBodyWaterMassRecord::class.java,
BoneMassRecord::class.java to ReactBoneMassRecord::class.java,
CervicalMucusRecord::class.java to ReactCervicalMucusRecord::class.java,
CyclingPedalingCadenceRecord::class.java to ReactCyclingPedalingCadenceRecord::class.java,
DistanceRecord::class.java to ReactDistanceRecord::class.java,
ElevationGainedRecord::class.java to ReactElevationGainedRecord::class.java,
ExerciseSessionRecord::class.java to ReactExerciseSessionRecord::class.java,
FloorsClimbedRecord::class.java to ReactFloorsClimbedRecord::class.java,
HeartRateRecord::class.java to ReactHeartRateRecord::class.java,
HeartRateVariabilityRmssdRecord::class.java to ReactHeartRateVariabilityRmssdRecord::class.java,
HeightRecord::class.java to ReactHeightRecord::class.java,
HydrationRecord::class.java to ReactHydrationRecord::class.java,
LeanBodyMassRecord::class.java to ReactLeanBodyMassRecord::class.java,
MenstruationFlowRecord::class.java to ReactMenstruationFlowRecord::class.java,
NutritionRecord::class.java to ReactNutritionRecord::class.java,
OvulationTestRecord::class.java to ReactOvulationTestRecord::class.java,
OxygenSaturationRecord::class.java to ReactOxygenSaturationRecord::class.java,
PowerRecord::class.java to ReactPowerRecord::class.java,
RespiratoryRateRecord::class.java to ReactRespiratoryRateRecord::class.java,
RestingHeartRateRecord::class.java to ReactRestingHeartRateRecord::class.java,
SexualActivityRecord::class.java to ReactSexualActivityRecord::class.java,
SleepSessionRecord::class.java to ReactSleepSessionRecord::class.java,
SpeedRecord::class.java to ReactSpeedRecord::class.java,
StepsCadenceRecord::class.java to ReactStepsCadenceRecord::class.java,
StepsRecord::class.java to ReactStepsRecord::class.java,
TotalCaloriesBurnedRecord::class.java to ReactTotalCaloriesBurnedRecord::class.java,
Vo2MaxRecord::class.java to ReactVo2MaxRecord::class.java,
WeightRecord::class.java to ReactWeightRecord::class.java,
WheelchairPushesRecord::class.java to ReactWheelchairPushesRecord::class.java,
IntermenstrualBleedingRecord::class.java to ReactIntermenstrualBleedingRecord::class.java,
MenstruationPeriodRecord::class.java to ReactMenstruationPeriodRecord::class.java
)

fun massToJsMap(mass: Mass?): WritableNativeMap {
return WritableNativeMap().apply {
putDouble("inGrams", mass?.inGrams ?: 0.0)
Expand Down Expand Up @@ -357,3 +409,10 @@ fun powerToJsMap(power: Power?): WritableNativeMap {
putDouble("inKilocaloriesPerDay", power?.inKilocaloriesPerDay ?: 0.0)
}
}

fun convertChangesTokenRequestOptionsFromJS(options: ReadableMap): ChangesTokenRequest {
return ChangesTokenRequest(
recordTypes = convertJsToRecordTypeSet(options.getArray("recordTypes")),
dataOriginFilters = convertJsToDataOriginSet(options.getArray("dataOriginFilters")),
)
}
3 changes: 3 additions & 0 deletions android/src/oldarch/HealthConnectSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ abstract class HealthConnectSpec internal constructor(context: ReactApplicationC
@ReactMethod
abstract fun aggregateRecord(record: ReadableMap, promise: Promise);

@ReactMethod
abstract fun getChanges(options: ReadableMap, promise: Promise);

@ReactMethod
abstract fun deleteRecordsByUuids(recordType: String, recordIdsList: ReadableArray, clientRecordIdsList: ReadableArray, promise: Promise);

Expand Down
5 changes: 5 additions & 0 deletions src/NativeHealthConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export interface Spec extends TurboModule {
startTime: string;
endTime: string;
}): Promise<{}>;
getChanges(request: {
changesToken?: string;
recordTypes?: string[];
dataOriginFilters?: string[];
}): Promise<{}>;
deleteRecordsByUuids(
recordType: string,
recordIdsList: string[],
Expand Down
8 changes: 8 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
ReadRecordsOptions,
RecordResult,
RecordType,
GetChangesRequest,
GetChangesResults,
} from './types';
import type { TimeRangeFilter } from './types/base.types';

Expand Down Expand Up @@ -147,6 +149,12 @@ export function aggregateRecord<T extends AggregateResultRecordType>(
return HealthConnect.aggregateRecord(request);
}

export function getChanges(
request: GetChangesRequest
): Promise<GetChangesResults> {
return HealthConnect.getChanges(request);
}

export function deleteRecordsByUuids(
recordType: RecordType,
recordIdsList: string[],
Expand Down
15 changes: 15 additions & 0 deletions src/types/changes.types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export interface Permission {
export * from './records.types';
export * from './results.types';
export * from './aggregate.types';
export * from './changes.types';

0 comments on commit 01e0d83

Please sign in to comment.