@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.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
+ }