@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,222 @@
1
+ package com.carivaexercisesdk
2
+
3
+ import androidx.health.connect.client.HealthConnectClient
4
+ import com.carivaexercisesdk.application.connection.command.ConnectCommand
5
+ import com.carivaexercisesdk.application.connection.command.MissingAppBehavior
6
+ import com.carivaexercisesdk.application.connection.handler.ConnectHandler
7
+ import com.carivaexercisesdk.application.connection.handler.InvalidPermissionScopeException
8
+ import com.carivaexercisesdk.application.datasource.command.SetDatasourcePolicyCommand
9
+ import com.carivaexercisesdk.application.datasource.handler.GetDatasourcePolicyHandler
10
+ import com.carivaexercisesdk.application.datasource.handler.SetDatasourcePolicyHandler
11
+ import com.carivaexercisesdk.application.exercise.handler.ExerciseDataException
12
+ import com.carivaexercisesdk.application.exercise.handler.GetExerciseDataHandler
13
+ import com.carivaexercisesdk.application.exercise.query.GetExerciseDataQuery
14
+ import com.carivaexercisesdk.infrastructure.healthconnect.HealthConnectConnectionAdapter
15
+ import com.carivaexercisesdk.infrastructure.healthconnect.HealthConnectExerciseRecordAdapter
16
+ import com.carivaexercisesdk.infrastructure.persistence.InMemoryDatasourcePolicyRepository
17
+ import com.carivaexercisesdk.infrastructure.reactnative.DatasourcePolicyJsonParser
18
+ import com.carivaexercisesdk.infrastructure.reactnative.ConnectResultMapper
19
+ import com.carivaexercisesdk.infrastructure.reactnative.DatasourcePolicyMapper
20
+ import com.carivaexercisesdk.infrastructure.reactnative.ExerciseDataMapper
21
+ import com.facebook.react.bridge.Promise
22
+ import com.facebook.react.bridge.ReactApplicationContext
23
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
24
+ import com.facebook.react.bridge.ReactMethod
25
+ import java.util.concurrent.atomic.AtomicInteger
26
+ import kotlinx.coroutines.CoroutineScope
27
+ import kotlinx.coroutines.Dispatchers
28
+ import kotlinx.coroutines.SupervisorJob
29
+ import kotlinx.coroutines.cancel
30
+ import kotlinx.coroutines.launch
31
+ import org.json.JSONObject
32
+
33
+ class HealthConnectModule(reactContext: ReactApplicationContext) :
34
+ ReactContextBaseJavaModule(reactContext) {
35
+
36
+ private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
37
+
38
+ private val healthConnectClient: HealthConnectClient by lazy {
39
+ HealthConnectClient.getOrCreate(reactApplicationContext)
40
+ }
41
+
42
+ private val permissionRequestKeySeed = AtomicInteger(0)
43
+
44
+ private val datasourcePolicyRepository = InMemoryDatasourcePolicyRepository()
45
+ private val setDatasourcePolicyHandler = SetDatasourcePolicyHandler(datasourcePolicyRepository)
46
+ private val getDatasourcePolicyHandler = GetDatasourcePolicyHandler(datasourcePolicyRepository)
47
+
48
+ private val healthConnectConnectionAdapter: HealthConnectConnectionAdapter by lazy {
49
+ HealthConnectConnectionAdapter(
50
+ reactApplicationContext = reactApplicationContext,
51
+ healthConnectClient = healthConnectClient,
52
+ permissionRequestKeySeed = permissionRequestKeySeed
53
+ )
54
+ }
55
+
56
+ private val connectHandler: ConnectHandler by lazy {
57
+ ConnectHandler(
58
+ connectionPort = healthConnectConnectionAdapter,
59
+ permissionRequestPort = healthConnectConnectionAdapter
60
+ )
61
+ }
62
+
63
+ private val healthConnectExerciseRecordAdapter: HealthConnectExerciseRecordAdapter by lazy {
64
+ HealthConnectExerciseRecordAdapter(
65
+ reactApplicationContext = reactApplicationContext,
66
+ healthConnectClient = healthConnectClient,
67
+ datasourceAllowlistProvider = {
68
+ datasourcePolicyRepository.get().allowlist.map { it.wireValue }.toSet()
69
+ },
70
+ datasourcePackageAllowlistProvider = {
71
+ datasourcePolicyRepository.get().packageAllowlist
72
+ }
73
+ )
74
+ }
75
+
76
+ private val getExerciseDataHandler: GetExerciseDataHandler by lazy {
77
+ GetExerciseDataHandler(healthConnectExerciseRecordAdapter)
78
+ }
79
+
80
+ override fun getName(): String = NAME
81
+
82
+ @ReactMethod
83
+ fun connect(optionsJson: String?, promise: Promise) {
84
+ moduleScope.launch {
85
+ try {
86
+ val command = parseConnectCommand(optionsJson)
87
+ val result = connectHandler.handle(command)
88
+ promise.resolve(ConnectResultMapper.toWritableMap(result))
89
+ } catch (e: InvalidPermissionScopeException) {
90
+ promise.reject(
91
+ "E_INVALID_PERMISSION_SCOPE",
92
+ "Unsupported permissionScope values: ${e.unsupportedScopes.joinToString(", ")}"
93
+ )
94
+ } catch (t: Throwable) {
95
+ promise.reject("E_CONNECT", "Failed to connect to Health Connect.", t)
96
+ }
97
+ }
98
+ }
99
+
100
+ @ReactMethod
101
+ fun getExerciseData(optionsJson: String, promise: Promise) {
102
+ moduleScope.launch {
103
+ try {
104
+ val query = parseExerciseQuery(optionsJson)
105
+ val result = getExerciseDataHandler.handle(query)
106
+ promise.resolve(ExerciseDataMapper.toWritableMap(result))
107
+ } catch (e: ExerciseDataException) {
108
+ promise.reject(e.code, e.detail)
109
+ } catch (t: Throwable) {
110
+ val rootMessage =
111
+ buildString {
112
+ append("Failed to get exercise data.")
113
+ append(" ")
114
+ append(t.javaClass.simpleName)
115
+ t.message?.takeIf { it.isNotBlank() }?.let { message ->
116
+ append(": ")
117
+ append(message)
118
+ }
119
+ }
120
+ promise.reject("E_GET_EXERCISE_DATA", rootMessage, t)
121
+ }
122
+ }
123
+ }
124
+
125
+ @ReactMethod
126
+ fun setAllowDatasource(configJson: String, promise: Promise) {
127
+ try {
128
+ val config = JSONObject(configJson)
129
+ val allowlist = DatasourcePolicyJsonParser.parseDatasourceAllowlist(config)
130
+ val packageAllowlist = DatasourcePolicyJsonParser.parseDatasourceScopePackages(config)
131
+
132
+ setDatasourcePolicyHandler.handle(
133
+ SetDatasourcePolicyCommand(
134
+ allowlist = allowlist,
135
+ packageAllowlist = packageAllowlist
136
+ )
137
+ )
138
+ promise.resolve(DatasourcePolicyMapper.toSuccessMap(success = true))
139
+ } catch (e: Exception) {
140
+ promise.reject("E_INVALID_JSON", "Invalid JSON format for datasource config.", e)
141
+ } catch (t: Throwable) {
142
+ promise.reject("E_SET_ALLOW_DATASOURCE", "Failed to set allow datasource.", t)
143
+ }
144
+ }
145
+
146
+ @ReactMethod
147
+ fun getAllowDatasource(promise: Promise) {
148
+ try {
149
+ val policy = getDatasourcePolicyHandler.handle()
150
+ promise.resolve(DatasourcePolicyMapper.toConfigMap(policy))
151
+ } catch (t: Throwable) {
152
+ promise.reject("E_GET_ALLOW_DATASOURCE", "Failed to get allow datasource.", t)
153
+ }
154
+ }
155
+
156
+ override fun invalidate() {
157
+ moduleScope.cancel()
158
+ super.invalidate()
159
+ }
160
+
161
+ private fun parseConnectCommand(optionsJson: String?): ConnectCommand {
162
+ val json =
163
+ if (optionsJson.isNullOrBlank()) {
164
+ JSONObject()
165
+ } else {
166
+ JSONObject(optionsJson)
167
+ }
168
+
169
+ val onMissingApp = json.optString("onMissingApp", STATUS_MISSING_APP).trim().lowercase()
170
+ val requestPermission = json.optBoolean("requestPermission", false)
171
+ val permissionScope =
172
+ json.optJSONArray("permissionScope")?.let { array ->
173
+ buildList {
174
+ for (i in 0 until array.length()) {
175
+ (array[i] as? String)?.let { add(it) }
176
+ }
177
+ }
178
+ }
179
+
180
+ return ConnectCommand(
181
+ onMissingApp =
182
+ if (onMissingApp == OPEN_MISSING_APP) {
183
+ MissingAppBehavior.OPEN
184
+ } else {
185
+ MissingAppBehavior.STATUS
186
+ },
187
+ requestPermission = requestPermission,
188
+ permissionScope = permissionScope
189
+ )
190
+ }
191
+
192
+ private fun parseExerciseQuery(optionsJson: String): GetExerciseDataQuery {
193
+ val json = JSONObject(optionsJson)
194
+
195
+ if (!json.has("sinceMillis") || !json.has("untilMillis")) {
196
+ throw IllegalArgumentException("Both sinceMillis and untilMillis are required.")
197
+ }
198
+
199
+ return GetExerciseDataQuery(
200
+ sinceMillis = parseRequiredMillis(json, "sinceMillis"),
201
+ untilMillis = parseRequiredMillis(json, "untilMillis"),
202
+ type = json.optString("type", "all").trim().lowercase(),
203
+ bucketPeriod = json.optString("bucketPeriod", "hourly").trim().lowercase(),
204
+ debug = json.optBoolean("debug", false)
205
+ )
206
+ }
207
+
208
+ private fun parseRequiredMillis(json: JSONObject, key: String): Long {
209
+ val rawValue = json.opt(key)
210
+ return when (rawValue) {
211
+ is Number -> rawValue.toLong()
212
+ is String -> rawValue.trim().toLongOrNull()
213
+ else -> null
214
+ } ?: throw IllegalArgumentException("$key must be a valid epoch millis number.")
215
+ }
216
+
217
+ companion object {
218
+ const val NAME = "HealthConnectModule"
219
+ private const val STATUS_MISSING_APP = "status"
220
+ private const val OPEN_MISSING_APP = "open"
221
+ }
222
+ }
@@ -0,0 +1,11 @@
1
+ package com.carivaexercisesdk
2
+
3
+ import android.app.Activity
4
+ import android.os.Bundle
5
+
6
+ class HealthConnectPermissionUsageActivity : Activity() {
7
+ override fun onCreate(savedInstanceState: Bundle?) {
8
+ super.onCreate(savedInstanceState)
9
+ finish()
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ package com.carivaexercisesdk
2
+
3
+ import android.app.Activity
4
+ import android.os.Bundle
5
+
6
+ class HealthConnectPermissionsRationaleActivity : Activity() {
7
+ override fun onCreate(savedInstanceState: Bundle?) {
8
+ super.onCreate(savedInstanceState)
9
+ finish()
10
+ }
11
+ }
@@ -0,0 +1,23 @@
1
+ package com.carivaexercisesdk
2
+
3
+ internal data class PagedResult<T>(
4
+ val records: List<T>,
5
+ val nextPageToken: String?
6
+ )
7
+
8
+ internal suspend fun <T> collectPagedRecords(
9
+ readPage: suspend (pageToken: String) -> PagedResult<T>
10
+ ): List<T> {
11
+ val allRecords = mutableListOf<T>()
12
+ var pageToken = ""
13
+
14
+ while (true) {
15
+ val page = readPage(pageToken)
16
+ allRecords.addAll(page.records)
17
+
18
+ val nextToken = page.nextPageToken?.takeUnless { it.isBlank() } ?: break
19
+ pageToken = nextToken
20
+ }
21
+
22
+ return allRecords
23
+ }
@@ -0,0 +1,12 @@
1
+ package com.carivaexercisesdk.application.connection.command
2
+
3
+ enum class MissingAppBehavior {
4
+ STATUS,
5
+ OPEN
6
+ }
7
+
8
+ data class ConnectCommand(
9
+ val onMissingApp: MissingAppBehavior = MissingAppBehavior.STATUS,
10
+ val requestPermission: Boolean = false,
11
+ val permissionScope: List<String>? = null
12
+ )
@@ -0,0 +1,13 @@
1
+ package com.carivaexercisesdk.application.connection.dto
2
+
3
+ import com.carivaexercisesdk.domain.connection.valueobject.ConnectionNextAction
4
+ import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
5
+
6
+ data class ConnectResultDto(
7
+ val ready: Boolean,
8
+ val healthConnectInstalled: Boolean,
9
+ val healthConnectStatus: HealthConnectStatus,
10
+ val hasPermissions: Boolean,
11
+ val nextAction: ConnectionNextAction,
12
+ val requestedPermissions: List<String>
13
+ )
@@ -0,0 +1,79 @@
1
+ package com.carivaexercisesdk.application.connection.handler
2
+
3
+ import com.carivaexercisesdk.application.connection.command.ConnectCommand
4
+ import com.carivaexercisesdk.application.connection.command.MissingAppBehavior
5
+ import com.carivaexercisesdk.application.connection.dto.ConnectResultDto
6
+ import com.carivaexercisesdk.application.connection.port.HealthConnectConnectionPort
7
+ import com.carivaexercisesdk.application.connection.port.PermissionRequestPort
8
+ import com.carivaexercisesdk.domain.connection.service.ConnectionDecisionService
9
+ import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
10
+ import com.carivaexercisesdk.domain.connection.valueobject.PermissionScope
11
+
12
+ class ConnectHandler(
13
+ private val connectionPort: HealthConnectConnectionPort,
14
+ private val permissionRequestPort: PermissionRequestPort,
15
+ private val decisionService: ConnectionDecisionService = ConnectionDecisionService()
16
+ ) {
17
+ suspend fun handle(command: ConnectCommand): ConnectResultDto {
18
+ val unsupportedScopes = PermissionScope.findUnsupported(command.permissionScope)
19
+ if (unsupportedScopes.isNotEmpty()) {
20
+ throw InvalidPermissionScopeException(unsupportedScopes)
21
+ }
22
+
23
+ val normalizedScopes = PermissionScope.normalizeAll(command.permissionScope)
24
+ val effectiveScopes = if (normalizedScopes.isEmpty()) PermissionScope.defaults() else normalizedScopes
25
+ val requestedPermissions = connectionPort.resolvePermissions(effectiveScopes)
26
+
27
+ val healthConnectStatus = connectionPort.getHealthConnectStatus()
28
+ val healthConnectInstalled =
29
+ healthConnectStatus == HealthConnectStatus.AVAILABLE ||
30
+ connectionPort.isHealthConnectInstalled()
31
+
32
+ var grantedPermissions =
33
+ if (healthConnectStatus == HealthConnectStatus.AVAILABLE) {
34
+ connectionPort.getGrantedPermissions()
35
+ } else {
36
+ emptySet()
37
+ }
38
+
39
+ var hasPermissions =
40
+ healthConnectStatus == HealthConnectStatus.AVAILABLE &&
41
+ requestedPermissions.all(grantedPermissions::contains)
42
+
43
+ val shouldRequestPermission =
44
+ healthConnectInstalled &&
45
+ healthConnectStatus == HealthConnectStatus.AVAILABLE &&
46
+ !hasPermissions &&
47
+ command.requestPermission
48
+
49
+ if (shouldRequestPermission) {
50
+ permissionRequestPort.requestPermissions(requestedPermissions)
51
+ grantedPermissions = connectionPort.getGrantedPermissions()
52
+ hasPermissions = requestedPermissions.all(grantedPermissions::contains)
53
+ }
54
+
55
+ if (command.onMissingApp == MissingAppBehavior.OPEN && !healthConnectInstalled) {
56
+ connectionPort.openHealthConnectStore()
57
+ }
58
+
59
+ val aggregate =
60
+ decisionService.decide(
61
+ healthConnectInstalled = healthConnectInstalled,
62
+ healthConnectStatus = healthConnectStatus,
63
+ hasPermissions = hasPermissions,
64
+ requestedPermissions = requestedPermissions
65
+ )
66
+
67
+ return ConnectResultDto(
68
+ ready = aggregate.ready,
69
+ healthConnectInstalled = aggregate.healthConnectInstalled,
70
+ healthConnectStatus = aggregate.healthConnectStatus,
71
+ hasPermissions = aggregate.hasPermissions,
72
+ nextAction = aggregate.nextAction,
73
+ requestedPermissions = aggregate.requestedPermissions.sorted()
74
+ )
75
+ }
76
+ }
77
+
78
+ class InvalidPermissionScopeException(val unsupportedScopes: Set<String>) :
79
+ IllegalArgumentException("Unsupported permission scope values")
@@ -0,0 +1,16 @@
1
+ package com.carivaexercisesdk.application.connection.port
2
+
3
+ import com.carivaexercisesdk.domain.connection.valueobject.HealthConnectStatus
4
+ import com.carivaexercisesdk.domain.connection.valueobject.PermissionScope
5
+
6
+ interface HealthConnectConnectionPort {
7
+ suspend fun getHealthConnectStatus(): HealthConnectStatus
8
+
9
+ fun isHealthConnectInstalled(): Boolean
10
+
11
+ suspend fun getGrantedPermissions(): Set<String>
12
+
13
+ fun resolvePermissions(scopes: Set<PermissionScope>): Set<String>
14
+
15
+ fun openHealthConnectStore()
16
+ }
@@ -0,0 +1,5 @@
1
+ package com.carivaexercisesdk.application.connection.port
2
+
3
+ interface PermissionRequestPort {
4
+ suspend fun requestPermissions(permissions: Set<String>)
5
+ }
@@ -0,0 +1,6 @@
1
+ package com.carivaexercisesdk.application.datasource.command
2
+
3
+ data class SetDatasourcePolicyCommand(
4
+ val allowlist: List<String>,
5
+ val packageAllowlist: List<String>
6
+ )
@@ -0,0 +1,10 @@
1
+ package com.carivaexercisesdk.application.datasource.handler
2
+
3
+ import com.carivaexercisesdk.application.datasource.port.DatasourcePolicyRepository
4
+ import com.carivaexercisesdk.domain.datasource.entity.DatasourcePolicy
5
+
6
+ class GetDatasourcePolicyHandler(private val repository: DatasourcePolicyRepository) {
7
+ fun handle(): DatasourcePolicy {
8
+ return repository.get()
9
+ }
10
+ }
@@ -0,0 +1,22 @@
1
+ package com.carivaexercisesdk.application.datasource.handler
2
+
3
+ import com.carivaexercisesdk.application.datasource.command.SetDatasourcePolicyCommand
4
+ import com.carivaexercisesdk.application.datasource.port.DatasourcePolicyRepository
5
+ import com.carivaexercisesdk.domain.datasource.entity.DatasourcePolicy
6
+ import com.carivaexercisesdk.domain.datasource.valueobject.DatasourceType
7
+ import com.carivaexercisesdk.domain.exercise.services.HealthConnectModuleInternals
8
+
9
+ class SetDatasourcePolicyHandler(private val repository: DatasourcePolicyRepository) {
10
+ fun handle(command: SetDatasourcePolicyCommand): DatasourcePolicy {
11
+ val allowlist =
12
+ command.allowlist
13
+ .mapNotNull { DatasourceType.fromWireValue(it) }
14
+ .toSet()
15
+ val packageAllowlist =
16
+ HealthConnectModuleInternals.normalizePackageAllowlist(command.packageAllowlist)
17
+
18
+ val policy = DatasourcePolicy(allowlist = allowlist, packageAllowlist = packageAllowlist)
19
+ repository.save(policy)
20
+ return policy
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ package com.carivaexercisesdk.application.datasource.port
2
+
3
+ import com.carivaexercisesdk.domain.datasource.entity.DatasourcePolicy
4
+
5
+ interface DatasourcePolicyRepository {
6
+ fun get(): DatasourcePolicy
7
+
8
+ fun save(policy: DatasourcePolicy)
9
+ }
@@ -0,0 +1,11 @@
1
+ package com.carivaexercisesdk.application.exercise.dto
2
+
3
+ import com.carivaexercisesdk.domain.exercise.services.HealthConnectModuleInternals
4
+
5
+ internal data class BasalReadResultDto(
6
+ val intervals: List<HealthConnectModuleInternals.IntervalInput>,
7
+ val basalSeedApplied: Boolean,
8
+ val seededFromBeforeSince: Boolean = false,
9
+ val basalSeedAgeHours: Double? = null,
10
+ val warnings: Set<String> = emptySet()
11
+ )
@@ -0,0 +1,41 @@
1
+ package com.carivaexercisesdk.application.exercise.dto
2
+
3
+ import com.carivaexercisesdk.domain.exercise.services.HealthConnectModuleInternals
4
+
5
+ internal data class ExerciseDataDto(
6
+ val totalKcal: Double,
7
+ val totalSteps: Double,
8
+ val totalActiveTimeMillis: Double,
9
+ val totalDistanceMeters: Double,
10
+ val timeZone: String,
11
+ val records: List<HealthConnectModuleInternals.UnifiedIntervalRecord>,
12
+ val warnings: List<String>,
13
+ val integrityProjectionPreserved: Boolean,
14
+ val integrityRoundingScale: Int,
15
+ val diagnostics: ExerciseDiagnosticsDto?,
16
+ val debug: ExerciseDebugDto?
17
+ )
18
+
19
+ internal data class ExerciseDiagnosticsDto(
20
+ val fallbackUsedSegments: Int,
21
+ val fallbackSuppressedSegments: Int,
22
+ val idleGateSuppressedSegments: Int,
23
+ val basalSeedApplied: Boolean,
24
+ val seededFromBeforeSince: Boolean,
25
+ val basalSeedAgeHours: Double?
26
+ )
27
+
28
+ internal data class ExerciseDebugDto(
29
+ val directActiveCaloriesKcal: Double,
30
+ val totalCaloriesKcal: Double,
31
+ val basalCaloriesKcal: Double,
32
+ val mergedActiveCaloriesKcal: Double,
33
+ val fallbackContributionKcal: Double,
34
+ val fallbackUsedSegments: Int,
35
+ val fallbackSuppressedSegments: Int,
36
+ val idleGateSuppressedSegments: Int,
37
+ val basalSeedApplied: Boolean,
38
+ val seededFromBeforeSince: Boolean,
39
+ val basalSeedAgeHours: Double?,
40
+ val basalIntervalsCount: Int
41
+ )