@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,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
|
+
}
|
package/android/src/main/java/com/carivaexercisesdk/HealthConnectPermissionsRationaleActivity.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/carivaexercisesdk/application/connection/command/ConnectCommand.kt
ADDED
|
@@ -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
|
+
)
|
package/android/src/main/java/com/carivaexercisesdk/application/connection/dto/ConnectResultDto.kt
ADDED
|
@@ -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
|
+
)
|
package/android/src/main/java/com/carivaexercisesdk/application/connection/handler/ConnectHandler.kt
ADDED
|
@@ -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,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
|
+
}
|
package/android/src/main/java/com/carivaexercisesdk/application/exercise/dto/BasalReadResultDto.kt
ADDED
|
@@ -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
|
+
)
|
package/android/src/main/java/com/carivaexercisesdk/application/exercise/dto/ExerciseDataDto.kt
ADDED
|
@@ -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
|
+
)
|