@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.
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/TerraReactNative.podspec +48 -0
- package/android/build.gradle +87 -0
- package/android/src/bridgeCore/kotlin/README.md +1 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +468 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +258 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +52 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +493 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +897 -0
- package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
- package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
- package/docs/availability.md +246 -0
- package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6473 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +81513 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2150 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2127 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6474 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6392 -0
- package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
- package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
- package/ios/TerraBridgeCore/Conversions.swift +467 -0
- package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
- package/ios/TerraBridgeCore/Generated/BridgeOps.swift +795 -0
- package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +1317 -0
- package/ios/TerraBridgeCore/Locked.swift +15 -0
- package/ios/TerraBridgeCore/README.md +1 -0
- package/ios/TerraBridgeCore/Rejections.swift +130 -0
- package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
- package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
- package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
- package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +254 -0
- package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
- package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
- package/ios/TerraBridgeModule.mm +79 -0
- package/ios/TerraBridgeModuleImpl.swift +49 -0
- package/ios/TerraReactNative.swift +19 -0
- package/lib/EndUser.d.ts +12 -0
- package/lib/EndUser.js +13 -0
- package/lib/NativeTerraBridge.d.ts +23 -0
- package/lib/NativeTerraBridge.js +13 -0
- package/lib/Terra.d.ts +154 -0
- package/lib/Terra.js +280 -0
- package/lib/TerraError.d.ts +42 -0
- package/lib/TerraError.js +95 -0
- package/lib/createTerra.d.ts +8 -0
- package/lib/createTerra.js +19 -0
- package/lib/generated/client.d.ts +92 -0
- package/lib/generated/client.js +196 -0
- package/lib/generated/codecs.d.ts +135 -0
- package/lib/generated/codecs.js +1358 -0
- package/lib/generated/errors.d.ts +13 -0
- package/lib/generated/errors.js +68 -0
- package/lib/generated/ops.d.ts +13 -0
- package/lib/generated/ops.js +137 -0
- package/lib/generated/streams.d.ts +23 -0
- package/lib/generated/streams.js +58 -0
- package/lib/generated/types.d.ts +481 -0
- package/lib/generated/types.js +92 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +55 -0
- package/package.json +71 -4
- package/src/EndUser.ts +15 -0
- package/src/NativeTerraBridge.ts +27 -0
- package/src/Terra.ts +462 -0
- package/src/TerraError.ts +116 -0
- package/src/createTerra.ts +18 -0
- package/src/generated/client.ts +243 -0
- package/src/generated/codecs.ts +1287 -0
- package/src/generated/errors.ts +113 -0
- package/src/generated/ops.ts +197 -0
- package/src/generated/streams.ts +74 -0
- package/src/generated/types.ts +667 -0
- 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
|
+
}
|