@flomentumsolutions/capacitor-health-extended 0.0.8 → 0.0.9
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.
|
@@ -300,37 +300,39 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
300
300
|
let bucket = call.getString("bucket"),
|
|
301
301
|
let startDate = self.isoDateFormatter.date(from: startDateString),
|
|
302
302
|
let endDate = self.isoDateFormatter.date(from: endDateString) else {
|
|
303
|
-
|
|
303
|
+
DispatchQueue.main.async {
|
|
304
|
+
call.reject("Invalid parameters")
|
|
305
|
+
}
|
|
304
306
|
return
|
|
305
307
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
if dataTypeString == "mindfulness" {
|
|
309
|
+
self.queryMindfulnessAggregated(startDate: startDate, endDate: endDate) { result, error in
|
|
310
|
+
DispatchQueue.main.async {
|
|
311
|
+
if let error = error {
|
|
312
|
+
call.reject(error.localizedDescription)
|
|
313
|
+
} else if let result = result {
|
|
314
|
+
call.resolve(["aggregatedData": result])
|
|
315
|
+
}
|
|
313
316
|
}
|
|
314
317
|
}
|
|
315
318
|
} else {
|
|
316
319
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
317
|
-
|
|
318
320
|
guard let interval = calculateInterval(bucket: bucket) else {
|
|
319
|
-
|
|
321
|
+
DispatchQueue.main.async {
|
|
322
|
+
call.reject("Invalid bucket")
|
|
323
|
+
}
|
|
320
324
|
return
|
|
321
325
|
}
|
|
322
|
-
|
|
323
326
|
guard let dataType = aggregateTypeToHKQuantityType(dataTypeString) else {
|
|
324
|
-
|
|
327
|
+
DispatchQueue.main.async {
|
|
328
|
+
call.reject("Invalid data type")
|
|
329
|
+
}
|
|
325
330
|
return
|
|
326
331
|
}
|
|
327
|
-
|
|
328
332
|
let options: HKStatisticsOptions = {
|
|
329
333
|
switch dataType.aggregationStyle {
|
|
330
334
|
case .cumulative:
|
|
331
335
|
return .cumulativeSum
|
|
332
|
-
|
|
333
|
-
// Newer discrete aggregation styles (iOS 15 +)
|
|
334
336
|
case .discreteAverage:
|
|
335
337
|
return .discreteAverage
|
|
336
338
|
@available(iOS 17.0, *)
|
|
@@ -342,16 +344,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
342
344
|
@available(iOS 17.0, *)
|
|
343
345
|
case .discreteArithmetic:
|
|
344
346
|
return .discreteAverage
|
|
345
|
-
|
|
346
|
-
// Legacy discrete fallback
|
|
347
347
|
case .discrete:
|
|
348
348
|
return .discreteAverage
|
|
349
|
-
|
|
350
349
|
@unknown default:
|
|
351
350
|
return .discreteAverage
|
|
352
351
|
}
|
|
353
352
|
}()
|
|
354
|
-
|
|
355
353
|
let query = HKStatisticsCollectionQuery(
|
|
356
354
|
quantityType: dataType,
|
|
357
355
|
quantitySamplePredicate: predicate,
|
|
@@ -359,139 +357,107 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
359
357
|
anchorDate: startDate,
|
|
360
358
|
intervalComponents: interval
|
|
361
359
|
)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
398
|
-
case "hrv":
|
|
399
|
-
return HKUnit.secondUnit(with: .milli)
|
|
400
|
-
case "mindfulness":
|
|
401
|
-
return HKUnit.second()
|
|
402
|
-
default:
|
|
403
|
-
return .count()
|
|
404
|
-
}
|
|
405
|
-
}()
|
|
406
|
-
|
|
407
|
-
let value = quantity.doubleValue(for: unit)
|
|
408
|
-
|
|
409
|
-
aggregatedSamples.append([
|
|
410
|
-
"startDate": bucketStart,
|
|
411
|
-
"endDate": bucketEnd,
|
|
412
|
-
"value": value
|
|
413
|
-
])
|
|
360
|
+
query.initialResultsHandler = { _, result, error in
|
|
361
|
+
DispatchQueue.main.async {
|
|
362
|
+
if let error = error {
|
|
363
|
+
call.reject("Error fetching aggregated data: \(error.localizedDescription)")
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
var aggregatedSamples: [[String: Any]] = []
|
|
367
|
+
result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
|
|
368
|
+
let quantity: HKQuantity? = options.contains(.cumulativeSum)
|
|
369
|
+
? statistics.sumQuantity()
|
|
370
|
+
: statistics.averageQuantity()
|
|
371
|
+
guard let quantity = quantity else { return }
|
|
372
|
+
let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
|
|
373
|
+
let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
|
|
374
|
+
let unit: HKUnit = {
|
|
375
|
+
switch dataTypeString {
|
|
376
|
+
case "steps": return .count()
|
|
377
|
+
case "active-calories", "total-calories": return .kilocalorie()
|
|
378
|
+
case "distance": return .meter()
|
|
379
|
+
case "weight": return .gramUnit(with: .kilo)
|
|
380
|
+
case "height": return .meter()
|
|
381
|
+
case "heart-rate": return HKUnit.count().unitDivided(by: HKUnit.minute())
|
|
382
|
+
case "hrv": return HKUnit.secondUnit(with: .milli)
|
|
383
|
+
case "mindfulness": return HKUnit.second()
|
|
384
|
+
default: return .count()
|
|
385
|
+
}
|
|
386
|
+
}()
|
|
387
|
+
let value = quantity.doubleValue(for: unit)
|
|
388
|
+
aggregatedSamples.append([
|
|
389
|
+
"startDate": bucketStart,
|
|
390
|
+
"endDate": bucketEnd,
|
|
391
|
+
"value": value
|
|
392
|
+
])
|
|
393
|
+
}
|
|
394
|
+
call.resolve(["aggregatedData": aggregatedSamples])
|
|
414
395
|
}
|
|
415
|
-
|
|
416
|
-
call.resolve(["aggregatedData": aggregatedSamples])
|
|
417
396
|
}
|
|
418
|
-
|
|
419
397
|
healthStore.execute(query)
|
|
420
398
|
}
|
|
421
399
|
}
|
|
422
|
-
|
|
423
|
-
func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping
|
|
400
|
+
|
|
401
|
+
func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
|
|
424
402
|
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
425
|
-
|
|
403
|
+
DispatchQueue.main.async {
|
|
404
|
+
completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "MindfulSession type unavailable"]))
|
|
405
|
+
}
|
|
426
406
|
return
|
|
427
407
|
}
|
|
428
|
-
|
|
429
408
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
430
409
|
let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
|
|
431
|
-
guard let categorySamples = samples as? [HKCategorySample], error == nil else {
|
|
432
|
-
completion(nil, error)
|
|
433
|
-
return
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Aggregate total time per day
|
|
437
|
-
|
|
438
410
|
var dailyDurations: [Date: TimeInterval] = [:]
|
|
439
411
|
let calendar = Calendar.current
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
412
|
+
if let categorySamples = samples as? [HKCategorySample], error == nil {
|
|
413
|
+
for sample in categorySamples {
|
|
414
|
+
let startOfDay = calendar.startOfDay(for: sample.startDate)
|
|
415
|
+
let duration = sample.endDate.timeIntervalSince(sample.startDate)
|
|
416
|
+
dailyDurations[startOfDay, default: 0] += duration
|
|
417
|
+
}
|
|
418
|
+
var aggregatedSamples: [[String: Any]] = []
|
|
419
|
+
let dayComponent = DateComponents(day: 1)
|
|
420
|
+
for (date, duration) in dailyDurations {
|
|
421
|
+
aggregatedSamples.append([
|
|
422
|
+
"startDate": date,
|
|
423
|
+
"endDate": calendar.date(byAdding: dayComponent, to: date) as Any,
|
|
424
|
+
"value": duration
|
|
425
|
+
])
|
|
426
|
+
}
|
|
427
|
+
DispatchQueue.main.async {
|
|
428
|
+
completion(aggregatedSamples, nil)
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
DispatchQueue.main.async {
|
|
432
|
+
completion(nil, error)
|
|
449
433
|
}
|
|
450
434
|
}
|
|
451
|
-
|
|
452
|
-
var aggregatedSamples: [[String: Any]] = []
|
|
453
|
-
var dayComponent = DateComponents()
|
|
454
|
-
dayComponent.day = 1
|
|
455
|
-
dailyDurations.forEach { (dateAndDuration) in
|
|
456
|
-
aggregatedSamples.append([
|
|
457
|
-
"startDate": dateAndDuration.key as Any,
|
|
458
|
-
"endDate": calendar.date(byAdding: dayComponent, to: dateAndDuration.key) as Any,
|
|
459
|
-
"value": dateAndDuration.value
|
|
460
|
-
])
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
completion(aggregatedSamples, nil)
|
|
464
435
|
}
|
|
465
|
-
|
|
466
436
|
healthStore.execute(query)
|
|
467
437
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping @Sendable(Double?) -> Void) {
|
|
472
|
-
|
|
473
|
-
|
|
438
|
+
|
|
439
|
+
private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping (Double?) -> Void) {
|
|
474
440
|
guard let quantityType = dataType else {
|
|
475
|
-
|
|
441
|
+
DispatchQueue.main.async {
|
|
442
|
+
completion(nil)
|
|
443
|
+
}
|
|
476
444
|
return
|
|
477
445
|
}
|
|
478
|
-
|
|
479
446
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
480
|
-
|
|
481
447
|
let query = HKStatisticsQuery(
|
|
482
448
|
quantityType: quantityType,
|
|
483
449
|
quantitySamplePredicate: predicate,
|
|
484
450
|
options: .cumulativeSum
|
|
485
451
|
) { _, result, _ in
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return
|
|
452
|
+
let value: Double? = {
|
|
453
|
+
guard let result = result, let sum = result.sumQuantity() else { return 0.0 }
|
|
454
|
+
return sum.doubleValue(for: HKUnit.count())
|
|
455
|
+
}()
|
|
456
|
+
DispatchQueue.main.async {
|
|
457
|
+
completion(value)
|
|
489
458
|
}
|
|
490
|
-
completion(sum.doubleValue(for: HKUnit.count()))
|
|
491
459
|
}
|
|
492
|
-
|
|
493
460
|
healthStore.execute(query)
|
|
494
|
-
|
|
495
461
|
}
|
|
496
462
|
|
|
497
463
|
|
|
@@ -529,30 +495,28 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
529
495
|
call.reject("Invalid parameters")
|
|
530
496
|
return
|
|
531
497
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
// Create a predicate to filter workouts by date
|
|
498
|
+
|
|
536
499
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
|
537
|
-
|
|
538
|
-
|
|
500
|
+
let workoutQuery = HKSampleQuery(sampleType: HKObjectType.workoutType(), predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { [weak self] query, samples, error in
|
|
501
|
+
guard let self = self else { return }
|
|
539
502
|
if let error = error {
|
|
540
|
-
|
|
503
|
+
DispatchQueue.main.async {
|
|
504
|
+
call.reject("Error querying workouts: \(error.localizedDescription)")
|
|
505
|
+
}
|
|
541
506
|
return
|
|
542
507
|
}
|
|
543
|
-
|
|
544
508
|
guard let workouts = samples as? [HKWorkout] else {
|
|
545
|
-
|
|
509
|
+
DispatchQueue.main.async {
|
|
510
|
+
call.resolve(["workouts": []])
|
|
511
|
+
}
|
|
546
512
|
return
|
|
547
513
|
}
|
|
548
|
-
|
|
549
|
-
var
|
|
514
|
+
let outerGroup = DispatchGroup()
|
|
515
|
+
var workoutResults: [[String: Any]] = []
|
|
550
516
|
var errors: [String: String] = [:]
|
|
551
|
-
let dispatchGroup = DispatchGroup()
|
|
552
|
-
|
|
553
|
-
// Process each workout
|
|
554
517
|
for workout in workouts {
|
|
555
|
-
|
|
518
|
+
outerGroup.enter()
|
|
519
|
+
var localDict: [String: Any] = [
|
|
556
520
|
"startDate": workout.startDate,
|
|
557
521
|
"endDate": workout.endDate,
|
|
558
522
|
"workoutType": self.workoutTypeMapping[workout.workoutActivityType.rawValue, default: "other"],
|
|
@@ -563,59 +527,45 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
563
527
|
"calories": workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? 0,
|
|
564
528
|
"distance": workout.totalDistance?.doubleValue(for: .meter()) ?? 0
|
|
565
529
|
]
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
var
|
|
569
|
-
var routeSamples: [[String: Any]] = []
|
|
570
|
-
|
|
571
|
-
// Query heart rate data if requested
|
|
530
|
+
let innerGroup = DispatchGroup()
|
|
531
|
+
var localHeartRates: [[String: Any]] = []
|
|
532
|
+
var localRoutes: [[String: Any]] = []
|
|
572
533
|
if includeHeartRate {
|
|
573
|
-
|
|
574
|
-
self.queryHeartRate(for: workout
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
dispatchGroup.leave()
|
|
580
|
-
})
|
|
534
|
+
innerGroup.enter()
|
|
535
|
+
self.queryHeartRate(for: workout) { rates, error in
|
|
536
|
+
localHeartRates = rates
|
|
537
|
+
if let error = error { errors["heart-rate"] = error }
|
|
538
|
+
innerGroup.leave()
|
|
539
|
+
}
|
|
581
540
|
}
|
|
582
|
-
|
|
583
|
-
// Query route data if requested
|
|
584
541
|
if includeRoute {
|
|
585
|
-
|
|
586
|
-
self.queryRoute(for: workout
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
dispatchGroup.leave()
|
|
592
|
-
})
|
|
542
|
+
innerGroup.enter()
|
|
543
|
+
self.queryRoute(for: workout) { routes, error in
|
|
544
|
+
localRoutes = routes
|
|
545
|
+
if let error = error { errors["route"] = error }
|
|
546
|
+
innerGroup.leave()
|
|
547
|
+
}
|
|
593
548
|
}
|
|
594
|
-
|
|
595
549
|
if includeSteps {
|
|
596
|
-
|
|
597
|
-
self.queryAggregated(for: workout.startDate, for: workout.endDate, for: HKObjectType.quantityType(forIdentifier: .stepCount)
|
|
598
|
-
if
|
|
599
|
-
|
|
550
|
+
innerGroup.enter()
|
|
551
|
+
self.queryAggregated(for: workout.startDate, for: workout.endDate, for: HKObjectType.quantityType(forIdentifier: .stepCount)) { steps in
|
|
552
|
+
if let steps = steps {
|
|
553
|
+
localDict["steps"] = steps
|
|
600
554
|
}
|
|
601
|
-
|
|
602
|
-
}
|
|
555
|
+
innerGroup.leave()
|
|
556
|
+
}
|
|
603
557
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
558
|
+
innerGroup.notify(queue: .main) {
|
|
559
|
+
localDict["heartRate"] = localHeartRates
|
|
560
|
+
localDict["route"] = localRoutes
|
|
561
|
+
workoutResults.append(localDict)
|
|
562
|
+
outerGroup.leave()
|
|
609
563
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
564
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
call.resolve(["workouts": workoutList, "errors": errors])
|
|
565
|
+
outerGroup.notify(queue: .main) {
|
|
566
|
+
call.resolve(["workouts": workoutResults, "errors": errors])
|
|
616
567
|
}
|
|
617
568
|
}
|
|
618
|
-
|
|
619
569
|
healthStore.execute(workoutQuery)
|
|
620
570
|
}
|
|
621
571
|
|
|
@@ -657,26 +607,25 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
657
607
|
let routeType = HKSeriesType.workoutRoute()
|
|
658
608
|
let predicate = HKQuery.predicateForObjects(from: workout)
|
|
659
609
|
|
|
660
|
-
let routeQuery = HKSampleQuery(sampleType: routeType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) {
|
|
661
|
-
guard let
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
610
|
+
let routeQuery = HKSampleQuery(sampleType: routeType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { [weak self] _, samples, error in
|
|
611
|
+
guard let self = self else { return }
|
|
612
|
+
if let routes = samples as? [HKWorkoutRoute], error == nil {
|
|
613
|
+
let routeDispatchGroup = DispatchGroup()
|
|
614
|
+
var allLocations: [[String: Any]] = []
|
|
615
|
+
for route in routes {
|
|
616
|
+
routeDispatchGroup.enter()
|
|
617
|
+
self.queryLocations(for: route) { locations in
|
|
618
|
+
allLocations.append(contentsOf: locations)
|
|
619
|
+
routeDispatchGroup.leave()
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
routeDispatchGroup.notify(queue: .main) {
|
|
623
|
+
completion(allLocations, nil)
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
DispatchQueue.main.async {
|
|
627
|
+
completion([], error?.localizedDescription)
|
|
675
628
|
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
routeDispatchGroup.notify(queue: .main) {
|
|
679
|
-
completion(routeLocations, nil)
|
|
680
629
|
}
|
|
681
630
|
}
|
|
682
631
|
|
|
@@ -687,9 +636,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
687
636
|
private func queryLocations(for route: HKWorkoutRoute, completion: @escaping @Sendable ([[String: Any]]) -> Void) {
|
|
688
637
|
var routeLocations: [[String: Any]] = []
|
|
689
638
|
|
|
690
|
-
let locationQuery = HKWorkoutRouteQuery(route: route) { _, locations, done, error in
|
|
639
|
+
let locationQuery = HKWorkoutRouteQuery(route: route) { [weak self] _, locations, done, error in
|
|
640
|
+
guard let self = self else { return }
|
|
691
641
|
guard let locations = locations, error == nil else {
|
|
692
|
-
|
|
642
|
+
DispatchQueue.main.async {
|
|
643
|
+
completion([])
|
|
644
|
+
}
|
|
693
645
|
return
|
|
694
646
|
}
|
|
695
647
|
|
|
@@ -706,7 +658,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
706
658
|
}
|
|
707
659
|
|
|
708
660
|
if done {
|
|
709
|
-
|
|
661
|
+
DispatchQueue.main.async {
|
|
662
|
+
completion(routeLocations)
|
|
663
|
+
}
|
|
710
664
|
}
|
|
711
665
|
}
|
|
712
666
|
}
|
package/package.json
CHANGED