@flomentumsolutions/capacitor-health-extended 0.4.3 → 0.4.5
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/Package.swift
CHANGED
|
@@ -19,7 +19,9 @@ let package = Package(
|
|
|
19
19
|
.target(
|
|
20
20
|
name: "HealthPluginPlugin",
|
|
21
21
|
dependencies: [
|
|
22
|
-
.product(name: "Capacitor", package: "capacitor-swift-pm")
|
|
22
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
23
|
+
// Capacitor binary depends on Cordova; add it so Xcode/SPM can find the module.
|
|
24
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
23
25
|
],
|
|
24
26
|
path: "ios/Sources/HealthPluginPlugin"
|
|
25
27
|
)
|
package/README.md
CHANGED
|
@@ -334,7 +334,7 @@ Query aggregated data
|
|
|
334
334
|
**Returns:** <code>Promise<<a href="#queryaggregatedresponse">QueryAggregatedResponse</a>></code>
|
|
335
335
|
|
|
336
336
|
- Blood-pressure aggregates return the systolic average in `value` plus `systolic`, `diastolic`, and `unit`.
|
|
337
|
-
-
|
|
337
|
+
- `total-calories` is derived as active + basal energy on both iOS and Android for latest samples, aggregated queries, and workouts. We fall back to the platform's total‑calories metric (or active calories) when basal data isn't available or permission is missing. Request both `READ_ACTIVE_CALORIES` and `READ_BASAL_CALORIES` for full totals.
|
|
338
338
|
- Weight/height aggregation returns the latest sample per day (no averaging).
|
|
339
339
|
- Android aggregation currently supports daily buckets; unsupported buckets will be rejected.
|
|
340
340
|
- Android `distance-cycling` aggregates distance recorded during biking exercise sessions (requires distance + workouts permissions).
|
|
@@ -374,6 +374,8 @@ Query latest sample for a specific data type
|
|
|
374
374
|
|
|
375
375
|
**Returns:** <code>Promise<<a href="#querylatestsampleresponse">QueryLatestSampleResponse</a>></code>
|
|
376
376
|
|
|
377
|
+
- Latest sleep sample returns the most recent complete sleep session (asleep states only) from the last ~36 hours; if a longer overnight session exists, shorter naps are ignored.
|
|
378
|
+
|
|
377
379
|
--------------------
|
|
378
380
|
|
|
379
381
|
|
|
@@ -804,7 +804,72 @@ class HealthPlugin : Plugin() {
|
|
|
804
804
|
}
|
|
805
805
|
}
|
|
806
806
|
|
|
807
|
+
private suspend fun sumActiveAndBasalCalories(
|
|
808
|
+
timeRange: TimeRangeFilter,
|
|
809
|
+
includeTotalMetricFallback: Boolean = false
|
|
810
|
+
): Double? {
|
|
811
|
+
val metrics = mutableSetOf<AggregateMetric<*>>()
|
|
812
|
+
val includeActive = hasPermission(CapHealthPermission.READ_ACTIVE_CALORIES)
|
|
813
|
+
val includeBasal = hasPermission(CapHealthPermission.READ_BASAL_CALORIES)
|
|
814
|
+
val includeTotal = includeTotalMetricFallback && hasPermission(CapHealthPermission.READ_TOTAL_CALORIES)
|
|
815
|
+
|
|
816
|
+
if (includeActive) metrics.add(ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL)
|
|
817
|
+
if (includeBasal) metrics.add(BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL)
|
|
818
|
+
if (includeTotal) metrics.add(TotalCaloriesBurnedRecord.ENERGY_TOTAL)
|
|
819
|
+
|
|
820
|
+
if (metrics.isEmpty()) {
|
|
821
|
+
return null
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
val aggregation = healthConnectClient.aggregate(
|
|
825
|
+
AggregateRequest(
|
|
826
|
+
metrics = metrics,
|
|
827
|
+
timeRangeFilter = timeRange,
|
|
828
|
+
dataOriginFilter = emptySet()
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
val active = if (includeActive) {
|
|
833
|
+
aggregation[ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL]?.inKilocalories
|
|
834
|
+
} else null
|
|
835
|
+
val basal = if (includeBasal) {
|
|
836
|
+
aggregation[BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL]?.inKilocalories
|
|
837
|
+
} else null
|
|
838
|
+
val total = if (includeTotal) {
|
|
839
|
+
aggregation[TotalCaloriesBurnedRecord.ENERGY_TOTAL]?.inKilocalories
|
|
840
|
+
} else null
|
|
841
|
+
|
|
842
|
+
return when {
|
|
843
|
+
active != null || basal != null -> (active ?: 0.0) + (basal ?: 0.0)
|
|
844
|
+
includeTotal -> total
|
|
845
|
+
else -> null
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
807
849
|
private suspend fun readLatestTotalCalories(): JSObject {
|
|
850
|
+
val zone = ZoneId.systemDefault()
|
|
851
|
+
val now = LocalDateTime.now(zone)
|
|
852
|
+
|
|
853
|
+
val derivedTotal = try {
|
|
854
|
+
sumActiveAndBasalCalories(
|
|
855
|
+
TimeRangeFilter.between(
|
|
856
|
+
now.toLocalDate().atStartOfDay(),
|
|
857
|
+
now
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
} catch (e: Exception) {
|
|
861
|
+
Log.w(tag, "readLatestTotalCalories: Failed to derive active+basal calories, falling back to total metric", e)
|
|
862
|
+
null
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (derivedTotal != null) {
|
|
866
|
+
return JSObject().apply {
|
|
867
|
+
put("value", derivedTotal)
|
|
868
|
+
put("timestamp", now.atZone(zone).toInstant().toEpochMilli())
|
|
869
|
+
put("unit", "kcal")
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
808
873
|
if (!hasPermission(CapHealthPermission.READ_TOTAL_CALORIES)) {
|
|
809
874
|
throw Exception("Permission for total calories not granted")
|
|
810
875
|
}
|
|
@@ -952,13 +1017,17 @@ class HealthPlugin : Plugin() {
|
|
|
952
1017
|
}
|
|
953
1018
|
|
|
954
1019
|
else -> {
|
|
955
|
-
val
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1020
|
+
val aggregated = if (dataType == "total-calories") {
|
|
1021
|
+
aggregateTotalCalories(timeRange, period)
|
|
1022
|
+
} else {
|
|
1023
|
+
val metricAndMapper = getMetricAndMapper(dataType)
|
|
1024
|
+
queryAggregatedMetric(
|
|
1025
|
+
metricAndMapper,
|
|
1026
|
+
timeRange,
|
|
1027
|
+
period
|
|
1028
|
+
)
|
|
1029
|
+
}
|
|
1030
|
+
aggregated.forEach { aggregatedList.put(it.toJs()) }
|
|
962
1031
|
}
|
|
963
1032
|
}
|
|
964
1033
|
|
|
@@ -1068,6 +1137,51 @@ class HealthPlugin : Plugin() {
|
|
|
1068
1137
|
|
|
1069
1138
|
}
|
|
1070
1139
|
|
|
1140
|
+
private suspend fun aggregateTotalCalories(
|
|
1141
|
+
timeRange: TimeRangeFilter,
|
|
1142
|
+
period: Period
|
|
1143
|
+
): List<AggregatedSample> {
|
|
1144
|
+
val includeActive = hasPermission(CapHealthPermission.READ_ACTIVE_CALORIES)
|
|
1145
|
+
val includeBasal = hasPermission(CapHealthPermission.READ_BASAL_CALORIES)
|
|
1146
|
+
val includeTotal = hasPermission(CapHealthPermission.READ_TOTAL_CALORIES)
|
|
1147
|
+
|
|
1148
|
+
val metrics = mutableSetOf<AggregateMetric<*>>()
|
|
1149
|
+
if (includeActive) metrics.add(ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL)
|
|
1150
|
+
if (includeBasal) metrics.add(BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL)
|
|
1151
|
+
if (includeTotal) metrics.add(TotalCaloriesBurnedRecord.ENERGY_TOTAL)
|
|
1152
|
+
|
|
1153
|
+
if (metrics.isEmpty()) {
|
|
1154
|
+
return emptyList()
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
val response: List<AggregationResultGroupedByPeriod> = healthConnectClient.aggregateGroupByPeriod(
|
|
1158
|
+
AggregateGroupByPeriodRequest(
|
|
1159
|
+
metrics = metrics,
|
|
1160
|
+
timeRangeFilter = timeRange,
|
|
1161
|
+
timeRangeSlicer = period
|
|
1162
|
+
)
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
return response.map {
|
|
1166
|
+
val active = if (includeActive) {
|
|
1167
|
+
it.result[ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL]?.inKilocalories
|
|
1168
|
+
} else null
|
|
1169
|
+
val basal = if (includeBasal) {
|
|
1170
|
+
it.result[BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL]?.inKilocalories
|
|
1171
|
+
} else null
|
|
1172
|
+
val total = if (includeTotal) {
|
|
1173
|
+
it.result[TotalCaloriesBurnedRecord.ENERGY_TOTAL]?.inKilocalories
|
|
1174
|
+
} else null
|
|
1175
|
+
|
|
1176
|
+
val value = when {
|
|
1177
|
+
active != null || basal != null -> (active ?: 0.0) + (basal ?: 0.0)
|
|
1178
|
+
else -> total ?: 0.0
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
AggregatedSample(it.startTime, it.endTime, value)
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1071
1185
|
private suspend fun aggregateHrvByPeriod(
|
|
1072
1186
|
timeRange: TimeRangeFilter,
|
|
1073
1187
|
period: Period
|
|
@@ -1409,9 +1523,12 @@ class HealthPlugin : Plugin() {
|
|
|
1409
1523
|
addWorkoutMetric(workout, workoutObject, getMetricAndMapper("steps"))
|
|
1410
1524
|
}
|
|
1411
1525
|
|
|
1412
|
-
val
|
|
1413
|
-
if(!
|
|
1414
|
-
addWorkoutMetric(workout, workoutObject, getMetricAndMapper("
|
|
1526
|
+
val derivedTotalAdded = addWorkoutTotalCalories(workout, workoutObject)
|
|
1527
|
+
if (!derivedTotalAdded) {
|
|
1528
|
+
val readTotalCaloriesResult = addWorkoutMetric(workout, workoutObject, getMetricAndMapper("total-calories"))
|
|
1529
|
+
if(!readTotalCaloriesResult) {
|
|
1530
|
+
addWorkoutMetric(workout, workoutObject, getMetricAndMapper("active-calories"))
|
|
1531
|
+
}
|
|
1415
1532
|
}
|
|
1416
1533
|
|
|
1417
1534
|
addWorkoutMetric(workout, workoutObject, getMetricAndMapper("distance"))
|
|
@@ -1451,6 +1568,27 @@ class HealthPlugin : Plugin() {
|
|
|
1451
1568
|
}
|
|
1452
1569
|
}
|
|
1453
1570
|
|
|
1571
|
+
private suspend fun addWorkoutTotalCalories(
|
|
1572
|
+
workout: ExerciseSessionRecord,
|
|
1573
|
+
jsWorkout: JSObject
|
|
1574
|
+
): Boolean {
|
|
1575
|
+
return try {
|
|
1576
|
+
val totalCalories = sumActiveAndBasalCalories(
|
|
1577
|
+
TimeRangeFilter.between(workout.startTime, workout.endTime),
|
|
1578
|
+
includeTotalMetricFallback = true
|
|
1579
|
+
)
|
|
1580
|
+
if (totalCalories != null) {
|
|
1581
|
+
jsWorkout.put("calories", totalCalories)
|
|
1582
|
+
true
|
|
1583
|
+
} else {
|
|
1584
|
+
false
|
|
1585
|
+
}
|
|
1586
|
+
} catch (e: Exception) {
|
|
1587
|
+
Log.e(tag, "addWorkoutTotalCalories: Failed to derive calories", e)
|
|
1588
|
+
false
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1454
1592
|
private suspend fun addWorkoutMetric(
|
|
1455
1593
|
workout: ExerciseSessionRecord,
|
|
1456
1594
|
jsWorkout: JSObject,
|
|
@@ -161,26 +161,87 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
161
161
|
call.reject("Sleep type not available")
|
|
162
162
|
return
|
|
163
163
|
}
|
|
164
|
-
|
|
165
|
-
let
|
|
166
|
-
let
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
|
|
165
|
+
let endDate = Date()
|
|
166
|
+
let startDate = Calendar.current.date(byAdding: .hour, value: -36, to: endDate) ?? endDate.addingTimeInterval(-36 * 3600)
|
|
167
|
+
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictEndDate)
|
|
168
|
+
|
|
169
|
+
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
|
|
170
|
+
if let error = error {
|
|
171
|
+
call.reject("Error fetching latest sleep sample", "NO_SAMPLE", error)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
guard let categorySamples = samples as? [HKCategorySample], !categorySamples.isEmpty else {
|
|
175
|
+
call.reject("No sleep sample found", "NO_SAMPLE")
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let asleepValues: Set<Int> = {
|
|
180
|
+
if #available(iOS 16.0, *) {
|
|
181
|
+
return Set([
|
|
182
|
+
HKCategoryValueSleepAnalysis.asleepUnspecified.rawValue,
|
|
183
|
+
HKCategoryValueSleepAnalysis.asleepCore.rawValue,
|
|
184
|
+
HKCategoryValueSleepAnalysis.asleepDeep.rawValue,
|
|
185
|
+
HKCategoryValueSleepAnalysis.asleepREM.rawValue
|
|
186
|
+
])
|
|
172
187
|
} else {
|
|
173
|
-
|
|
188
|
+
// Pre-iOS 16 only exposes the legacy "asleep" value.
|
|
189
|
+
return Set([HKCategoryValueSleepAnalysis.asleep.rawValue])
|
|
190
|
+
}
|
|
191
|
+
}()
|
|
192
|
+
|
|
193
|
+
func isAsleep(_ value: Int) -> Bool {
|
|
194
|
+
if asleepValues.contains(value) {
|
|
195
|
+
return true
|
|
174
196
|
}
|
|
197
|
+
// Fallback: treat any non in-bed/awake as asleep
|
|
198
|
+
return value != HKCategoryValueSleepAnalysis.inBed.rawValue &&
|
|
199
|
+
value != HKCategoryValueSleepAnalysis.awake.rawValue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let asleepSamples = categorySamples
|
|
203
|
+
.filter { isAsleep($0.value) }
|
|
204
|
+
.sorted { $0.startDate < $1.startDate }
|
|
205
|
+
|
|
206
|
+
guard !asleepSamples.isEmpty else {
|
|
207
|
+
call.reject("No sleep sample found", "NO_SAMPLE")
|
|
175
208
|
return
|
|
176
209
|
}
|
|
177
|
-
|
|
210
|
+
|
|
211
|
+
let maxGap: TimeInterval = 90 * 60 // 90 minutes separates sessions
|
|
212
|
+
var sessions: [(start: Date, end: Date, duration: TimeInterval)] = []
|
|
213
|
+
var currentStart: Date?
|
|
214
|
+
var currentEnd: Date?
|
|
215
|
+
var currentDuration: TimeInterval = 0
|
|
216
|
+
|
|
217
|
+
for sample in asleepSamples {
|
|
218
|
+
if let lastEnd = currentEnd, sample.startDate.timeIntervalSince(lastEnd) > maxGap {
|
|
219
|
+
sessions.append((start: currentStart ?? lastEnd, end: lastEnd, duration: currentDuration))
|
|
220
|
+
currentStart = nil
|
|
221
|
+
currentEnd = nil
|
|
222
|
+
currentDuration = 0
|
|
223
|
+
}
|
|
224
|
+
if currentStart == nil { currentStart = sample.startDate }
|
|
225
|
+
currentEnd = sample.endDate
|
|
226
|
+
currentDuration += sample.endDate.timeIntervalSince(sample.startDate)
|
|
227
|
+
}
|
|
228
|
+
if let start = currentStart, let end = currentEnd {
|
|
229
|
+
sessions.append((start: start, end: end, duration: currentDuration))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
guard !sessions.isEmpty else {
|
|
233
|
+
call.reject("No sleep sample found", "NO_SAMPLE")
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let minSessionDuration: TimeInterval = 3 * 3600 // prefer sessions 3h+
|
|
238
|
+
let preferredSession = sessions.reversed().first { $0.duration >= minSessionDuration } ?? sessions.last!
|
|
239
|
+
|
|
178
240
|
call.resolve([
|
|
179
|
-
"value":
|
|
180
|
-
"timestamp":
|
|
181
|
-
"endTimestamp":
|
|
182
|
-
"unit": "min"
|
|
183
|
-
"metadata": ["state": sleepSample.value]
|
|
241
|
+
"value": preferredSession.duration / 60,
|
|
242
|
+
"timestamp": preferredSession.start.timeIntervalSince1970 * 1000,
|
|
243
|
+
"endTimestamp": preferredSession.end.timeIntervalSince1970 * 1000,
|
|
244
|
+
"unit": "min"
|
|
184
245
|
])
|
|
185
246
|
}
|
|
186
247
|
healthStore.execute(query)
|
|
@@ -538,7 +599,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
538
599
|
if bucket == "day" {
|
|
539
600
|
startDate = calendar.startOfDay(for: rawStartDate)
|
|
540
601
|
let endDayStart = calendar.startOfDay(for: rawEndDate)
|
|
541
|
-
endDate = calendar.date(byAdding: .day, to: endDayStart) ?? endDayStart
|
|
602
|
+
endDate = calendar.date(byAdding: .day, value: 1, to: endDayStart) ?? endDayStart
|
|
542
603
|
}
|
|
543
604
|
if dataTypeString == "mindfulness" {
|
|
544
605
|
self.queryMindfulnessAggregated(startDate: startDate, endDate: endDate) { result, error in
|
|
@@ -725,8 +786,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
725
786
|
let calendar = Calendar.current
|
|
726
787
|
if let categorySamples = samples as? [HKCategorySample], error == nil {
|
|
727
788
|
for sample in categorySamples {
|
|
728
|
-
// Ignore in-bed samples; we care about actual sleep
|
|
789
|
+
// Ignore in-bed and awake samples; we care about actual sleep
|
|
729
790
|
if sample.value == HKCategoryValueSleepAnalysis.inBed.rawValue { continue }
|
|
791
|
+
if sample.value == HKCategoryValueSleepAnalysis.awake.rawValue { continue }
|
|
730
792
|
let startOfDay = calendar.startOfDay(for: sample.startDate)
|
|
731
793
|
let duration = sample.endDate.timeIntervalSince(sample.startDate)
|
|
732
794
|
dailyDurations[startOfDay, default: 0] += duration
|
|
@@ -821,52 +883,55 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
821
883
|
healthStore.execute(query)
|
|
822
884
|
}
|
|
823
885
|
|
|
886
|
+
private func sumEnergy(
|
|
887
|
+
_ identifier: HKQuantityTypeIdentifier,
|
|
888
|
+
startDate: Date,
|
|
889
|
+
endDate: Date,
|
|
890
|
+
completion: @escaping (Double?, Error?) -> Void
|
|
891
|
+
) {
|
|
892
|
+
guard let type = HKObjectType.quantityType(forIdentifier: identifier) else {
|
|
893
|
+
completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "Quantity type unavailable"]))
|
|
894
|
+
return
|
|
895
|
+
}
|
|
896
|
+
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
897
|
+
let query = HKStatisticsQuery(
|
|
898
|
+
quantityType: type,
|
|
899
|
+
quantitySamplePredicate: predicate,
|
|
900
|
+
options: .cumulativeSum
|
|
901
|
+
) { _, result, error in
|
|
902
|
+
if let error = error {
|
|
903
|
+
completion(nil, error)
|
|
904
|
+
return
|
|
905
|
+
}
|
|
906
|
+
let value = result?.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie())
|
|
907
|
+
completion(value, nil)
|
|
908
|
+
}
|
|
909
|
+
healthStore.execute(query)
|
|
910
|
+
}
|
|
911
|
+
|
|
824
912
|
private func queryLatestTotalCalories(_ call: CAPPluginCall) {
|
|
825
913
|
let unit = HKUnit.kilocalorie()
|
|
826
914
|
let group = DispatchGroup()
|
|
915
|
+
let now = Date()
|
|
916
|
+
let startOfDay = Calendar.current.startOfDay(for: now)
|
|
827
917
|
|
|
828
|
-
var
|
|
829
|
-
var
|
|
830
|
-
var activeDate: Date?
|
|
831
|
-
var basalDate: Date?
|
|
918
|
+
var activeTotal: Double?
|
|
919
|
+
var basalTotal: Double?
|
|
832
920
|
var queryError: Error?
|
|
833
921
|
let basalSupported = HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) != nil
|
|
834
922
|
|
|
835
|
-
func fetchLatest(_ identifier: HKQuantityTypeIdentifier, completion: @escaping (Double?, Date?, Error?) -> Void) {
|
|
836
|
-
guard let type = HKObjectType.quantityType(forIdentifier: identifier) else {
|
|
837
|
-
completion(nil, nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "Quantity type unavailable"]))
|
|
838
|
-
return
|
|
839
|
-
}
|
|
840
|
-
let predicate = HKQuery.predicateForSamples(withStart: Date.distantPast, end: Date(), options: .strictEndDate)
|
|
841
|
-
let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
|
|
842
|
-
let query = HKSampleQuery(sampleType: type, predicate: predicate, limit: 1, sortDescriptors: [sort]) { _, samples, error in
|
|
843
|
-
if let error = error {
|
|
844
|
-
completion(nil, nil, error)
|
|
845
|
-
return
|
|
846
|
-
}
|
|
847
|
-
guard let sample = samples?.first as? HKQuantitySample else {
|
|
848
|
-
completion(nil, nil, nil)
|
|
849
|
-
return
|
|
850
|
-
}
|
|
851
|
-
completion(sample.quantity.doubleValue(for: unit), sample.startDate, nil)
|
|
852
|
-
}
|
|
853
|
-
healthStore.execute(query)
|
|
854
|
-
}
|
|
855
|
-
|
|
856
923
|
group.enter()
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
queryError = queryError ?? error
|
|
924
|
+
sumEnergy(.activeEnergyBurned, startDate: startOfDay, endDate: now) { value, error in
|
|
925
|
+
activeTotal = value
|
|
926
|
+
if queryError == nil { queryError = error }
|
|
861
927
|
group.leave()
|
|
862
928
|
}
|
|
863
929
|
|
|
864
930
|
if basalSupported {
|
|
865
931
|
group.enter()
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
queryError = queryError ?? error
|
|
932
|
+
sumEnergy(.basalEnergyBurned, startDate: startOfDay, endDate: now) { value, error in
|
|
933
|
+
basalTotal = value
|
|
934
|
+
if queryError == nil { queryError = error }
|
|
870
935
|
group.leave()
|
|
871
936
|
}
|
|
872
937
|
}
|
|
@@ -876,15 +941,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
876
941
|
call.reject("Error fetching total calories: \(error.localizedDescription)")
|
|
877
942
|
return
|
|
878
943
|
}
|
|
879
|
-
guard
|
|
944
|
+
guard activeTotal != nil || basalTotal != nil else {
|
|
880
945
|
call.reject("No sample found", "NO_SAMPLE")
|
|
881
946
|
return
|
|
882
947
|
}
|
|
883
|
-
let total = (
|
|
884
|
-
let timestamp = max(activeDate?.timeIntervalSince1970 ?? 0, basalDate?.timeIntervalSince1970 ?? 0) * 1000
|
|
948
|
+
let total = (activeTotal ?? 0) + (basalTotal ?? 0)
|
|
885
949
|
call.resolve([
|
|
886
950
|
"value": total,
|
|
887
|
-
"timestamp":
|
|
951
|
+
"timestamp": now.timeIntervalSince1970 * 1000,
|
|
888
952
|
"unit": unit.unitString
|
|
889
953
|
])
|
|
890
954
|
}
|
package/package.json
CHANGED