Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getChanges differential API #130

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Loading