@tryterra/react-native 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +31 -0
  2. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +103 -14
  3. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/ReadConversions.kt +250 -0
  4. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +47 -5
  5. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +1 -3
  6. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +89 -0
  7. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +672 -0
  8. package/docs/availability.md +154 -4
  9. package/frameworks/TerraSDK.xcframework/Info.plist +5 -5
  10. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  11. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +1385 -226
  12. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +37 -1
  13. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +37 -1
  14. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  15. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +37 -1
  16. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  17. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +2 -2
  18. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  19. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6589 -6468
  20. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  21. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +1385 -226
  22. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +37 -1
  23. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +37 -1
  24. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  25. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +37 -1
  26. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +1385 -226
  27. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +37 -1
  28. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +37 -1
  29. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +37 -1
  31. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +21 -21
  33. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +2 -2
  34. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6590 -6469
  36. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6508 -6387
  37. package/ios/TerraBridgeCore/Conversions+Reads.swift +259 -0
  38. package/ios/TerraBridgeCore/Conversions.swift +129 -2
  39. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +172 -0
  40. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +734 -1
  41. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +64 -0
  42. package/lib/Terra.d.ts +74 -5
  43. package/lib/Terra.js +69 -13
  44. package/lib/generated/client.d.ts +37 -1
  45. package/lib/generated/client.js +60 -0
  46. package/lib/generated/codecs.d.ts +85 -1
  47. package/lib/generated/codecs.js +710 -2
  48. package/lib/generated/ops.d.ts +1 -1
  49. package/lib/generated/ops.js +24 -0
  50. package/lib/generated/types.d.ts +314 -1
  51. package/lib/generated/types.js +133 -1
  52. package/lib/index.d.ts +1 -1
  53. package/package.json +3 -3
  54. package/src/Terra.ts +146 -16
  55. package/src/generated/client.ts +74 -2
  56. package/src/generated/codecs.ts +705 -3
  57. package/src/generated/ops.ts +36 -0
  58. package/src/generated/types.ts +556 -1
  59. package/src/index.ts +2 -0
package/README.md CHANGED
@@ -18,6 +18,37 @@ There is no sync call: once connected, the SDK uploads on its own, in the backgr
18
18
  rejection is a `TerraError` with a stable `code` (see `docs/availability.md` for the full list and
19
19
  what exists on which platform).
20
20
 
21
+ ## Local reads
22
+
23
+ Every read goes straight to the device's store and never touches the network:
24
+
25
+ | Member | Answers | Platforms |
26
+ |---|---|---|
27
+ | `body.heartRate(range)` | `HeartRateSample[]` | both |
28
+ | `body.restingHeartRate(range)` | `RestingHeartRateSample[]`, the store's own daily figure | both; Android grants it under `daily` |
29
+ | `body.readings(range)` | `BodyReading[]`, each `kind` carrying its unit | both; `bmi` and wrist temperature iOS only |
30
+ | `daily.steps(range)` | `StepCount[]` | both |
31
+ | `daily.totals(range)` | `DayTotal[]`, one per metric per whole local day | both; `appleExerciseTime` iOS only |
32
+ | `daily.intraday(range)` | `IntradaySample[]` | both; `basalEnergy` iOS only |
33
+ | `sleep.segments(range)` | `SleepSegment[]` | both |
34
+ | `activity.workouts(range)` | `Workout[]`, with the writer's `title` where it set one | both |
35
+ | `activity.routes(range)` | `WorkoutRoute[]`, each naming its workout | both |
36
+ | `activity.intensity(range)` | `WorkoutIntensitySample[]` | both; no `workoutId` on Android |
37
+ | `nutrition.quantities(range)` | `DietaryQuantity[]`, one per nutrient, not grouped into meals | both |
38
+ | `nutrition.hydration(range)` | `HydrationSample[]`, in millilitres | both |
39
+ | `menstruation.flow(range)` | `MenstruationFlowDay[]`, one per logged day | both |
40
+ | `menstruation.events(range)` | `MenstrualEvent[]`: ovulation tests, cervical mucus, intermenstrual bleeding | both |
41
+ | `athlete?.profile()` | `AthleteProfile`, or `undefined` where the store has none | iOS; `terra.athlete` is `undefined` on Android |
42
+
43
+ Ask for the category first: `permissions.request({ read: ["nutrition", "menstruation"] })`, plus
44
+ `"athlete"` on iOS, without which HealthKit hides the characteristics and `profile()` answers
45
+ `notSet` and nothing else. A read rejects with `not_connected` before `connect`, and with
46
+ `permission_denied` naming the category in `details.category` when the grant was refused.
47
+
48
+ `terra.availability(source?)` answers before any of them, with no connection: `available`, or why
49
+ not - `unsupportedDevice` on iOS, and on Android the reason Health Connect gives, such as
50
+ `providerAppMissing` when it needs installing.
51
+
21
52
  ## Native setup
22
53
 
23
54
  The client is built natively at launch, before any JavaScript runs, because background delivery
@@ -1,6 +1,11 @@
1
1
  package co.tryterra.sdk.bridge
2
2
 
3
3
  import co.tryterra.sdk.DataCategory
4
+ import co.tryterra.sdk.DietaryQuantity
5
+ import co.tryterra.sdk.MealSample
6
+ import co.tryterra.sdk.MenstruationFlowDay
7
+ import co.tryterra.sdk.MenstruationFlowSample
8
+ import co.tryterra.sdk.Nutrient
4
9
  import co.tryterra.sdk.EndUser
