@flomentumsolutions/capacitor-health-extended 0.1.0 → 0.4.1
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/{FlomentumsolutionsCapacitorHealthExtended.podspec → FlomentumSolutionsCapacitorHealthExtended.podspec} +6 -6
- package/LICENSE +1 -0
- package/Package.swift +27 -0
- package/README.md +123 -24
- package/android/build.gradle +15 -10
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/com/flomentum/health/capacitor/HealthPlugin.kt +732 -45
- package/android/src/main/java/com/flomentum/health/capacitor/PermissionsRationaleActivity.kt +1 -1
- package/dist/esm/definitions.d.ts +10 -6
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/HealthPluginPlugin/HealthPlugin.swift +544 -90
- package/package.json +40 -17
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import UIKit
|
|
2
3
|
import Capacitor
|
|
3
4
|
import HealthKit
|
|
4
5
|
|
|
@@ -17,17 +18,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
17
18
|
CAPPluginMethod(name: "openAppleHealthSettings", returnType: CAPPluginReturnPromise),
|
|
18
19
|
CAPPluginMethod(name: "queryAggregated", returnType: CAPPluginReturnPromise),
|
|
19
20
|
CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise),
|
|
20
|
-
CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise)
|
|
21
|
-
CAPPluginMethod(name: "queryWeight", returnType: CAPPluginReturnPromise),
|
|
22
|
-
CAPPluginMethod(name: "queryHeight", returnType: CAPPluginReturnPromise),
|
|
23
|
-
CAPPluginMethod(name: "queryHeartRate", returnType: CAPPluginReturnPromise),
|
|
24
|
-
CAPPluginMethod(name: "querySteps", returnType: CAPPluginReturnPromise)
|
|
21
|
+
CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise)
|
|
25
22
|
]
|
|
26
23
|
|
|
27
24
|
let healthStore = HKHealthStore()
|
|
28
25
|
|
|
29
26
|
/// Serial queue to make route‑location mutations thread‑safe without locks
|
|
30
|
-
private let routeSyncQueue = DispatchQueue(label: "com.
|
|
27
|
+
private let routeSyncQueue = DispatchQueue(label: "com.flomentumsolutions.healthplugin.routeSync")
|
|
31
28
|
|
|
32
29
|
@objc func isHealthAvailable(_ call: CAPPluginCall) {
|
|
33
30
|
let isAvailable = HKHealthStore.isHealthDataAvailable()
|
|
@@ -153,6 +150,73 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
153
150
|
healthStore.execute(query)
|
|
154
151
|
return
|
|
155
152
|
}
|
|
153
|
+
// ---- Derived total calories (active + basal) ----
|
|
154
|
+
if dataTypeString == "total-calories" {
|
|
155
|
+
queryLatestTotalCalories(call)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
// ---- Special handling for sleep sessions (category samples) ----
|
|
159
|
+
if dataTypeString == "sleep" {
|
|
160
|
+
guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
|
|
161
|
+
call.reject("Sleep type not available")
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
|
|
166
|
+
let predicate = HKQuery.predicateForSamples(withStart: Date.distantPast, end: Date(), options: .strictEndDate)
|
|
167
|
+
|
|
168
|
+
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { _, samples, error in
|
|
169
|
+
guard let sleepSample = samples?.first as? HKCategorySample else {
|
|
170
|
+
if let error = error {
|
|
171
|
+
call.reject("Error fetching latest sleep sample", "NO_SAMPLE", error)
|
|
172
|
+
} else {
|
|
173
|
+
call.reject("No sleep sample found", "NO_SAMPLE")
|
|
174
|
+
}
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
let durationMinutes = sleepSample.endDate.timeIntervalSince(sleepSample.startDate) / 60
|
|
178
|
+
call.resolve([
|
|
179
|
+
"value": durationMinutes,
|
|
180
|
+
"timestamp": sleepSample.startDate.timeIntervalSince1970 * 1000,
|
|
181
|
+
"endTimestamp": sleepSample.endDate.timeIntervalSince1970 * 1000,
|
|
182
|
+
"unit": "min",
|
|
183
|
+
"metadata": ["state": sleepSample.value]
|
|
184
|
+
])
|
|
185
|
+
}
|
|
186
|
+
healthStore.execute(query)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
// ---- Special handling for mindfulness sessions (category samples) ----
|
|
190
|
+
if dataTypeString == "mindfulness" {
|
|
191
|
+
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
192
|
+
call.reject("Mindfulness type not available")
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
|
|
197
|
+
let predicate = HKQuery.predicateForSamples(withStart: Date.distantPast, end: Date(), options: .strictEndDate)
|
|
198
|
+
|
|
199
|
+
let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { _, samples, error in
|
|
200
|
+
guard let mindfulSample = samples?.first as? HKCategorySample else {
|
|
201
|
+
if let error = error {
|
|
202
|
+
call.reject("Error fetching latest mindfulness sample", "NO_SAMPLE", error)
|
|
203
|
+
} else {
|
|
204
|
+
call.reject("No mindfulness sample found", "NO_SAMPLE")
|
|
205
|
+
}
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
let durationMinutes = mindfulSample.endDate.timeIntervalSince(mindfulSample.startDate) / 60
|
|
209
|
+
call.resolve([
|
|
210
|
+
"value": durationMinutes,
|
|
211
|
+
"timestamp": mindfulSample.startDate.timeIntervalSince1970 * 1000,
|
|
212
|
+
"endTimestamp": mindfulSample.endDate.timeIntervalSince1970 * 1000,
|
|
213
|
+
"unit": "min",
|
|
214
|
+
"metadata": ["value": mindfulSample.value]
|
|
215
|
+
])
|
|
216
|
+
}
|
|
217
|
+
healthStore.execute(query)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
156
220
|
guard aggregateTypeToHKQuantityType(dataTypeString) != nil else {
|
|
157
221
|
call.reject("Invalid data type")
|
|
158
222
|
return
|
|
@@ -162,6 +226,10 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
162
226
|
switch dataTypeString {
|
|
163
227
|
case "heart-rate":
|
|
164
228
|
return HKObjectType.quantityType(forIdentifier: .heartRate)
|
|
229
|
+
case "resting-heart-rate":
|
|
230
|
+
return HKObjectType.quantityType(forIdentifier: .restingHeartRate)
|
|
231
|
+
case "respiratory-rate":
|
|
232
|
+
return HKObjectType.quantityType(forIdentifier: .respiratoryRate)
|
|
165
233
|
case "weight":
|
|
166
234
|
return HKObjectType.quantityType(forIdentifier: .bodyMass)
|
|
167
235
|
case "steps":
|
|
@@ -172,12 +240,28 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
172
240
|
return HKObjectType.quantityType(forIdentifier: .height)
|
|
173
241
|
case "distance":
|
|
174
242
|
return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
243
|
+
case "distance-cycling":
|
|
244
|
+
return HKObjectType.quantityType(forIdentifier: .distanceCycling)
|
|
245
|
+
case "active-calories":
|
|
246
|
+
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
247
|
+
case "basal-calories":
|
|
248
|
+
return HKObjectType.quantityType(forIdentifier: .basalEnergyBurned)
|
|
249
|
+
case "blood-pressure":
|
|
250
|
+
return nil // handled above
|
|
251
|
+
case "oxygen-saturation":
|
|
252
|
+
return HKObjectType.quantityType(forIdentifier: .oxygenSaturation)
|
|
253
|
+
case "blood-glucose":
|
|
254
|
+
return HKObjectType.quantityType(forIdentifier: .bloodGlucose)
|
|
255
|
+
case "body-temperature":
|
|
256
|
+
return HKObjectType.quantityType(forIdentifier: .bodyTemperature)
|
|
257
|
+
case "basal-body-temperature":
|
|
258
|
+
return HKObjectType.quantityType(forIdentifier: .basalBodyTemperature)
|
|
259
|
+
case "body-fat":
|
|
260
|
+
return HKObjectType.quantityType(forIdentifier: .bodyFatPercentage)
|
|
261
|
+
case "flights-climbed":
|
|
262
|
+
return HKObjectType.quantityType(forIdentifier: .flightsClimbed)
|
|
263
|
+
case "exercise-time":
|
|
264
|
+
return HKObjectType.quantityType(forIdentifier: .appleExerciseTime)
|
|
181
265
|
default:
|
|
182
266
|
return nil
|
|
183
267
|
}
|
|
@@ -209,16 +293,36 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
209
293
|
var unit: HKUnit = .count()
|
|
210
294
|
if dataTypeString == "heart-rate" {
|
|
211
295
|
unit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
296
|
+
} else if dataTypeString == "resting-heart-rate" {
|
|
297
|
+
unit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
212
298
|
} else if dataTypeString == "weight" {
|
|
213
299
|
unit = .gramUnit(with: .kilo)
|
|
214
300
|
} else if dataTypeString == "hrv" {
|
|
215
301
|
unit = HKUnit.secondUnit(with: .milli)
|
|
216
302
|
} else if dataTypeString == "distance" {
|
|
217
303
|
unit = HKUnit.meter()
|
|
304
|
+
} else if dataTypeString == "distance-cycling" {
|
|
305
|
+
unit = HKUnit.meter()
|
|
218
306
|
} else if dataTypeString == "active-calories" || dataTypeString == "total-calories" {
|
|
219
307
|
unit = HKUnit.kilocalorie()
|
|
308
|
+
} else if dataTypeString == "basal-calories" {
|
|
309
|
+
unit = HKUnit.kilocalorie()
|
|
220
310
|
} else if dataTypeString == "height" {
|
|
221
311
|
unit = HKUnit.meter()
|
|
312
|
+
} else if dataTypeString == "oxygen-saturation" {
|
|
313
|
+
unit = HKUnit.percent()
|
|
314
|
+
} else if dataTypeString == "blood-glucose" {
|
|
315
|
+
unit = HKUnit(from: "mg/dL")
|
|
316
|
+
} else if dataTypeString == "body-temperature" || dataTypeString == "basal-body-temperature" {
|
|
317
|
+
unit = HKUnit.degreeCelsius()
|
|
318
|
+
} else if dataTypeString == "body-fat" {
|
|
319
|
+
unit = HKUnit.percent()
|
|
320
|
+
} else if dataTypeString == "flights-climbed" {
|
|
321
|
+
unit = HKUnit.count()
|
|
322
|
+
} else if dataTypeString == "exercise-time" {
|
|
323
|
+
unit = HKUnit.minute()
|
|
324
|
+
} else if dataTypeString == "respiratory-rate" {
|
|
325
|
+
unit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
222
326
|
}
|
|
223
327
|
let value = quantitySample.quantity.doubleValue(for: unit)
|
|
224
328
|
let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
|
|
@@ -235,57 +339,6 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
235
339
|
healthStore.execute(query)
|
|
236
340
|
}
|
|
237
341
|
|
|
238
|
-
// Convenience methods for specific data types
|
|
239
|
-
@objc func queryWeight(_ call: CAPPluginCall) {
|
|
240
|
-
queryLatestSampleWithType(call, dataType: "weight")
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
@objc func queryHeight(_ call: CAPPluginCall) {
|
|
244
|
-
queryLatestSampleWithType(call, dataType: "height")
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
@objc func queryHeartRate(_ call: CAPPluginCall) {
|
|
248
|
-
queryLatestSampleWithType(call, dataType: "heart-rate")
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
@objc func querySteps(_ call: CAPPluginCall) {
|
|
252
|
-
queryLatestSampleWithType(call, dataType: "steps")
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private func queryLatestSampleWithType(_ call: CAPPluginCall, dataType: String) {
|
|
256
|
-
// Safely coerce the original options into a [String: Any] JSObject.
|
|
257
|
-
let originalOptions = call.options as? [String: Any] ?? [:]
|
|
258
|
-
var params = originalOptions
|
|
259
|
-
params["dataType"] = dataType
|
|
260
|
-
|
|
261
|
-
// Create a proxy CAPPluginCall using the CURRENT (Capacitor 6) designated initializer.
|
|
262
|
-
// NOTE: The older init(callbackId:options:success:error:) is deprecated and *failable*,
|
|
263
|
-
// so we use the newer initializer that requires a method name. Guard against failure.
|
|
264
|
-
guard let proxyCall = CAPPluginCall(
|
|
265
|
-
callbackId: call.callbackId,
|
|
266
|
-
methodName: "queryLatestSample", // required in new API
|
|
267
|
-
options: params,
|
|
268
|
-
success: { result, _ in
|
|
269
|
-
// Forward the resolved data back to the original JS caller.
|
|
270
|
-
call.resolve(result?.data ?? [:])
|
|
271
|
-
},
|
|
272
|
-
error: { capError in
|
|
273
|
-
// Forward the error to the original call in the legacy reject format.
|
|
274
|
-
if let capError = capError {
|
|
275
|
-
call.reject(capError.message, capError.code, capError.error, capError.data)
|
|
276
|
-
} else {
|
|
277
|
-
call.reject("Unknown native error")
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
) else {
|
|
281
|
-
call.reject("Failed to create proxy call")
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Delegate the actual HealthKit fetch to the common implementation.
|
|
286
|
-
queryLatestSample(proxyCall)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
342
|
@objc func openAppleHealthSettings(_ call: CAPPluginCall) {
|
|
290
343
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
|
291
344
|
DispatchQueue.main.async {
|
|
@@ -317,6 +370,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
317
370
|
return [HKObjectType.workoutType()].compactMap{$0}
|
|
318
371
|
case "READ_HEART_RATE":
|
|
319
372
|
return [HKObjectType.quantityType(forIdentifier: .heartRate)].compactMap{$0}
|
|
373
|
+
case "READ_RESTING_HEART_RATE":
|
|
374
|
+
return [HKObjectType.quantityType(forIdentifier: .restingHeartRate)].compactMap { $0 }
|
|
320
375
|
case "READ_ROUTE":
|
|
321
376
|
return [HKSeriesType.workoutRoute()].compactMap{$0}
|
|
322
377
|
case "READ_DISTANCE":
|
|
@@ -335,6 +390,28 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
335
390
|
HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
|
|
336
391
|
HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)
|
|
337
392
|
].compactMap { $0 }
|
|
393
|
+
case "READ_RESPIRATORY_RATE":
|
|
394
|
+
return [HKObjectType.quantityType(forIdentifier: .respiratoryRate)].compactMap { $0 }
|
|
395
|
+
case "READ_OXYGEN_SATURATION":
|
|
396
|
+
return [HKObjectType.quantityType(forIdentifier: .oxygenSaturation)].compactMap { $0 }
|
|
397
|
+
case "READ_BLOOD_GLUCOSE":
|
|
398
|
+
return [HKObjectType.quantityType(forIdentifier: .bloodGlucose)].compactMap { $0 }
|
|
399
|
+
case "READ_BODY_TEMPERATURE":
|
|
400
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyTemperature)].compactMap { $0 }
|
|
401
|
+
case "READ_BASAL_BODY_TEMPERATURE":
|
|
402
|
+
return [HKObjectType.quantityType(forIdentifier: .basalBodyTemperature)].compactMap { $0 }
|
|
403
|
+
case "READ_BODY_FAT":
|
|
404
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyFatPercentage)].compactMap { $0 }
|
|
405
|
+
case "READ_FLOORS_CLIMBED":
|
|
406
|
+
return [HKObjectType.quantityType(forIdentifier: .flightsClimbed)].compactMap { $0 }
|
|
407
|
+
case "READ_BASAL_CALORIES":
|
|
408
|
+
return [
|
|
409
|
+
HKObjectType.quantityType(forIdentifier: .basalEnergyBurned)
|
|
410
|
+
].compactMap { $0 }
|
|
411
|
+
case "READ_SLEEP":
|
|
412
|
+
return [HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!].compactMap { $0 }
|
|
413
|
+
case "READ_EXERCISE_TIME":
|
|
414
|
+
return [HKObjectType.quantityType(forIdentifier: .appleExerciseTime)].compactMap { $0 }
|
|
338
415
|
// Add common alternative permission names
|
|
339
416
|
case "steps":
|
|
340
417
|
return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
|
|
@@ -371,6 +448,26 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
371
448
|
HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
|
|
372
449
|
HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)
|
|
373
450
|
].compactMap { $0 }
|
|
451
|
+
case "respiratory-rate":
|
|
452
|
+
return [HKObjectType.quantityType(forIdentifier: .respiratoryRate)].compactMap { $0 }
|
|
453
|
+
case "oxygen-saturation":
|
|
454
|
+
return [HKObjectType.quantityType(forIdentifier: .oxygenSaturation)].compactMap { $0 }
|
|
455
|
+
case "blood-glucose":
|
|
456
|
+
return [HKObjectType.quantityType(forIdentifier: .bloodGlucose)].compactMap { $0 }
|
|
457
|
+
case "body-temperature":
|
|
458
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyTemperature)].compactMap { $0 }
|
|
459
|
+
case "basal-body-temperature":
|
|
460
|
+
return [HKObjectType.quantityType(forIdentifier: .basalBodyTemperature)].compactMap { $0 }
|
|
461
|
+
case "body-fat":
|
|
462
|
+
return [HKObjectType.quantityType(forIdentifier: .bodyFatPercentage)].compactMap { $0 }
|
|
463
|
+
case "flights-climbed":
|
|
464
|
+
return [HKObjectType.quantityType(forIdentifier: .flightsClimbed)].compactMap { $0 }
|
|
465
|
+
case "basal-calories":
|
|
466
|
+
return [HKObjectType.quantityType(forIdentifier: .basalEnergyBurned)].compactMap { $0 }
|
|
467
|
+
case "exercise-time":
|
|
468
|
+
return [HKObjectType.quantityType(forIdentifier: .appleExerciseTime)].compactMap { $0 }
|
|
469
|
+
case "sleep":
|
|
470
|
+
return [HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!].compactMap { $0 }
|
|
374
471
|
default:
|
|
375
472
|
print("⚡️ [HealthPlugin] Unknown permission: \(permission)")
|
|
376
473
|
return []
|
|
@@ -385,16 +482,38 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
385
482
|
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
386
483
|
case "heart-rate":
|
|
387
484
|
return HKObjectType.quantityType(forIdentifier: .heartRate)
|
|
485
|
+
case "resting-heart-rate":
|
|
486
|
+
return HKObjectType.quantityType(forIdentifier: .restingHeartRate)
|
|
388
487
|
case "weight":
|
|
389
488
|
return HKObjectType.quantityType(forIdentifier: .bodyMass)
|
|
390
489
|
case "hrv":
|
|
391
490
|
return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
|
|
392
491
|
case "distance":
|
|
393
492
|
return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) // pick one rep type
|
|
493
|
+
case "distance-cycling":
|
|
494
|
+
return HKObjectType.quantityType(forIdentifier: .distanceCycling)
|
|
394
495
|
case "total-calories":
|
|
395
496
|
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
497
|
+
case "basal-calories":
|
|
498
|
+
return HKObjectType.quantityType(forIdentifier: .basalEnergyBurned)
|
|
396
499
|
case "height":
|
|
397
500
|
return HKObjectType.quantityType(forIdentifier: .height)
|
|
501
|
+
case "respiratory-rate":
|
|
502
|
+
return HKObjectType.quantityType(forIdentifier: .respiratoryRate)
|
|
503
|
+
case "oxygen-saturation":
|
|
504
|
+
return HKObjectType.quantityType(forIdentifier: .oxygenSaturation)
|
|
505
|
+
case "blood-glucose":
|
|
506
|
+
return HKObjectType.quantityType(forIdentifier: .bloodGlucose)
|
|
507
|
+
case "body-temperature":
|
|
508
|
+
return HKObjectType.quantityType(forIdentifier: .bodyTemperature)
|
|
509
|
+
case "basal-body-temperature":
|
|
510
|
+
return HKObjectType.quantityType(forIdentifier: .basalBodyTemperature)
|
|
511
|
+
case "body-fat":
|
|
512
|
+
return HKObjectType.quantityType(forIdentifier: .bodyFatPercentage)
|
|
513
|
+
case "flights-climbed":
|
|
514
|
+
return HKObjectType.quantityType(forIdentifier: .flightsClimbed)
|
|
515
|
+
case "exercise-time":
|
|
516
|
+
return HKObjectType.quantityType(forIdentifier: .appleExerciseTime)
|
|
398
517
|
default:
|
|
399
518
|
return nil
|
|
400
519
|
}
|
|
@@ -406,13 +525,21 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
406
525
|
let endDateString = call.getString("endDate"),
|
|
407
526
|
let dataTypeString = call.getString("dataType"),
|
|
408
527
|
let bucket = call.getString("bucket"),
|
|
409
|
-
let
|
|
410
|
-
let
|
|
528
|
+
let rawStartDate = self.isoDateFormatter.date(from: startDateString),
|
|
529
|
+
let rawEndDate = self.isoDateFormatter.date(from: endDateString) else {
|
|
411
530
|
DispatchQueue.main.async {
|
|
412
531
|
call.reject("Invalid parameters")
|
|
413
532
|
}
|
|
414
533
|
return
|
|
415
534
|
}
|
|
535
|
+
let calendar = Calendar.current
|
|
536
|
+
var startDate = rawStartDate
|
|
537
|
+
var endDate = rawEndDate
|
|
538
|
+
if bucket == "day" {
|
|
539
|
+
startDate = calendar.startOfDay(for: rawStartDate)
|
|
540
|
+
let endDayStart = calendar.startOfDay(for: rawEndDate)
|
|
541
|
+
endDate = calendar.date(byAdding: .day, to: endDayStart) ?? endDayStart
|
|
542
|
+
}
|
|
416
543
|
if dataTypeString == "mindfulness" {
|
|
417
544
|
self.queryMindfulnessAggregated(startDate: startDate, endDate: endDate) { result, error in
|
|
418
545
|
DispatchQueue.main.async {
|
|
@@ -423,6 +550,48 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
423
550
|
}
|
|
424
551
|
}
|
|
425
552
|
}
|
|
553
|
+
} else if dataTypeString == "blood-pressure" {
|
|
554
|
+
guard bucket == "day" else {
|
|
555
|
+
DispatchQueue.main.async {
|
|
556
|
+
call.reject("Blood pressure aggregation only supports daily buckets")
|
|
557
|
+
}
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
self.queryBloodPressureAggregated(startDate: startDate, endDate: endDate) { result, error in
|
|
561
|
+
DispatchQueue.main.async {
|
|
562
|
+
if let error = error {
|
|
563
|
+
call.reject(error.localizedDescription)
|
|
564
|
+
} else if let result = result {
|
|
565
|
+
call.resolve(["aggregatedData": result])
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
} else if dataTypeString == "total-calories" {
|
|
570
|
+
guard let interval = calculateInterval(bucket: bucket) else {
|
|
571
|
+
DispatchQueue.main.async {
|
|
572
|
+
call.reject("Invalid bucket")
|
|
573
|
+
}
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
self.queryTotalCaloriesAggregated(startDate: startDate, endDate: endDate, interval: interval) { result, error in
|
|
577
|
+
DispatchQueue.main.async {
|
|
578
|
+
if let error = error {
|
|
579
|
+
call.reject(error.localizedDescription)
|
|
580
|
+
} else if let result = result {
|
|
581
|
+
call.resolve(["aggregatedData": result])
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} else if dataTypeString == "sleep" {
|
|
586
|
+
self.querySleepAggregated(startDate: startDate, endDate: endDate) { result, error in
|
|
587
|
+
DispatchQueue.main.async {
|
|
588
|
+
if let error = error {
|
|
589
|
+
call.reject(error.localizedDescription)
|
|
590
|
+
} else if let result = result {
|
|
591
|
+
call.resolve(["aggregatedData": result])
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
426
595
|
} else {
|
|
427
596
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
428
597
|
guard let interval = calculateInterval(bucket: bucket) else {
|
|
@@ -441,7 +610,10 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
441
610
|
switch dataType.aggregationStyle {
|
|
442
611
|
case .cumulative:
|
|
443
612
|
return .cumulativeSum
|
|
444
|
-
case .discrete
|
|
613
|
+
case .discrete,
|
|
614
|
+
.discreteArithmetic,
|
|
615
|
+
.discreteTemporallyWeighted,
|
|
616
|
+
.discreteEquivalentContinuousLevel:
|
|
445
617
|
return .discreteAverage
|
|
446
618
|
@unknown default:
|
|
447
619
|
return .discreteAverage
|
|
@@ -471,12 +643,19 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
471
643
|
let unit: HKUnit = {
|
|
472
644
|
switch dataTypeString {
|
|
473
645
|
case "steps": return .count()
|
|
474
|
-
case "active-calories", "total-calories": return .kilocalorie()
|
|
475
|
-
case "distance": return .meter()
|
|
646
|
+
case "active-calories", "total-calories", "basal-calories": return .kilocalorie()
|
|
647
|
+
case "distance", "distance-cycling": return .meter()
|
|
476
648
|
case "weight": return .gramUnit(with: .kilo)
|
|
477
649
|
case "height": return .meter()
|
|
478
|
-
case "heart-rate": return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
650
|
+
case "heart-rate", "resting-heart-rate": return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
651
|
+
case "respiratory-rate": return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
479
652
|
case "hrv": return HKUnit.secondUnit(with: .milli)
|
|
653
|
+
case "oxygen-saturation": return HKUnit.percent()
|
|
654
|
+
case "blood-glucose": return HKUnit(from: "mg/dL")
|
|
655
|
+
case "body-temperature", "basal-body-temperature": return HKUnit.degreeCelsius()
|
|
656
|
+
case "body-fat": return HKUnit.percent()
|
|
657
|
+
case "flights-climbed": return .count()
|
|
658
|
+
case "exercise-time": return HKUnit.minute()
|
|
480
659
|
case "mindfulness": return HKUnit.second()
|
|
481
660
|
default: return .count()
|
|
482
661
|
}
|
|
@@ -532,6 +711,275 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
532
711
|
}
|
|
533
712
|
healthStore.execute(query)
|
|
534
713
|
}
|
|
714
|
+
|
|
715
|
+
func querySleepAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
|
|
716
|
+
guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
|
|
717
|
+
DispatchQueue.main.async {
|
|
718
|
+
completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "SleepAnalysis type unavailable"]))
|
|
719
|
+
}
|
|
720
|
+
return
|
|
721
|
+
}
|
|
722
|
+
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
723
|
+
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
|
|
724
|
+
var dailyDurations: [Date: TimeInterval] = [:]
|
|
725
|
+
let calendar = Calendar.current
|
|
726
|
+
if let categorySamples = samples as? [HKCategorySample], error == nil {
|
|
727
|
+
for sample in categorySamples {
|
|
728
|
+
// Ignore in-bed samples; we care about actual sleep
|
|
729
|
+
if sample.value == HKCategoryValueSleepAnalysis.inBed.rawValue { continue }
|
|
730
|
+
let startOfDay = calendar.startOfDay(for: sample.startDate)
|
|
731
|
+
let duration = sample.endDate.timeIntervalSince(sample.startDate)
|
|
732
|
+
dailyDurations[startOfDay, default: 0] += duration
|
|
733
|
+
}
|
|
734
|
+
var aggregatedSamples: [[String: Any]] = []
|
|
735
|
+
let dayComponent = DateComponents(day: 1)
|
|
736
|
+
for (date, duration) in dailyDurations {
|
|
737
|
+
aggregatedSamples.append([
|
|
738
|
+
"startDate": date,
|
|
739
|
+
"endDate": calendar.date(byAdding: dayComponent, to: date) as Any,
|
|
740
|
+
"value": duration
|
|
741
|
+
])
|
|
742
|
+
}
|
|
743
|
+
DispatchQueue.main.async {
|
|
744
|
+
completion(aggregatedSamples, nil)
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
DispatchQueue.main.async {
|
|
748
|
+
completion(nil, error)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
healthStore.execute(query)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
func queryBloodPressureAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
|
|
756
|
+
guard let bpType = HKObjectType.correlationType(forIdentifier: .bloodPressure),
|
|
757
|
+
let systolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
|
|
758
|
+
let diastolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
|
|
759
|
+
DispatchQueue.main.async {
|
|
760
|
+
completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "Blood pressure types unavailable"]))
|
|
761
|
+
}
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
766
|
+
let query = HKSampleQuery(sampleType: bpType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
|
|
767
|
+
if let error = error {
|
|
768
|
+
DispatchQueue.main.async {
|
|
769
|
+
completion(nil, error)
|
|
770
|
+
}
|
|
771
|
+
return
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
guard let correlations = samples as? [HKCorrelation] else {
|
|
775
|
+
DispatchQueue.main.async {
|
|
776
|
+
completion([], nil)
|
|
777
|
+
}
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
let calendar = Calendar.current
|
|
782
|
+
let dayComponent = DateComponents(day: 1)
|
|
783
|
+
let unit = HKUnit.millimeterOfMercury()
|
|
784
|
+
|
|
785
|
+
var grouped: [Date: [(Double, Double)]] = [:]
|
|
786
|
+
for correlation in correlations {
|
|
787
|
+
guard let systolicSample = correlation.objects(for: systolicType).first as? HKQuantitySample,
|
|
788
|
+
let diastolicSample = correlation.objects(for: diastolicType).first as? HKQuantitySample else { continue }
|
|
789
|
+
let dayStart = calendar.startOfDay(for: correlation.startDate)
|
|
790
|
+
let systolicValue = systolicSample.quantity.doubleValue(for: unit)
|
|
791
|
+
let diastolicValue = diastolicSample.quantity.doubleValue(for: unit)
|
|
792
|
+
grouped[dayStart, default: []].append((systolicValue, diastolicValue))
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
var aggregatedSamples: [[String: Any]] = []
|
|
796
|
+
for (dayStart, readings) in grouped {
|
|
797
|
+
guard !readings.isEmpty else { continue }
|
|
798
|
+
let systolicAvg = readings.map { $0.0 }.reduce(0, +) / Double(readings.count)
|
|
799
|
+
let diastolicAvg = readings.map { $0.1 }.reduce(0, +) / Double(readings.count)
|
|
800
|
+
let bucketStart = dayStart.timeIntervalSince1970 * 1000
|
|
801
|
+
let bucketEnd = (calendar.date(byAdding: dayComponent, to: dayStart)?.timeIntervalSince1970 ?? dayStart.timeIntervalSince1970) * 1000
|
|
802
|
+
aggregatedSamples.append([
|
|
803
|
+
"startDate": bucketStart,
|
|
804
|
+
"endDate": bucketEnd,
|
|
805
|
+
"systolic": systolicAvg,
|
|
806
|
+
"diastolic": diastolicAvg,
|
|
807
|
+
"value": systolicAvg,
|
|
808
|
+
"unit": unit.unitString
|
|
809
|
+
])
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
aggregatedSamples.sort { (lhs, rhs) in
|
|
813
|
+
guard let lhsStart = lhs["startDate"] as? Double, let rhsStart = rhs["startDate"] as? Double else { return false }
|
|
814
|
+
return lhsStart < rhsStart
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
DispatchQueue.main.async {
|
|
818
|
+
completion(aggregatedSamples, nil)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
healthStore.execute(query)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private func queryLatestTotalCalories(_ call: CAPPluginCall) {
|
|
825
|
+
let unit = HKUnit.kilocalorie()
|
|
826
|
+
let group = DispatchGroup()
|
|
827
|
+
|
|
828
|
+
var activeValue: Double?
|
|
829
|
+
var basalValue: Double?
|
|
830
|
+
var activeDate: Date?
|
|
831
|
+
var basalDate: Date?
|
|
832
|
+
var queryError: Error?
|
|
833
|
+
let basalSupported = HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) != nil
|
|
834
|
+
|
|
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
|
+
group.enter()
|
|
857
|
+
fetchLatest(.activeEnergyBurned) { value, date, error in
|
|
858
|
+
activeValue = value
|
|
859
|
+
activeDate = date
|
|
860
|
+
queryError = queryError ?? error
|
|
861
|
+
group.leave()
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if basalSupported {
|
|
865
|
+
group.enter()
|
|
866
|
+
fetchLatest(.basalEnergyBurned) { value, date, error in
|
|
867
|
+
basalValue = value
|
|
868
|
+
basalDate = date
|
|
869
|
+
queryError = queryError ?? error
|
|
870
|
+
group.leave()
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
group.notify(queue: .main) {
|
|
875
|
+
if let error = queryError {
|
|
876
|
+
call.reject("Error fetching total calories: \(error.localizedDescription)")
|
|
877
|
+
return
|
|
878
|
+
}
|
|
879
|
+
guard activeValue != nil || basalValue != nil else {
|
|
880
|
+
call.reject("No sample found", "NO_SAMPLE")
|
|
881
|
+
return
|
|
882
|
+
}
|
|
883
|
+
let total = (activeValue ?? 0) + (basalValue ?? 0)
|
|
884
|
+
let timestamp = max(activeDate?.timeIntervalSince1970 ?? 0, basalDate?.timeIntervalSince1970 ?? 0) * 1000
|
|
885
|
+
call.resolve([
|
|
886
|
+
"value": total,
|
|
887
|
+
"timestamp": timestamp,
|
|
888
|
+
"unit": unit.unitString
|
|
889
|
+
])
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private func collectEnergyBuckets(
|
|
894
|
+
_ identifier: HKQuantityTypeIdentifier,
|
|
895
|
+
startDate: Date,
|
|
896
|
+
endDate: Date,
|
|
897
|
+
interval: DateComponents,
|
|
898
|
+
completion: @escaping ([TimeInterval: (start: TimeInterval, end: TimeInterval, value: Double)]?, Error?) -> Void
|
|
899
|
+
) {
|
|
900
|
+
guard let type = HKObjectType.quantityType(forIdentifier: identifier) else {
|
|
901
|
+
completion([:], nil)
|
|
902
|
+
return
|
|
903
|
+
}
|
|
904
|
+
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
905
|
+
let query = HKStatisticsCollectionQuery(
|
|
906
|
+
quantityType: type,
|
|
907
|
+
quantitySamplePredicate: predicate,
|
|
908
|
+
options: .cumulativeSum,
|
|
909
|
+
anchorDate: startDate,
|
|
910
|
+
intervalComponents: interval
|
|
911
|
+
)
|
|
912
|
+
query.initialResultsHandler = { _, result, error in
|
|
913
|
+
if let error = error {
|
|
914
|
+
completion(nil, error)
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
var buckets: [TimeInterval: (start: TimeInterval, end: TimeInterval, value: Double)] = [:]
|
|
918
|
+
result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
|
|
919
|
+
guard let quantity = statistics.sumQuantity() else { return }
|
|
920
|
+
let startMs = statistics.startDate.timeIntervalSince1970 * 1000
|
|
921
|
+
let endMs = statistics.endDate.timeIntervalSince1970 * 1000
|
|
922
|
+
buckets[startMs] = (startMs, endMs, quantity.doubleValue(for: HKUnit.kilocalorie()))
|
|
923
|
+
}
|
|
924
|
+
completion(buckets, nil)
|
|
925
|
+
}
|
|
926
|
+
healthStore.execute(query)
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private func queryTotalCaloriesAggregated(
|
|
930
|
+
startDate: Date,
|
|
931
|
+
endDate: Date,
|
|
932
|
+
interval: DateComponents,
|
|
933
|
+
completion: @escaping ([[String: Any]]?, Error?) -> Void
|
|
934
|
+
) {
|
|
935
|
+
let group = DispatchGroup()
|
|
936
|
+
let basalSupported = HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) != nil
|
|
937
|
+
var activeBuckets: [TimeInterval: (start: TimeInterval, end: TimeInterval, value: Double)]?
|
|
938
|
+
var basalBuckets: [TimeInterval: (start: TimeInterval, end: TimeInterval, value: Double)]?
|
|
939
|
+
var queryError: Error?
|
|
940
|
+
|
|
941
|
+
group.enter()
|
|
942
|
+
collectEnergyBuckets(.activeEnergyBurned, startDate: startDate, endDate: endDate, interval: interval) { buckets, error in
|
|
943
|
+
activeBuckets = buckets
|
|
944
|
+
queryError = queryError ?? error
|
|
945
|
+
group.leave()
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if basalSupported {
|
|
949
|
+
group.enter()
|
|
950
|
+
collectEnergyBuckets(.basalEnergyBurned, startDate: startDate, endDate: endDate, interval: interval) { buckets, error in
|
|
951
|
+
basalBuckets = buckets
|
|
952
|
+
queryError = queryError ?? error
|
|
953
|
+
group.leave()
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
group.notify(queue: .main) {
|
|
958
|
+
if let error = queryError {
|
|
959
|
+
completion(nil, error)
|
|
960
|
+
return
|
|
961
|
+
}
|
|
962
|
+
let active = activeBuckets ?? [:]
|
|
963
|
+
let basal = basalBuckets ?? [:]
|
|
964
|
+
let allKeys = Set(active.keys).union(basal.keys).sorted()
|
|
965
|
+
var aggregated: [[String: Any]] = []
|
|
966
|
+
let calendar = Calendar.current
|
|
967
|
+
for key in allKeys {
|
|
968
|
+
let startMs = key
|
|
969
|
+
let endMs = active[key]?.end ?? basal[key]?.end ?? {
|
|
970
|
+
let date = Date(timeIntervalSince1970: startMs / 1000)
|
|
971
|
+
return (calendar.date(byAdding: interval, to: date) ?? date).timeIntervalSince1970 * 1000
|
|
972
|
+
}()
|
|
973
|
+
let value = (active[key]?.value ?? 0) + (basal[key]?.value ?? 0)
|
|
974
|
+
aggregated.append([
|
|
975
|
+
"startDate": startMs,
|
|
976
|
+
"endDate": endMs,
|
|
977
|
+
"value": value
|
|
978
|
+
])
|
|
979
|
+
}
|
|
980
|
+
completion(aggregated.sorted { ($0["startDate"] as? Double ?? 0) < ($1["startDate"] as? Double ?? 0) }, nil)
|
|
981
|
+
}
|
|
982
|
+
}
|
|
535
983
|
|
|
536
984
|
private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping (Double?) -> Void) {
|
|
537
985
|
guard let quantityType = dataType else {
|
|
@@ -609,7 +1057,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
609
1057
|
return
|
|
610
1058
|
}
|
|
611
1059
|
let outerGroup = DispatchGroup()
|
|
612
|
-
let resultsQueue = DispatchQueue(label: "com.
|
|
1060
|
+
let resultsQueue = DispatchQueue(label: "com.flomentumsolutions.healthplugin.workoutResults")
|
|
613
1061
|
var workoutResults: [[String: Any]] = []
|
|
614
1062
|
var errors: [String: String] = [:]
|
|
615
1063
|
|
|
@@ -627,48 +1075,54 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
627
1075
|
"distance": workout.totalDistance?.doubleValue(for: .meter()) ?? 0
|
|
628
1076
|
]
|
|
629
1077
|
let innerGroup = DispatchGroup()
|
|
1078
|
+
let heartRateQueue = DispatchQueue(label: "com.flomentumsolutions.healthplugin.heartRates")
|
|
1079
|
+
let routeQueue = DispatchQueue(label: "com.flomentumsolutions.healthplugin.routes")
|
|
630
1080
|
var localHeartRates: [[String: Any]] = []
|
|
631
1081
|
var localRoutes: [[String: Any]] = []
|
|
632
1082
|
|
|
633
1083
|
if includeHeartRate {
|
|
634
1084
|
innerGroup.enter()
|
|
635
1085
|
self.queryHeartRate(for: workout) { rates, error in
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
errors["heart-rate"] = error
|
|
1086
|
+
heartRateQueue.async {
|
|
1087
|
+
localHeartRates = rates
|
|
1088
|
+
if let error = error {
|
|
1089
|
+
errors["heart-rate"] = error
|
|
640
1090
|
}
|
|
1091
|
+
innerGroup.leave()
|
|
641
1092
|
}
|
|
642
|
-
innerGroup.leave()
|
|
643
1093
|
}
|
|
644
1094
|
}
|
|
645
1095
|
if includeRoute {
|
|
646
1096
|
innerGroup.enter()
|
|
647
1097
|
self.queryRoute(for: workout) { routes, error in
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
errors["route"] = error
|
|
1098
|
+
routeQueue.async {
|
|
1099
|
+
localRoutes = routes
|
|
1100
|
+
if let error = error {
|
|
1101
|
+
errors["route"] = error
|
|
652
1102
|
}
|
|
1103
|
+
innerGroup.leave()
|
|
653
1104
|
}
|
|
654
|
-
innerGroup.leave()
|
|
655
1105
|
}
|
|
656
1106
|
}
|
|
657
1107
|
if includeSteps {
|
|
658
1108
|
innerGroup.enter()
|
|
659
1109
|
self.queryAggregated(for: workout.startDate, for: workout.endDate, for: HKObjectType.quantityType(forIdentifier: .stepCount)) { steps in
|
|
660
|
-
|
|
661
|
-
|
|
1110
|
+
resultsQueue.async {
|
|
1111
|
+
if let steps = steps {
|
|
1112
|
+
localDict["steps"] = steps
|
|
1113
|
+
}
|
|
1114
|
+
innerGroup.leave()
|
|
662
1115
|
}
|
|
663
|
-
innerGroup.leave()
|
|
664
1116
|
}
|
|
665
1117
|
}
|
|
666
|
-
innerGroup.notify(queue:
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1118
|
+
innerGroup.notify(queue: resultsQueue) {
|
|
1119
|
+
heartRateQueue.sync {
|
|
1120
|
+
localDict["heartRate"] = localHeartRates
|
|
1121
|
+
}
|
|
1122
|
+
routeQueue.sync {
|
|
1123
|
+
localDict["route"] = localRoutes
|
|
671
1124
|
}
|
|
1125
|
+
workoutResults.append(localDict)
|
|
672
1126
|
outerGroup.leave()
|
|
673
1127
|
}
|
|
674
1128
|
}
|
|
@@ -721,7 +1175,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
721
1175
|
guard let self = self else { return }
|
|
722
1176
|
if let routes = samples as? [HKWorkoutRoute], error == nil {
|
|
723
1177
|
let routeDispatchGroup = DispatchGroup()
|
|
724
|
-
let allLocationsQueue = DispatchQueue(label: "com.
|
|
1178
|
+
let allLocationsQueue = DispatchQueue(label: "com.flomentumsolutions.healthplugin.allLocations")
|
|
725
1179
|
var allLocations: [[String: Any]] = []
|
|
726
1180
|
|
|
727
1181
|
for route in routes {
|
|
@@ -870,4 +1324,4 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
870
1324
|
3000 : "other"
|
|
871
1325
|
]
|
|
872
1326
|
|
|
873
|
-
}
|
|
1327
|
+
}
|