@tryterra/react-native 0.0.0-stage → 2.1.0

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 (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/TerraReactNative.podspec +48 -0
  4. package/android/build.gradle +87 -0
  5. package/android/src/bridgeCore/kotlin/README.md +1 -0
  6. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
  7. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
  8. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +468 -0
  9. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
  10. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
  11. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
  12. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
  13. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +258 -0
  14. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +52 -0
  15. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
  16. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
  17. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +493 -0
  18. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +897 -0
  19. package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
  20. package/android/src/main/AndroidManifest.xml +24 -0
  21. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
  22. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
  23. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
  24. package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
  25. package/docs/availability.md +246 -0
  26. package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
  27. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  28. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +81513 -0
  29. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2150 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2150 -0
  31. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2127 -0
  33. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  34. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  36. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  37. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6473 -0
  38. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  39. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +81513 -0
  40. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2150 -0
  41. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2150 -0
  42. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  43. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2127 -0
  44. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +81513 -0
  45. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2150 -0
  46. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2150 -0
  47. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  48. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2127 -0
  49. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  50. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  51. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
  52. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  53. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  54. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6474 -0
  55. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6392 -0
  56. package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
  57. package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
  58. package/ios/TerraBridgeCore/Conversions.swift +467 -0
  59. package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
  60. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +795 -0
  61. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +1317 -0
  62. package/ios/TerraBridgeCore/Locked.swift +15 -0
  63. package/ios/TerraBridgeCore/README.md +1 -0
  64. package/ios/TerraBridgeCore/Rejections.swift +130 -0
  65. package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
  66. package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
  67. package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
  68. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +254 -0
  69. package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
  70. package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
  71. package/ios/TerraBridgeModule.mm +79 -0
  72. package/ios/TerraBridgeModuleImpl.swift +49 -0
  73. package/ios/TerraReactNative.swift +19 -0
  74. package/lib/EndUser.d.ts +12 -0
  75. package/lib/EndUser.js +13 -0
  76. package/lib/NativeTerraBridge.d.ts +23 -0
  77. package/lib/NativeTerraBridge.js +13 -0
  78. package/lib/Terra.d.ts +154 -0
  79. package/lib/Terra.js +280 -0
  80. package/lib/TerraError.d.ts +42 -0
  81. package/lib/TerraError.js +95 -0
  82. package/lib/createTerra.d.ts +8 -0
  83. package/lib/createTerra.js +19 -0
  84. package/lib/generated/client.d.ts +92 -0
  85. package/lib/generated/client.js +196 -0
  86. package/lib/generated/codecs.d.ts +135 -0
  87. package/lib/generated/codecs.js +1358 -0
  88. package/lib/generated/errors.d.ts +13 -0
  89. package/lib/generated/errors.js +68 -0
  90. package/lib/generated/ops.d.ts +13 -0
  91. package/lib/generated/ops.js +137 -0
  92. package/lib/generated/streams.d.ts +23 -0
  93. package/lib/generated/streams.js +58 -0
  94. package/lib/generated/types.d.ts +481 -0
  95. package/lib/generated/types.js +92 -0
  96. package/lib/index.d.ts +29 -0
  97. package/lib/index.js +55 -0
  98. package/package.json +71 -4
  99. package/src/EndUser.ts +15 -0
  100. package/src/NativeTerraBridge.ts +27 -0
  101. package/src/Terra.ts +462 -0
  102. package/src/TerraError.ts +116 -0
  103. package/src/createTerra.ts +18 -0
  104. package/src/generated/client.ts +243 -0
  105. package/src/generated/codecs.ts +1287 -0
  106. package/src/generated/errors.ts +113 -0
  107. package/src/generated/ops.ts +197 -0
  108. package/src/generated/streams.ts +74 -0
  109. package/src/generated/types.ts +667 -0
  110. package/src/index.ts +52 -0
@@ -0,0 +1,468 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.DataCategory
4
+ import co.tryterra.sdk.EndUser
5
+ import co.tryterra.sdk.BodyEntry
6
+ import co.tryterra.sdk.HeartRateEntry
7
+ import co.tryterra.sdk.NutritionEntry
8
+ import co.tryterra.sdk.HeartRateSample
9
+ import co.tryterra.sdk.PermissionStatus
10
+ import co.tryterra.sdk.RecordId
11
+ import co.tryterra.sdk.SampleInterval
12
+ import co.tryterra.sdk.SampleSource
13
+ import co.tryterra.sdk.SleepEntry
14
+ import co.tryterra.sdk.SleepSegment
15
+ import co.tryterra.sdk.SleepStage
16
+ import co.tryterra.sdk.Source
17
+ import co.tryterra.sdk.StepCount
18
+ import co.tryterra.sdk.StepEntry
19
+ import co.tryterra.sdk.TimeRange
20
+ import co.tryterra.sdk.Workout
21
+ import co.tryterra.sdk.WorkoutActivity
22
+ import co.tryterra.sdk.WorkoutEntry
23
+ import co.tryterra.sdk.WorkoutScope
24
+ import co.tryterra.sdk.bridge.generated.BridgeInstant
25
+ import co.tryterra.sdk.bridge.generated.BridgeError
26
+ import co.tryterra.sdk.diagnostics.BackgroundRestrictionReason
27
+ import co.tryterra.sdk.diagnostics.DiagnosticsState
28
+ import co.tryterra.sdk.diagnostics.DiagnosticsWire
29
+ import co.tryterra.sdk.diagnostics.SupportReport
30
+ import co.tryterra.sdk.diagnostics.TerraEvent
31
+ import co.tryterra.sdk.health.HealthAccess
32
+ import co.tryterra.sdk.HealthChange
33
+ import co.tryterra.sdk.store.StoredConnection
34
+ import java.time.ZoneId
35
+ import co.tryterra.sdk.bridge.generated.BackgroundRestrictionReason as WireRestriction
36
+ import co.tryterra.sdk.bridge.generated.BodyEntry as WireBodyEntry
37
+ import co.tryterra.sdk.bridge.generated.Connection as WireConnection
38
+ import co.tryterra.sdk.bridge.generated.NutritionEntry as WireNutritionEntry
39
+ import co.tryterra.sdk.bridge.generated.Nutrients as WireNutrients
40
+ import co.tryterra.sdk.bridge.generated.DataCategory as WireCategory
41
+ import co.tryterra.sdk.bridge.generated.DiagnosticsState as WireDiagnosticsState
42
+ import co.tryterra.sdk.bridge.generated.EndUser as WireEndUser
43
+ import co.tryterra.sdk.bridge.generated.HealthAccess as WireHealthAccess
44
+ import co.tryterra.sdk.bridge.generated.HealthChange as WireHealthChange
45
+ import co.tryterra.sdk.bridge.generated.HeartRateEntry as WireHeartRateEntry
46
+ import co.tryterra.sdk.bridge.generated.HeartRateSample as WireHeartRateSample
47
+ import co.tryterra.sdk.bridge.generated.MotionContext as WireMotionContext
48
+ import co.tryterra.sdk.bridge.generated.PermissionStatus as WirePermissionStatus
49
+ import co.tryterra.sdk.bridge.generated.SampleSource as WireSampleSource
50
+ import co.tryterra.sdk.bridge.generated.SleepEntry as WireSleepEntry
51
+ import co.tryterra.sdk.bridge.generated.SleepSegment as WireSleepSegment
52
+ import co.tryterra.sdk.bridge.generated.SleepStage as WireSleepStage
53
+ import co.tryterra.sdk.bridge.generated.Source as WireSource
54
+ import co.tryterra.sdk.bridge.generated.StepCount as WireStepCount
55
+ import co.tryterra.sdk.bridge.generated.StepEntry as WireStepEntry
56
+ import co.tryterra.sdk.bridge.generated.SupportReport as WireSupportReport
57
+ import co.tryterra.sdk.bridge.generated.TerraEvent as WireTerraEvent
58
+ import co.tryterra.sdk.bridge.generated.TimeRange as WireTimeRange
59
+ import co.tryterra.sdk.bridge.generated.Workout as WireWorkout
60
+ import co.tryterra.sdk.bridge.generated.WorkoutActivity as WireWorkoutActivity
61
+ import co.tryterra.sdk.bridge.generated.WorkoutEntry as WireWorkoutEntry
62
+ import co.tryterra.sdk.bridge.generated.WorkoutScope as WireWorkoutScope
63
+
64
+ /**
65
+ * Between the SDK's types and the generated wire DTOs, in one place.
66
+ *
67
+ * Every `when` here is exhaustive over a sealed type or an enum with no `else`, which is what makes
68
+ * a case added to the SDK a compile error in this file rather than a value that quietly maps to
69
+ * something plausible. That is lock 1 of the four, and it is the lock the old wrapper did not have:
70
+ * `TerraReactModule.java` and `TerraReact.swift` each carried a `switch` with a fall-through, and a
71
+ * data type stopped travelling without anything going red.
72
+ *
73
+ * The SDK's models are deliberately not `@Serializable`. Keeping the wire shape out of the SDK's
74
+ * public types is the better boundary, and the price of it is this file.
75
+ */
76
+ internal object Conversions {
77
+
78
+ // ---- enums ------------------------------------------------------------------------------
79
+
80
+ fun source(wire: WireSource): Source = when (wire) {
81
+ WireSource.HEALTH -> Source.HEALTH
82
+ WireSource.SAMSUNG_HEALTH -> Source.SAMSUNG_HEALTH
83
+ }
84
+
85
+ fun source(source: Source): WireSource = when (source) {
86
+ Source.HEALTH -> WireSource.HEALTH
87
+ Source.SAMSUNG_HEALTH -> WireSource.SAMSUNG_HEALTH
88
+ }
89
+
90
+ /**
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.
97
+ */
98
+ fun category(wire: WireCategory): DataCategory = when (wire) {
99
+ WireCategory.ACTIVITY -> DataCategory.ACTIVITY
100
+ WireCategory.BODY -> DataCategory.BODY
101
+ WireCategory.DAILY -> DataCategory.DAILY
102
+ WireCategory.SLEEP -> DataCategory.SLEEP
103
+ WireCategory.NUTRITION -> DataCategory.NUTRITION
104
+ }
105
+
106
+ fun category(category: DataCategory): WireCategory? = when (category) {
107
+ DataCategory.ACTIVITY -> WireCategory.ACTIVITY
108
+ DataCategory.BODY -> WireCategory.BODY
109
+ DataCategory.DAILY -> WireCategory.DAILY
110
+ DataCategory.SLEEP -> WireCategory.SLEEP
111
+ DataCategory.NUTRITION -> WireCategory.NUTRITION
112
+ DataCategory.MENSTRUATION -> null
113
+ }
114
+
115
+ /**
116
+ * `UNAVAILABLE` is Apple's and has no Android counterpart, so nothing here can produce it.
117
+ *
118
+ * The surface types it as iOS-only for exactly that reason: a caller narrowing on it is writing
119
+ * iOS-specific code, and the type says so rather than letting them discover it.
120
+ */
121
+ fun permissionStatus(status: PermissionStatus): WirePermissionStatus = when (status) {
122
+ PermissionStatus.GRANTED -> WirePermissionStatus.GRANTED
123
+ PermissionStatus.DENIED -> WirePermissionStatus.DENIED
124
+ PermissionStatus.NOT_DETERMINED -> WirePermissionStatus.NOT_DETERMINED
125
+ PermissionStatus.NOT_DISCLOSED -> WirePermissionStatus.NOT_DISCLOSED
126
+ PermissionStatus.UNSUPPORTED -> WirePermissionStatus.UNSUPPORTED
127
+ }
128
+
129
+ fun restriction(reason: BackgroundRestrictionReason): WireRestriction = when (reason) {
130
+ BackgroundRestrictionReason.OEM_RESTRICTED -> WireRestriction.OEM_RESTRICTED
131
+ BackgroundRestrictionReason.BACKGROUND_READ_DENIED -> WireRestriction.BACKGROUND_READ_DENIED
132
+ BackgroundRestrictionReason.HEALTH_CONNECT_FEATURE_UNAVAILABLE ->
133
+ WireRestriction.HEALTH_CONNECT_FEATURE_UNAVAILABLE
134
+ BackgroundRestrictionReason.STORE_FEATURE_UNAVAILABLE -> WireRestriction.STORE_FEATURE_UNAVAILABLE
135
+ BackgroundRestrictionReason.TICKS_NOT_RUNNING -> WireRestriction.TICKS_NOT_RUNNING
136
+ }
137
+
138
+ // ---- unions -----------------------------------------------------------------------------
139
+
140
+ fun endUser(wire: WireEndUser): EndUser = when (wire) {
141
+ is WireEndUser.Reference -> EndUser.Reference(wire.id)
142
+ WireEndUser.Anonymous -> EndUser.Anonymous
143
+ }
144
+
145
+ fun timeRange(wire: WireTimeRange): TimeRange = when (wire) {
146
+ is WireTimeRange.LastHours -> TimeRange.LastHours(wire.hours)
147
+ is WireTimeRange.LastDays -> TimeRange.LastDays(wire.days)
148
+ WireTimeRange.Today -> TimeRange.Today
149
+ is WireTimeRange.Between -> TimeRange.Between(
150
+ start = BridgeInstant.parse(wire.start),
151
+ end = BridgeInstant.parse(wire.end),
152
+ )
153
+ }
154
+
155
+ fun sleepStage(stage: SleepStage): WireSleepStage = when (stage) {
156
+ SleepStage.InBed -> WireSleepStage.InBed
157
+ SleepStage.Awake -> WireSleepStage.Awake
158
+ SleepStage.Asleep -> WireSleepStage.Asleep
159
+ SleepStage.Light -> WireSleepStage.Light
160
+ SleepStage.Deep -> WireSleepStage.Deep
161
+ SleepStage.Rem -> WireSleepStage.Rem
162
+ is SleepStage.Unknown -> WireSleepStage.Unknown(stage.raw)
163
+ }
164
+
165
+ fun sleepStage(wire: WireSleepStage): SleepStage = when (wire) {
166
+ WireSleepStage.InBed -> SleepStage.InBed
167
+ WireSleepStage.Awake -> SleepStage.Awake
168
+ WireSleepStage.Asleep -> SleepStage.Asleep
169
+ WireSleepStage.Light -> SleepStage.Light
170
+ WireSleepStage.Deep -> SleepStage.Deep
171
+ WireSleepStage.Rem -> SleepStage.Rem
172
+ is WireSleepStage.Unknown -> SleepStage.Unknown(wire.raw)
173
+ }
174
+
175
+ fun activity(activity: WorkoutActivity): WireWorkoutActivity = when (activity) {
176
+ WorkoutActivity.Running -> WireWorkoutActivity.Running
177
+ WorkoutActivity.Walking -> WireWorkoutActivity.Walking
178
+ WorkoutActivity.Cycling -> WireWorkoutActivity.Cycling
179
+ WorkoutActivity.Swimming -> WireWorkoutActivity.Swimming
180
+ WorkoutActivity.StrengthTraining -> WireWorkoutActivity.StrengthTraining
181
+ is WorkoutActivity.Other -> WireWorkoutActivity.Other(activity.label)
182
+ }
183
+
184
+ fun activity(wire: WireWorkoutActivity): WorkoutActivity = when (wire) {
185
+ WireWorkoutActivity.Running -> WorkoutActivity.Running
186
+ WireWorkoutActivity.Walking -> WorkoutActivity.Walking
187
+ WireWorkoutActivity.Cycling -> WorkoutActivity.Cycling
188
+ WireWorkoutActivity.Swimming -> WorkoutActivity.Swimming
189
+ WireWorkoutActivity.StrengthTraining -> WorkoutActivity.StrengthTraining
190
+ is WireWorkoutActivity.Other -> WorkoutActivity.Other(wire.label)
191
+ }
192
+
193
+ /**
194
+ * Android has no `devIDMismatch` state - it raises `dev_id_mismatch` instead - so nothing here
195
+ * produces that variant. The surface types it as iOS-only.
196
+ */
197
+ fun diagnosticsState(state: DiagnosticsState): WireDiagnosticsState = when (state) {
198
+ DiagnosticsState.Healthy -> WireDiagnosticsState.Healthy
199
+ is DiagnosticsState.Quiet -> WireDiagnosticsState.Quiet(BridgeInstant.format(state.since))
200
+ is DiagnosticsState.BackgroundRestricted ->
201
+ WireDiagnosticsState.BackgroundRestricted(restriction(state.reason))
202
+ DiagnosticsState.BackgroundRefreshUnscheduled -> WireDiagnosticsState.BackgroundRefreshUnscheduled
203
+ DiagnosticsState.PendingRetry -> WireDiagnosticsState.PendingRetry
204
+ DiagnosticsState.LinkRevoked -> WireDiagnosticsState.LinkRevoked
205
+ }
206
+
207
+ // ---- records ----------------------------------------------------------------------------
208
+
209
+ fun sampleSource(source: SampleSource): WireSampleSource = WireSampleSource(
210
+ name = source.name,
211
+ bundleId = source.bundleId,
212
+ productType = source.productType,
213
+ deviceType = source.deviceType,
214
+ )
215
+
216
+ fun connection(stored: StoredConnection): WireConnection = WireConnection(
217
+ source = source(stored.source),
218
+ userId = stored.userId,
219
+ providerUserId = stored.providerUserId,
220
+ )
221
+
222
+ fun heartRate(sample: HeartRateSample): WireHeartRateSample = WireHeartRateSample(
223
+ id = sample.id.value,
224
+ time = BridgeInstant.format(sample.time),
225
+ beatsPerMinute = sample.beatsPerMinute,
226
+ source = sampleSource(sample.source),
227
+ motionContext = sample.motionContext?.let {
228
+ when (it) {
229
+ co.tryterra.sdk.MotionContext.UNKNOWN -> WireMotionContext.UNKNOWN
230
+ co.tryterra.sdk.MotionContext.SEDENTARY -> WireMotionContext.SEDENTARY
231
+ co.tryterra.sdk.MotionContext.ACTIVE -> WireMotionContext.ACTIVE
232
+ }
233
+ },
234
+ userEntered = sample.userEntered,
235
+ timeZone = sample.timeZone?.id,
236
+ )
237
+
238
+ fun steps(sample: StepCount): WireStepCount = WireStepCount(
239
+ id = sample.id?.value,
240
+ start = BridgeInstant.format(sample.interval.start),
241
+ end = BridgeInstant.format(sample.interval.end),
242
+ steps = sample.steps,
243
+ source = sampleSource(sample.source),
244
+ userEntered = sample.userEntered,
245
+ timeZone = sample.timeZone?.id,
246
+ )
247
+
248
+ fun segment(segment: SleepSegment): WireSleepSegment = WireSleepSegment(
249
+ id = segment.id.value,
250
+ start = BridgeInstant.format(segment.interval.start),
251
+ end = BridgeInstant.format(segment.interval.end),
252
+ stage = sleepStage(segment.stage),
253
+ source = sampleSource(segment.source),
254
+ userEntered = segment.userEntered,
255
+ timeZone = segment.timeZone?.id,
256
+ )
257
+
258
+ fun workout(workout: Workout): WireWorkout = WireWorkout(
259
+ id = workout.id.value,
260
+ start = BridgeInstant.format(workout.interval.start),
261
+ end = BridgeInstant.format(workout.interval.end),
262
+ activity = activity(workout.activity),
263
+ activityTypeCode = workout.activityTypeCode,
264
+ indoor = workout.indoor,
265
+ activeEnergyKilocalories = workout.activeEnergyKilocalories,
266
+ distanceMeters = workout.distanceMeters,
267
+ source = sampleSource(workout.source),
268
+ userEntered = workout.userEntered,
269
+ timeZone = workout.timeZone?.id,
270
+ )
271
+
272
+ /**
273
+ * A change carries a day, not an instant.
274
+ *
275
+ * Sending the instant would make the receiving side re-derive the day in its own zone, and get
276
+ * a different one either side of midnight.
277
+ */
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
+ }
282
+
283
+ /**
284
+ * An event is the object `DiagnosticsWire` already produces, carried unchanged.
285
+ *
286
+ * Not reshaped into a `{kind, payload}` of the wrapper's own: that would be a hand-written
287
+ * second contract here and another in the Swift half, sitting between the SDK and the generated
288
+ * codec where no vector can reach either of them. `EventPrivacyTest` guards this object against
289
+ * carrying a health value, and passing it through is what keeps that guarantee the only one.
290
+ */
291
+ fun event(event: TerraEvent): WireTerraEvent = DiagnosticsWire.event(event)
292
+
293
+ fun report(report: SupportReport): WireSupportReport = WireSupportReport(
294
+ sdkVersion = report.sdkVersion,
295
+ generatedAt = BridgeInstant.format(report.generatedAt),
296
+ state = diagnosticsState(report.state),
297
+ lastDelivery = report.lastDelivery?.let { BridgeInstant.format(it) },
298
+ isConnected = report.isConnected,
299
+ requestedCategories = report.requestedCategories,
300
+ requestedWrites = report.requestedWrites,
301
+ requestedWorkouts = report.requestedWorkouts,
302
+ recentEvents = report.recentEvents.map(::event),
303
+ )
304
+
305
+ /**
306
+ * Sorted and merged, because a `Set` has no order and JSON has no set.
307
+ *
308
+ * Two scopes for one activity are one grant with both flags, not two entries: `HealthAccess`
309
+ * can hold `Running+energy` and `Running+distance` separately, and sending both would let a
310
+ * caller read a grant that contradicts itself depending on which it looked at first. Sorting is
311
+ * what makes two grants differing only in iteration order compare equal on the wire, without
312
+ * which the vectors would be flaky.
313
+ */
314
+ fun access(access: HealthAccess): WireHealthAccess = WireHealthAccess(
315
+ read = access.read.mapNotNull(::category).sortedBy { it.name },
316
+ write = access.write.mapNotNull(::category).sortedBy { it.name },
317
+ workouts = access.workouts
318
+ .groupBy { WorkoutActivity.reportName(it.activity) }
319
+ .toSortedMap()
320
+ .map { (_, scopes) ->
321
+ WireWorkoutScope(
322
+ activity = activity(scopes.first().activity),
323
+ energy = scopes.any { it.energy },
324
+ distance = scopes.any { it.distance },
325
+ )
326
+ },
327
+ )
328
+
329
+ fun scope(scope: WorkoutScope): WireWorkoutScope = WireWorkoutScope(
330
+ activity = activity(scope.activity),
331
+ energy = scope.energy,
332
+ distance = scope.distance,
333
+ )
334
+
335
+ fun scope(wire: WireWorkoutScope): WorkoutScope = WorkoutScope(
336
+ activity = activity(wire.activity),
337
+ energy = wire.energy,
338
+ distance = wire.distance,
339
+ )
340
+
341
+ // ---- write entries ----------------------------------------------------------------------
342
+
343
+ /**
344
+ * A caller-supplied record id is honoured; an absent one is minted by the SDK's own default.
345
+ *
346
+ * Minting one here would use a different scheme from `RecordHandles.mint`, and `delete` only
347
+ * accepts handles that scheme produced.
348
+ */
349
+ fun entry(wire: WireHeartRateEntry): HeartRateEntry =
350
+ wire.recordId?.let { HeartRateEntry(BridgeInstant.parse(wire.time), wire.beatsPerMinute, RecordId(it)) }
351
+ ?: HeartRateEntry(BridgeInstant.parse(wire.time), wire.beatsPerMinute)
352
+
353
+ fun entry(wire: WireStepEntry): StepEntry {
354
+ val interval = SampleInterval(BridgeInstant.parse(wire.start), BridgeInstant.parse(wire.end))
355
+ return wire.recordId?.let { StepEntry(interval, wire.steps, RecordId(it)) }
356
+ ?: StepEntry(interval, wire.steps)
357
+ }
358
+
359
+ fun entry(wire: WireSleepEntry): SleepEntry {
360
+ val interval = SampleInterval(BridgeInstant.parse(wire.start), BridgeInstant.parse(wire.end))
361
+ val stage = sleepStage(wire.stage)
362
+ return wire.recordId?.let { SleepEntry(interval, stage, RecordId(it)) } ?: SleepEntry(interval, stage)
363
+ }
364
+
365
+ fun entry(wire: WireWorkoutEntry): WorkoutEntry {
366
+ val interval = SampleInterval(BridgeInstant.parse(wire.start), BridgeInstant.parse(wire.end))
367
+ val activity = activity(wire.activity)
368
+ return wire.recordId?.let {
369
+ WorkoutEntry(interval, activity, wire.totalEnergyKilocalories, wire.totalDistanceMeters, RecordId(it))
370
+ } ?: WorkoutEntry(interval, activity, wire.totalEnergyKilocalories, wire.totalDistanceMeters)
371
+ }
372
+
373
+ /**
374
+ * Ten optional measurements and a time, one for one with `BodyEntry`.
375
+ *
376
+ * Spelled out rather than reflected: a field added to `BodyEntry` should be a decision here,
377
+ * and reflection would carry it silently with whatever name the SDK happened to give it.
378
+ */
379
+ fun entry(wire: WireBodyEntry): BodyEntry {
380
+ val time = BridgeInstant.parse(wire.time)
381
+ return wire.recordId?.let {
382
+ BodyEntry(
383
+ time = time,
384
+ bodyMassKilograms = wire.bodyMassKilograms,
385
+ bodyFatPercent = wire.bodyFatPercent,
386
+ bodyMassIndex = wire.bodyMassIndex,
387
+ heightMeters = wire.heightMeters,
388
+ leanBodyMassKilograms = wire.leanBodyMassKilograms,
389
+ bodyTemperatureCelsius = wire.bodyTemperatureCelsius,
390
+ oxygenSaturationPercent = wire.oxygenSaturationPercent,
391
+ bloodGlucoseMilligramsPerDeciliter = wire.bloodGlucoseMilligramsPerDeciliter,
392
+ bloodPressureSystolicMillimetersOfMercury = wire.bloodPressureSystolicMillimetersOfMercury,
393
+ bloodPressureDiastolicMillimetersOfMercury = wire.bloodPressureDiastolicMillimetersOfMercury,
394
+ recordId = RecordId(it),
395
+ )
396
+ } ?: BodyEntry(
397
+ time = time,
398
+ bodyMassKilograms = wire.bodyMassKilograms,
399
+ bodyFatPercent = wire.bodyFatPercent,
400
+ bodyMassIndex = wire.bodyMassIndex,
401
+ heightMeters = wire.heightMeters,
402
+ leanBodyMassKilograms = wire.leanBodyMassKilograms,
403
+ bodyTemperatureCelsius = wire.bodyTemperatureCelsius,
404
+ oxygenSaturationPercent = wire.oxygenSaturationPercent,
405
+ bloodGlucoseMilligramsPerDeciliter = wire.bloodGlucoseMilligramsPerDeciliter,
406
+ bloodPressureSystolicMillimetersOfMercury = wire.bloodPressureSystolicMillimetersOfMercury,
407
+ bloodPressureDiastolicMillimetersOfMercury = wire.bloodPressureDiastolicMillimetersOfMercury,
408
+ )
409
+ }
410
+
411
+ fun entry(wire: WireNutritionEntry): NutritionEntry {
412
+ val interval = SampleInterval(BridgeInstant.parse(wire.start), BridgeInstant.parse(wire.end))
413
+ val nutrients = nutrients(wire.nutrients)
414
+ return wire.recordId?.let { NutritionEntry(interval, wire.name, nutrients, RecordId(it)) }
415
+ ?: NutritionEntry(interval, wire.name, nutrients)
416
+ }
417
+
418
+ /**
419
+ * All thirty-nine, by name.
420
+ *
421
+ * The unit is in every name on both sides, so a mismatched pair is a compile error rather than
422
+ * a value that arrives in the wrong unit - which is the class of bug that would be invisible in
423
+ * every test that did not know what the number should be.
424
+ */
425
+ private fun nutrients(wire: WireNutrients): NutritionEntry.Nutrients = NutritionEntry.Nutrients(
426
+ energyKilocalories = wire.energyKilocalories,
427
+ proteinGrams = wire.proteinGrams,
428
+ carbohydratesGrams = wire.carbohydratesGrams,
429
+ fiberGrams = wire.fiberGrams,
430
+ sugarGrams = wire.sugarGrams,
431
+ fatTotalGrams = wire.fatTotalGrams,
432
+ fatSaturatedGrams = wire.fatSaturatedGrams,
433
+ fatMonounsaturatedGrams = wire.fatMonounsaturatedGrams,
434
+ fatPolyunsaturatedGrams = wire.fatPolyunsaturatedGrams,
435
+ cholesterolMilligrams = wire.cholesterolMilligrams,
436
+ sodiumMilligrams = wire.sodiumMilligrams,
437
+ potassiumMilligrams = wire.potassiumMilligrams,
438
+ calciumMilligrams = wire.calciumMilligrams,
439
+ ironMilligrams = wire.ironMilligrams,
440
+ magnesiumMilligrams = wire.magnesiumMilligrams,
441
+ zincMilligrams = wire.zincMilligrams,
442
+ waterLiters = wire.waterLiters,
443
+ caffeineMilligrams = wire.caffeineMilligrams,
444
+ vitaminAMicrograms = wire.vitaminAMicrograms,
445
+ vitaminCMilligrams = wire.vitaminCMilligrams,
446
+ vitaminDMicrograms = wire.vitaminDMicrograms,
447
+ vitaminEMilligrams = wire.vitaminEMilligrams,
448
+ vitaminKMicrograms = wire.vitaminKMicrograms,
449
+ vitaminB6Milligrams = wire.vitaminB6Milligrams,
450
+ vitaminB12Micrograms = wire.vitaminB12Micrograms,
451
+ folateMicrograms = wire.folateMicrograms,
452
+ thiaminMilligrams = wire.thiaminMilligrams,
453
+ riboflavinMilligrams = wire.riboflavinMilligrams,
454
+ niacinMilligrams = wire.niacinMilligrams,
455
+ biotinMicrograms = wire.biotinMicrograms,
456
+ pantothenicAcidMilligrams = wire.pantothenicAcidMilligrams,
457
+ phosphorusMilligrams = wire.phosphorusMilligrams,
458
+ iodineMicrograms = wire.iodineMicrograms,
459
+ chromiumMicrograms = wire.chromiumMicrograms,
460
+ copperMilligrams = wire.copperMilligrams,
461
+ manganeseMilligrams = wire.manganeseMilligrams,
462
+ molybdenumMicrograms = wire.molybdenumMicrograms,
463
+ seleniumMicrograms = wire.seleniumMicrograms,
464
+ chlorideMilligrams = wire.chlorideMilligrams,
465
+ )
466
+
467
+
468
+ }
@@ -0,0 +1,41 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.Source
4
+
5
+ /**
6
+ * Which activity's launcher presents a permission sheet: the most recently started one that is
7
+ * still started.
8
+ *
9
+ * Creation order is not that. An app with two tasks, or one that returns to an older activity,
10
+ * keeps a newer activity alive but stopped, and a sheet launched through it would not be seen.
11
+ */
12
+ internal class LauncherSlots<A : Any, L : Any> {
13
+ private val made = HashMap<A, Map<Source, L>>()
14
+ private val started = LinkedHashSet<A>()
15
+
16
+ @Synchronized
17
+ fun created(activity: A, launchers: Map<Source, L>) {
18
+ made[activity] = launchers
19
+ }
20
+
21
+ @Synchronized
22
+ fun started(activity: A) {
23
+ started.remove(activity)
24
+ started.add(activity)
25
+ }
26
+
27
+ @Synchronized
28
+ fun stopped(activity: A) {
29
+ started.remove(activity)
30
+ }
31
+
32
+ /** Forgets [activity], answering the launchers it held so the caller can unregister them. */
33
+ @Synchronized
34
+ fun destroyed(activity: A): Map<Source, L>? {
35
+ started.remove(activity)
36
+ return made.remove(activity)
37
+ }
38
+
39
+ @Synchronized
40
+ fun current(source: Source): L? = started.toList().asReversed().firstNotNullOfOrNull { made[it]?.get(source) }
41
+ }
@@ -0,0 +1,150 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.NetworkFailure
4
+ import co.tryterra.sdk.TerraError
5
+ import co.tryterra.sdk.bridge.generated.BridgeError
6
+ import co.tryterra.sdk.bridge.generated.Source
7
+ import co.tryterra.sdk.bridge.generated.TerraErrorPayload
8
+ import kotlinx.serialization.json.JsonObject
9
+ import kotlinx.serialization.json.JsonPrimitive
10
+
11
+ /**
12
+ * How a thrown error becomes something a caller in another language can switch on.
13
+ *
14
+ * The code comes from the SDK - [TerraError.code] is the vocabulary in `docs/sdk-reference.md`, and
15
+ * the surface asserts this module speaks exactly it - so there is no mapping table here to drift.
16
+ * What this file owns is the *detail bag*: which pieces of an error's associated values are safe to
17
+ * send, and which are not.
18
+ *
19
+ * Nothing here may put a health value in `details`. An error reaches a customer's crash reporter and
20
+ * their logs, so the bag carries identifiers, counts and reasons, never a sample.
21
+ */
22
+ internal object Rejections {
23
+
24
+ /** The payload for any throwable, including one that is not a [TerraError]. */
25
+ fun of(thrown: Throwable): TerraErrorPayload = when (thrown) {
26
+ is TerraError -> ofTerraError(thrown)
27
+ is BridgeError -> TerraErrorPayload(
28
+ code = thrown.code,
29
+ retryable = thrown.isRetryable,
30
+ message = thrown.message ?: thrown.code,
31
+ details = bridgeDetails(thrown),
32
+ )
33
+ else -> TerraErrorPayload(
34
+ code = "platform",
35
+ retryable = false,
36
+ message = "a platform call failed (${thrown::class.java.name})",
37
+ )
38
+ }
39
+
40
+ private fun ofTerraError(error: TerraError): TerraErrorPayload = TerraErrorPayload(
41
+ code = error.code,
42
+ retryable = error.isRetryable,
43
+ message = message(error),
44
+ source = sourceOf(error),
45
+ details = details(error),
46
+ )
47
+
48
+ /**
49
+ * The text that crosses, which is not always the exception's own.
50
+ *
51
+ * Two cases wrap a throwable this SDK did not raise, and `Exception.message` embeds it: a
52
+ * platform failure's comes from whatever library was reading health records, and a token
53
+ * provider's is as likely to carry the token it was minting as to describe the failure.
54
+ * Dropping the details bag for those was not enough while the message still carried the same
55
+ * text, which is what the test that found this asserts.
56
+ */
57
+ private fun message(error: TerraError): String = when (error) {
58
+ is TerraError.TokenProviderFailed -> "the host's token provider failed"
59
+ is TerraError.Platform -> "a platform call failed"
60
+ else -> error.message ?: error.code
61
+ }
62
+
63
+ /** Only the cases that carry one: a default here would be a claim about where a failure happened. */
64
+ private fun sourceOf(error: TerraError): Source? = when (error) {
65
+ is TerraError.NotConnected -> wire(error.source)
66
+ is TerraError.LinkRevoked -> wire(error.source)
67
+ is TerraError.DevIdMismatch -> wire(error.source)
68
+ is TerraError.LinkAlreadyRunning -> wire(error.source)
69
+ is TerraError.ReadQuotaExceeded -> wire(error.source)
70
+ else -> null
71
+ }
72
+
73
+ private fun wire(source: co.tryterra.sdk.Source): Source = when (source) {
74
+ co.tryterra.sdk.Source.HEALTH -> Source.HEALTH
75
+ co.tryterra.sdk.Source.SAMSUNG_HEALTH -> Source.SAMSUNG_HEALTH
76
+ }
77
+
78
+ /**
79
+ * Exhaustive over the sealed hierarchy with no `else`, which is the point.
80
+ *
81
+ * A case added to [TerraError] stops this module compiling until someone decides what, if
82
+ * anything, of it is safe to send. An `else` here would answer that question with silence.
83
+ */
84
+ private fun details(error: TerraError): JsonObject? = when (error) {
85
+ is TerraError.HealthDataUnavailable -> obj("availability" to JsonPrimitive(error.reason::class.java.simpleName))
86
+ is TerraError.PermissionDenied -> obj("category" to JsonPrimitive(error.category.name.lowercase()))
87
+ is TerraError.RouteAwaitingConverter -> obj("dataType" to JsonPrimitive(error.dataType.wire))
88
+ is TerraError.SyncAlreadyRunning -> obj("dataType" to JsonPrimitive(error.dataType.wire))
89
+ is TerraError.SampleNotFound -> obj("id" to JsonPrimitive(error.id.value))
90
+ is TerraError.DeviceKeyUnavailable -> obj("reason" to JsonPrimitive(error.reason))
91
+ // The cause travels as a code: a nested throwable carries a stack and a message.
92
+ is TerraError.RollbackIncomplete -> obj(
93
+ "remaining" to JsonPrimitive(error.remaining.size),
94
+ "cause" to JsonPrimitive(error.underlying.code),
95
+ )
96
+ is TerraError.DistanceNotAttributable -> obj(
97
+ "activity" to JsonPrimitive(co.tryterra.sdk.WorkoutActivity.reportName(error.activity)),
98
+ )
99
+ // No message. The host's own provider threw, and the text it chose is as likely to contain
100
+ // the token it was minting as it is to describe the failure - `IllegalStateException(
101
+ // "token=...")` is not a hypothetical. The host already knows why its provider failed; what
102
+ // it needs from here is the code.
103
+ is TerraError.TokenProviderFailed -> null
104
+ // No message, for the same reason with a different source. A platform failure's text comes
105
+ // from whatever library raised it, which on this path has been reading health records.
106
+ is TerraError.Platform -> null
107
+ is TerraError.Network -> networkDetails(error.failure)
108
+
109
+ is TerraError.NotConnected,
110
+ is TerraError.LinkRevoked,
111
+ is TerraError.DevIdMismatch,
112
+ is TerraError.LinkAlreadyRunning,
113
+ is TerraError.ReadQuotaExceeded,
114
+ -> null
115
+
116
+ TerraError.TokenRejected,
117
+ TerraError.TokenUnavailable,
118
+ TerraError.SignatureRejected,
119
+ TerraError.ClockSkewed,
120
+ TerraError.UserLimitReached,
121
+ TerraError.LinkConflict,
122
+ TerraError.UpgradeRequired,
123
+ TerraError.StoreLocked,
124
+ TerraError.StoreUnreadable,
125
+ TerraError.DeadlineExceeded,
126
+ TerraError.AuthorizationNotDetermined,
127
+ TerraError.BloodPressureIncomplete,
128
+ TerraError.AuthorizationUnavailableInExtension,
129
+ -> null
130
+ }
131
+
132
+ private fun networkDetails(failure: NetworkFailure): JsonObject? = when (failure) {
133
+ is NetworkFailure.ServerError -> obj("status" to JsonPrimitive(failure.status))
134
+ is NetworkFailure.Throttled -> failure.retryAfterSeconds?.let { obj("retryAfterSeconds" to JsonPrimitive(it)) }
135
+ // The IOException's message names a host and sometimes a path; the code alone is the signal.
136
+ is NetworkFailure.Transport -> null
137
+ NetworkFailure.MalformedResponse -> null
138
+ }
139
+
140
+ private fun bridgeDetails(error: BridgeError): JsonObject? = when (error) {
141
+ is BridgeError.NotBootstrapped -> obj("line" to JsonPrimitive(error.line))
142
+ is BridgeError.UnsupportedOnPlatform -> obj("op" to JsonPrimitive(error.op))
143
+ is BridgeError.TokenTimeout -> obj("timeoutMs" to JsonPrimitive(error.timeoutMs))
144
+ is BridgeError.InvalidPayload -> obj("path" to JsonPrimitive(error.path))
145
+ BridgeError.NoActivity -> null
146
+ BridgeError.PermissionSheetUnavailable -> null
147
+ }
148
+
149
+ private fun obj(vararg pairs: Pair<String, JsonPrimitive>): JsonObject = JsonObject(pairs.toMap())
150
+ }