@cariva-dev/exercise-sdk 2.0.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 (82) hide show
  1. package/CarivaExerciseSdk.podspec +20 -0
  2. package/README.md +374 -0
  3. package/android/build.gradle +66 -0
  4. package/android/src/main/AndroidManifest.xml +40 -0
  5. package/android/src/main/java/com/carivaexercisesdk/CarivaExerciseSdkPackage.kt +30 -0
  6. package/android/src/main/java/com/carivaexercisesdk/HealthConnectModule.kt +222 -0
  7. package/android/src/main/java/com/carivaexercisesdk/HealthConnectPermissionUsageActivity.kt +11 -0
  8. package/android/src/main/java/com/carivaexercisesdk/HealthConnectPermissionsRationaleActivity.kt +11 -0
  9. package/android/src/main/java/com/carivaexercisesdk/Pagination.kt +23 -0
  10. package/android/src/main/java/com/carivaexercisesdk/application/connection/command/ConnectCommand.kt +12 -0
  11. package/android/src/main/java/com/carivaexercisesdk/application/connection/dto/ConnectResultDto.kt +13 -0
  12. package/android/src/main/java/com/carivaexercisesdk/application/connection/handler/ConnectHandler.kt +79 -0
  13. package/android/src/main/java/com/carivaexercisesdk/application/connection/port/HealthConnectConnectionPort.kt +16 -0
  14. package/android/src/main/java/com/carivaexercisesdk/application/connection/port/PermissionRequestPort.kt +5 -0
  15. package/android/src/main/java/com/carivaexercisesdk/application/datasource/command/SetDatasourcePolicyCommand.kt +6 -0
  16. package/android/src/main/java/com/carivaexercisesdk/application/datasource/handler/GetDatasourcePolicyHandler.kt +10 -0
  17. package/android/src/main/java/com/carivaexercisesdk/application/datasource/handler/SetDatasourcePolicyHandler.kt +22 -0
  18. package/android/src/main/java/com/carivaexercisesdk/application/datasource/port/DatasourcePolicyRepository.kt +9 -0
  19. package/android/src/main/java/com/carivaexercisesdk/application/exercise/dto/BasalReadResultDto.kt +11 -0
  20. package/android/src/main/java/com/carivaexercisesdk/application/exercise/dto/ExerciseDataDto.kt +41 -0
  21. package/android/src/main/java/com/carivaexercisesdk/application/exercise/handler/GetExerciseDataHandler.kt +346 -0
  22. package/android/src/main/java/com/carivaexercisesdk/application/exercise/port/ExerciseRecordPort.kt +45 -0
  23. package/android/src/main/java/com/carivaexercisesdk/application/exercise/query/GetExerciseDataQuery.kt +9 -0
  24. package/android/src/main/java/com/carivaexercisesdk/domain/connection/entity/ConnectionAggregate.kt +13 -0
  25. package/android/src/main/java/com/carivaexercisesdk/domain/connection/service/ConnectionDecisionService.kt +37 -0
  26. package/android/src/main/java/com/carivaexercisesdk/domain/connection/valueobject/ConnectionNextAction.kt +8 -0
  27. package/android/src/main/java/com/carivaexercisesdk/domain/connection/valueobject/HealthConnectStatus.kt +8 -0
  28. package/android/src/main/java/com/carivaexercisesdk/domain/connection/valueobject/PermissionScope.kt +36 -0
  29. package/android/src/main/java/com/carivaexercisesdk/domain/datasource/entity/DatasourcePolicy.kt +8 -0
  30. package/android/src/main/java/com/carivaexercisesdk/domain/datasource/valueobject/DatasourceType.kt +14 -0
  31. package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/ExerciseDomainService.kt +89 -0
  32. package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectCaloriesFallbackMerger.kt +351 -0
  33. package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectInternalsConstants.kt +55 -0
  34. package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectModuleInternals.kt +316 -0
  35. package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectOverlapNormalizer.kt +400 -0
  36. package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectSourceTrust.kt +249 -0
  37. package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectUnifiedRecordBuilder.kt +316 -0
  38. package/android/src/main/java/com/carivaexercisesdk/infrastructure/healthconnect/HealthConnectConnectionAdapter.kt +156 -0
  39. package/android/src/main/java/com/carivaexercisesdk/infrastructure/healthconnect/HealthConnectExerciseRecordAdapter.kt +464 -0
  40. package/android/src/main/java/com/carivaexercisesdk/infrastructure/persistence/InMemoryDatasourcePolicyRepository.kt +21 -0
  41. package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/ConnectResultMapper.kt +23 -0
  42. package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/DatasourcePolicyJsonParser.kt +51 -0
  43. package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/DatasourcePolicyMapper.kt +46 -0
  44. package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/ExerciseDataMapper.kt +99 -0
  45. package/android/src/test/java/com/carivaexercisesdk/ArchitectureDependencyRuleTest.kt +60 -0
  46. package/android/src/test/java/com/carivaexercisesdk/HealthConnectModuleDatasourceParserTest.kt +69 -0
  47. package/android/src/test/java/com/carivaexercisesdk/HealthConnectModuleInternalsTest.kt +406 -0
  48. package/android/src/test/java/com/carivaexercisesdk/application/connection/handler/ConnectHandlerTest.kt +153 -0
  49. package/android/src/test/java/com/carivaexercisesdk/application/datasource/handler/DatasourcePolicyRoundTripTest.kt +63 -0
  50. package/android/src/test/java/com/carivaexercisesdk/application/datasource/handler/SetDatasourcePolicyHandlerTest.kt +42 -0
  51. package/android/src/test/java/com/carivaexercisesdk/domain/connection/service/ConnectionDecisionServiceTest.kt +68 -0
  52. package/android/src/test/java/com/carivaexercisesdk/infrastructure/reactnative/DatasourcePolicyMapperTest.kt +22 -0
  53. package/ios/CarivaExerciseSdk.h +5 -0
  54. package/ios/CarivaExerciseSdk.mm +7 -0
  55. package/lib/module/connect/index.js +7 -0
  56. package/lib/module/connect/index.js.map +1 -0
  57. package/lib/module/datasource/index.js +10 -0
  58. package/lib/module/datasource/index.js.map +1 -0
  59. package/lib/module/exercise/index.js +7 -0
  60. package/lib/module/exercise/index.js.map +1 -0
  61. package/lib/module/index.js +6 -0
  62. package/lib/module/index.js.map +1 -0
  63. package/lib/module/native/module.js +13 -0
  64. package/lib/module/native/module.js.map +1 -0
  65. package/lib/module/package.json +1 -0
  66. package/lib/typescript/package.json +1 -0
  67. package/lib/typescript/src/connect/index.d.ts +16 -0
  68. package/lib/typescript/src/connect/index.d.ts.map +1 -0
  69. package/lib/typescript/src/datasource/index.d.ts +12 -0
  70. package/lib/typescript/src/datasource/index.d.ts.map +1 -0
  71. package/lib/typescript/src/exercise/index.d.ts +64 -0
  72. package/lib/typescript/src/exercise/index.d.ts.map +1 -0
  73. package/lib/typescript/src/index.d.ts +4 -0
  74. package/lib/typescript/src/index.d.ts.map +1 -0
  75. package/lib/typescript/src/native/module.d.ts +14 -0
  76. package/lib/typescript/src/native/module.d.ts.map +1 -0
  77. package/package.json +127 -0
  78. package/src/connect/index.ts +34 -0
  79. package/src/datasource/index.ts +20 -0
  80. package/src/exercise/index.ts +75 -0
  81. package/src/index.tsx +22 -0
  82. package/src/native/module.ts +23 -0
