@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,316 @@
|
|
|
1
|
+
package com.carivaexercisesdk.domain.exercise.services
|
|
2
|
+
|
|
3
|
+
import java.time.ZoneId
|
|
4
|
+
|
|
5
|
+
internal object HealthConnectUnifiedRecordBuilder {
|
|
6
|
+
fun buildUnifiedIntervalRecords(
|
|
7
|
+
stepsSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
8
|
+
activeTimeSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
9
|
+
caloriesSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
10
|
+
distanceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
11
|
+
zoneId: ZoneId,
|
|
12
|
+
roundingScale: Int = 5
|
|
13
|
+
): List<HealthConnectModuleInternals.UnifiedIntervalRecord> {
|
|
14
|
+
return buildUnifiedRecordsResult(
|
|
15
|
+
stepsSegments = stepsSegments,
|
|
16
|
+
activeTimeSegments = activeTimeSegments,
|
|
17
|
+
caloriesSegments = caloriesSegments,
|
|
18
|
+
distanceSegments = distanceSegments,
|
|
19
|
+
zoneId = zoneId,
|
|
20
|
+
roundingScale = roundingScale
|
|
21
|
+
)
|
|
22
|
+
.records
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun buildUnifiedRecordsResult(
|
|
26
|
+
stepsSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
27
|
+
activeTimeSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
28
|
+
caloriesSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
29
|
+
distanceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
30
|
+
zoneId: ZoneId,
|
|
31
|
+
roundingScale: Int = 5
|
|
32
|
+
): HealthConnectModuleInternals.UnifiedRecordsBuildResult {
|
|
33
|
+
val breakpoints =
|
|
34
|
+
listOf(stepsSegments, activeTimeSegments, caloriesSegments, distanceSegments)
|
|
35
|
+
.flatMap { segments ->
|
|
36
|
+
segments.flatMap { segment -> listOf(segment.startMs, segment.endMs) }
|
|
37
|
+
}
|
|
38
|
+
.toSortedSet()
|
|
39
|
+
.toList()
|
|
40
|
+
|
|
41
|
+
if (breakpoints.size < 2) {
|
|
42
|
+
return HealthConnectModuleInternals.UnifiedRecordsBuildResult(
|
|
43
|
+
records = emptyList(),
|
|
44
|
+
warnings = emptyList(),
|
|
45
|
+
projectionPreserved = true,
|
|
46
|
+
roundingScale = roundingScale
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
val records = mutableListOf<HealthConnectModuleInternals.UnifiedIntervalRecord>()
|
|
51
|
+
val warnings = linkedSetOf<String>()
|
|
52
|
+
var projectedStepsRaw = 0.0
|
|
53
|
+
var projectedCaloriesRaw = 0.0
|
|
54
|
+
var projectedDistanceMetersRaw = 0.0
|
|
55
|
+
var projectedActiveTimeRaw = 0.0
|
|
56
|
+
|
|
57
|
+
for (index in 0 until breakpoints.lastIndex) {
|
|
58
|
+
val startMs = breakpoints[index]
|
|
59
|
+
val endMs = breakpoints[index + 1]
|
|
60
|
+
if (endMs <= startMs) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
val rawSteps = projectSegmentValue(stepsSegments, startMs, endMs)
|
|
65
|
+
val rawActiveCalories = projectSegmentValue(caloriesSegments, startMs, endMs)
|
|
66
|
+
val rawDistanceMeters = projectSegmentValue(distanceSegments, startMs, endMs)
|
|
67
|
+
val rawActiveTime = projectSegmentValue(activeTimeSegments, startMs, endMs)
|
|
68
|
+
|
|
69
|
+
val steps = sanitizeMetricValue(rawSteps, warnings)
|
|
70
|
+
val activeCalories = sanitizeMetricValue(rawActiveCalories, warnings)
|
|
71
|
+
val distanceMeters = sanitizeMetricValue(rawDistanceMeters, warnings)
|
|
72
|
+
val activeTime = sanitizeMetricValue(rawActiveTime, warnings)
|
|
73
|
+
|
|
74
|
+
if (steps <= 0.0 && activeCalories <= 0.0 && distanceMeters <= 0.0 && activeTime <= 0.0) {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
projectedStepsRaw += steps
|
|
79
|
+
projectedCaloriesRaw += activeCalories
|
|
80
|
+
projectedDistanceMetersRaw += distanceMeters
|
|
81
|
+
projectedActiveTimeRaw += activeTime
|
|
82
|
+
|
|
83
|
+
records.add(
|
|
84
|
+
HealthConnectModuleInternals.UnifiedIntervalRecord(
|
|
85
|
+
steps =
|
|
86
|
+
normalizeNegativeZero(
|
|
87
|
+
HealthConnectModuleInternals.roundToScale(
|
|
88
|
+
steps,
|
|
89
|
+
roundingScale
|
|
90
|
+
)
|
|
91
|
+
),
|
|
92
|
+
activeCalories =
|
|
93
|
+
normalizeNegativeZero(
|
|
94
|
+
HealthConnectModuleInternals.roundToScale(
|
|
95
|
+
activeCalories,
|
|
96
|
+
roundingScale
|
|
97
|
+
)
|
|
98
|
+
),
|
|
99
|
+
activeCaloriesUnit = "kcal",
|
|
100
|
+
distance =
|
|
101
|
+
normalizeNegativeZero(
|
|
102
|
+
HealthConnectModuleInternals.roundToScale(
|
|
103
|
+
HealthConnectModuleInternals.toKilometers(
|
|
104
|
+
distanceMeters
|
|
105
|
+
),
|
|
106
|
+
roundingScale
|
|
107
|
+
)
|
|
108
|
+
),
|
|
109
|
+
distanceUnit = "km",
|
|
110
|
+
activeTime =
|
|
111
|
+
normalizeNegativeZero(
|
|
112
|
+
HealthConnectModuleInternals.roundToScale(
|
|
113
|
+
activeTime,
|
|
114
|
+
roundingScale
|
|
115
|
+
)
|
|
116
|
+
),
|
|
117
|
+
activeTimeUnit = "milliseconds",
|
|
118
|
+
startDate = HealthConnectModuleInternals.toIsoOffsetString(startMs, zoneId),
|
|
119
|
+
endDate = HealthConnectModuleInternals.toIsoOffsetString(endMs, zoneId),
|
|
120
|
+
meta =
|
|
121
|
+
resolveUnifiedMeta(
|
|
122
|
+
stepsSegments = stepsSegments,
|
|
123
|
+
activeTimeSegments = activeTimeSegments,
|
|
124
|
+
caloriesSegments = caloriesSegments,
|
|
125
|
+
distanceSegments = distanceSegments,
|
|
126
|
+
startMs = startMs,
|
|
127
|
+
endMs = endMs
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
val sourceTotals =
|
|
134
|
+
ProjectionTotals(
|
|
135
|
+
steps = sanitizeMetricValue(stepsSegments.sumOf { it.value }, warnings),
|
|
136
|
+
activeCalories =
|
|
137
|
+
sanitizeMetricValue(caloriesSegments.sumOf { it.value }, warnings),
|
|
138
|
+
distanceMeters =
|
|
139
|
+
sanitizeMetricValue(distanceSegments.sumOf { it.value }, warnings),
|
|
140
|
+
activeTime =
|
|
141
|
+
sanitizeMetricValue(activeTimeSegments.sumOf { it.value }, warnings)
|
|
142
|
+
)
|
|
143
|
+
val projectedTotals =
|
|
144
|
+
ProjectionTotals(
|
|
145
|
+
steps = projectedStepsRaw,
|
|
146
|
+
activeCalories = projectedCaloriesRaw,
|
|
147
|
+
distanceMeters = projectedDistanceMetersRaw,
|
|
148
|
+
activeTime = projectedActiveTimeRaw
|
|
149
|
+
)
|
|
150
|
+
val projectionPreserved = isProjectionPreserved(sourceTotals, projectedTotals)
|
|
151
|
+
if (!projectionPreserved) {
|
|
152
|
+
warnings.add("projection_totals_mismatch")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return HealthConnectModuleInternals.UnifiedRecordsBuildResult(
|
|
156
|
+
records = records,
|
|
157
|
+
warnings = warnings.toList(),
|
|
158
|
+
projectionPreserved = projectionPreserved,
|
|
159
|
+
roundingScale = roundingScale
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun resolveUnifiedMeta(
|
|
164
|
+
stepsSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
165
|
+
activeTimeSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
166
|
+
caloriesSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
167
|
+
distanceSegments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
168
|
+
startMs: Long,
|
|
169
|
+
endMs: Long
|
|
170
|
+
): HealthConnectModuleInternals.UnifiedRecordMeta {
|
|
171
|
+
val allSegments =
|
|
172
|
+
listOf(stepsSegments, activeTimeSegments, caloriesSegments, distanceSegments).flatten()
|
|
173
|
+
|
|
174
|
+
val packageWeights = linkedMapOf<String, Double>()
|
|
175
|
+
val modelWeights = linkedMapOf<String, Double>()
|
|
176
|
+
allSegments.forEach { segment ->
|
|
177
|
+
if (segment.packageName.isEmpty()) {
|
|
178
|
+
return@forEach
|
|
179
|
+
}
|
|
180
|
+
if (segment.value <= 0.0) {
|
|
181
|
+
return@forEach
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
val overlapStart = maxOf(startMs, segment.startMs)
|
|
185
|
+
val overlapEnd = minOf(endMs, segment.endMs)
|
|
186
|
+
if (overlapEnd <= overlapStart) {
|
|
187
|
+
return@forEach
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
val segmentDuration = (segment.endMs - segment.startMs).toDouble()
|
|
191
|
+
if (segmentDuration <= 0.0) {
|
|
192
|
+
return@forEach
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
val overlapDuration = (overlapEnd - overlapStart).toDouble()
|
|
196
|
+
val projectedValue = segment.value * (overlapDuration / segmentDuration)
|
|
197
|
+
packageWeights[segment.packageName] =
|
|
198
|
+
(packageWeights[segment.packageName] ?: 0.0) + projectedValue
|
|
199
|
+
if (segment.deviceModel.isNotEmpty()) {
|
|
200
|
+
modelWeights[segment.deviceModel] = (modelWeights[segment.deviceModel] ?: 0.0) + projectedValue
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
val selectedPackage =
|
|
205
|
+
packageWeights
|
|
206
|
+
.entries
|
|
207
|
+
.sortedWith(
|
|
208
|
+
compareByDescending<Map.Entry<String, Double>> { it.value }.thenBy {
|
|
209
|
+
it.key
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
.firstOrNull()
|
|
213
|
+
?.key
|
|
214
|
+
.orEmpty()
|
|
215
|
+
|
|
216
|
+
val selectedSource =
|
|
217
|
+
if (selectedPackage.isEmpty()) {
|
|
218
|
+
HealthConnectInternalsConstants.SOURCE_UNKNOWN
|
|
219
|
+
} else {
|
|
220
|
+
allSegments
|
|
221
|
+
.asSequence()
|
|
222
|
+
.filter { segment ->
|
|
223
|
+
segment.packageName == selectedPackage &&
|
|
224
|
+
segment.value > 0.0 &&
|
|
225
|
+
minOf(endMs, segment.endMs) > maxOf(startMs, segment.startMs)
|
|
226
|
+
}
|
|
227
|
+
.map { HealthConnectSourceTrust.normalizeSourceType(it.sourceType) }
|
|
228
|
+
.maxByOrNull { HealthConnectSourceTrust.resolveSourcePriority(it) }
|
|
229
|
+
?: HealthConnectInternalsConstants.SOURCE_UNKNOWN
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
val selectedModel =
|
|
233
|
+
modelWeights
|
|
234
|
+
.entries
|
|
235
|
+
.sortedWith(
|
|
236
|
+
compareByDescending<Map.Entry<String, Double>> { it.value }.thenBy {
|
|
237
|
+
it.key
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
.firstOrNull()
|
|
241
|
+
?.key
|
|
242
|
+
.orEmpty()
|
|
243
|
+
|
|
244
|
+
return HealthConnectModuleInternals.UnifiedRecordMeta(
|
|
245
|
+
source = selectedSource,
|
|
246
|
+
device = selectedPackage,
|
|
247
|
+
deviceModel = selectedModel,
|
|
248
|
+
trustLevel = HealthConnectSourceTrust.resolveTrustLevelFromSource(selectedSource)
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun projectSegmentValue(
|
|
253
|
+
segments: List<HealthConnectModuleInternals.NormalizedSegment>,
|
|
254
|
+
startMs: Long,
|
|
255
|
+
endMs: Long
|
|
256
|
+
): Double {
|
|
257
|
+
if (segments.isEmpty() || endMs <= startMs) {
|
|
258
|
+
return 0.0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
var total = 0.0
|
|
262
|
+
segments.forEach { segment ->
|
|
263
|
+
val overlapStart = maxOf(startMs, segment.startMs)
|
|
264
|
+
val overlapEnd = minOf(endMs, segment.endMs)
|
|
265
|
+
if (overlapEnd <= overlapStart) {
|
|
266
|
+
return@forEach
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
val segmentDuration = (segment.endMs - segment.startMs).toDouble()
|
|
270
|
+
if (segmentDuration <= 0.0) {
|
|
271
|
+
return@forEach
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
val overlapDuration = (overlapEnd - overlapStart).toDouble()
|
|
275
|
+
total += segment.value * (overlapDuration / segmentDuration)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return total
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private data class ProjectionTotals(
|
|
282
|
+
val steps: Double,
|
|
283
|
+
val activeCalories: Double,
|
|
284
|
+
val distanceMeters: Double,
|
|
285
|
+
val activeTime: Double
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
private fun sanitizeMetricValue(value: Double, warnings: MutableSet<String>): Double {
|
|
289
|
+
if (!value.isFinite()) {
|
|
290
|
+
warnings.add("non_finite_metric_value_sanitized")
|
|
291
|
+
return 0.0
|
|
292
|
+
}
|
|
293
|
+
if (value < 0.0) {
|
|
294
|
+
warnings.add("negative_metric_value_clamped")
|
|
295
|
+
return 0.0
|
|
296
|
+
}
|
|
297
|
+
return normalizeNegativeZero(value)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private fun normalizeNegativeZero(value: Double): Double {
|
|
301
|
+
return if (value == -0.0) 0.0 else value
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private fun isProjectionPreserved(
|
|
305
|
+
sourceTotals: ProjectionTotals,
|
|
306
|
+
projectedTotals: ProjectionTotals,
|
|
307
|
+
tolerance: Double = 1e-6
|
|
308
|
+
): Boolean {
|
|
309
|
+
return kotlin.math.abs(sourceTotals.steps - projectedTotals.steps) <= tolerance &&
|
|
310
|
+
kotlin.math.abs(sourceTotals.activeCalories - projectedTotals.activeCalories) <=
|
|
311
|
+
tolerance &&
|
|
312
|
+
kotlin.math.abs(sourceTotals.distanceMeters - projectedTotals.distanceMeters) <=
|
|
313
|
+
tolerance &&
|
|
314
|
+
kotlin.math.abs(sourceTotals.activeTime - projectedTotals.activeTime) <= tolerance
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package com.carivaexercisesdk.infrastructure.healthconnect
|
|
2
|
+
|
|
3
|
+
import android.content.ActivityNotFoundException
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import androidx.activity.ComponentActivity
|
|
7
|
+
import androidx.activity.result.ActivityResultLauncher
|
|
8
|
+
import androidx.health.connect.client.HealthConnectClient
|
|
9
|
+
import androidx.health.connect.client.PermissionController
|
|
10
|
+
import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
|
|
11
|
+
import androidx.health.connect.client.records.BasalMetabolicRateRecord
|
|
12
|
+
import androidx.health.connect.client.records.DistanceRecord
|
|
13
|
+
import androidx.health.connect.client.records.ExerciseSessionRecord
|
|
14
|
+
import androidx.health.connect.client.records.StepsRecord
|
|
15
|
+
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
|
|
16
|
+
import com.carivaexercisesdk.application.connection.port.HealthConnectConnectionPort
|
|
17
|
+
import com.carivaexercisesdk.application.connection.port.PermissionRequestPort
|
|
18
|
+
import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
|
|
19
|
+
import com.carivaexercisesdk.domain.connection.valueobject.PermissionScope
|
|
20
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
21
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
22
|
+
import kotlin.coroutines.resume
|
|
23
|
+
import kotlin.coroutines.resumeWithException
|
|
24
|
+
import kotlinx.coroutines.Dispatchers
|
|
25
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
26
|
+
import kotlinx.coroutines.withContext
|
|
27
|
+
|
|
28
|
+
class HealthConnectConnectionAdapter(
|
|
29
|
+
private val reactApplicationContext: ReactApplicationContext,
|
|
30
|
+
private val healthConnectClient: HealthConnectClient,
|
|
31
|
+
private val permissionRequestKeySeed: AtomicInteger
|
|
32
|
+
) : HealthConnectConnectionPort, PermissionRequestPort {
|
|
33
|
+
|
|
34
|
+
private val permissionByScope =
|
|
35
|
+
mapOf(
|
|
36
|
+
PermissionScope.CALORIES to
|
|
37
|
+
setOf(
|
|
38
|
+
androidx.health.connect.client.permission.HealthPermission
|
|
39
|
+
.getReadPermission(ActiveCaloriesBurnedRecord::class),
|
|
40
|
+
androidx.health.connect.client.permission.HealthPermission
|
|
41
|
+
.getReadPermission(TotalCaloriesBurnedRecord::class),
|
|
42
|
+
androidx.health.connect.client.permission.HealthPermission
|
|
43
|
+
.getReadPermission(BasalMetabolicRateRecord::class)
|
|
44
|
+
),
|
|
45
|
+
PermissionScope.STEPS to
|
|
46
|
+
setOf(
|
|
47
|
+
androidx.health.connect.client.permission.HealthPermission
|
|
48
|
+
.getReadPermission(StepsRecord::class)
|
|
49
|
+
),
|
|
50
|
+
PermissionScope.DISTANCES to
|
|
51
|
+
setOf(
|
|
52
|
+
androidx.health.connect.client.permission.HealthPermission
|
|
53
|
+
.getReadPermission(DistanceRecord::class)
|
|
54
|
+
),
|
|
55
|
+
PermissionScope.ACTIVETIMES to
|
|
56
|
+
setOf(
|
|
57
|
+
androidx.health.connect.client.permission.HealthPermission
|
|
58
|
+
.getReadPermission(ExerciseSessionRecord::class)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
override suspend fun getHealthConnectStatus(): HealthConnectStatus {
|
|
63
|
+
val sdkStatus = HealthConnectClient.getSdkStatus(reactApplicationContext)
|
|
64
|
+
return when (sdkStatus) {
|
|
65
|
+
HealthConnectClient.SDK_AVAILABLE -> HealthConnectStatus.AVAILABLE
|
|
66
|
+
HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED ->
|
|
67
|
+
HealthConnectStatus.PROVIDER_UPDATE_REQUIRED
|
|
68
|
+
HealthConnectClient.SDK_UNAVAILABLE -> HealthConnectStatus.UNAVAILABLE
|
|
69
|
+
else -> HealthConnectStatus.UNKNOWN
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun isHealthConnectInstalled(): Boolean {
|
|
74
|
+
return HEALTH_CONNECT_PACKAGES.any(::isPackageInstalled)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override suspend fun getGrantedPermissions(): Set<String> {
|
|
78
|
+
return withContext(Dispatchers.IO) {
|
|
79
|
+
healthConnectClient.permissionController.getGrantedPermissions()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun resolvePermissions(scopes: Set<PermissionScope>): Set<String> {
|
|
84
|
+
return scopes.flatMapTo(mutableSetOf()) { scope -> permissionByScope[scope].orEmpty() }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override suspend fun requestPermissions(permissions: Set<String>) {
|
|
88
|
+
val activity = reactApplicationContext.currentActivity as? ComponentActivity ?: return
|
|
89
|
+
|
|
90
|
+
suspendCancellableCoroutine { continuation ->
|
|
91
|
+
val requestKey = "health_connect_permission_${permissionRequestKeySeed.incrementAndGet()}"
|
|
92
|
+
lateinit var launcher: ActivityResultLauncher<Set<String>>
|
|
93
|
+
|
|
94
|
+
launcher =
|
|
95
|
+
activity.activityResultRegistry.register(
|
|
96
|
+
requestKey,
|
|
97
|
+
PermissionController.createRequestPermissionResultContract()
|
|
98
|
+
) {
|
|
99
|
+
launcher.unregister()
|
|
100
|
+
if (continuation.isActive) {
|
|
101
|
+
continuation.resume(Unit)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
continuation.invokeOnCancellation { launcher.unregister() }
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
launcher.launch(permissions)
|
|
109
|
+
} catch (t: Throwable) {
|
|
110
|
+
launcher.unregister()
|
|
111
|
+
if (continuation.isActive) {
|
|
112
|
+
continuation.resumeWithException(t)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override fun openHealthConnectStore() {
|
|
119
|
+
val marketIntent =
|
|
120
|
+
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$HEALTH_CONNECT_PLAY_STORE_PACKAGE"))
|
|
121
|
+
.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
|
122
|
+
|
|
123
|
+
val webIntent =
|
|
124
|
+
Intent(
|
|
125
|
+
Intent.ACTION_VIEW,
|
|
126
|
+
Uri.parse(
|
|
127
|
+
"https://play.google.com/store/apps/details?id=$HEALTH_CONNECT_PLAY_STORE_PACKAGE"
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
reactApplicationContext.startActivity(marketIntent)
|
|
134
|
+
} catch (_: ActivityNotFoundException) {
|
|
135
|
+
reactApplicationContext.startActivity(webIntent)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun isPackageInstalled(packageName: String): Boolean {
|
|
140
|
+
return try {
|
|
141
|
+
reactApplicationContext.packageManager.getPackageInfo(packageName, 0)
|
|
142
|
+
true
|
|
143
|
+
} catch (_: Throwable) {
|
|
144
|
+
false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private companion object {
|
|
149
|
+
private const val HEALTH_CONNECT_PLAY_STORE_PACKAGE = "com.google.android.apps.healthdata"
|
|
150
|
+
private val HEALTH_CONNECT_PACKAGES =
|
|
151
|
+
listOf(
|
|
152
|
+
"com.google.android.apps.healthdata",
|
|
153
|
+
"com.google.android.healthconnect.controller"
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|