@flomentumsolutions/capacitor-health-extended 0.0.15 → 0.0.16
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/android/build.gradle
CHANGED
|
@@ -7,14 +7,14 @@ ext {
|
|
|
7
7
|
|
|
8
8
|
buildscript {
|
|
9
9
|
ext {
|
|
10
|
-
kotlin_version = '2.0
|
|
10
|
+
kotlin_version = '2.2.0'
|
|
11
11
|
}
|
|
12
12
|
repositories {
|
|
13
13
|
google()
|
|
14
14
|
mavenCentral()
|
|
15
15
|
}
|
|
16
16
|
dependencies {
|
|
17
|
-
classpath 'com.android.tools.build:gradle:8.
|
|
17
|
+
classpath 'com.android.tools.build:gradle:8.11.1'
|
|
18
18
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -26,7 +26,7 @@ android {
|
|
|
26
26
|
namespace "com.flomentum.health.capacitor"
|
|
27
27
|
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
28
28
|
defaultConfig {
|
|
29
|
-
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion :
|
|
29
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 36
|
|
30
30
|
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
31
31
|
versionCode 1
|
|
32
32
|
versionName "1.0"
|
|
@@ -83,7 +83,7 @@ class HealthPlugin : Plugin() {
|
|
|
83
83
|
CapHealthPermission.READ_TOTAL_CALORIES to HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
|
|
84
84
|
CapHealthPermission.READ_DISTANCE to HealthPermission.getReadPermission(DistanceRecord::class),
|
|
85
85
|
CapHealthPermission.READ_WORKOUTS to HealthPermission.getReadPermission(ExerciseSessionRecord::class),
|
|
86
|
-
CapHealthPermission.READ_HRV to HealthPermission.getReadPermission(
|
|
86
|
+
CapHealthPermission.READ_HRV to HealthPermission.getReadPermission(HeartRateVariabilityRmssdRecord::class),
|
|
87
87
|
CapHealthPermission.READ_BLOOD_PRESSURE to HealthPermission.getReadPermission(BloodPressureRecord::class),
|
|
88
88
|
CapHealthPermission.READ_ROUTE to HealthPermission.getReadPermission(ExerciseSessionRecord::class),
|
|
89
89
|
CapHealthPermission.READ_MINDFULNESS to HealthPermission.getReadPermission(SleepSessionRecord::class)
|
|
@@ -275,7 +275,6 @@ class HealthPlugin : Plugin() {
|
|
|
275
275
|
TotalCaloriesBurnedRecord.ENERGY_TOTAL
|
|
276
276
|
) { it?.inKilocalories }
|
|
277
277
|
"distance" -> metricAndMapper("distance", CapHealthPermission.READ_DISTANCE, DistanceRecord.DISTANCE_TOTAL) { it?.inMeters }
|
|
278
|
-
"hrv" -> metricAndMapper("hrv", CapHealthPermission.READ_HRV, HeartRateVariabilitySdnnRecord.SDNN_AVG) { it }
|
|
279
278
|
else -> throw RuntimeException("Unsupported dataType: $dataType")
|
|
280
279
|
}
|
|
281
280
|
}
|
|
@@ -288,7 +287,11 @@ class HealthPlugin : Plugin() {
|
|
|
288
287
|
call.reject("Missing required parameter: dataType")
|
|
289
288
|
return
|
|
290
289
|
}
|
|
290
|
+
queryLatestSampleInternal(call, dataType)
|
|
291
|
+
}
|
|
291
292
|
|
|
293
|
+
private fun queryLatestSampleInternal(call: PluginCall, dataType: String) {
|
|
294
|
+
if (!ensureClientInitialized(call)) return
|
|
292
295
|
CoroutineScope(Dispatchers.IO).launch {
|
|
293
296
|
try {
|
|
294
297
|
val result = when (dataType) {
|
|
@@ -301,14 +304,11 @@ class HealthPlugin : Plugin() {
|
|
|
301
304
|
"distance" -> readLatestDistance()
|
|
302
305
|
"active-calories" -> readLatestActiveCalories()
|
|
303
306
|
"total-calories" -> readLatestTotalCalories()
|
|
304
|
-
else ->
|
|
305
|
-
call.reject("Unsupported data type: $dataType")
|
|
306
|
-
return@launch
|
|
307
|
-
}
|
|
307
|
+
else -> throw IllegalArgumentException("Unsupported data type: $dataType")
|
|
308
308
|
}
|
|
309
309
|
call.resolve(result)
|
|
310
310
|
} catch (e: Exception) {
|
|
311
|
-
Log.e(tag, "
|
|
311
|
+
Log.e(tag, "queryLatestSampleInternal: Error fetching latest $dataType", e)
|
|
312
312
|
call.reject("Error fetching latest $dataType: ${e.message}")
|
|
313
313
|
}
|
|
314
314
|
}
|
|
@@ -317,26 +317,22 @@ class HealthPlugin : Plugin() {
|
|
|
317
317
|
// Convenience methods for specific data types
|
|
318
318
|
@PluginMethod
|
|
319
319
|
fun queryWeight(call: PluginCall) {
|
|
320
|
-
call
|
|
321
|
-
queryLatestSample(call)
|
|
320
|
+
queryLatestSampleInternal(call, "weight")
|
|
322
321
|
}
|
|
323
322
|
|
|
324
323
|
@PluginMethod
|
|
325
324
|
fun queryHeight(call: PluginCall) {
|
|
326
|
-
call
|
|
327
|
-
queryLatestSample(call)
|
|
325
|
+
queryLatestSampleInternal(call, "height")
|
|
328
326
|
}
|
|
329
327
|
|
|
330
328
|
@PluginMethod
|
|
331
329
|
fun queryHeartRate(call: PluginCall) {
|
|
332
|
-
call
|
|
333
|
-
queryLatestSample(call)
|
|
330
|
+
queryLatestSampleInternal(call, "heart-rate")
|
|
334
331
|
}
|
|
335
332
|
|
|
336
333
|
@PluginMethod
|
|
337
334
|
fun querySteps(call: PluginCall) {
|
|
338
|
-
call
|
|
339
|
-
queryLatestSample(call)
|
|
335
|
+
queryLatestSampleInternal(call, "steps")
|
|
340
336
|
}
|
|
341
337
|
|
|
342
338
|
private suspend fun readLatestHeartRate(): JSObject {
|
|
@@ -400,14 +396,14 @@ class HealthPlugin : Plugin() {
|
|
|
400
396
|
throw Exception("Permission for HRV not granted")
|
|
401
397
|
}
|
|
402
398
|
val request = ReadRecordsRequest(
|
|
403
|
-
recordType =
|
|
399
|
+
recordType = HeartRateVariabilityRmssdRecord::class,
|
|
404
400
|
timeRangeFilter = TimeRangeFilter.after(Instant.EPOCH),
|
|
405
401
|
pageSize = 1
|
|
406
402
|
)
|
|
407
403
|
val result = healthConnectClient.readRecords(request)
|
|
408
404
|
val record = result.records.firstOrNull() ?: throw Exception("No HRV data found")
|
|
409
405
|
return JSObject().apply {
|
|
410
|
-
put("value", record.
|
|
406
|
+
put("value", record.heartRateVariabilityMillis)
|
|
411
407
|
put("timestamp", record.time.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
412
408
|
put("unit", "ms")
|
|
413
409
|
}
|
|
@@ -463,7 +459,7 @@ class HealthPlugin : Plugin() {
|
|
|
463
459
|
val record = result.records.firstOrNull() ?: throw Exception("No distance data found")
|
|
464
460
|
return JSObject().apply {
|
|
465
461
|
put("value", record.distance.inMeters)
|
|
466
|
-
put("timestamp", record.
|
|
462
|
+
put("timestamp", record.endTime.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
467
463
|
put("unit", "m")
|
|
468
464
|
}
|
|
469
465
|
}
|
|
@@ -480,8 +476,8 @@ class HealthPlugin : Plugin() {
|
|
|
480
476
|
val result = healthConnectClient.readRecords(request)
|
|
481
477
|
val record = result.records.firstOrNull() ?: throw Exception("No active calories data found")
|
|
482
478
|
return JSObject().apply {
|
|
483
|
-
put("value", record.
|
|
484
|
-
put("timestamp", record.
|
|
479
|
+
put("value", record.energy.inKilocalories)
|
|
480
|
+
put("timestamp", record.endTime.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
485
481
|
put("unit", "kcal")
|
|
486
482
|
}
|
|
487
483
|
}
|
|
@@ -499,7 +495,7 @@ class HealthPlugin : Plugin() {
|
|
|
499
495
|
val record = result.records.firstOrNull() ?: throw Exception("No total calories data found")
|
|
500
496
|
return JSObject().apply {
|
|
501
497
|
put("value", record.energy.inKilocalories)
|
|
502
|
-
put("timestamp", record.
|
|
498
|
+
put("timestamp", record.endTime.epochSecond * 1000) // Convert to milliseconds like iOS
|
|
503
499
|
put("unit", "kcal")
|
|
504
500
|
}
|
|
505
501
|
}
|
|
@@ -521,16 +517,41 @@ class HealthPlugin : Plugin() {
|
|
|
521
517
|
val startDateTime = Instant.parse(startDate).atZone(ZoneId.systemDefault()).toLocalDateTime()
|
|
522
518
|
val endDateTime = Instant.parse(endDate).atZone(ZoneId.systemDefault()).toLocalDateTime()
|
|
523
519
|
|
|
524
|
-
val metricAndMapper = getMetricAndMapper(dataType)
|
|
525
|
-
|
|
526
520
|
val period = when (bucket) {
|
|
527
521
|
"day" -> Period.ofDays(1)
|
|
528
522
|
else -> throw RuntimeException("Unsupported bucket: $bucket")
|
|
529
523
|
}
|
|
530
524
|
|
|
525
|
+
// Special handling for HRV (RMSSD) because aggregate metrics were
|
|
526
|
+
// removed in Health Connect 1.1‑rc03. We calculate the daily average
|
|
527
|
+
// from raw samples instead.
|
|
528
|
+
if (dataType == "hrv") {
|
|
529
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
530
|
+
try {
|
|
531
|
+
val hrvSamples = aggregateHrvByPeriod(
|
|
532
|
+
TimeRangeFilter.between(startDateTime, endDateTime),
|
|
533
|
+
period
|
|
534
|
+
)
|
|
535
|
+
val aggregatedList = JSArray()
|
|
536
|
+
hrvSamples.forEach { aggregatedList.put(it.toJs()) }
|
|
537
|
+
val finalResult = JSObject()
|
|
538
|
+
finalResult.put("aggregatedData", aggregatedList)
|
|
539
|
+
call.resolve(finalResult)
|
|
540
|
+
} catch (e: Exception) {
|
|
541
|
+
call.reject("Error querying aggregated HRV data: ${e.message}")
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return // skip the normal aggregate path
|
|
545
|
+
}
|
|
546
|
+
|
|
531
547
|
CoroutineScope(Dispatchers.IO).launch {
|
|
532
548
|
try {
|
|
533
|
-
val
|
|
549
|
+
val metricAndMapper = getMetricAndMapper(dataType)
|
|
550
|
+
val r = queryAggregatedMetric(
|
|
551
|
+
metricAndMapper,
|
|
552
|
+
TimeRangeFilter.between(startDateTime, endDateTime),
|
|
553
|
+
period
|
|
554
|
+
)
|
|
534
555
|
val aggregatedList = JSArray()
|
|
535
556
|
r.forEach { aggregatedList.put(it.toJs()) }
|
|
536
557
|
val finalResult = JSObject()
|
|
@@ -602,13 +623,46 @@ class HealthPlugin : Plugin() {
|
|
|
602
623
|
|
|
603
624
|
}
|
|
604
625
|
|
|
626
|
+
private suspend fun aggregateHrvByPeriod(
|
|
627
|
+
timeRange: TimeRangeFilter,
|
|
628
|
+
period: Period
|
|
629
|
+
): List<AggregatedSample> {
|
|
630
|
+
if (!hasPermission(CapHealthPermission.READ_HRV)) {
|
|
631
|
+
return emptyList()
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Currently only daily buckets are supported.
|
|
635
|
+
if (period != Period.ofDays(1)) {
|
|
636
|
+
throw RuntimeException("Unsupported bucket for HRV aggregation")
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
val response = healthConnectClient.readRecords(
|
|
640
|
+
ReadRecordsRequest(
|
|
641
|
+
recordType = HeartRateVariabilityRmssdRecord::class,
|
|
642
|
+
timeRangeFilter = timeRange
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
// Group raw RMSSD samples by local date and compute the arithmetic mean.
|
|
647
|
+
return response.records
|
|
648
|
+
.groupBy { it.time.atZone(ZoneId.systemDefault()).toLocalDate() }
|
|
649
|
+
.map { (localDate, recs) ->
|
|
650
|
+
val avg = recs.map { it.heartRateVariabilityMillis }.average()
|
|
651
|
+
AggregatedSample(
|
|
652
|
+
localDate.atStartOfDay(),
|
|
653
|
+
localDate.plusDays(1).atStartOfDay(),
|
|
654
|
+
if (avg.isNaN()) null else avg
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
.sortedBy { it.startDate }
|
|
658
|
+
}
|
|
659
|
+
|
|
605
660
|
private suspend fun hasPermission(p: CapHealthPermission): Boolean {
|
|
606
661
|
val granted = healthConnectClient.permissionController.getGrantedPermissions()
|
|
607
662
|
val targetPermission = permissionMapping[p]
|
|
608
663
|
return granted.contains(targetPermission)
|
|
609
664
|
}
|
|
610
665
|
|
|
611
|
-
|
|
612
666
|
@PluginMethod
|
|
613
667
|
fun queryWorkouts(call: PluginCall) {
|
|
614
668
|
if (!ensureClientInitialized(call)) return
|
package/package.json
CHANGED