5
10
  import co.tryterra.sdk.BodyEntry
6
11
  import co.tryterra.sdk.HeartRateEntry
@@ -38,6 +43,11 @@ import co.tryterra.sdk.bridge.generated.Connection as WireConnection
38
43
  import co.tryterra.sdk.bridge.generated.NutritionEntry as WireNutritionEntry
39
44
  import co.tryterra.sdk.bridge.generated.Nutrients as WireNutrients
40
45
  import co.tryterra.sdk.bridge.generated.DataCategory as WireCategory
46
+ import co.tryterra.sdk.bridge.generated.DietaryQuantity as WireDietaryQuantity
47
+ import co.tryterra.sdk.bridge.generated.MealType as WireMealType
48
+ import co.tryterra.sdk.bridge.generated.MenstruationFlow as WireMenstruationFlow
49
+ import co.tryterra.sdk.bridge.generated.MenstruationFlowDay as WireMenstruationFlowDay
50
+ import co.tryterra.sdk.bridge.generated.Nutrient as WireNutrient
41
51
  import co.tryterra.sdk.bridge.generated.DiagnosticsState as WireDiagnosticsState
42
52
  import co.tryterra.sdk.bridge.generated.EndUser as WireEndUser
43
53
  import co.tryterra.sdk.bridge.generated.HealthAccess as WireHealthAccess
@@ -88,12 +98,8 @@ internal object Conversions {
88
98
  }
89
99
 
90
100
  /**
91
- * The four categories the wrapper projects.
92
- *
93
- * `MENSTRUATION` is excluded in the surface with a reason, so a request naming it cannot arrive
94
- * - the generated enum has no entry for it - and a value coming *out* of the SDK for it would
95
- * be a category the wrapper has no type for. Refusing it names the situation instead of
96
- * substituting a neighbour.
101
+ * `ATHLETE` is HealthKit's characteristics and has no Health Connect counterpart, so a request
102
+ * naming it is refused as this platform lacking it rather than quietly dropped from the sheet.
97
103
  */
