Skip to content

Commit

Permalink
VIT-6636: Android: Honour the perDeviceActivityTs feature flag (#123)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
andersio authored Jun 12, 2024
1 parent 79aa876 commit 6dbc3dd
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -381,6 +382,7 @@ class VitalHealthConnectManager private constructor(
currentDevice = Build.MODEL,
reader = recordReader,
processor = recordProcessor,
processorOptions = processorOptions,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -107,6 +111,7 @@ interface RecordProcessor {
distance: List<DistanceRecord>,
floorsClimbed: List<FloorsClimbedRecord>,
vo2Max: List<Vo2MaxRecord>,
options: ProcessorOptions,
): SummaryData.Activities
}

Expand Down Expand Up @@ -404,7 +409,8 @@ internal class HealthConnectRecordProcessor(
steps: List<StepsRecord>,
distance: List<DistanceRecord>,
floorsClimbed: List<FloorsClimbedRecord>,
vo2Max: List<Vo2MaxRecord>
vo2Max: List<Vo2MaxRecord>,
options: ProcessorOptions,
): SummaryData.Activities = coroutineScope {
val zoneId = timeZone.toZoneId()

Expand Down Expand Up @@ -500,14 +506,29 @@ internal class HealthConnectRecordProcessor(
}
val daySummariesByDate = awaitAll(*summaryAggregators)

fun merge(
discovered: Map<LocalDate, List<QuantitySample>>,
hourlyTotals: Map<LocalDate, List<QuantitySample>>,
date: LocalDate,
options: ProcessorOptions,
): List<QuantitySample> {
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(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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,
)
}

Expand All @@ -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,
)
}

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -352,6 +363,7 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam
currentDevice = Build.MODEL,
reader = recordReader,
processor = recordProcessor,
processorOptions = processorOptions,
)

var changes: ChangesResponse
Expand All @@ -371,6 +383,7 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam
currentDevice = Build.MODEL,
reader = recordReader,
processor = recordProcessor,
processorOptions = processorOptions,
)

} while (changes.hasMore)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,6 +37,7 @@ internal suspend fun processChangesResponse(
currentDevice: String,
reader: RecordReader,
processor: RecordProcessor,
processorOptions: ProcessorOptions,
end: Instant? = null,
): ProcessedResourceData {
val records = responses.changes
Expand Down Expand Up @@ -66,6 +68,7 @@ internal suspend fun processChangesResponse(
distance = records.get<DistanceRecord>().filter { it.endTime <= endAdjusted },
steps = records.get<StepsRecord>().filter { it.endTime <= endAdjusted },
vo2Max = records.get<Vo2MaxRecord>().filter { it.time <= endAdjusted },
options = processorOptions,
).let(ProcessedResourceData::Summary)

VitalResource.Workout -> processor.processWorkoutsFromRecords(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +18,7 @@ internal suspend fun readResourceByTimeRange(
currentDevice: String,
reader: RecordReader,
processor: RecordProcessor,
processorOptions: ProcessorOptions,
): ProcessedResourceData {
suspend fun <Record, T: TimeSeriesData> readTimeseries(
read: suspend (Instant, Instant) -> List<Record>,
Expand All @@ -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(
Expand Down

0 comments on commit 6dbc3dd

Please sign in to comment.