@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.
- package/CarivaExerciseSdk.podspec +20 -0
- package/README.md +374 -0
- package/android/build.gradle +66 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/carivaexercisesdk/CarivaExerciseSdkPackage.kt +30 -0
- package/android/src/main/java/com/carivaexercisesdk/HealthConnectModule.kt +222 -0
- package/android/src/main/java/com/carivaexercisesdk/HealthConnectPermissionUsageActivity.kt +11 -0
- package/android/src/main/java/com/carivaexercisesdk/HealthConnectPermissionsRationaleActivity.kt +11 -0
- package/android/src/main/java/com/carivaexercisesdk/Pagination.kt +23 -0
- package/android/src/main/java/com/carivaexercisesdk/application/connection/command/ConnectCommand.kt +12 -0
- package/android/src/main/java/com/carivaexercisesdk/application/connection/dto/ConnectResultDto.kt +13 -0
- package/android/src/main/java/com/carivaexercisesdk/application/connection/handler/ConnectHandler.kt +79 -0
- package/android/src/main/java/com/carivaexercisesdk/application/connection/port/HealthConnectConnectionPort.kt +16 -0
- package/android/src/main/java/com/carivaexercisesdk/application/connection/port/PermissionRequestPort.kt +5 -0
- package/android/src/main/java/com/carivaexercisesdk/application/datasource/command/SetDatasourcePolicyCommand.kt +6 -0
- package/android/src/main/java/com/carivaexercisesdk/application/datasource/handler/GetDatasourcePolicyHandler.kt +10 -0
- package/android/src/main/java/com/carivaexercisesdk/application/datasource/handler/SetDatasourcePolicyHandler.kt +22 -0
- package/android/src/main/java/com/carivaexercisesdk/application/datasource/port/DatasourcePolicyRepository.kt +9 -0
- package/android/src/main/java/com/carivaexercisesdk/application/exercise/dto/BasalReadResultDto.kt +11 -0
- package/android/src/main/java/com/carivaexercisesdk/application/exercise/dto/ExerciseDataDto.kt +41 -0
- package/android/src/main/java/com/carivaexercisesdk/application/exercise/handler/GetExerciseDataHandler.kt +346 -0
- package/android/src/main/java/com/carivaexercisesdk/application/exercise/port/ExerciseRecordPort.kt +45 -0
- package/android/src/main/java/com/carivaexercisesdk/application/exercise/query/GetExerciseDataQuery.kt +9 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/connection/entity/ConnectionAggregate.kt +13 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/connection/service/ConnectionDecisionService.kt +37 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/connection/valueobject/ConnectionNextAction.kt +8 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/connection/valueobject/HealthConnectStatus.kt +8 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/connection/valueobject/PermissionScope.kt +36 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/datasource/entity/DatasourcePolicy.kt +8 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/datasource/valueobject/DatasourceType.kt +14 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/ExerciseDomainService.kt +89 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectCaloriesFallbackMerger.kt +351 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectInternalsConstants.kt +55 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectModuleInternals.kt +316 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectOverlapNormalizer.kt +400 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectSourceTrust.kt +249 -0
- package/android/src/main/java/com/carivaexercisesdk/domain/exercise/services/HealthConnectUnifiedRecordBuilder.kt +316 -0
- package/android/src/main/java/com/carivaexercisesdk/infrastructure/healthconnect/HealthConnectConnectionAdapter.kt +156 -0
- package/android/src/main/java/com/carivaexercisesdk/infrastructure/healthconnect/HealthConnectExerciseRecordAdapter.kt +464 -0
- package/android/src/main/java/com/carivaexercisesdk/infrastructure/persistence/InMemoryDatasourcePolicyRepository.kt +21 -0
- package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/ConnectResultMapper.kt +23 -0
- package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/DatasourcePolicyJsonParser.kt +51 -0
- package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/DatasourcePolicyMapper.kt +46 -0
- package/android/src/main/java/com/carivaexercisesdk/infrastructure/reactnative/ExerciseDataMapper.kt +99 -0
- package/android/src/test/java/com/carivaexercisesdk/ArchitectureDependencyRuleTest.kt +60 -0
- package/android/src/test/java/com/carivaexercisesdk/HealthConnectModuleDatasourceParserTest.kt +69 -0
- package/android/src/test/java/com/carivaexercisesdk/HealthConnectModuleInternalsTest.kt +406 -0
- package/android/src/test/java/com/carivaexercisesdk/application/connection/handler/ConnectHandlerTest.kt +153 -0
- package/android/src/test/java/com/carivaexercisesdk/application/datasource/handler/DatasourcePolicyRoundTripTest.kt +63 -0
- package/android/src/test/java/com/carivaexercisesdk/application/datasource/handler/SetDatasourcePolicyHandlerTest.kt +42 -0
- package/android/src/test/java/com/carivaexercisesdk/domain/connection/service/ConnectionDecisionServiceTest.kt +68 -0
- package/android/src/test/java/com/carivaexercisesdk/infrastructure/reactnative/DatasourcePolicyMapperTest.kt +22 -0
- package/ios/CarivaExerciseSdk.h +5 -0
- package/ios/CarivaExerciseSdk.mm +7 -0
- package/lib/module/connect/index.js +7 -0
- package/lib/module/connect/index.js.map +1 -0
- package/lib/module/datasource/index.js +10 -0
- package/lib/module/datasource/index.js.map +1 -0
- package/lib/module/exercise/index.js +7 -0
- package/lib/module/exercise/index.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/module.js +13 -0
- package/lib/module/native/module.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/connect/index.d.ts +16 -0
- package/lib/typescript/src/connect/index.d.ts.map +1 -0
- package/lib/typescript/src/datasource/index.d.ts +12 -0
- package/lib/typescript/src/datasource/index.d.ts.map +1 -0
- package/lib/typescript/src/exercise/index.d.ts +64 -0
- package/lib/typescript/src/exercise/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/native/module.d.ts +14 -0
- package/lib/typescript/src/native/module.d.ts.map +1 -0
- package/package.json +127 -0
- package/src/connect/index.ts +34 -0
- package/src/datasource/index.ts +20 -0
- package/src/exercise/index.ts +75 -0
- package/src/index.tsx +22 -0
- package/src/native/module.ts +23 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
package com.carivaexercisesdk.application.exercise.handler
|
|
2
|
+
|
|
3
|
+
import com.carivaexercisesdk.domain.exercise.services.HealthConnectModuleInternals
|
|
4
|
+
import com.carivaexercisesdk.application.exercise.dto.BasalReadResultDto
|
|
5
|
+
import com.carivaexercisesdk.application.exercise.dto.ExerciseDataDto
|
|
6
|
+
import com.carivaexercisesdk.application.exercise.dto.ExerciseDebugDto
|
|
7
|
+
import com.carivaexercisesdk.application.exercise.dto.ExerciseDiagnosticsDto
|
|
8
|
+
import com.carivaexercisesdk.application.exercise.port.ExerciseRecordPort
|
|
9
|
+
import com.carivaexercisesdk.application.exercise.query.GetExerciseDataQuery
|
|
10
|
+
import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
|
|
11
|
+
import com.carivaexercisesdk.domain.exercise.services.ExerciseDomainService
|
|
12
|
+
import java.time.Instant
|
|
13
|
+
import java.time.ZoneId
|
|
14
|
+
import kotlinx.coroutines.async
|
|
15
|
+
import kotlinx.coroutines.coroutineScope
|
|
16
|
+
|
|
17
|
+
internal class GetExerciseDataHandler(private val exerciseRecordPort: ExerciseRecordPort) {
|
|
18
|
+
suspend fun handle(query: GetExerciseDataQuery): ExerciseDataDto {
|
|
19
|
+
val healthConnectStatus = exerciseRecordPort.getHealthConnectStatus()
|
|
20
|
+
if (healthConnectStatus != HealthConnectStatus.AVAILABLE) {
|
|
21
|
+
throw ExerciseDataException(
|
|
22
|
+
code = "E_HEALTH_CONNECT_UNAVAILABLE",
|
|
23
|
+
detail = "Health Connect SDK status is not available: $healthConnectStatus"
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
val startInstant = Instant.ofEpochMilli(query.sinceMillis)
|
|
28
|
+
val endInstant = Instant.ofEpochMilli(query.untilMillis)
|
|
29
|
+
|
|
30
|
+
if (!ExerciseDomainService.isValidPeriod(startInstant, endInstant)) {
|
|
31
|
+
throw ExerciseDataException(
|
|
32
|
+
code = "E_INVALID_PERIOD",
|
|
33
|
+
detail = "Maximum period is 90 days and start must be <= end."
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!ExerciseDomainService.isSupportedBucketPeriod(query.bucketPeriod)) {
|
|
38
|
+
throw ExerciseDataException(
|
|
39
|
+
code = "E_UNSUPPORTED_BUCKET_PERIOD",
|
|
40
|
+
detail =
|
|
41
|
+
"Unsupported bucketPeriod '${query.bucketPeriod}'. Supported values: hourly, daily."
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
val normalizedBucketPeriod = ExerciseDomainService.normalizeBucketPeriod(query.bucketPeriod)
|
|
46
|
+
val zoneId = ZoneId.systemDefault()
|
|
47
|
+
|
|
48
|
+
val includeSteps = query.type == "steps" || query.type == "all"
|
|
49
|
+
val includeActiveTimes = query.type == "activetimes" || query.type == "all"
|
|
50
|
+
val includeCalories = query.type == "calories" || query.type == "all"
|
|
51
|
+
val includeDistances = query.type == "distances" || query.type == "all"
|
|
52
|
+
|
|
53
|
+
if (!includeSteps && !includeActiveTimes && !includeCalories && !includeDistances) {
|
|
54
|
+
throw ExerciseDataException(
|
|
55
|
+
code = "E_UNSUPPORTED_TYPE",
|
|
56
|
+
detail =
|
|
57
|
+
"Unsupported type '${query.type}'. Supported values: steps, activetimes, calories, distances, all."
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
val normalizedSteps =
|
|
62
|
+
if (includeSteps) {
|
|
63
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
64
|
+
metricType = HealthConnectModuleInternals.MetricType.STEPS,
|
|
65
|
+
rawIntervals = exerciseRecordPort.readStepsIntervals(startInstant, endInstant),
|
|
66
|
+
sinceMs = query.sinceMillis,
|
|
67
|
+
untilMs = query.untilMillis,
|
|
68
|
+
bucketPeriod = normalizedBucketPeriod,
|
|
69
|
+
zoneId = zoneId
|
|
70
|
+
)
|
|
71
|
+
} else {
|
|
72
|
+
ExerciseDomainService.emptyNormalizationResult("source_priority")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
val normalizedActiveTimes =
|
|
76
|
+
if (includeActiveTimes) {
|
|
77
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
78
|
+
metricType = HealthConnectModuleInternals.MetricType.ACTIVE_TIME,
|
|
79
|
+
rawIntervals =
|
|
80
|
+
exerciseRecordPort.readActiveTimeIntervals(startInstant, endInstant),
|
|
81
|
+
sinceMs = query.sinceMillis,
|
|
82
|
+
untilMs = query.untilMillis,
|
|
83
|
+
bucketPeriod = normalizedBucketPeriod,
|
|
84
|
+
zoneId = zoneId
|
|
85
|
+
)
|
|
86
|
+
} else {
|
|
87
|
+
ExerciseDomainService.emptyNormalizationResult("proportional")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
val normalizedDistances =
|
|
91
|
+
if (includeDistances) {
|
|
92
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
93
|
+
metricType = HealthConnectModuleInternals.MetricType.DISTANCE,
|
|
94
|
+
rawIntervals = exerciseRecordPort.readDistanceIntervals(startInstant, endInstant),
|
|
95
|
+
sinceMs = query.sinceMillis,
|
|
96
|
+
untilMs = query.untilMillis,
|
|
97
|
+
bucketPeriod = normalizedBucketPeriod,
|
|
98
|
+
zoneId = zoneId
|
|
99
|
+
)
|
|
100
|
+
} else {
|
|
101
|
+
ExerciseDomainService.emptyNormalizationResult("proportional")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
val caloriesFallbackEvidence =
|
|
105
|
+
if (includeCalories && query.type == "calories") {
|
|
106
|
+
loadCaloriesFallbackEvidence(
|
|
107
|
+
startInstant = startInstant,
|
|
108
|
+
endInstant = endInstant,
|
|
109
|
+
sinceMs = query.sinceMillis,
|
|
110
|
+
untilMs = query.untilMillis,
|
|
111
|
+
bucketPeriod = normalizedBucketPeriod,
|
|
112
|
+
zoneId = zoneId
|
|
113
|
+
)
|
|
114
|
+
} else {
|
|
115
|
+
CaloriesFallbackEvidenceDto(
|
|
116
|
+
stepsSegments = normalizedSteps.segments,
|
|
117
|
+
activeTimeSegments = normalizedActiveTimes.segments,
|
|
118
|
+
distanceSegments = normalizedDistances.segments
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var basalReadResult = BasalReadResultDto(intervals = emptyList(), basalSeedApplied = false)
|
|
123
|
+
val normalizedCalories =
|
|
124
|
+
if (includeCalories) {
|
|
125
|
+
val normalizedActiveCalories =
|
|
126
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
127
|
+
metricType = HealthConnectModuleInternals.MetricType.CALORIES,
|
|
128
|
+
rawIntervals = exerciseRecordPort.readCaloriesIntervals(startInstant, endInstant),
|
|
129
|
+
sinceMs = query.sinceMillis,
|
|
130
|
+
untilMs = query.untilMillis,
|
|
131
|
+
bucketPeriod = normalizedBucketPeriod,
|
|
132
|
+
zoneId = zoneId
|
|
133
|
+
)
|
|
134
|
+
val normalizedTotalCalories =
|
|
135
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
136
|
+
metricType = HealthConnectModuleInternals.MetricType.CALORIES,
|
|
137
|
+
rawIntervals =
|
|
138
|
+
exerciseRecordPort.readTotalCaloriesIntervals(
|
|
139
|
+
startInstant,
|
|
140
|
+
endInstant
|
|
141
|
+
),
|
|
142
|
+
sinceMs = query.sinceMillis,
|
|
143
|
+
untilMs = query.untilMillis,
|
|
144
|
+
bucketPeriod = normalizedBucketPeriod,
|
|
145
|
+
zoneId = zoneId
|
|
146
|
+
)
|
|
147
|
+
basalReadResult = exerciseRecordPort.readBasalCaloriesIntervals(startInstant, endInstant)
|
|
148
|
+
val normalizedBasalCalories =
|
|
149
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
150
|
+
metricType = HealthConnectModuleInternals.MetricType.CALORIES,
|
|
151
|
+
rawIntervals = basalReadResult.intervals,
|
|
152
|
+
sinceMs = query.sinceMillis,
|
|
153
|
+
untilMs = query.untilMillis,
|
|
154
|
+
bucketPeriod = normalizedBucketPeriod,
|
|
155
|
+
zoneId = zoneId
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
ExerciseDomainService.mergeCaloriesWithFallback(
|
|
159
|
+
primaryActiveCalories = normalizedActiveCalories,
|
|
160
|
+
totalCalories = normalizedTotalCalories,
|
|
161
|
+
basalCalories = normalizedBasalCalories,
|
|
162
|
+
stepsEvidenceSegments = caloriesFallbackEvidence.stepsSegments,
|
|
163
|
+
activeTimeEvidenceSegments = caloriesFallbackEvidence.activeTimeSegments,
|
|
164
|
+
distanceEvidenceSegments = caloriesFallbackEvidence.distanceSegments,
|
|
165
|
+
basalSeedApplied = basalReadResult.basalSeedApplied,
|
|
166
|
+
seededFromBeforeSince = basalReadResult.seededFromBeforeSince,
|
|
167
|
+
basalSeedAgeHours = basalReadResult.basalSeedAgeHours
|
|
168
|
+
)
|
|
169
|
+
} else {
|
|
170
|
+
ExerciseDomainService.emptyNormalizationResult("proportional")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
val unifiedRecordsResult =
|
|
174
|
+
ExerciseDomainService.buildUnifiedRecordsResult(
|
|
175
|
+
stepsSegments = normalizedSteps.segments,
|
|
176
|
+
activeTimeSegments = normalizedActiveTimes.segments,
|
|
177
|
+
caloriesSegments = normalizedCalories.segments,
|
|
178
|
+
distanceSegments = normalizedDistances.segments,
|
|
179
|
+
zoneId = zoneId,
|
|
180
|
+
roundingScale = 5
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
val warnings =
|
|
184
|
+
linkedSetOf<String>().apply {
|
|
185
|
+
addAll(normalizedSteps.warnings)
|
|
186
|
+
addAll(normalizedActiveTimes.warnings)
|
|
187
|
+
addAll(normalizedCalories.warnings)
|
|
188
|
+
addAll(normalizedDistances.warnings)
|
|
189
|
+
addAll(basalReadResult.warnings)
|
|
190
|
+
addAll(caloriesFallbackEvidence.warnings)
|
|
191
|
+
addAll(unifiedRecordsResult.warnings)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
val diagnosticsDto =
|
|
195
|
+
normalizedCalories.diagnostics?.let { diagnostics ->
|
|
196
|
+
ExerciseDiagnosticsDto(
|
|
197
|
+
fallbackUsedSegments = diagnostics.fallbackUsedSegments,
|
|
198
|
+
fallbackSuppressedSegments = diagnostics.fallbackSuppressedSegments,
|
|
199
|
+
idleGateSuppressedSegments = diagnostics.idleGateSuppressedSegments,
|
|
200
|
+
basalSeedApplied = diagnostics.basalSeedApplied,
|
|
201
|
+
seededFromBeforeSince = basalReadResult.seededFromBeforeSince,
|
|
202
|
+
basalSeedAgeHours = diagnostics.basalSeedAgeHours
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
val debugDto =
|
|
207
|
+
if (query.debug) {
|
|
208
|
+
normalizedCalories.diagnostics?.let { diagnostics ->
|
|
209
|
+
ExerciseDebugDto(
|
|
210
|
+
directActiveCaloriesKcal = diagnostics.directActiveCaloriesKcal,
|
|
211
|
+
totalCaloriesKcal = diagnostics.totalCaloriesKcal,
|
|
212
|
+
basalCaloriesKcal = diagnostics.basalCaloriesKcal,
|
|
213
|
+
mergedActiveCaloriesKcal = diagnostics.mergedActiveCaloriesKcal,
|
|
214
|
+
fallbackContributionKcal = diagnostics.fallbackContributionKcal,
|
|
215
|
+
fallbackUsedSegments = diagnostics.fallbackUsedSegments,
|
|
216
|
+
fallbackSuppressedSegments = diagnostics.fallbackSuppressedSegments,
|
|
217
|
+
idleGateSuppressedSegments = diagnostics.idleGateSuppressedSegments,
|
|
218
|
+
basalSeedApplied = diagnostics.basalSeedApplied,
|
|
219
|
+
seededFromBeforeSince = basalReadResult.seededFromBeforeSince,
|
|
220
|
+
basalSeedAgeHours = diagnostics.basalSeedAgeHours,
|
|
221
|
+
basalIntervalsCount = basalReadResult.intervals.size
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return ExerciseDataDto(
|
|
229
|
+
totalKcal = normalizedCalories.segments.sumOf { it.value },
|
|
230
|
+
totalSteps = normalizedSteps.segments.sumOf { it.value },
|
|
231
|
+
totalActiveTimeMillis = normalizedActiveTimes.segments.sumOf { it.value },
|
|
232
|
+
totalDistanceMeters = normalizedDistances.segments.sumOf { it.value },
|
|
233
|
+
timeZone = zoneId.id,
|
|
234
|
+
records = unifiedRecordsResult.records,
|
|
235
|
+
warnings = warnings.toList(),
|
|
236
|
+
integrityProjectionPreserved = unifiedRecordsResult.projectionPreserved,
|
|
237
|
+
integrityRoundingScale = unifiedRecordsResult.roundingScale,
|
|
238
|
+
diagnostics = diagnosticsDto,
|
|
239
|
+
debug = debugDto
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private suspend fun loadCaloriesFallbackEvidence(
|
|
244
|
+
startInstant: Instant,
|
|
245
|
+
endInstant: Instant,
|
|
246
|
+
sinceMs: Long,
|
|
247
|
+
untilMs: Long,
|
|
248
|
+
bucketPeriod: String,
|
|
249
|
+
zoneId: ZoneId
|
|
250
|
+
): CaloriesFallbackEvidenceDto = coroutineScope {
|
|
251
|
+
val warnings = linkedSetOf<String>()
|
|
252
|
+
|
|
253
|
+
val stepsDeferred = async {
|
|
254
|
+
runCatching {
|
|
255
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
256
|
+
metricType = HealthConnectModuleInternals.MetricType.STEPS,
|
|
257
|
+
rawIntervals =
|
|
258
|
+
exerciseRecordPort.readStepsIntervals(
|
|
259
|
+
startInstant = startInstant,
|
|
260
|
+
endInstant = endInstant,
|
|
261
|
+
includeManualWithDevice = false
|
|
262
|
+
),
|
|
263
|
+
sinceMs = sinceMs,
|
|
264
|
+
untilMs = untilMs,
|
|
265
|
+
bucketPeriod = bucketPeriod,
|
|
266
|
+
zoneId = zoneId
|
|
267
|
+
)
|
|
268
|
+
.segments
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
val activeTimeDeferred = async {
|
|
272
|
+
runCatching {
|
|
273
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
274
|
+
metricType = HealthConnectModuleInternals.MetricType.ACTIVE_TIME,
|
|
275
|
+
rawIntervals =
|
|
276
|
+
exerciseRecordPort.readActiveTimeIntervals(
|
|
277
|
+
startInstant = startInstant,
|
|
278
|
+
endInstant = endInstant,
|
|
279
|
+
includeManualWithDevice = false
|
|
280
|
+
),
|
|
281
|
+
sinceMs = sinceMs,
|
|
282
|
+
untilMs = untilMs,
|
|
283
|
+
bucketPeriod = bucketPeriod,
|
|
284
|
+
zoneId = zoneId
|
|
285
|
+
)
|
|
286
|
+
.segments
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
val distanceDeferred = async {
|
|
290
|
+
runCatching {
|
|
291
|
+
ExerciseDomainService.normalizeOverlaps(
|
|
292
|
+
metricType = HealthConnectModuleInternals.MetricType.DISTANCE,
|
|
293
|
+
rawIntervals =
|
|
294
|
+
exerciseRecordPort.readDistanceIntervals(
|
|
295
|
+
startInstant = startInstant,
|
|
296
|
+
endInstant = endInstant,
|
|
297
|
+
includeManualWithDevice = false
|
|
298
|
+
),
|
|
299
|
+
sinceMs = sinceMs,
|
|
300
|
+
untilMs = untilMs,
|
|
301
|
+
bucketPeriod = bucketPeriod,
|
|
302
|
+
zoneId = zoneId
|
|
303
|
+
)
|
|
304
|
+
.segments
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
val stepsSegments =
|
|
309
|
+
stepsDeferred.await().getOrElse {
|
|
310
|
+
warnings.add(WARNING_MOVEMENT_EVIDENCE_PARTIALLY_UNAVAILABLE_FOR_FALLBACK_GATE)
|
|
311
|
+
emptyList()
|
|
312
|
+
}
|
|
313
|
+
val activeTimeSegments =
|
|
314
|
+
activeTimeDeferred.await().getOrElse {
|
|
315
|
+
warnings.add(WARNING_MOVEMENT_EVIDENCE_PARTIALLY_UNAVAILABLE_FOR_FALLBACK_GATE)
|
|
316
|
+
emptyList()
|
|
317
|
+
}
|
|
318
|
+
val distanceSegments =
|
|
319
|
+
distanceDeferred.await().getOrElse {
|
|
320
|
+
warnings.add(WARNING_MOVEMENT_EVIDENCE_PARTIALLY_UNAVAILABLE_FOR_FALLBACK_GATE)
|
|
321
|
+
emptyList()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
CaloriesFallbackEvidenceDto(
|
|
325
|
+
stepsSegments = stepsSegments,
|
|
326
|
+
activeTimeSegments = activeTimeSegments,
|
|
327
|
+
distanceSegments = distanceSegments,
|
|
328
|
+
warnings = warnings
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private data class CaloriesFallbackEvidenceDto(
|
|
333
|
+
val stepsSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
334
|
+
val activeTimeSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
335
|
+
val distanceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
336
|
+
val warnings: Set<String> = emptySet()
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
private companion object {
|
|
340
|
+
private const val WARNING_MOVEMENT_EVIDENCE_PARTIALLY_UNAVAILABLE_FOR_FALLBACK_GATE =
|
|
341
|
+
"movement_evidence_partially_unavailable_for_fallback_gate"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
internal class ExerciseDataException(val code: String, val detail: String) :
|
|
346
|
+
IllegalArgumentException(detail)
|
package/android/src/main/java/com/carivaexercisesdk/application/exercise/port/ExerciseRecordPort.kt
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.carivaexercisesdk.application.exercise.port
|
|
2
|
+
|
|
3
|
+
import com.carivaexercisesdk.domain.exercise.services.HealthConnectModuleInternals
|
|
4
|
+
import com.carivaexercisesdk.application.exercise.dto.BasalReadResultDto
|
|
5
|
+
import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
|
|
6
|
+
import java.time.Instant
|
|
7
|
+
|
|
8
|
+
internal interface ExerciseRecordPort {
|
|
9
|
+
suspend fun getHealthConnectStatus(): HealthConnectStatus
|
|
10
|
+
|
|
11
|
+
suspend fun readStepsIntervals(
|
|
12
|
+
startInstant: Instant,
|
|
13
|
+
endInstant: Instant,
|
|
14
|
+
includeManualWithDevice: Boolean = true
|
|
15
|
+
): List<HealthConnectModuleInternals.IntervalInput>
|
|
16
|
+
|
|
17
|
+
suspend fun readActiveTimeIntervals(
|
|
18
|
+
startInstant: Instant,
|
|
19
|
+
endInstant: Instant,
|
|
20
|
+
includeManualWithDevice: Boolean = true
|
|
21
|
+
): List<HealthConnectModuleInternals.IntervalInput>
|
|
22
|
+
|
|
23
|
+
suspend fun readCaloriesIntervals(
|
|
24
|
+
startInstant: Instant,
|
|
25
|
+
endInstant: Instant,
|
|
26
|
+
includeManualWithDevice: Boolean = true
|
|
27
|
+
): List<HealthConnectModuleInternals.IntervalInput>
|
|
28
|
+
|
|
29
|
+
suspend fun readDistanceIntervals(
|
|
30
|
+
startInstant: Instant,
|
|
31
|
+
endInstant: Instant,
|
|
32
|
+
includeManualWithDevice: Boolean = true
|
|
33
|
+
): List<HealthConnectModuleInternals.IntervalInput>
|
|
34
|
+
|
|
35
|
+
suspend fun readTotalCaloriesIntervals(
|
|
36
|
+
startInstant: Instant,
|
|
37
|
+
endInstant: Instant,
|
|
38
|
+
includeManualWithDevice: Boolean = true
|
|
39
|
+
): List<HealthConnectModuleInternals.IntervalInput>
|
|
40
|
+
|
|
41
|
+
suspend fun readBasalCaloriesIntervals(
|
|
42
|
+
startInstant: Instant,
|
|
43
|
+
endInstant: Instant
|
|
44
|
+
): BasalReadResultDto
|
|
45
|
+
}
|
package/android/src/main/java/com/carivaexercisesdk/domain/connection/entity/ConnectionAggregate.kt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.connection.entity
|
|
2
|
+
|
|
3
|
+
import com.carivaexercisesdk.domain.connection.valueobject.ConnectionNextAction
|
|
4
|
+
import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
|
|
5
|
+
|
|
6
|
+
data class ConnectionAggregate(
|
|
7
|
+
val ready: Boolean,
|
|
8
|
+
val healthConnectInstalled: Boolean,
|
|
9
|
+
val healthConnectStatus: HealthConnectStatus,
|
|
10
|
+
val hasPermissions: Boolean,
|
|
11
|
+
val nextAction: ConnectionNextAction,
|
|
12
|
+
val requestedPermissions: Set<String>
|
|
13
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.connection.service
|
|
2
|
+
|
|
3
|
+
import com.carivaexercisesdk.domain.connection.entity.ConnectionAggregate
|
|
4
|
+
import com.carivaexercisesdk.domain.connection.valueobject.ConnectionNextAction
|
|
5
|
+
import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
|
|
6
|
+
|
|
7
|
+
class ConnectionDecisionService {
|
|
8
|
+
fun decide(
|
|
9
|
+
healthConnectInstalled: Boolean,
|
|
10
|
+
healthConnectStatus: HealthConnectStatus,
|
|
11
|
+
hasPermissions: Boolean,
|
|
12
|
+
requestedPermissions: Set<String>
|
|
13
|
+
): ConnectionAggregate {
|
|
14
|
+
val nextAction =
|
|
15
|
+
when {
|
|
16
|
+
!healthConnectInstalled -> ConnectionNextAction.INSTALL_HEALTH_CONNECT
|
|
17
|
+
healthConnectStatus != HealthConnectStatus.AVAILABLE ->
|
|
18
|
+
ConnectionNextAction.UPDATE_HEALTH_CONNECT
|
|
19
|
+
!hasPermissions -> ConnectionNextAction.REQUEST_PERMISSION
|
|
20
|
+
else -> ConnectionNextAction.NONE
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
val ready =
|
|
24
|
+
healthConnectInstalled &&
|
|
25
|
+
healthConnectStatus == HealthConnectStatus.AVAILABLE &&
|
|
26
|
+
hasPermissions
|
|
27
|
+
|
|
28
|
+
return ConnectionAggregate(
|
|
29
|
+
ready = ready,
|
|
30
|
+
healthConnectInstalled = healthConnectInstalled,
|
|
31
|
+
healthConnectStatus = healthConnectStatus,
|
|
32
|
+
hasPermissions = hasPermissions,
|
|
33
|
+
nextAction = nextAction,
|
|
34
|
+
requestedPermissions = requestedPermissions
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.connection.valueobject
|
|
2
|
+
|
|
3
|
+
enum class ConnectionNextAction(val wireValue: String) {
|
|
4
|
+
INSTALL_HEALTH_CONNECT("INSTALL_HEALTH_CONNECT"),
|
|
5
|
+
UPDATE_HEALTH_CONNECT("UPDATE_HEALTH_CONNECT"),
|
|
6
|
+
REQUEST_PERMISSION("REQUEST_PERMISSION"),
|
|
7
|
+
NONE("NONE")
|
|
8
|
+
}
|
package/android/src/main/java/com/carivaexercisesdk/domain/connection/valueobject/PermissionScope.kt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.connection.valueobject
|
|
2
|
+
|
|
3
|
+
enum class PermissionScope(val wireValue: String) {
|
|
4
|
+
STEPS("steps"),
|
|
5
|
+
ACTIVETIMES("activetimes"),
|
|
6
|
+
CALORIES("calories"),
|
|
7
|
+
DISTANCES("distances");
|
|
8
|
+
|
|
9
|
+
companion object {
|
|
10
|
+
fun normalize(scopeValue: String): PermissionScope? {
|
|
11
|
+
return entries.firstOrNull { it.wireValue == scopeValue.trim().lowercase() }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fun normalizeAll(scopeValues: List<String>?): Set<PermissionScope> {
|
|
15
|
+
if (scopeValues.isNullOrEmpty()) {
|
|
16
|
+
return emptySet()
|
|
17
|
+
}
|
|
18
|
+
return scopeValues.mapNotNull(::normalize).toSet()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun findUnsupported(scopeValues: List<String>?): Set<String> {
|
|
22
|
+
if (scopeValues.isNullOrEmpty()) {
|
|
23
|
+
return emptySet()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return scopeValues
|
|
27
|
+
.map { it.trim() }
|
|
28
|
+
.filter { normalize(it) == null }
|
|
29
|
+
.toSet()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun defaults(): Set<PermissionScope> {
|
|
33
|
+
return entries.toSet()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/android/src/main/java/com/carivaexercisesdk/domain/datasource/valueobject/DatasourceType.kt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.datasource.valueobject
|
|
2
|
+
|
|
3
|
+
enum class DatasourceType(val wireValue: String) {
|
|
4
|
+
MANUAL_INPUT("manual_input"),
|
|
5
|
+
DEVICE("device"),
|
|
6
|
+
APP("app"),
|
|
7
|
+
UNKNOWN("unknown");
|
|
8
|
+
|
|
9
|
+
companion object {
|
|
10
|
+
fun fromWireValue(value: String): DatasourceType? {
|
|
11
|
+
return entries.firstOrNull { it.wireValue == value.trim().lowercase() }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.exercise.services
|
|
2
|
+
|
|
3
|
+
import java.time.Instant
|
|
4
|
+
import java.time.ZoneId
|
|
5
|
+
|
|
6
|
+
internal object ExerciseDomainService {
|
|
7
|
+
fun isValidPeriod(startInstant: Instant, endInstant: Instant): Boolean {
|
|
8
|
+
return HealthConnectModuleInternals.isValidPeriod(startInstant, endInstant)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fun isSupportedBucketPeriod(bucketPeriod: String): Boolean {
|
|
12
|
+
return HealthConnectModuleInternals.isSupportedBucketPeriod(bucketPeriod)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fun normalizeBucketPeriod(bucketPeriod: String): String {
|
|
16
|
+
return HealthConnectModuleInternals.normalizeBucketPeriod(bucketPeriod)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun normalizeOverlaps(
|
|
20
|
+
metricType: HealthConnectModuleInternals.MetricType,
|
|
21
|
+
rawIntervals: List<HealthConnectModuleInternals.IntervalInput>,
|
|
22
|
+
sinceMs: Long,
|
|
23
|
+
untilMs: Long,
|
|
24
|
+
bucketPeriod: String,
|
|
25
|
+
zoneId: ZoneId
|
|
26
|
+
): HealthConnectModuleInternals.NormalizationResult {
|
|
27
|
+
return HealthConnectModuleInternals.normalizeOverlaps(
|
|
28
|
+
metricType = metricType,
|
|
29
|
+
rawIntervals = rawIntervals,
|
|
30
|
+
sinceMs = sinceMs,
|
|
31
|
+
untilMs = untilMs,
|
|
32
|
+
bucketPeriod = bucketPeriod,
|
|
33
|
+
zoneId = zoneId
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun mergeCaloriesWithFallback(
|
|
38
|
+
primaryActiveCalories: HealthConnectModuleInternals.NormalizationResult,
|
|
39
|
+
totalCalories: HealthConnectModuleInternals.NormalizationResult,
|
|
40
|
+
basalCalories: HealthConnectModuleInternals.NormalizationResult,
|
|
41
|
+
stepsEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
42
|
+
activeTimeEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
43
|
+
distanceEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
44
|
+
basalSeedApplied: Boolean,
|
|
45
|
+
seededFromBeforeSince: Boolean,
|
|
46
|
+
basalSeedAgeHours: Double?
|
|
47
|
+
): HealthConnectModuleInternals.NormalizationResult {
|
|
48
|
+
return HealthConnectModuleInternals.mergeCaloriesWithFallback(
|
|
49
|
+
primaryActiveCalories = primaryActiveCalories,
|
|
50
|
+
totalCalories = totalCalories,
|
|
51
|
+
basalCalories = basalCalories,
|
|
52
|
+
stepsEvidenceSegments = stepsEvidenceSegments,
|
|
53
|
+
activeTimeEvidenceSegments = activeTimeEvidenceSegments,
|
|
54
|
+
distanceEvidenceSegments = distanceEvidenceSegments,
|
|
55
|
+
basalSeedApplied = basalSeedApplied,
|
|
56
|
+
seededFromBeforeSince = seededFromBeforeSince,
|
|
57
|
+
basalSeedAgeHours = basalSeedAgeHours
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fun buildUnifiedRecordsResult(
|
|
62
|
+
stepsSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
63
|
+
activeTimeSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
64
|
+
caloriesSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
65
|
+
distanceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
66
|
+
zoneId: ZoneId,
|
|
67
|
+
roundingScale: Int
|
|
68
|
+
): HealthConnectModuleInternals.UnifiedRecordsBuildResult {
|
|
69
|
+
return HealthConnectModuleInternals.buildUnifiedRecordsResult(
|
|
70
|
+
stepsSegments = stepsSegments,
|
|
71
|
+
activeTimeSegments = activeTimeSegments,
|
|
72
|
+
caloriesSegments = caloriesSegments,
|
|
73
|
+
distanceSegments = distanceSegments,
|
|
74
|
+
zoneId = zoneId,
|
|
75
|
+
roundingScale = roundingScale
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fun emptyNormalizationResult(
|
|
80
|
+
overlapStrategy: String
|
|
81
|
+
): HealthConnectModuleInternals.NormalizationResult {
|
|
82
|
+
return HealthConnectModuleInternals.NormalizationResult(
|
|
83
|
+
segments = emptyList(),
|
|
84
|
+
stats = HealthConnectModuleInternals.NormalizationStats(),
|
|
85
|
+
overlapStrategy = overlapStrategy,
|
|
86
|
+
warnings = emptyList()
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|