From 6dbc3dd120188bac2e08e2223421292ce47fa2a2 Mon Sep 17 00:00:00 2001 From: Anders Ha Date: Wed, 12 Jun 2024 12:25:35 +0100 Subject: [PATCH] VIT-6636: Android: Honour the perDeviceActivityTs feature flag (#123) Align Android SDK behaviour with iOS, where per-device activity timeseries is sent only if the team opts into the `sdk_per_device_activity_timeseries` feature flag. --- .../VitalHealthConnectManager.kt | 4 ++- .../records/RecordProcessor.kt | 33 +++++++++++++++---- .../workers/ResourceSyncWorker.kt | 27 +++++++++++---- .../workers/processChangesResponse.kt | 3 ++ .../workers/readResource.kt | 3 ++ 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/VitalHealthConnectManager.kt b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/VitalHealthConnectManager.kt index edb95722..32ff6016 100644 --- a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/VitalHealthConnectManager.kt +++ b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/VitalHealthConnectManager.kt @@ -371,7 +371,8 @@ class VitalHealthConnectManager private constructor( suspend fun read( resource: VitalResource, startTime: Instant, - endTime: Instant + endTime: Instant, + processorOptions: ProcessorOptions = ProcessorOptions(), ): ProcessedResourceData { return readResourceByTimeRange( resource, @@ -381,6 +382,7 @@ class VitalHealthConnectManager private constructor( currentDevice = Build.MODEL, reader = recordReader, processor = recordProcessor, + processorOptions = processorOptions, ) } diff --git a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt index edcc77e6..ba1b43f8 100644 --- a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt +++ b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt @@ -49,6 +49,10 @@ import java.util.Date import java.util.TimeZone import kotlin.math.roundToInt +data class ProcessorOptions( + val perDeviceActivityTS: Boolean = false +) + interface RecordProcessor { suspend fun processBloodPressureFromRecords( @@ -107,6 +111,7 @@ interface RecordProcessor { distance: List, floorsClimbed: List, vo2Max: List, + options: ProcessorOptions, ): SummaryData.Activities } @@ -404,7 +409,8 @@ internal class HealthConnectRecordProcessor( steps: List, distance: List, floorsClimbed: List, - vo2Max: List + vo2Max: List, + options: ProcessorOptions, ): SummaryData.Activities = coroutineScope { val zoneId = timeZone.toZoneId() @@ -500,14 +506,29 @@ internal class HealthConnectRecordProcessor( } val daySummariesByDate = awaitAll(*summaryAggregators) + fun merge( + discovered: Map>, + hourlyTotals: Map>, + date: LocalDate, + options: ProcessorOptions, + ): List { + return if (options.perDeviceActivityTS) { + (discovered[date] ?: emptyList()) + (hourlyTotals[date] ?: emptyList()) + } else { + hourlyTotals[date] ?: emptyList() + } + } + + // TODO: On-device computed hourly totals + val activities = daySummariesByDate.map { (date, summary) -> Activity( daySummary = summary, - activeEnergyBurned = activeEnergyBurnedByDate[date] ?: emptyList(), - basalEnergyBurned = basalMetabolicRateByDate[date] ?: emptyList(), - distanceWalkingRunning = distanceByDate[date] ?: emptyList(), - floorsClimbed = floorsClimbedByDate[date] ?: emptyList(), - steps = stepsByDate[date] ?: emptyList(), + activeEnergyBurned = merge(activeEnergyBurnedByDate, emptyMap(), date, options), + basalEnergyBurned = merge(basalMetabolicRateByDate, emptyMap(), date, options), + distanceWalkingRunning = merge(distanceByDate, emptyMap(), date, options), + floorsClimbed = merge(floorsClimbedByDate, emptyMap(), date, options), + steps = merge(stepsByDate, emptyMap(), date, options), vo2Max = vo2MaxByDate[date] ?: emptyList(), ) } diff --git a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/ResourceSyncWorker.kt b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/ResourceSyncWorker.kt index 38563916..68d7564f 100644 --- a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/ResourceSyncWorker.kt +++ b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/ResourceSyncWorker.kt @@ -34,6 +34,7 @@ import io.tryvital.vitalhealthconnect.model.recordTypeChangesToTriggerSync import io.tryvital.vitalhealthconnect.records.HealthConnectRecordAggregator import io.tryvital.vitalhealthconnect.records.HealthConnectRecordProcessor import io.tryvital.vitalhealthconnect.records.HealthConnectRecordReader +import io.tryvital.vitalhealthconnect.records.ProcessorOptions import io.tryvital.vitalhealthconnect.records.RecordProcessor import io.tryvital.vitalhealthconnect.records.RecordReader import io.tryvital.vitalhealthconnect.records.RecordUploader @@ -177,7 +178,6 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam * a. Fetch the maximum timestamp of the resource type as `max`. * b. `generic_backfill(stage="daily", start=max(), end=now())` */ - @Suppress("UNUSED_VARIABLE") override suspend fun doWork(): Result { val timeZone = TimeZone.getDefault() @@ -190,9 +190,13 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam vitalLogger.logI("${input.resource}: $instruction") + val processorOptions = ProcessorOptions( + perDeviceActivityTS = localSyncState.perDeviceActivityTS + ) + when (instruction) { - is SyncInstruction.DoHistorical -> historicalBackfill(instruction, timeZone) - is SyncInstruction.DoIncremental -> incrementalBackfill(instruction, timeZone) + is SyncInstruction.DoHistorical -> historicalBackfill(instruction, timeZone, processorOptions) + is SyncInstruction.DoIncremental -> incrementalBackfill(instruction, timeZone, processorOptions) } // TODO: Report synced vs nothing to sync @@ -231,19 +235,22 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam private suspend fun historicalBackfill( state: SyncInstruction.DoHistorical, - timeZone: TimeZone + timeZone: TimeZone, + processorOptions: ProcessorOptions, ) { genericBackfill( stage = DataStage.Historical, start = state.start, end = state.end, timeZone = timeZone, + processorOptions = processorOptions, ) } private suspend fun incrementalBackfill( state: SyncInstruction.DoIncremental, - timeZone: TimeZone + timeZone: TimeZone, + processorOptions: ProcessorOptions, ) { val userId = VitalClient.checkUserId() val client = healthConnectClientProvider.getHealthConnectClient(applicationContext) @@ -262,6 +269,7 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam start = state.lastSync, end = minOf(Instant.now(), state.end ?: Instant.now()), timeZone = timeZone, + processorOptions = processorOptions, ) } @@ -279,6 +287,7 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam start = state.lastSync, end = minOf(Instant.now(), state.end ?: Instant.now()), timeZone = timeZone, + processorOptions = processorOptions, ) } @@ -292,7 +301,8 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam currentDevice = Build.MODEL, reader = recordReader, processor = recordProcessor, - end = state.end + processorOptions = processorOptions, + end = state.end, ) // Skip empty POST requests @@ -322,7 +332,8 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam stage: DataStage, start: Instant, end: Instant, - timeZone: TimeZone + timeZone: TimeZone, + processorOptions: ProcessorOptions, ) { val userId = VitalClient.checkUserId() val client = healthConnectClientProvider.getHealthConnectClient(applicationContext) @@ -352,6 +363,7 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam currentDevice = Build.MODEL, reader = recordReader, processor = recordProcessor, + processorOptions = processorOptions, ) var changes: ChangesResponse @@ -371,6 +383,7 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam currentDevice = Build.MODEL, reader = recordReader, processor = recordProcessor, + processorOptions = processorOptions, ) } while (changes.hasMore) diff --git a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/processChangesResponse.kt b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/processChangesResponse.kt index f40a7ab5..d9149735 100644 --- a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/processChangesResponse.kt +++ b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/processChangesResponse.kt @@ -23,6 +23,7 @@ import io.tryvital.vitalhealthconnect.model.VitalResource import io.tryvital.vitalhealthconnect.model.processedresource.ProcessedResourceData import io.tryvital.vitalhealthconnect.model.processedresource.TimeSeriesData import io.tryvital.vitalhealthconnect.model.remapped +import io.tryvital.vitalhealthconnect.records.ProcessorOptions import io.tryvital.vitalhealthconnect.records.RecordProcessor import io.tryvital.vitalhealthconnect.records.RecordReader import java.time.Instant @@ -36,6 +37,7 @@ internal suspend fun processChangesResponse( currentDevice: String, reader: RecordReader, processor: RecordProcessor, + processorOptions: ProcessorOptions, end: Instant? = null, ): ProcessedResourceData { val records = responses.changes @@ -66,6 +68,7 @@ internal suspend fun processChangesResponse( distance = records.get().filter { it.endTime <= endAdjusted }, steps = records.get().filter { it.endTime <= endAdjusted }, vo2Max = records.get().filter { it.time <= endAdjusted }, + options = processorOptions, ).let(ProcessedResourceData::Summary) VitalResource.Workout -> processor.processWorkoutsFromRecords( diff --git a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/readResource.kt b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/readResource.kt index cfb185e5..6f29b9b7 100644 --- a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/readResource.kt +++ b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/workers/readResource.kt @@ -4,6 +4,7 @@ import io.tryvital.vitalhealthconnect.model.VitalResource import io.tryvital.vitalhealthconnect.model.processedresource.ProcessedResourceData import io.tryvital.vitalhealthconnect.model.processedresource.TimeSeriesData import io.tryvital.vitalhealthconnect.model.remapped +import io.tryvital.vitalhealthconnect.records.ProcessorOptions import io.tryvital.vitalhealthconnect.records.RecordProcessor import io.tryvital.vitalhealthconnect.records.RecordReader import java.time.Instant @@ -17,6 +18,7 @@ internal suspend fun readResourceByTimeRange( currentDevice: String, reader: RecordReader, processor: RecordProcessor, + processorOptions: ProcessorOptions, ): ProcessedResourceData { suspend fun readTimeseries( read: suspend (Instant, Instant) -> List, @@ -39,6 +41,7 @@ internal suspend fun readResourceByTimeRange( distance = reader.readDistance(startTime, endTime), steps = reader.readSteps(startTime, endTime), vo2Max = reader.readVo2Max(startTime, endTime), + options = processorOptions, ).let(ProcessedResourceData::Summary) VitalResource.Workout -> processor.processWorkoutsFromRecords(