@@ -0,0 +1,316 @@
1
+ package com.carivaexercisesdk.domain.exercise.services
2
+
3
+ import java.time.Duration
4
+ import java.time.Instant
5
+ import java.time.ZoneId
6
+ import java.time.format.DateTimeFormatter
7
+ import kotlin.math.pow
8
+ import kotlin.math.round
9
+
10
+ internal object HealthConnectModuleInternals {
11
+ enum class MetricType {
12
+ STEPS,
13
+ ACTIVE_TIME,
14
+ CALORIES,
15
+ DISTANCE
16
+ }
17
+
18
+ data class IntervalInput(
19
+ val startMs: Long,
20
+ val endMs: Long,
21
+ val totalValue: Double,
22
+ val sourceType: String,
23
+ val packageName: String,
24
+ val deviceModel: String = ""
25
+ )
26
+
27
+ data class NormalizedSegment(
28
+ val startMs: Long,
29
+ val endMs: Long,
30
+ val value: Double,
31
+ val sourceType: String,
32
+ val packageName: String,
33
+ val deviceModel: String = "",
34
+ val normalized: Boolean = true
35
+ )
36
+
37
+ data class NormalizationStats(
38
+ val droppedNonPositiveCount: Int = 0,
39
+ val unknownSourceCount: Int = 0,
40
+ val observedIntervals: Int = 0
41
+ ) {
42
+ operator fun plus(other: NormalizationStats): NormalizationStats {
43
+ return NormalizationStats(
44
+ droppedNonPositiveCount = droppedNonPositiveCount + other.droppedNonPositiveCount,
45
+ unknownSourceCount = unknownSourceCount + other.unknownSourceCount,
46
+ observedIntervals = observedIntervals + other.observedIntervals
47
+ )
48
+ }
49
+ }
50
+
51
+ data class NormalizationResult(
52
+ val segments: List<NormalizedSegment>,
53
+ val stats: NormalizationStats,
54
+ val overlapStrategy: String,
55
+ val warnings: List<String> = emptyList(),
56
+ val diagnostics: Diagnostics? = null
57
+ )
58
+
59
+ data class Diagnostics(
60
+ val fallbackUsedSegments: Int = 0,
61
+ val fallbackSuppressedSegments: Int = 0,
62
+ val idleGateSuppressedSegments: Int = 0,
63
+ val basalSeedApplied: Boolean = false,
64
+ val seededFromBeforeSince: Boolean = false,
65
+ val basalSeedAgeHours: Double? = null,
66
+ val directActiveCaloriesKcal: Double = 0.0,
67
+ val totalCaloriesKcal: Double = 0.0,
68
+ val basalCaloriesKcal: Double = 0.0,
69
+ val mergedActiveCaloriesKcal: Double = 0.0,
70
+ val fallbackContributionKcal: Double = 0.0
71
+ )
72
+
73
+ data class UnifiedIntervalRecord(
74
+ val steps: Double,
75
+ val activeCalories: Double,
76
+ val activeCaloriesUnit: String,
77
+ val distance: Double,
78
+ val distanceUnit: String,
79
+ val activeTime: Double,
80
+ val activeTimeUnit: String,
81
+ val startDate: String,
82
+ val endDate: String,
83
+ val meta: UnifiedRecordMeta
84
+ )
85
+
86
+ data class UnifiedRecordMeta(
87
+ val source: String,
88
+ val device: String,
89
+ val deviceModel: String,
90
+ val trustLevel: String
91
+ )
92
+
93
+ data class UnifiedRecordsBuildResult(
94
+ val records: List<UnifiedIntervalRecord>,
95
+ val warnings: List<String>,
96
+ val projectionPreserved: Boolean,
97
+ val roundingScale: Int
98
+ )
99
+
100
+ data class RecordTrustResult(
101
+ val allowed: Boolean,
102
+ val reason: String,
103
+ val sourceType: String,
104
+ val trustLevel: String
105
+ )
106
+
107
+ fun isSupportedBucketPeriod(bucketPeriod: String): Boolean {
108
+ val normalized = bucketPeriod.trim().lowercase()
109
+ return normalized == HealthConnectInternalsConstants.SUPPORTED_HOURLY ||
110
+ normalized == HealthConnectInternalsConstants.SUPPORTED_DAILY
111
+ }
112
+
113
+ fun normalizeBucketPeriod(bucketPeriod: String): String {
114
+ return bucketPeriod.trim().lowercase()
115
+ }
116
+
117
+ fun normalizePermissionScope(scopeValue: String): String? {
118
+ return when (scopeValue.trim().lowercase()) {
119
+ HealthConnectInternalsConstants.SCOPE_STEPS -> HealthConnectInternalsConstants.SCOPE_STEPS
120
+ HealthConnectInternalsConstants.SCOPE_ACTIVE_TIMES ->
121
+ HealthConnectInternalsConstants.SCOPE_ACTIVE_TIMES
122
+ HealthConnectInternalsConstants.SCOPE_CALORIES ->
123
+ HealthConnectInternalsConstants.SCOPE_CALORIES
124
+ HealthConnectInternalsConstants.SCOPE_DISTANCES ->
125
+ HealthConnectInternalsConstants.SCOPE_DISTANCES
126
+ else -> null
127
+ }
128
+ }
129
+
130
+ fun normalizePermissionScopes(scopeValues: List<String>?): Set<String> {
131
+ if (scopeValues.isNullOrEmpty()) {
132
+ return emptySet()
133
+ }
134
+
135
+ return scopeValues.mapNotNull { normalizePermissionScope(it) }.toSet()
136
+ }
137
+
138
+ fun findUnsupportedPermissionScopes(scopeValues: List<String>?): Set<String> {
139
+ if (scopeValues.isNullOrEmpty()) {
140
+ return emptySet()
141
+ }
142
+
143
+ return scopeValues
144
+ .map { it.trim() }
145
+ .filter { normalizePermissionScope(it) == null }
146
+ .toSet()
147
+ }
148
+
149
+ fun defaultPermissionScopes(): Set<String> {
150
+ return setOf(
151
+ HealthConnectInternalsConstants.SCOPE_STEPS,
152
+ HealthConnectInternalsConstants.SCOPE_ACTIVE_TIMES,
153
+ HealthConnectInternalsConstants.SCOPE_CALORIES,
154
+ HealthConnectInternalsConstants.SCOPE_DISTANCES
155
+ )
156
+ }
157
+
158
+ fun toKilometers(distanceMeters: Double): Double {
159
+ return distanceMeters / 1000.0
160
+ }
161
+
162
+ fun roundToScale(value: Double, scale: Int = 5): Double {
163
+ val factor = 10.0.pow(scale.toDouble())
164
+ return round(value * factor) / factor
165
+ }
166
+
167
+ fun toIsoOffsetString(epochMillis: Long, zoneId: ZoneId): String {
168
+ return Instant.ofEpochMilli(epochMillis)
169
+ .atZone(zoneId)
170
+ .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
171
+ }
172
+
173
+ fun buildUnifiedIntervalRecords(
174
+ stepsSegments: List<NormalizedSegment>,
175
+ activeTimeSegments: List<NormalizedSegment>,
176
+ caloriesSegments: List<NormalizedSegment>,
177
+ distanceSegments: List<NormalizedSegment>,
178
+ zoneId: ZoneId,
179
+ roundingScale: Int = 5
180
+ ): List<UnifiedIntervalRecord> {
181
+ return HealthConnectUnifiedRecordBuilder.buildUnifiedIntervalRecords(
182
+ stepsSegments = stepsSegments,
183
+ activeTimeSegments = activeTimeSegments,
184
+ caloriesSegments = caloriesSegments,
185
+ distanceSegments = distanceSegments,
186
+ zoneId = zoneId,
187
+ roundingScale = roundingScale
188
+ )
189
+ }
190
+
191
+ fun buildUnifiedRecordsResult(
192
+ stepsSegments: List<NormalizedSegment>,
193
+ activeTimeSegments: List<NormalizedSegment>,
194
+ caloriesSegments: List<NormalizedSegment>,
195
+ distanceSegments: List<NormalizedSegment>,
196
+ zoneId: ZoneId,
197
+ roundingScale: Int = 5
198
+ ): UnifiedRecordsBuildResult {
199
+ return HealthConnectUnifiedRecordBuilder.buildUnifiedRecordsResult(
200
+ stepsSegments = stepsSegments,
201
+ activeTimeSegments = activeTimeSegments,
202
+ caloriesSegments = caloriesSegments,
203
+ distanceSegments = distanceSegments,
204
+ zoneId = zoneId,
205
+ roundingScale = roundingScale
206
+ )
207
+ }
208
+
209
+ fun isValidPeriod(startInstant: Instant, endInstant: Instant, maxDays: Long = 90): Boolean {
210
+ if (startInstant.isAfter(endInstant)) {
211
+ return false
212
+ }
213
+
214
+ val duration = Duration.between(startInstant, endInstant)
215
+ val maxDuration = Duration.ofDays(maxDays)
216
+ return duration <= maxDuration
217
+ }
218
+
219
+ fun normalizeAllowlist(sources: List<String>): Set<String> {
220
+ return sources
221
+ .map { it.trim() }
222
+ .filter { it.isNotEmpty() }
223
+ .map { normalizeSourceType(it) }
224
+ .toSet()
225
+ }
226
+
227
+ fun normalizePackageAllowlist(packages: List<String>): Set<String> {
228
+ return packages
229
+ .map { it.trim().lowercase() }
230
+ .filter { it.isNotEmpty() }
231
+ .toSet()
232
+ }
233
+
234
+ fun normalizeSourceType(rawValue: String): String {
235
+ return HealthConnectSourceTrust.normalizeSourceType(rawValue)
236
+ }
237
+
238
+ fun normalizeSourceType(rawValue: Any?): String {
239
+ return HealthConnectSourceTrust.normalizeSourceType(rawValue)
240
+ }
241
+
242
+ fun evaluateRecordTrust(metadata: Any?): RecordTrustResult {
243
+ return HealthConnectSourceTrust.evaluateRecordTrust(metadata)
244
+ }
245
+
246
+ fun resolveTrustLevelFromSource(sourceType: String): String {
247
+ return HealthConnectSourceTrust.resolveTrustLevelFromSource(sourceType)
248
+ }
249
+
250
+ fun resolveDeviceModel(metadata: Any?): String {
251
+ return HealthConnectSourceTrust.resolveDeviceModel(metadata)
252
+ }
253
+
254
+ fun isRecordAllowed(sourceType: String, allowlist: Set<String>): Boolean {
255
+ return HealthConnectSourceTrust.isRecordAllowed(sourceType, allowlist)
256
+ }
257
+
258
+ fun isPackageAllowed(packageName: String?, packageAllowlist: Set<String>): Boolean {
259
+ if (packageAllowlist.isEmpty()) {
260
+ return true
261
+ }
262
+
263
+ val normalizedPackageName = packageName?.trim()?.lowercase().orEmpty()
264
+ if (normalizedPackageName.isEmpty()) {
265
+ return false
266
+ }
267
+
268
+ return packageAllowlist.contains(normalizedPackageName)
269
+ }
270
+
271
+ fun normalizeOverlaps(
272
+ metricType: MetricType,
273
+ rawIntervals: List<IntervalInput>,
274
+ sinceMs: Long,
275
+ untilMs: Long,
276
+ bucketPeriod: String,
277
+ zoneId: ZoneId
278
+ ): NormalizationResult {
279
+ return HealthConnectOverlapNormalizer.normalizeOverlaps(
280
+ metricType = metricType,
281
+ rawIntervals = rawIntervals,
282
+ sinceMs = sinceMs,
283
+ untilMs = untilMs,
284
+ bucketPeriod = bucketPeriod,
285
+ zoneId = zoneId
286
+ )
287
+ }
288
+
289
+ fun mergeCaloriesWithFallback(
290
+ primaryActiveCalories: NormalizationResult,
291
+ totalCalories: NormalizationResult,
292
+ basalCalories: NormalizationResult,
293
+ stepsEvidenceSegments: List<NormalizedSegment> = emptyList(),
294
+ activeTimeEvidenceSegments: List<NormalizedSegment> = emptyList(),
295
+ distanceEvidenceSegments: List<NormalizedSegment> = emptyList(),
296
+ basalSeedApplied: Boolean = false,
297
+ seededFromBeforeSince: Boolean = false,
298
+ basalSeedAgeHours: Double? = null
299
+ ): NormalizationResult {
300
+ return HealthConnectCaloriesFallbackMerger.mergeCaloriesWithFallback(
301
+ primaryActiveCalories = primaryActiveCalories,
302
+ totalCalories = totalCalories,
303
+ basalCalories = basalCalories,
304
+ stepsEvidenceSegments = stepsEvidenceSegments,
305
+ activeTimeEvidenceSegments = activeTimeEvidenceSegments,
306
+ distanceEvidenceSegments = distanceEvidenceSegments,
307
+ basalSeedApplied = basalSeedApplied,
308
+ seededFromBeforeSince = seededFromBeforeSince,
309
+ basalSeedAgeHours = basalSeedAgeHours
310
+ )
311
+ }
312
+
313
+ fun nextBucketBoundary(timeMs: Long, zoneId: ZoneId, bucketPeriod: String): Long {
314
+ return HealthConnectOverlapNormalizer.nextBucketBoundary(timeMs, zoneId, bucketPeriod)
315
+ }
316
+ }