@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,351 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.exercise.services
|
|
2
|
+
|
|
3
|
+
internal object HealthConnectCaloriesFallbackMerger {
|
|
4
|
+
fun mergeCaloriesWithFallback(
|
|
5
|
+
primaryActiveCalories: HealthConnectModuleInternals.NormalizationResult,
|
|
6
|
+
totalCalories: HealthConnectModuleInternals.NormalizationResult,
|
|
7
|
+
basalCalories: HealthConnectModuleInternals.NormalizationResult,
|
|
8
|
+
stepsEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment> = emptyList(),
|
|
9
|
+
activeTimeEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment> =
|
|
10
|
+
emptyList(),
|
|
11
|
+
distanceEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment> =
|
|
12
|
+
emptyList(),
|
|
13
|
+
basalSeedApplied: Boolean = false,
|
|
14
|
+
seededFromBeforeSince: Boolean = false,
|
|
15
|
+
basalSeedAgeHours: Double? = null
|
|
16
|
+
): HealthConnectModuleInternals.NormalizationResult {
|
|
17
|
+
val directActiveCaloriesKcal = primaryActiveCalories.segments.sumOf { it.value }
|
|
18
|
+
val totalCaloriesKcal = totalCalories.segments.sumOf { it.value }
|
|
19
|
+
val basalCaloriesKcal = basalCalories.segments.sumOf { it.value }
|
|
20
|
+
|
|
21
|
+
val fallbackDerived =
|
|
22
|
+
deriveFallbackCaloriesSegments(
|
|
23
|
+
primaryActiveCalories = primaryActiveCalories.segments,
|
|
24
|
+
totalCalories = totalCalories.segments,
|
|
25
|
+
basalCalories = basalCalories.segments,
|
|
26
|
+
stepsEvidenceSegments = stepsEvidenceSegments,
|
|
27
|
+
activeTimeEvidenceSegments = activeTimeEvidenceSegments,
|
|
28
|
+
distanceEvidenceSegments = distanceEvidenceSegments
|
|
29
|
+
)
|
|
30
|
+
val fallbackFilled =
|
|
31
|
+
fillUncoveredIntervals(
|
|
32
|
+
primarySegments = primaryActiveCalories.segments,
|
|
33
|
+
fallbackSegments = fallbackDerived.segments
|
|
34
|
+
)
|
|
35
|
+
val warnings =
|
|
36
|
+
linkedSetOf<String>().apply {
|
|
37
|
+
addAll(primaryActiveCalories.warnings)
|
|
38
|
+
addAll(totalCalories.warnings)
|
|
39
|
+
addAll(basalCalories.warnings)
|
|
40
|
+
if (basalSeedApplied) {
|
|
41
|
+
add(HealthConnectInternalsConstants.WARNING_BMR_SEED_FROM_LATEST_BEFORE_SINCE_USED)
|
|
42
|
+
}
|
|
43
|
+
if (fallbackDerived.fallbackSuppressedSegments > 0) {
|
|
44
|
+
add(HealthConnectInternalsConstants.WARNING_BMR_MISSING_FOR_FALLBACK_WINDOW)
|
|
45
|
+
add(HealthConnectInternalsConstants.WARNING_FALLBACK_SUPPRESSED_DUE_TO_MISSING_BASAL)
|
|
46
|
+
}
|
|
47
|
+
if (fallbackDerived.idleGateSuppressedSegments > 0) {
|
|
48
|
+
add(HealthConnectInternalsConstants.WARNING_FALLBACK_SUPPRESSED_DUE_TO_IDLE_WINDOW)
|
|
49
|
+
}
|
|
50
|
+
if (fallbackFilled.fallbackUsedSegments > 0) {
|
|
51
|
+
add(HealthConnectInternalsConstants.WARNING_CALORIES_FALLBACK_USED)
|
|
52
|
+
if (primaryActiveCalories.segments.isEmpty()) {
|
|
53
|
+
add(HealthConnectInternalsConstants.WARNING_CALORIES_FALLBACK_ONLY_USED)
|
|
54
|
+
} else {
|
|
55
|
+
add(HealthConnectInternalsConstants.WARNING_CALORIES_FALLBACK_PARTIAL_USED)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
val mergedActiveCaloriesKcal = fallbackFilled.segments.sumOf { it.value }
|
|
61
|
+
val fallbackContributionKcal = maxOf(mergedActiveCaloriesKcal - directActiveCaloriesKcal, 0.0)
|
|
62
|
+
|
|
63
|
+
return HealthConnectModuleInternals.NormalizationResult(
|
|
64
|
+
segments = fallbackFilled.segments,
|
|
65
|
+
stats = primaryActiveCalories.stats + totalCalories.stats + basalCalories.stats,
|
|
66
|
+
overlapStrategy = HealthConnectInternalsConstants.OVERLAP_STRATEGY_PROPORTIONAL,
|
|
67
|
+
warnings = warnings.toList(),
|
|
68
|
+
diagnostics =
|
|
69
|
+
HealthConnectModuleInternals.Diagnostics(
|
|
70
|
+
fallbackUsedSegments = fallbackFilled.fallbackUsedSegments,
|
|
71
|
+
fallbackSuppressedSegments = fallbackDerived.fallbackSuppressedSegments,
|
|
72
|
+
idleGateSuppressedSegments = fallbackDerived.idleGateSuppressedSegments,
|
|
73
|
+
basalSeedApplied = basalSeedApplied,
|
|
74
|
+
seededFromBeforeSince = seededFromBeforeSince,
|
|
75
|
+
basalSeedAgeHours = basalSeedAgeHours,
|
|
76
|
+
directActiveCaloriesKcal = directActiveCaloriesKcal,
|
|
77
|
+
totalCaloriesKcal = totalCaloriesKcal,
|
|
78
|
+
basalCaloriesKcal = basalCaloriesKcal,
|
|
79
|
+
mergedActiveCaloriesKcal = mergedActiveCaloriesKcal,
|
|
80
|
+
fallbackContributionKcal = fallbackContributionKcal
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private data class FallbackDeriveResult(
|
|
86
|
+
val segments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
87
|
+
val fallbackSuppressedSegments: Int,
|
|
88
|
+
val idleGateSuppressedSegments: Int
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
private fun deriveFallbackCaloriesSegments(
|
|
92
|
+
primaryActiveCalories: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
93
|
+
totalCalories: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
94
|
+
basalCalories: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
95
|
+
stepsEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
96
|
+
activeTimeEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
97
|
+
distanceEvidenceSegments: List<HealthConnectModuleInternals.NormalizedSegment>
|
|
98
|
+
): FallbackDeriveResult {
|
|
99
|
+
if (totalCalories.isEmpty()) {
|
|
100
|
+
return FallbackDeriveResult(
|
|
101
|
+
segments = emptyList(),
|
|
102
|
+
fallbackSuppressedSegments = 0,
|
|
103
|
+
idleGateSuppressedSegments = 0
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
val breakpoints =
|
|
108
|
+
listOf(
|
|
109
|
+
primaryActiveCalories,
|
|
110
|
+
totalCalories,
|
|
111
|
+
basalCalories,
|
|
112
|
+
stepsEvidenceSegments,
|
|
113
|
+
activeTimeEvidenceSegments,
|
|
114
|
+
distanceEvidenceSegments
|
|
115
|
+
)
|
|
116
|
+
.flatMap { segments -> segments.flatMap { listOf(it.startMs, it.endMs) } }
|
|
117
|
+
.toSortedSet()
|
|
118
|
+
.toList()
|
|
119
|
+
|
|
120
|
+
if (breakpoints.size < 2) {
|
|
121
|
+
return FallbackDeriveResult(
|
|
122
|
+
segments = emptyList(),
|
|
123
|
+
fallbackSuppressedSegments = 0,
|
|
124
|
+
idleGateSuppressedSegments = 0
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
val derived = mutableListOf<HealthConnectModuleInternals.NormalizedSegment>()
|
|
129
|
+
var fallbackSuppressedSegments = 0
|
|
130
|
+
var idleGateSuppressedSegments = 0
|
|
131
|
+
for (index in 0 until breakpoints.lastIndex) {
|
|
132
|
+
val startMs = breakpoints[index]
|
|
133
|
+
val endMs = breakpoints[index + 1]
|
|
134
|
+
if (endMs <= startMs) {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val totalValue = projectSegmentValue(totalCalories, startMs, endMs)
|
|
139
|
+
if (totalValue <= 0.0) {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if (!hasCoverage(basalCalories, startMs, endMs)) {
|
|
143
|
+
fallbackSuppressedSegments += 1
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
val basalValue = projectSegmentValue(basalCalories, startMs, endMs)
|
|
147
|
+
val activeValue = maxOf(totalValue - basalValue, 0.0)
|
|
148
|
+
if (activeValue <= 0.0) {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
val projectedDirectActive = projectSegmentValue(primaryActiveCalories, startMs, endMs)
|
|
152
|
+
val projectedSteps = projectSegmentValue(stepsEvidenceSegments, startMs, endMs)
|
|
153
|
+
val projectedDistance = projectSegmentValue(distanceEvidenceSegments, startMs, endMs)
|
|
154
|
+
val projectedActiveTime = projectSegmentValue(activeTimeEvidenceSegments, startMs, endMs)
|
|
155
|
+
if (projectedDirectActive == 0.0 &&
|
|
156
|
+
projectedSteps == 0.0 &&
|
|
157
|
+
projectedDistance == 0.0 &&
|
|
158
|
+
projectedActiveTime == 0.0
|
|
159
|
+
) {
|
|
160
|
+
idleGateSuppressedSegments += 1
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
derived.add(
|
|
165
|
+
HealthConnectModuleInternals.NormalizedSegment(
|
|
166
|
+
startMs = startMs,
|
|
167
|
+
endMs = endMs,
|
|
168
|
+
value = activeValue,
|
|
169
|
+
sourceType = HealthConnectInternalsConstants.SOURCE_FALLBACK,
|
|
170
|
+
packageName = HealthConnectInternalsConstants.PACKAGE_FALLBACK_TOTAL_MINUS_BASAL,
|
|
171
|
+
deviceModel = ""
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return FallbackDeriveResult(
|
|
177
|
+
segments = derived,
|
|
178
|
+
fallbackSuppressedSegments = fallbackSuppressedSegments,
|
|
179
|
+
idleGateSuppressedSegments = idleGateSuppressedSegments
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private data class FillFallbackResult(
|
|
184
|
+
val segments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
185
|
+
val fallbackUsedSegments: Int
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
private data class ProjectedSegmentRepresentative(
|
|
189
|
+
val sourceType: String,
|
|
190
|
+
val packageName: String,
|
|
191
|
+
val deviceModel: String
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
private data class WeightedSegmentProjection(
|
|
195
|
+
val segment: HealthConnectModuleInternals.NormalizedSegment,
|
|
196
|
+
val projectedValue: Double
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
private fun fillUncoveredIntervals(
|
|
200
|
+
primarySegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
201
|
+
fallbackSegments: List<HealthConnectModuleInternals.NormalizedSegment>
|
|
202
|
+
): FillFallbackResult {
|
|
203
|
+
if (primarySegments.isEmpty()) {
|
|
204
|
+
return FillFallbackResult(
|
|
205
|
+
segments = fallbackSegments,
|
|
206
|
+
fallbackUsedSegments = fallbackSegments.size
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
if (fallbackSegments.isEmpty()) {
|
|
210
|
+
return FillFallbackResult(segments = primarySegments, fallbackUsedSegments = 0)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
val breakpoints =
|
|
214
|
+
listOf(primarySegments, fallbackSegments)
|
|
215
|
+
.flatMap { segments -> segments.flatMap { listOf(it.startMs, it.endMs) } }
|
|
216
|
+
.toSortedSet()
|
|
217
|
+
.toList()
|
|
218
|
+
if (breakpoints.size < 2) {
|
|
219
|
+
return FillFallbackResult(segments = primarySegments, fallbackUsedSegments = 0)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
val merged = mutableListOf<HealthConnectModuleInternals.NormalizedSegment>()
|
|
223
|
+
var fallbackUsedSegments = 0
|
|
224
|
+
for (index in 0 until breakpoints.lastIndex) {
|
|
225
|
+
val startMs = breakpoints[index]
|
|
226
|
+
val endMs = breakpoints[index + 1]
|
|
227
|
+
if (endMs <= startMs) {
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
val primaryValue = projectSegmentValue(primarySegments, startMs, endMs)
|
|
232
|
+
val fallbackValue = projectSegmentValue(fallbackSegments, startMs, endMs)
|
|
233
|
+
if (primaryValue <= 0.0 && fallbackValue <= 0.0) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (primaryValue <= 0.0 && fallbackValue > 0.0) {
|
|
238
|
+
fallbackUsedSegments += 1
|
|
239
|
+
merged.add(
|
|
240
|
+
HealthConnectModuleInternals.NormalizedSegment(
|
|
241
|
+
startMs = startMs,
|
|
242
|
+
endMs = endMs,
|
|
243
|
+
value = fallbackValue,
|
|
244
|
+
sourceType = HealthConnectInternalsConstants.SOURCE_FALLBACK,
|
|
245
|
+
packageName = HealthConnectInternalsConstants.PACKAGE_FALLBACK_TOTAL_MINUS_BASAL,
|
|
246
|
+
deviceModel = ""
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (primaryValue > 0.0) {
|
|
253
|
+
val representative = resolveProjectedSegmentRepresentative(primarySegments, startMs, endMs)
|
|
254
|
+
merged.add(
|
|
255
|
+
HealthConnectModuleInternals.NormalizedSegment(
|
|
256
|
+
startMs = startMs,
|
|
257
|
+
endMs = endMs,
|
|
258
|
+
value = primaryValue,
|
|
259
|
+
sourceType =
|
|
260
|
+
representative?.sourceType
|
|
261
|
+
?: HealthConnectInternalsConstants.SOURCE_UNKNOWN,
|
|
262
|
+
packageName = representative?.packageName.orEmpty(),
|
|
263
|
+
deviceModel = representative?.deviceModel.orEmpty()
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return FillFallbackResult(segments = merged, fallbackUsedSegments = fallbackUsedSegments)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private fun resolveProjectedSegmentRepresentative(
|
|
273
|
+
segments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
274
|
+
startMs: Long,
|
|
275
|
+
endMs: Long
|
|
276
|
+
): ProjectedSegmentRepresentative? {
|
|
277
|
+
return segments
|
|
278
|
+
.asSequence()
|
|
279
|
+
.mapNotNull { segment ->
|
|
280
|
+
val overlapStart = maxOf(startMs, segment.startMs)
|
|
281
|
+
val overlapEnd = minOf(endMs, segment.endMs)
|
|
282
|
+
if (overlapEnd <= overlapStart) {
|
|
283
|
+
return@mapNotNull null
|
|
284
|
+
}
|
|
285
|
+
val segmentDuration = (segment.endMs - segment.startMs).toDouble()
|
|
286
|
+
if (segmentDuration <= 0.0) {
|
|
287
|
+
return@mapNotNull null
|
|
288
|
+
}
|
|
289
|
+
val overlapDuration = (overlapEnd - overlapStart).toDouble()
|
|
290
|
+
val projectedValue = segment.value * (overlapDuration / segmentDuration)
|
|
291
|
+
if (projectedValue <= 0.0) {
|
|
292
|
+
return@mapNotNull null
|
|
293
|
+
}
|
|
294
|
+
WeightedSegmentProjection(segment, projectedValue)
|
|
295
|
+
}
|
|
296
|
+
.sortedWith(
|
|
297
|
+
compareByDescending<WeightedSegmentProjection> { it.projectedValue }
|
|
298
|
+
.thenByDescending {
|
|
299
|
+
HealthConnectSourceTrust.resolveSourcePriority(it.segment.sourceType)
|
|
300
|
+
}
|
|
301
|
+
.thenBy { it.segment.packageName }
|
|
302
|
+
.thenBy { it.segment.startMs }
|
|
303
|
+
.thenBy { it.segment.endMs }
|
|
304
|
+
)
|
|
305
|
+
.firstOrNull()
|
|
306
|
+
?.let { projection ->
|
|
307
|
+
ProjectedSegmentRepresentative(
|
|
308
|
+
sourceType = projection.segment.sourceType,
|
|
309
|
+
packageName = projection.segment.packageName,
|
|
310
|
+
deviceModel = projection.segment.deviceModel
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private fun projectSegmentValue(
|
|
316
|
+
segments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
317
|
+
startMs: Long,
|
|
318
|
+
endMs: Long
|
|
319
|
+
): Double {
|
|
320
|
+
if (segments.isEmpty() || endMs <= startMs) {
|
|
321
|
+
return 0.0
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
var total = 0.0
|
|
325
|
+
segments.forEach { segment ->
|
|
326
|
+
val overlapStart = maxOf(startMs, segment.startMs)
|
|
327
|
+
val overlapEnd = minOf(endMs, segment.endMs)
|
|
328
|
+
if (overlapEnd <= overlapStart) {
|
|
329
|
+
return@forEach
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
val segmentDuration = (segment.endMs - segment.startMs).toDouble()
|
|
333
|
+
if (segmentDuration <= 0.0) {
|
|
334
|
+
return@forEach
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
val overlapDuration = (overlapEnd - overlapStart).toDouble()
|
|
338
|
+
total += segment.value * (overlapDuration / segmentDuration)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return total
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private fun hasCoverage(
|
|
345
|
+
segments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
346
|
+
startMs: Long,
|
|
347
|
+
endMs: Long
|
|
348
|
+
): Boolean {
|
|
349
|
+
return segments.any { segment -> minOf(endMs, segment.endMs) > maxOf(startMs, segment.startMs) }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.exercise.services
|
|
2
|
+
|
|
3
|
+
internal object HealthConnectInternalsConstants {
|
|
4
|
+
const val SUPPORTED_HOURLY = "hourly"
|
|
5
|
+
const val SUPPORTED_DAILY = "daily"
|
|
6
|
+
|
|
7
|
+
const val SCOPE_STEPS = "steps"
|
|
8
|
+
const val SCOPE_ACTIVE_TIMES = "activetimes"
|
|
9
|
+
const val SCOPE_CALORIES = "calories"
|
|
10
|
+
const val SCOPE_DISTANCES = "distances"
|
|
11
|
+
|
|
12
|
+
const val SOURCE_MANUAL_INPUT = "manual_input"
|
|
13
|
+
const val SOURCE_DEVICE = "device"
|
|
14
|
+
const val SOURCE_APP = "app"
|
|
15
|
+
const val SOURCE_UNKNOWN = "unknown"
|
|
16
|
+
const val SOURCE_FALLBACK = "fallback"
|
|
17
|
+
|
|
18
|
+
const val PACKAGE_FALLBACK_TOTAL_MINUS_BASAL = "fallback_total_minus_basal"
|
|
19
|
+
|
|
20
|
+
const val OVERLAP_STRATEGY_SOURCE_PRIORITY = "source_priority"
|
|
21
|
+
const val OVERLAP_STRATEGY_PROPORTIONAL = "proportional"
|
|
22
|
+
|
|
23
|
+
const val WARNING_CALORIES_FALLBACK_USED = "calories_fallback_total_minus_basal_used"
|
|
24
|
+
const val WARNING_CALORIES_FALLBACK_ONLY_USED = "calories_fallback_only_used"
|
|
25
|
+
const val WARNING_CALORIES_FALLBACK_PARTIAL_USED = "calories_fallback_partial_used"
|
|
26
|
+
const val WARNING_BMR_SEED_FROM_LATEST_BEFORE_SINCE_USED =
|
|
27
|
+
"bmr_seed_from_latest_before_since_used"
|
|
28
|
+
const val WARNING_BMR_MISSING_FOR_FALLBACK_WINDOW = "bmr_missing_for_fallback_window"
|
|
29
|
+
const val WARNING_FALLBACK_SUPPRESSED_DUE_TO_MISSING_BASAL =
|
|
30
|
+
"fallback_suppressed_due_to_missing_basal"
|
|
31
|
+
const val WARNING_FALLBACK_SUPPRESSED_DUE_TO_IDLE_WINDOW =
|
|
32
|
+
"fallback_suppressed_due_to_idle_window"
|
|
33
|
+
const val WARNING_UNKNOWN_SOURCE_TYPE = "unknown_source_type_detected"
|
|
34
|
+
|
|
35
|
+
const val TRUST_REASON_ALLOWED = "allowed"
|
|
36
|
+
const val TRUST_REASON_MANUAL_WITH_DEVICE_LOW_CONFIDENCE =
|
|
37
|
+
"manual_with_device_low_confidence"
|
|
38
|
+
const val TRUST_REASON_UNKNOWN_WITH_DEVICE_LOW_CONFIDENCE =
|
|
39
|
+
"unknown_with_device_low_confidence"
|
|
40
|
+
const val TRUST_REASON_MANUAL_INPUT = "manual_input"
|
|
41
|
+
const val TRUST_REASON_UNKNOWN_WITHOUT_DEVICE = "unknown_without_device"
|
|
42
|
+
const val TRUST_REASON_MALFORMED_METADATA = "malformed_metadata"
|
|
43
|
+
|
|
44
|
+
const val TRUST_LEVEL_TRUSTED = "trusted"
|
|
45
|
+
const val TRUST_LEVEL_LOW_CONFIDENCE = "low_confidence"
|
|
46
|
+
const val TRUST_LEVEL_REJECTED = "rejected"
|
|
47
|
+
|
|
48
|
+
val SOURCE_PRIORITY =
|
|
49
|
+
mapOf(
|
|
50
|
+
SOURCE_DEVICE to 4,
|
|
51
|
+
SOURCE_APP to 3,
|
|
52
|
+
SOURCE_UNKNOWN to 2,
|
|
53
|
+
SOURCE_MANUAL_INPUT to 1
|
|
54
|
+
)
|
|
55
|
+
}
|