98
104
  fun category(wire: WireCategory): DataCategory = when (wire) {
99
105
  WireCategory.ACTIVITY -> DataCategory.ACTIVITY
@@ -101,15 +107,17 @@ internal object Conversions {
101
107
  WireCategory.DAILY -> DataCategory.DAILY
102
108
  WireCategory.SLEEP -> DataCategory.SLEEP
103
109
  WireCategory.NUTRITION -> DataCategory.NUTRITION
110
+ WireCategory.MENSTRUATION -> DataCategory.MENSTRUATION
111
+ WireCategory.ATHLETE -> throw BridgeError.UnsupportedOnPlatform("athlete")
104
112
  }
105
113
 
106
- fun category(category: DataCategory): WireCategory? = when (category) {
114
+ fun category(category: DataCategory): WireCategory = when (category) {
107
115
  DataCategory.ACTIVITY -> WireCategory.ACTIVITY
108
116
  DataCategory.BODY -> WireCategory.BODY
109
117
  DataCategory.DAILY -> WireCategory.DAILY
110
118
  DataCategory.SLEEP -> WireCategory.SLEEP
111
119
  DataCategory.NUTRITION -> WireCategory.NUTRITION
112
- DataCategory.MENSTRUATION -> null
120
+ DataCategory.MENSTRUATION -> WireCategory.MENSTRUATION
113
121
  }
114
122
 
115
123
  /**
@@ -255,6 +263,88 @@ internal object Conversions {
255
263
  timeZone = segment.timeZone?.id,
256
264
  )
257
265
 
266
+ fun nutrient(nutrient: Nutrient): WireNutrient = when (nutrient) {
267
+ Nutrient.ENERGY_CONSUMED -> WireNutrient.ENERGY_CONSUMED
268
+ Nutrient.PROTEIN -> WireNutrient.PROTEIN
269
+ Nutrient.CARBOHYDRATES -> WireNutrient.CARBOHYDRATES
270
+ Nutrient.FAT_TOTAL -> WireNutrient.FAT_TOTAL
271
+ Nutrient.FAT_SATURATED -> WireNutrient.FAT_SATURATED
272
+ Nutrient.FAT_MONOUNSATURATED -> WireNutrient.FAT_MONOUNSATURATED
273
+ Nutrient.FAT_POLYUNSATURATED -> WireNutrient.FAT_POLYUNSATURATED
274
+ Nutrient.SUGAR -> WireNutrient.SUGAR
275
+ Nutrient.FIBER -> WireNutrient.FIBER
276
+ Nutrient.CHOLESTEROL -> WireNutrient.CHOLESTEROL
277
+ Nutrient.SODIUM -> WireNutrient.SODIUM
278
+ Nutrient.POTASSIUM -> WireNutrient.POTASSIUM
279
+ Nutrient.CALCIUM -> WireNutrient.CALCIUM
280
+ Nutrient.IRON -> WireNutrient.IRON
281
+ Nutrient.MAGNESIUM -> WireNutrient.MAGNESIUM
282
+ Nutrient.ZINC -> WireNutrient.ZINC
283
+ Nutrient.PHOSPHORUS -> WireNutrient.PHOSPHORUS
284
+ Nutrient.IODINE -> WireNutrient.IODINE
285
+ Nutrient.MANGANESE -> WireNutrient.MANGANESE
286
+ Nutrient.MOLYBDENUM -> WireNutrient.MOLYBDENUM
287
+ Nutrient.SELENIUM -> WireNutrient.SELENIUM
288
+ Nutrient.COPPER -> WireNutrient.COPPER
289
+ Nutrient.CHROMIUM -> WireNutrient.CHROMIUM
290
+ Nutrient.CHLORIDE -> WireNutrient.CHLORIDE
291
+ Nutrient.CAFFEINE -> WireNutrient.CAFFEINE
292
+ Nutrient.VITAMIN_A -> WireNutrient.VITAMIN_A
293
+ Nutrient.VITAMIN_C -> WireNutrient.VITAMIN_C
294
+ Nutrient.VITAMIN_D -> WireNutrient.VITAMIN_D
295
+ Nutrient.VITAMIN_E -> WireNutrient.VITAMIN_E
296
+ Nutrient.VITAMIN_K -> WireNutrient.VITAMIN_K
297
+ Nutrient.VITAMIN_B6 -> WireNutrient.VITAMIN_B6
298
+ Nutrient.VITAMIN_B12 -> WireNutrient.VITAMIN_B12
299
+ Nutrient.THIAMIN -> WireNutrient.THIAMIN
300
+ Nutrient.RIBOFLAVIN -> WireNutrient.RIBOFLAVIN
301
+ Nutrient.NIACIN -> WireNutrient.NIACIN
302
+ Nutrient.FOLATE -> WireNutrient.FOLATE
303
+ Nutrient.BIOTIN -> WireNutrient.BIOTIN
304
+ Nutrient.PANTOTHENIC_ACID -> WireNutrient.PANTOTHENIC_ACID
305
+ }
306
+
307
+ fun mealType(type: MealSample.MealType): WireMealType = when (type) {
308
+ MealSample.MealType.BREAKFAST -> WireMealType.BREAKFAST
309
+ MealSample.MealType.LUNCH -> WireMealType.LUNCH
310
+ MealSample.MealType.DINNER -> WireMealType.DINNER
311
+ MealSample.MealType.SNACK -> WireMealType.SNACK
312
+ MealSample.MealType.UNKNOWN -> WireMealType.UNKNOWN
313
+ }
314
+
315
+ fun flow(flow: MenstruationFlowSample.Flow): WireMenstruationFlow = when (flow) {
316
+ MenstruationFlowSample.Flow.UNSPECIFIED -> WireMenstruationFlow.UNSPECIFIED
317
+ MenstruationFlowSample.Flow.LIGHT -> WireMenstruationFlow.LIGHT
318
+ MenstruationFlowSample.Flow.MEDIUM -> WireMenstruationFlow.MEDIUM
319
+ MenstruationFlowSample.Flow.HEAVY -> WireMenstruationFlow.HEAVY
320
+ MenstruationFlowSample.Flow.NONE -> WireMenstruationFlow.NO_FLOW
321
+ }
322
+
323
+ fun quantity(quantity: DietaryQuantity): WireDietaryQuantity = WireDietaryQuantity(
324
+ id = quantity.id.value,
325
+ nutrient = nutrient(quantity.nutrient),
326
+ value = quantity.value,
327
+ start = BridgeInstant.format(quantity.interval.start),
328
+ end = BridgeInstant.format(quantity.interval.end),
329
+ mealType = mealType(quantity.mealType),
330
+ name = quantity.name,
331
+ foodCorrelationId = quantity.foodCorrelationId?.value,
332
+ source = sampleSource(quantity.source),
333
+ userEntered = quantity.userEntered,
334
+ timeZone = quantity.timeZone?.id,
335
+ )
336
+
337
+ fun flowDay(day: MenstruationFlowDay): WireMenstruationFlowDay = WireMenstruationFlowDay(
338
+ id = day.id.value,
339
+ start = BridgeInstant.format(day.interval.start),
340
+ end = BridgeInstant.format(day.interval.end),
341
+ flow = flow(day.flow),
342
+ cycleStart = day.cycleStart,
343
+ source = sampleSource(day.source),
344
+ userEntered = day.userEntered,
345
+ timeZone = day.timeZone?.id,
346
+ )
347
+
258
348
  fun workout(workout: Workout): WireWorkout = WireWorkout(
259
349
  id = workout.id.value,
260
350
  start = BridgeInstant.format(workout.interval.start),
@@ -267,6 +357,7 @@ internal object Conversions {
267
357
  source = sampleSource(workout.source),
268
358
  userEntered = workout.userEntered,
269
359
  timeZone = workout.timeZone?.id,
360
+ title = workout.title,
270
361
  )
271
362
 
272
363
  /**
@@ -275,10 +366,8 @@ internal object Conversions {
275
366
  * Sending the instant would make the receiving side re-derive the day in its own zone, and get
276
367
  * a different one either side of midnight.
277
368
  */
278
- fun change(change: HealthChange, zone: ZoneId): WireHealthChange? =
279
- category(change.category)?.let {
280
- WireHealthChange(category = it, day = change.day.atZone(zone).toLocalDate().toString())
281
- }
369
+ fun change(change: HealthChange, zone: ZoneId): WireHealthChange =
370
+ WireHealthChange(category = category(change.category), day = change.day.atZone(zone).toLocalDate().toString())
282
371
 
283
372
  /**
284
373
  * An event is the object `DiagnosticsWire` already produces, carried unchanged.
@@ -312,8 +401,8 @@ internal object Conversions {
312
401
  * which the vectors would be flaky.
313
402
  */
314
403
  fun access(access: HealthAccess): WireHealthAccess = WireHealthAccess(
315
- read = access.read.mapNotNull(::category).sortedBy { it.name },
316
- write = access.write.mapNotNull(::category).sortedBy { it.name },
404
+ read = access.read.map(::category).sortedBy { it.name },
405
+ write = access.write.map(::category).sortedBy { it.name },
317
406
  workouts = access.workouts
318
407
  .groupBy { WorkoutActivity.reportName(it.activity) }
319
408
  .toSortedMap()
@@ -0,0 +1,250 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.BloodGlucoseSample
4
+ import co.tryterra.sdk.BloodGlucoseUnit
5
+ import co.tryterra.sdk.BodyMeasurement
6
+ import co.tryterra.sdk.BodyReading
7
+ import co.tryterra.sdk.BodyTemperatureSample
8
+ import co.tryterra.sdk.DayTotal
9
+ import co.tryterra.sdk.HydrationSample
10
+ import co.tryterra.sdk.IntradaySample
11
+ import co.tryterra.sdk.MenstrualEvent
12
+ import co.tryterra.sdk.MenstruationEventSample
13
+ import co.tryterra.sdk.RestingHeartRateSample
14
+ import co.tryterra.sdk.Vo2MaxSample
15
+ import co.tryterra.sdk.WorkoutIntensitySample
16
+ import co.tryterra.sdk.WorkoutRoute
17
+ import co.tryterra.sdk.bridge.generated.BridgeInstant
18
+ import co.tryterra.sdk.health.StoreAvailability
19
+ import co.tryterra.sdk.bridge.generated.BloodGlucoseUnit as WireGlucoseUnit
20
+ import co.tryterra.sdk.bridge.generated.BodyMetric as WireBodyMetric
21
+ import co.tryterra.sdk.bridge.generated.BodyReading as WireBodyReading
22
+ import co.tryterra.sdk.bridge.generated.BodyReadingKind as WireBodyReadingKind
23
+ import co.tryterra.sdk.bridge.generated.BodyTemperatureMetric as WireTemperatureMetric
24
+ import co.tryterra.sdk.bridge.generated.DayTotal as WireDayTotal
25
+ import co.tryterra.sdk.bridge.generated.DayTotalMetric as WireDayTotalMetric
26
+ import co.tryterra.sdk.bridge.generated.GlucoseMealTime as WireMealTime
27
+ import co.tryterra.sdk.bridge.generated.GlucoseSpecimen as WireSpecimen
28
+ import co.tryterra.sdk.bridge.generated.HeartRateVariabilityMetric as WireVariabilityMetric
29
+ import co.tryterra.sdk.bridge.generated.HydrationSample as WireHydrationSample
30
+ import co.tryterra.sdk.bridge.generated.IntradayMetric as WireIntradayMetric
31
+ import co.tryterra.sdk.bridge.generated.IntradaySample as WireIntradaySample
32
+ import co.tryterra.sdk.bridge.generated.MenstrualEvent as WireMenstrualEvent
33
+ import co.tryterra.sdk.bridge.generated.MenstrualEventKind as WireEventKind
34
+ import co.tryterra.sdk.bridge.generated.RestingHeartRateSample as WireRestingHeartRateSample
35
+ import co.tryterra.sdk.bridge.generated.RoutePoint as WireRoutePoint
36
+ import co.tryterra.sdk.bridge.generated.StoreAvailability as WireStoreAvailability
37
+ import co.tryterra.sdk.bridge.generated.VO2MaxTestType as WireTestType
38
+ import co.tryterra.sdk.bridge.generated.WorkoutIntensityMetric as WireIntensityMetric
39
+ import co.tryterra.sdk.bridge.generated.WorkoutIntensitySample as WireIntensitySample
40
+ import co.tryterra.sdk.bridge.generated.WorkoutRoute as WireWorkoutRoute
41
+
42
+ /**
43
+ * The reads that close the rest of the 1.x local-read gap, between the SDK's types and the wire's.
44
+ *
45
+ * Apart from [Conversions] only because there are many of them, and under its rules: every `when`
46
+ * is exhaustive with no `else`, so a case added on either side stops the build here, and a case the
47
+ * read never returns maps to null and is dropped rather than sent under a neighbour's name.
48
+ */
49
+ internal object ReadConversions {
50
+
51
+ /** Null for the metrics `daily.totals` leaves out, which the read never hands over. */
52
+ fun dayTotalMetric(metric: DayTotal.Metric): WireDayTotalMetric? = when (metric) {
53
+ DayTotal.Metric.DISTANCE_WALKING_RUNNING -> WireDayTotalMetric.DISTANCE_WALKING_RUNNING
54
+ DayTotal.Metric.DISTANCE_CYCLING -> WireDayTotalMetric.DISTANCE_CYCLING
55
+ DayTotal.Metric.DISTANCE_SWIMMING -> WireDayTotalMetric.DISTANCE_SWIMMING
56
+ DayTotal.Metric.DISTANCE_WHEELCHAIR -> WireDayTotalMetric.DISTANCE_WHEELCHAIR
57
+ DayTotal.Metric.ACTIVE_ENERGY_BURNED -> WireDayTotalMetric.ACTIVE_ENERGY_BURNED
58
+ DayTotal.Metric.BASAL_ENERGY_BURNED -> WireDayTotalMetric.BASAL_ENERGY_BURNED
59
+ DayTotal.Metric.FLIGHTS_CLIMBED -> WireDayTotalMetric.FLIGHTS_CLIMBED
60
+ DayTotal.Metric.STEPS, DayTotal.Metric.SWIMMING_STROKE_COUNT -> null
61
+ }
62
+
63
+ fun dayTotal(total: DayTotal): WireDayTotal? = dayTotalMetric(total.metric)?.let {
64
+ WireDayTotal(
65
+ metric = it,
66
+ start = BridgeInstant.format(total.interval.start),
67
+ end = BridgeInstant.format(total.interval.end),
68
+ value = total.value,
69
+ unit = total.unit,
70
+ timeZone = total.timeZone?.id,
71
+ )
72
+ }
73
+
74
+ fun intradayMetric(metric: IntradaySample.Metric): WireIntradayMetric = when (metric) {
75
+ IntradaySample.Metric.ACTIVE_ENERGY -> WireIntradayMetric.ACTIVE_ENERGY
76
+ IntradaySample.Metric.DISTANCE -> WireIntradayMetric.DISTANCE
77
+ IntradaySample.Metric.FLOORS_CLIMBED -> WireIntradayMetric.FLOORS_CLIMBED
78
+ }
79
+
80
+ fun intraday(sample: IntradaySample): WireIntradaySample = WireIntradaySample(
81
+ id = sample.id.value,
82
+ metric = intradayMetric(sample.metric),
83
+ start = BridgeInstant.format(sample.interval.start),
84
+ end = BridgeInstant.format(sample.interval.end),
85
+ value = sample.value,
86
+ source = Conversions.sampleSource(sample.source),
87
+ userEntered = sample.userEntered,
88
+ timeZone = sample.timeZone?.id,
89
+ )
90
+
91
+ fun restingHeartRate(sample: RestingHeartRateSample): WireRestingHeartRateSample = WireRestingHeartRateSample(
92
+ id = sample.id.value,
93
+ start = BridgeInstant.format(sample.interval.start),
94
+ end = BridgeInstant.format(sample.interval.end),
95
+ beatsPerMinute = sample.beatsPerMinute,
96
+ source = Conversions.sampleSource(sample.source),
97
+ userEntered = sample.userEntered,
98
+ timeZone = sample.timeZone?.id,
99
+ )
100
+
101
+ /** Null for the three `body.readings` leaves out. */
102
+ fun bodyMetric(metric: BodyMeasurement.Metric): WireBodyMetric? = when (metric) {
103
+ BodyMeasurement.Metric.WEIGHT -> WireBodyMetric.WEIGHT
104
+ BodyMeasurement.Metric.HEIGHT -> WireBodyMetric.HEIGHT
105
+ BodyMeasurement.Metric.BODY_FAT_PERCENTAGE -> WireBodyMetric.BODY_FAT_PERCENTAGE
106
+ BodyMeasurement.Metric.LEAN_BODY_MASS -> WireBodyMetric.LEAN_BODY_MASS
107
+ BodyMeasurement.Metric.BONE_MASS,
108
+ BodyMeasurement.Metric.BODY_WATER_MASS,
109
+ BodyMeasurement.Metric.BASAL_METABOLIC_RATE,
110
+ -> null
111
+ }
112
+
113
+ fun glucoseUnit(unit: BloodGlucoseUnit): WireGlucoseUnit = when (unit) {
114
+ BloodGlucoseUnit.MILLIGRAMS_PER_DECILITRE -> WireGlucoseUnit.MILLIGRAMS_PER_DECILITRE
115
+ BloodGlucoseUnit.MILLIMOLES_PER_LITRE -> WireGlucoseUnit.MILLIMOLES_PER_LITRE
116
+ }
117
+
118
+ fun mealTime(time: BloodGlucoseSample.MealTime): WireMealTime = when (time) {
119
+ BloodGlucoseSample.MealTime.PREPRANDIAL -> WireMealTime.PREPRANDIAL
120
+ BloodGlucoseSample.MealTime.POSTPRANDIAL -> WireMealTime.POSTPRANDIAL
121
+ BloodGlucoseSample.MealTime.UNKNOWN -> WireMealTime.UNKNOWN
122
+ }
123
+
124
+ fun specimen(specimen: BloodGlucoseSample.Specimen): WireSpecimen = when (specimen) {
125
+ BloodGlucoseSample.Specimen.CAPILLARY -> WireSpecimen.CAPILLARY
126
+ BloodGlucoseSample.Specimen.INTERSTITIAL -> WireSpecimen.INTERSTITIAL
127
+ BloodGlucoseSample.Specimen.PLASMA -> WireSpecimen.PLASMA
128
+ BloodGlucoseSample.Specimen.SERUM -> WireSpecimen.SERUM
129
+ BloodGlucoseSample.Specimen.TEARS -> WireSpecimen.TEARS
130
+ BloodGlucoseSample.Specimen.WHOLE_BLOOD -> WireSpecimen.WHOLE_BLOOD
131
+ BloodGlucoseSample.Specimen.UNKNOWN -> WireSpecimen.UNKNOWN
132
+ }
133
+
134
+ fun temperatureMetric(metric: BodyTemperatureSample.Metric): WireTemperatureMetric = when (metric) {
135
+ BodyTemperatureSample.Metric.BODY -> WireTemperatureMetric.BODY
136
+ BodyTemperatureSample.Metric.BASAL -> WireTemperatureMetric.BASAL
137
+ BodyTemperatureSample.Metric.SKIN -> WireTemperatureMetric.SKIN
138
+ }
139
+
140
+ fun testType(type: Vo2MaxSample.TestType): WireTestType = when (type) {
141
+ Vo2MaxSample.TestType.MAX_EXERCISE -> WireTestType.MAX_EXERCISE
142
+ Vo2MaxSample.TestType.PREDICTED_SUB_MAX_EXERCISE -> WireTestType.PREDICTED_SUB_MAX_EXERCISE
143
+ Vo2MaxSample.TestType.PREDICTED_NON_EXERCISE -> WireTestType.PREDICTED_NON_EXERCISE
144
+ }
145
+
146
+ fun bodyReadingKind(kind: BodyReading.Kind): WireBodyReadingKind? = when (kind) {
147
+ is BodyReading.Kind.Measurement -> bodyMetric(kind.metric)?.let { WireBodyReadingKind.Measurement(it, kind.value) }
148
+ is BodyReading.Kind.BloodPressure ->
149
+ WireBodyReadingKind.BloodPressure(kind.systolicMillimetersOfMercury, kind.diastolicMillimetersOfMercury)
150
+ is BodyReading.Kind.BloodGlucose -> WireBodyReadingKind.BloodGlucose(
151
+ value = kind.value,
152
+ unit = glucoseUnit(kind.unit),
153
+ mealTime = kind.mealTime?.let(::mealTime),
154
+ specimen = kind.specimen?.let(::specimen),
155
+ )
156
+ is BodyReading.Kind.OxygenSaturation -> WireBodyReadingKind.OxygenSaturation(kind.fraction)
157
+ is BodyReading.Kind.Temperature -> WireBodyReadingKind.Temperature(temperatureMetric(kind.metric), kind.celsius)
158
+ // RMSSD is the only statistic Health Connect records.
159
+ is BodyReading.Kind.HeartRateVariability ->
160
+ WireBodyReadingKind.HeartRateVariability(WireVariabilityMetric.RMSSD, kind.rmssdMilliseconds)
161
+ is BodyReading.Kind.RespiratoryRate -> WireBodyReadingKind.RespiratoryRate(kind.breathsPerMinute)
162
+ is BodyReading.Kind.Vo2Max ->
163
+ WireBodyReadingKind.Vo2Max(kind.millilitresPerKilogramPerMinute, kind.testType?.let(::testType))
164
+ }
165
+
166
+ fun bodyReading(reading: BodyReading): WireBodyReading? = bodyReadingKind(reading.kind)?.let {
167
+ WireBodyReading(
168
+ id = reading.id.value,
169
+ kind = it,
170
+ start = BridgeInstant.format(reading.interval.start),
171
+ end = BridgeInstant.format(reading.interval.end),
172
+ source = Conversions.sampleSource(reading.source),
173
+ userEntered = reading.userEntered,
174
+ timeZone = reading.timeZone?.id,
175
+ )
176
+ }
177
+
178
+ fun route(route: WorkoutRoute): WireWorkoutRoute = WireWorkoutRoute(
179
+ id = route.id.value,
180
+ workoutId = route.workoutId.value,
181
+ start = BridgeInstant.format(route.interval.start),
182
+ end = BridgeInstant.format(route.interval.end),
183
+ points = route.points.map { WireRoutePoint(BridgeInstant.format(it.time), it.latitude, it.longitude) },
184
+ source = Conversions.sampleSource(route.source),
185
+ timeZone = route.timeZone?.id,
186
+ )
187
+
188
+ fun intensityMetric(metric: WorkoutIntensitySample.Metric): WireIntensityMetric = when (metric) {
189
+ WorkoutIntensitySample.Metric.SPEED -> WireIntensityMetric.SPEED
190
+ WorkoutIntensitySample.Metric.POWER -> WireIntensityMetric.POWER
191
+ WorkoutIntensitySample.Metric.CADENCE -> WireIntensityMetric.CADENCE
192
+ }
193
+
194
+ /** An instant on Health Connect, so both ends are its time, and no workout it could name. */
195
+ fun intensity(sample: WorkoutIntensitySample): WireIntensitySample = WireIntensitySample(
196
+ id = sample.id.value,
197
+ metric = intensityMetric(sample.metric),
198
+ workoutId = null,
199
+ start = BridgeInstant.format(sample.time),
200
+ end = BridgeInstant.format(sample.time),
201
+ value = sample.value,
202
+ source = Conversions.sampleSource(sample.source),
203
+ userEntered = sample.userEntered,
204
+ timeZone = sample.timeZone?.id,
205
+ )
206
+
207
+ fun hydration(sample: HydrationSample): WireHydrationSample = WireHydrationSample(
208
+ id = sample.id.value,
209
+ start = BridgeInstant.format(sample.interval.start),
210
+ end = BridgeInstant.format(sample.interval.end),
211
+ millilitres = sample.millilitres,
212
+ source = Conversions.sampleSource(sample.source),
213
+ userEntered = sample.userEntered,
214
+ timeZone = sample.timeZone?.id,
215
+ )
216
+
217
+ /**
218
+ * Which types a partner policy refuses, and which feature is missing, stay in the native report:
219
+ * the wire names the reason, and the remedy a wrapper app can offer is the same either way.
220
+ */
221
+ fun availability(availability: StoreAvailability): WireStoreAvailability = when (availability) {
222
+ StoreAvailability.Available -> WireStoreAvailability.Available
223
+ StoreAvailability.ModuleMissing -> WireStoreAvailability.ModuleMissing
224
+ StoreAvailability.ProviderAppMissing -> WireStoreAvailability.ProviderAppMissing
225
+ is StoreAvailability.ProviderAppTooOld -> WireStoreAvailability.ProviderAppTooOld(availability.installed)
226
+ is StoreAvailability.PartnerPolicyMissing -> WireStoreAvailability.PartnerPolicyMissing
227
+ is StoreAvailability.FeatureUnavailable -> WireStoreAvailability.FeatureUnavailable
228
+ }
229
+
230
+ /** Null for sexual activity, which `menstruation.events` leaves out. */
231
+ fun eventKind(event: MenstruationEventSample.Event): WireEventKind? = when (event) {
232
+ MenstruationEventSample.Event.INTERMENSTRUAL_BLEEDING -> WireEventKind.INTERMENSTRUAL_BLEEDING
233
+ MenstruationEventSample.Event.OVULATION_TEST -> WireEventKind.OVULATION_TEST
234
+ MenstruationEventSample.Event.CERVICAL_MUCUS -> WireEventKind.CERVICAL_MUCUS
235
+ MenstruationEventSample.Event.SEXUAL_ACTIVITY -> null
236
+ }
237
+
238
+ fun menstrualEvent(event: MenstrualEvent): WireMenstrualEvent? = eventKind(event.event)?.let {
239
+ WireMenstrualEvent(
240
+ id = event.id.value,
241
+ kind = it,
242
+ time = BridgeInstant.format(event.time),
243
+ value = event.value?.wire,
244
+ storeIdentifier = event.storeIdentifier,
245
+ source = Conversions.sampleSource(event.source),
246
+ userEntered = event.userEntered,
247
+ timeZone = event.timeZone?.id,
248
+ )
249
+ }
250
+ }
@@ -12,11 +12,13 @@ import co.tryterra.sdk.bridge.generated.Connection
12
12
  import co.tryterra.sdk.bridge.generated.ConnectRequest
13
13
  import co.tryterra.sdk.bridge.generated.ConnectionList
14
14
  import co.tryterra.sdk.bridge.generated.DiagnosticsState
15
+ import co.tryterra.sdk.bridge.generated.DietaryQuantityList
15
16
  import co.tryterra.sdk.bridge.generated.Empty
16
17
  import co.tryterra.sdk.bridge.generated.HealthAccess
17
18
  import co.tryterra.sdk.bridge.generated.HeartRateEntryList
18
19
  import co.tryterra.sdk.bridge.generated.HeartRateList
19
20
  import co.tryterra.sdk.bridge.generated.LastDeliveryResult
21
+ import co.tryterra.sdk.bridge.generated.MenstruationFlowDayList
20
22
  import co.tryterra.sdk.bridge.generated.NutritionEntryList
21
23
  import co.tryterra.sdk.bridge.generated.PermissionGrant
22
24
  import co.tryterra.sdk.bridge.generated.PermissionRequest
@@ -37,7 +39,16 @@ import co.tryterra.sdk.bridge.generated.TokenRejectRequest
37
39
  import co.tryterra.sdk.bridge.generated.TokenResolveRequest
38
40
  import co.tryterra.sdk.bridge.generated.WorkoutEntryList
39
41
  import co.tryterra.sdk.bridge.generated.WorkoutList
42
+ import co.tryterra.sdk.bridge.generated.AvailabilityResult
40
43
  import co.tryterra.sdk.bridge.generated.BodyEntryList
44
+ import co.tryterra.sdk.bridge.generated.BodyReadingList
45
+ import co.tryterra.sdk.bridge.generated.DayTotalList
46
+ import co.tryterra.sdk.bridge.generated.HydrationList
47
+ import co.tryterra.sdk.bridge.generated.IntradaySampleList
48
+ import co.tryterra.sdk.bridge.generated.MenstrualEventList
49
+ import co.tryterra.sdk.bridge.generated.RestingHeartRateList
50
+ import co.tryterra.sdk.bridge.generated.WorkoutIntensityList
51
+ import co.tryterra.sdk.bridge.generated.WorkoutRouteList
41
52
  import co.tryterra.sdk.bridge.generated.Source as WireSource
42
53
  import kotlinx.coroutines.CancellationException
43
54
 
@@ -78,6 +89,9 @@ internal class TerraBridgeOpsImpl(
78
89
  override suspend fun clientConnections(request: Empty): ConnectionList =
79
90
  ConnectionList(terra.connections().map(Conversions::connection))
80
91
 
92
+ override suspend fun clientAvailability(request: SourceRequest): AvailabilityResult =
93
+ AvailabilityResult(ReadConversions.availability(terra.availability(sourceOf(request.source))))
94
+
81
95
  override suspend fun clientDisconnect(request: SourceRequest): Empty {
82
96
  terra.disconnect(sourceOf(request.source))
83
97
  return Empty()
@@ -143,11 +157,7 @@ internal class TerraBridgeOpsImpl(
143
157
 
144
158
  override suspend fun permissionsStatuses(request: SourceRequest): StatusesResult {
145
159
  val statuses = permissionsOf(request.source).statuses()
146
- .mapNotNull { (category, status) ->
147
- // A category the wrapper does not project has no key to report it under. Dropping
148
- // it is right: inventing one would put a name in the map that no type admits.
149
- Conversions.category(category)?.let { it to Conversions.permissionStatus(status) }
150
- }
160
+ .map { (category, status) -> Conversions.category(category) to Conversions.permissionStatus(status) }
151
161
  .toMap()
152
162
  return StatusesResult(statuses)
153
163
  }
@@ -187,6 +197,12 @@ internal class TerraBridgeOpsImpl(
187
197
  override suspend fun bodyHeartRate(request: RangeRequest): HeartRateList =
188
198
  HeartRateList(terra.body.heartRate(Conversions.timeRange(request.range)).map(Conversions::heartRate))
189
199
 
200
+ override suspend fun bodyRestingHeartRate(request: RangeRequest): RestingHeartRateList =
201
+ RestingHeartRateList(terra.body.restingHeartRate(Conversions.timeRange(request.range)).map(ReadConversions::restingHeartRate))
202
+
203
+ override suspend fun bodyReadings(request: RangeRequest): BodyReadingList =
204
+ BodyReadingList(terra.body.readings(Conversions.timeRange(request.range)).mapNotNull(ReadConversions::bodyReading))
205
+
190
206
  override suspend fun bodyLogHeartRate(request: HeartRateEntryList): RecordIdList =
191
207
  RecordIdList(terra.body.log(request.entries.map(Conversions::entry)).map { it.value })
192
208
 
@@ -203,6 +219,12 @@ internal class TerraBridgeOpsImpl(
203
219
  override suspend fun dailySteps(request: RangeRequest): StepCountList =
204
220
  StepCountList(terra.daily.steps(Conversions.timeRange(request.range)).map(Conversions::steps))
205
221
 
222
+ override suspend fun dailyTotals(request: RangeRequest): DayTotalList =
223
+ DayTotalList(terra.daily.totals(Conversions.timeRange(request.range)).mapNotNull(ReadConversions::dayTotal))
224
+
225
+ override suspend fun dailyIntraday(request: RangeRequest): IntradaySampleList =
226
+ IntradaySampleList(terra.daily.intraday(Conversions.timeRange(request.range)).map(ReadConversions::intraday))
227
+
206
228
  override suspend fun dailyLog(request: StepEntryList): RecordIdList =
207
229
  RecordIdList(terra.daily.log(request.entries.map(Conversions::entry)).map { it.value })
208
230
 
@@ -229,6 +251,12 @@ internal class TerraBridgeOpsImpl(
229
251
  override suspend fun activityWorkouts(request: RangeRequest): WorkoutList =
230
252
  WorkoutList(terra.activity.workouts(Conversions.timeRange(request.range)).map(Conversions::workout))
231
253
 
254
+ override suspend fun activityRoutes(request: RangeRequest): WorkoutRouteList =
255
+ WorkoutRouteList(terra.activity.routes(Conversions.timeRange(request.range)).map(ReadConversions::route))
256
+
257
+ override suspend fun activityIntensity(request: RangeRequest): WorkoutIntensityList =
258
+ WorkoutIntensityList(terra.activity.intensity(Conversions.timeRange(request.range)).map(ReadConversions::intensity))
259
+
232
260
  override suspend fun activityLog(request: WorkoutEntryList): RecordIdList =
233
261
  RecordIdList(terra.activity.log(request.entries.map(Conversions::entry)).map { it.value })
234
262
 
@@ -239,6 +267,9 @@ internal class TerraBridgeOpsImpl(
239
267
 
240
268
  // ---- nutrition --------------------------------------------------------------------------
241
269
 
270
+ override suspend fun nutritionQuantities(request: RangeRequest): DietaryQuantityList =
271
+ DietaryQuantityList(terra.nutrition.quantities(Conversions.timeRange(request.range)).map(Conversions::quantity))
272
+
242
273
  override suspend fun nutritionLog(request: NutritionEntryList): RecordIdList =
243
274
  RecordIdList(terra.nutrition.log(request.entries.map(Conversions::entry)).map { it.value })
244
275
 
@@ -247,6 +278,17 @@ internal class TerraBridgeOpsImpl(
247
278
  return Empty()
248
279
  }
249
280
 
281
+ override suspend fun nutritionHydration(request: RangeRequest): HydrationList =
282
+ HydrationList(terra.nutrition.hydration(Conversions.timeRange(request.range)).map(ReadConversions::hydration))
283
+
284
+ // ---- menstruation -----------------------------------------------------------------------
285
+
286
+ override suspend fun menstruationFlow(request: RangeRequest): MenstruationFlowDayList =
287
+ MenstruationFlowDayList(terra.menstruation.flow(Conversions.timeRange(request.range)).map(Conversions::flowDay))
288
+
289
+ override suspend fun menstruationEvents(request: RangeRequest): MenstrualEventList =
290
+ MenstrualEventList(terra.menstruation.events(Conversions.timeRange(request.range)).mapNotNull(ReadConversions::menstrualEvent))
291
+
250
292
  /** Absent means the default source, which is what every other entry point on the SDK assumes. */
251
293
  private fun sourceOf(wire: WireSource?): Source = wire?.let(Conversions::source) ?: Source.HEALTH
252
294
 
@@ -10,7 +10,6 @@ import co.tryterra.sdk.bridge.generated.TokenRequest
10
10
  import java.time.ZoneId
11
11
  import kotlinx.coroutines.flow.Flow
12
12
  import kotlinx.coroutines.flow.map
13
- import kotlinx.coroutines.flow.mapNotNull
14
13
 
15
14
  /**
16
15
  * The hand-written half of the streams: which SDK flow answers each stream, as typed values.
@@ -43,10 +42,9 @@ internal class TerraBridgeStreamsImpl(
43
42
  DataCategory.DAILY -> terra.daily.changes()
44
43
  DataCategory.SLEEP -> terra.sleep.changes()
45
44
  DataCategory.ACTIVITY -> terra.activity.changes()
46
- // Nutrition is write-only on both platforms and has no `changes()`.
47
45
  DataCategory.NUTRITION, DataCategory.MENSTRUATION ->
48
46
  error("no change stream for $category")
49
47
  }
50
- return source.mapNotNull { change -> Conversions.change(change, deviceZone) }
48
+ return source.map { change -> Conversions.change(change, deviceZone) }
51
49
  }
52
50
  }