@flomentumsolutions/capacitor-health-extended 0.0.6 → 0.0.8
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 +7 -4
- package/README.md +19 -14
- package/android/src/main/java/com/flomentum/health/capacitor/HealthPlugin.kt +1 -0
- package/dist/esm/definitions.d.ts +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/HealthPluginPlugin/HealthPlugin.swift +124 -52
- package/package.json +2 -2
- package/Package.swift +0 -28
|
@@ -10,8 +10,11 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.homepage = package['repository']['url']
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
-
|
|
13
|
+
# Only include Swift/Obj-C source files that belong to the plugin
|
|
14
|
+
s.source_files = 'ios/Sources/HealthPluginPlugin/**/*.{swift,h,m}'
|
|
14
15
|
s.ios.deployment_target = '13.0'
|
|
15
|
-
s.dependency 'Capacitor'
|
|
16
|
-
s.
|
|
17
|
-
|
|
16
|
+
s.dependency 'Capacitor', '~> 6.2'
|
|
17
|
+
s.dependency 'CapacitorCordova', '~> 6.2'
|
|
18
|
+
# Match the Swift shipped with Xcode 16 (use 5.9 for Xcode 15.x)
|
|
19
|
+
s.swift_version = '6.0'
|
|
20
|
+
end
|
package/README.md
CHANGED
|
@@ -35,14 +35,17 @@ npx cap sync
|
|
|
35
35
|
</queries>
|
|
36
36
|
|
|
37
37
|
<!-- Declare permissions you’ll request -->
|
|
38
|
-
<uses-permission android:name="android.permission.
|
|
39
|
-
<uses-permission android:name="android.permission.health.
|
|
40
|
-
<uses-permission android:name="android.permission.health.
|
|
38
|
+
<uses-permission android:name="android.permission.health.READ_STEPS" />
|
|
39
|
+
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
|
|
40
|
+
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
|
|
41
|
+
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
|
|
42
|
+
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
|
|
43
|
+
<uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTE" />
|
|
44
|
+
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
|
|
41
45
|
<uses-permission android:name="android.permission.health.READ_WEIGHT" />
|
|
42
|
-
<uses-permission android:name="android.permission.health.
|
|
43
|
-
<uses-permission android:name="android.permission.health.
|
|
44
|
-
<uses-permission android:name="android.permission.health.
|
|
45
|
-
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
|
|
46
|
+
<uses-permission android:name="android.permission.health.READ_HEIGHT" />
|
|
47
|
+
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
|
|
48
|
+
<uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE" />
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
|
|
@@ -277,12 +280,12 @@ Query workouts
|
|
|
277
280
|
|
|
278
281
|
#### QueryAggregatedRequest
|
|
279
282
|
|
|
280
|
-
| Prop | Type
|
|
281
|
-
| --------------- |
|
|
282
|
-
| **`startDate`** | <code>string</code>
|
|
283
|
-
| **`endDate`** | <code>string</code>
|
|
284
|
-
| **`dataType`** | <code>'steps' \| 'calories'</code> |
|
|
285
|
-
| **`bucket`** | <code>string</code>
|
|
283
|
+
| Prop | Type |
|
|
284
|
+
| --------------- | --------------------------------------------------------------------------------------- |
|
|
285
|
+
| **`startDate`** | <code>string</code> |
|
|
286
|
+
| **`endDate`** | <code>string</code> |
|
|
287
|
+
| **`dataType`** | <code>'steps' \| 'active-calories' \| 'mindfulness' \| 'hrv' \| 'blood-pressure'</code> |
|
|
288
|
+
| **`bucket`** | <code>string</code> |
|
|
286
289
|
|
|
287
290
|
|
|
288
291
|
#### QueryWorkoutResponse
|
|
@@ -303,6 +306,7 @@ Query workouts
|
|
|
303
306
|
| **`id`** | <code>string</code> |
|
|
304
307
|
| **`duration`** | <code>number</code> |
|
|
305
308
|
| **`distance`** | <code>number</code> |
|
|
309
|
+
| **`steps`** | <code>number</code> |
|
|
306
310
|
| **`calories`** | <code>number</code> |
|
|
307
311
|
| **`sourceBundleId`** | <code>string</code> |
|
|
308
312
|
| **`route`** | <code>RouteSample[]</code> |
|
|
@@ -335,6 +339,7 @@ Query workouts
|
|
|
335
339
|
| **`endDate`** | <code>string</code> |
|
|
336
340
|
| **`includeHeartRate`** | <code>boolean</code> |
|
|
337
341
|
| **`includeRoute`** | <code>boolean</code> |
|
|
342
|
+
| **`includeSteps`** | <code>boolean</code> |
|
|
338
343
|
|
|
339
344
|
|
|
340
345
|
### Type Aliases
|
|
@@ -342,6 +347,6 @@ Query workouts
|
|
|
342
347
|
|
|
343
348
|
#### HealthPermission
|
|
344
349
|
|
|
345
|
-
<code>'READ_STEPS' | 'READ_WORKOUTS' | '
|
|
350
|
+
<code>'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE'</code>
|
|
346
351
|
|
|
347
352
|
</docgen-api>
|
|
@@ -51,6 +51,7 @@ enum class CapHealthPermission {
|
|
|
51
51
|
permissions = [
|
|
52
52
|
Permission(alias = "READ_STEPS", strings = ["android.permission.health.READ_STEPS"]),
|
|
53
53
|
Permission(alias = "READ_WEIGHT", strings = ["android.permission.health.READ_WEIGHT"]),
|
|
54
|
+
Permission(alias = "READ_HEIGHT", strings = ["android.permission.health.READ_HEIGHT"]),
|
|
54
55
|
Permission(alias = "READ_WORKOUTS", strings = ["android.permission.health.READ_EXERCISE"]),
|
|
55
56
|
Permission(alias = "READ_DISTANCE", strings = ["android.permission.health.READ_DISTANCE"]),
|
|
56
57
|
Permission(alias = "READ_ACTIVE_CALORIES", strings = ["android.permission.health.READ_ACTIVE_CALORIES_BURNED"]),
|
|
@@ -51,7 +51,7 @@ export interface HealthPlugin {
|
|
|
51
51
|
*/
|
|
52
52
|
queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;
|
|
53
53
|
}
|
|
54
|
-
export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE';
|
|
54
|
+
export declare type HealthPermission = 'READ_STEPS' | 'READ_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_HRV' | 'READ_BLOOD_PRESSURE';
|
|
55
55
|
export interface PermissionsRequest {
|
|
56
56
|
permissions: HealthPermission[];
|
|
57
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface HealthPlugin {\n /**\n * Checks if health API is available.\n * Android: If false is returned, the Google Health Connect app is probably not installed.\n * See showHealthConnectInPlayStore()\n *\n */\n isHealthAvailable(): Promise<{ available: boolean }>;\n\n /**\n * Android only: Returns for each given permission, if it was granted by the underlying health API\n * @param permissions permissions to query\n */\n checkHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Requests the permissions from the user.\n *\n * Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in\n * the Health Connect app. See openHealthConnectSettings()\n *\n * iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS\n * we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all\n * permissions were granted.\n *\n * @param permissions permissions to request\n */\n requestHealthPermissions(permissions: PermissionsRequest): Promise<PermissionResponse>;\n\n /**\n * Opens the apps settings, which is kind of wrong, because health permissions are configured under:\n * Settings > Apps > (Apple) Health > Access and Devices > [app-name]\n * But we can't go there directly.\n */\n openAppleHealthSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Opens the Google Health Connect app in PlayStore\n */\n showHealthConnectInPlayStore(): Promise<void>;\n\n /**\n * Query aggregated data\n * @param request\n */\n queryAggregated(request: QueryAggregatedRequest): Promise<QueryAggregatedResponse>;\n\n /**\n * Query workouts\n * @param request\n */\n queryWorkouts(request: QueryWorkoutRequest): Promise<QueryWorkoutResponse>;\n}\n\nexport declare type HealthPermission =\n | 'READ_STEPS'\n | 'READ_WORKOUTS'\n | 'READ_ACTIVE_CALORIES'\n | 'READ_TOTAL_CALORIES'\n | 'READ_DISTANCE'\n | 'READ_WEIGHT'\n | 'READ_HEIGHT'\n | 'READ_HEART_RATE'\n | 'READ_ROUTE'\n | 'READ_MINDFULNESS'\n | 'READ_HRV'\n | 'READ_BLOOD_PRESSURE';\n\nexport interface PermissionsRequest {\n permissions: HealthPermission[];\n}\n\nexport interface PermissionResponse {\n permissions: { [key: string]: boolean }[];\n}\n\nexport interface QueryWorkoutRequest {\n startDate: string;\n endDate: string;\n includeHeartRate: boolean;\n includeRoute: boolean;\n includeSteps: boolean;\n}\n\nexport interface HeartRateSample {\n timestamp: string;\n bpm: number;\n}\n\nexport interface RouteSample {\n timestamp: string;\n lat: number;\n lng: number;\n alt?: number;\n}\n\nexport interface QueryWorkoutResponse {\n workouts: Workout[];\n}\n\nexport interface Workout {\n startDate: string;\n endDate: string;\n workoutType: string;\n sourceName: string;\n id?: string;\n duration: number;\n distance?: number;\n steps?: number;\n calories: number;\n sourceBundleId: string;\n route?: RouteSample[];\n heartRate?: HeartRateSample[];\n}\n\nexport interface QueryAggregatedRequest {\n startDate: string;\n endDate: string;\n dataType: 'steps' | 'active-calories' | 'mindfulness' | 'hrv' | 'blood-pressure';\n bucket: string;\n}\n\nexport interface QueryAggregatedResponse {\n aggregatedData: AggregatedSample[];\n}\n\nexport interface AggregatedSample {\n startDate: string;\n endDate: string;\n value: number;\n}\n"]}
|
|
@@ -6,6 +6,7 @@ import HealthKit
|
|
|
6
6
|
* Please read the Capacitor iOS Plugin Development Guide
|
|
7
7
|
* here: https://capacitorjs.com/docs/plugins/ios
|
|
8
8
|
*/
|
|
9
|
+
@MainActor
|
|
9
10
|
@objc(HealthPlugin)
|
|
10
11
|
public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
12
|
public let identifier = "HealthPlugin"
|
|
@@ -22,6 +23,9 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
22
23
|
|
|
23
24
|
let healthStore = HKHealthStore()
|
|
24
25
|
|
|
26
|
+
/// Serial queue to make route‑location mutations thread‑safe without locks
|
|
27
|
+
private let routeSyncQueue = DispatchQueue(label: "com.flomentum.healthplugin.routeSync")
|
|
28
|
+
|
|
25
29
|
@objc func isHealthAvailable(_ call: CAPPluginCall) {
|
|
26
30
|
let isAvailable = HKHealthStore.isHealthDataAvailable()
|
|
27
31
|
call.resolve(["available": isAvailable])
|
|
@@ -148,6 +152,14 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
148
152
|
return HKObjectType.quantityType(forIdentifier: .stepCount)
|
|
149
153
|
case "hrv":
|
|
150
154
|
return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
|
|
155
|
+
case "height":
|
|
156
|
+
return HKObjectType.quantityType(forIdentifier: .height)
|
|
157
|
+
case "distance":
|
|
158
|
+
return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)
|
|
159
|
+
case "active-calories":
|
|
160
|
+
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
161
|
+
case "total-calories":
|
|
162
|
+
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
151
163
|
case "blood-pressure":
|
|
152
164
|
return nil // handled above
|
|
153
165
|
default:
|
|
@@ -183,8 +195,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
183
195
|
unit = .gramUnit(with: .kilo)
|
|
184
196
|
} else if dataTypeString == "hrv" {
|
|
185
197
|
unit = HKUnit.secondUnit(with: .milli)
|
|
198
|
+
} else if dataTypeString == "distance" {
|
|
199
|
+
unit = HKUnit.meter()
|
|
200
|
+
} else if dataTypeString == "active-calories" || dataTypeString == "total-calories" {
|
|
201
|
+
unit = HKUnit.kilocalorie()
|
|
202
|
+
} else if dataTypeString == "height" {
|
|
203
|
+
unit = HKUnit.meter()
|
|
186
204
|
}
|
|
187
|
-
|
|
188
205
|
let value = quantitySample.quantity.doubleValue(for: unit)
|
|
189
206
|
let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
|
|
190
207
|
|
|
@@ -216,6 +233,13 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
216
233
|
return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
|
|
217
234
|
case "READ_WEIGHT":
|
|
218
235
|
return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap{$0}
|
|
236
|
+
case "READ_HEIGHT":
|
|
237
|
+
return [HKObjectType.quantityType(forIdentifier: .height)].compactMap { $0 }
|
|
238
|
+
case "READ_TOTAL_CALORIES":
|
|
239
|
+
return [
|
|
240
|
+
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
|
|
241
|
+
HKObjectType.quantityType(forIdentifier: .basalEnergyBurned) // iOS 16+
|
|
242
|
+
].compactMap { $0 }
|
|
219
243
|
case "READ_ACTIVE_CALORIES":
|
|
220
244
|
return [HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)].compactMap{$0}
|
|
221
245
|
case "READ_WORKOUTS":
|
|
@@ -257,6 +281,12 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
257
281
|
return HKObjectType.quantityType(forIdentifier: .bodyMass)
|
|
258
282
|
case "hrv":
|
|
259
283
|
return HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)
|
|
284
|
+
case "distance":
|
|
285
|
+
return HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) // pick one rep type
|
|
286
|
+
case "total-calories":
|
|
287
|
+
return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
|
|
288
|
+
case "height":
|
|
289
|
+
return HKObjectType.quantityType(forIdentifier: .height)
|
|
260
290
|
default:
|
|
261
291
|
return nil
|
|
262
292
|
}
|
|
@@ -296,12 +326,29 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
296
326
|
}
|
|
297
327
|
|
|
298
328
|
let options: HKStatisticsOptions = {
|
|
299
|
-
switch dataType.
|
|
300
|
-
case
|
|
301
|
-
HKQuantityTypeIdentifier.bodyMass.rawValue:
|
|
302
|
-
return .discreteAverage
|
|
303
|
-
default:
|
|
329
|
+
switch dataType.aggregationStyle {
|
|
330
|
+
case .cumulative:
|
|
304
331
|
return .cumulativeSum
|
|
332
|
+
|
|
333
|
+
// Newer discrete aggregation styles (iOS 15 +)
|
|
334
|
+
case .discreteAverage:
|
|
335
|
+
return .discreteAverage
|
|
336
|
+
@available(iOS 17.0, *)
|
|
337
|
+
case .discreteTemporallyWeighted:
|
|
338
|
+
return .discreteAverage
|
|
339
|
+
@available(iOS 17.0, *)
|
|
340
|
+
case .discreteEquivalentContinuousLevel:
|
|
341
|
+
return .discreteAverage
|
|
342
|
+
@available(iOS 17.0, *)
|
|
343
|
+
case .discreteArithmetic:
|
|
344
|
+
return .discreteAverage
|
|
345
|
+
|
|
346
|
+
// Legacy discrete fallback
|
|
347
|
+
case .discrete:
|
|
348
|
+
return .discreteAverage
|
|
349
|
+
|
|
350
|
+
@unknown default:
|
|
351
|
+
return .discreteAverage
|
|
305
352
|
}
|
|
306
353
|
}()
|
|
307
354
|
|
|
@@ -312,46 +359,68 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
312
359
|
anchorDate: startDate,
|
|
313
360
|
intervalComponents: interval
|
|
314
361
|
)
|
|
315
|
-
|
|
362
|
+
|
|
316
363
|
query.initialResultsHandler = { query, result, error in
|
|
317
364
|
if let error = error {
|
|
318
365
|
call.reject("Error fetching aggregated data: \(error.localizedDescription)")
|
|
319
366
|
return
|
|
320
367
|
}
|
|
321
|
-
|
|
368
|
+
|
|
322
369
|
var aggregatedSamples: [[String: Any]] = []
|
|
323
|
-
|
|
324
|
-
result?.enumerateStatistics(from: startDate, to: endDate) { statistics,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
370
|
+
|
|
371
|
+
result?.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
|
|
372
|
+
// Choose sum or average based on the options we picked
|
|
373
|
+
let quantity: HKQuantity? = options.contains(.cumulativeSum)
|
|
374
|
+
? statistics.sumQuantity()
|
|
375
|
+
: statistics.averageQuantity()
|
|
376
|
+
|
|
377
|
+
guard let quantity = quantity else { return }
|
|
378
|
+
|
|
379
|
+
// Time‑bounds of this bucket
|
|
380
|
+
let bucketStart = statistics.startDate.timeIntervalSince1970 * 1000
|
|
381
|
+
let bucketEnd = statistics.endDate.timeIntervalSince1970 * 1000
|
|
382
|
+
|
|
383
|
+
// Map dataType → correct HKUnit
|
|
384
|
+
let unit: HKUnit = {
|
|
385
|
+
switch dataTypeString {
|
|
386
|
+
case "steps":
|
|
387
|
+
return .count()
|
|
388
|
+
case "active-calories", "total-calories":
|
|
389
|
+
return .kilocalorie()
|
|
390
|
+
case "distance":
|
|
391
|
+
return .meter()
|
|
392
|
+
case "weight":
|
|
393
|
+
return .gramUnit(with: .kilo)
|
|
394
|
+
case "height":
|
|
395
|
+
return .meter()
|
|
396
|
+
case "heart-rate":
|
|
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()
|
|
336
404
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
405
|
+
}()
|
|
406
|
+
|
|
407
|
+
let value = quantity.doubleValue(for: unit)
|
|
408
|
+
|
|
409
|
+
aggregatedSamples.append([
|
|
410
|
+
"startDate": bucketStart,
|
|
411
|
+
"endDate": bucketEnd,
|
|
412
|
+
"value": value
|
|
413
|
+
])
|
|
345
414
|
}
|
|
346
|
-
|
|
415
|
+
|
|
347
416
|
call.resolve(["aggregatedData": aggregatedSamples])
|
|
348
417
|
}
|
|
349
|
-
|
|
418
|
+
|
|
350
419
|
healthStore.execute(query)
|
|
351
420
|
}
|
|
352
421
|
}
|
|
353
422
|
|
|
354
|
-
func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
|
|
423
|
+
func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping @Sendable ([[String: Any]]?, Error?) -> Void) {
|
|
355
424
|
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
356
425
|
completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "MindfulSession type unavailable"]))
|
|
357
426
|
return
|
|
@@ -399,7 +468,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
399
468
|
|
|
400
469
|
|
|
401
470
|
|
|
402
|
-
private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping(Double?) -> Void) {
|
|
471
|
+
private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping @Sendable(Double?) -> Void) {
|
|
403
472
|
|
|
404
473
|
|
|
405
474
|
guard let quantityType = dataType else {
|
|
@@ -553,7 +622,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
553
622
|
|
|
554
623
|
|
|
555
624
|
// MARK: - Query Heart Rate Data
|
|
556
|
-
private func queryHeartRate(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
|
|
625
|
+
private func queryHeartRate(for workout: HKWorkout, completion: @escaping @Sendable ([[String: Any]], String?) -> Void) {
|
|
557
626
|
let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
|
|
558
627
|
let predicate = HKQuery.predicateForSamples(withStart: workout.startDate, end: workout.endDate, options: .strictStartDate)
|
|
559
628
|
|
|
@@ -584,7 +653,7 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
584
653
|
}
|
|
585
654
|
|
|
586
655
|
// MARK: - Query Route Data
|
|
587
|
-
private func queryRoute(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
|
|
656
|
+
private func queryRoute(for workout: HKWorkout, completion: @escaping @Sendable ([[String: Any]], String?) -> Void) {
|
|
588
657
|
let routeType = HKSeriesType.workoutRoute()
|
|
589
658
|
let predicate = HKQuery.predicateForObjects(from: workout)
|
|
590
659
|
|
|
@@ -615,30 +684,33 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
615
684
|
}
|
|
616
685
|
|
|
617
686
|
// MARK: - Query Route Locations
|
|
618
|
-
private func queryLocations(for route: HKWorkoutRoute, completion: @escaping ([[String: Any]]) -> Void) {
|
|
687
|
+
private func queryLocations(for route: HKWorkoutRoute, completion: @escaping @Sendable ([[String: Any]]) -> Void) {
|
|
619
688
|
var routeLocations: [[String: Any]] = []
|
|
620
|
-
|
|
621
|
-
let locationQuery = HKWorkoutRouteQuery(route: route) {
|
|
689
|
+
|
|
690
|
+
let locationQuery = HKWorkoutRouteQuery(route: route) { _, locations, done, error in
|
|
622
691
|
guard let locations = locations, error == nil else {
|
|
623
692
|
completion([])
|
|
624
693
|
return
|
|
625
694
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
695
|
+
|
|
696
|
+
// Append on a dedicated serial queue so we’re race‑free without NSLock
|
|
697
|
+
self.routeSyncQueue.async {
|
|
698
|
+
for location in locations {
|
|
699
|
+
let locationDict: [String: Any] = [
|
|
700
|
+
"timestamp": location.timestamp,
|
|
701
|
+
"lat": location.coordinate.latitude,
|
|
702
|
+
"lng": location.coordinate.longitude,
|
|
703
|
+
"alt": location.altitude
|
|
704
|
+
]
|
|
705
|
+
routeLocations.append(locationDict)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if done {
|
|
709
|
+
completion(routeLocations)
|
|
710
|
+
}
|
|
639
711
|
}
|
|
640
712
|
}
|
|
641
|
-
|
|
713
|
+
|
|
642
714
|
healthStore.execute(locationQuery)
|
|
643
715
|
}
|
|
644
716
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flomentumsolutions/capacitor-health-extended",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Capacitor plugin for Apple HealthKit and Google Health Connect Platform",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "
|
|
22
|
+
"url": "https://github.com/Flomentum-Solutions/capacitor-health-extended.git"
|
|
23
23
|
},
|
|
24
24
|
"bugs": {
|
|
25
25
|
"url": "https://github.com/Flomentum-Solutions/capacitor-health-extended/issues"
|
package/Package.swift
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// swift-tools-version: 5.9
|
|
2
|
-
import PackageDescription
|
|
3
|
-
|
|
4
|
-
let package = Package(
|
|
5
|
-
name: "CapacitorHealthExtended",
|
|
6
|
-
platforms: [.iOS(.v13)],
|
|
7
|
-
products: [
|
|
8
|
-
.library(
|
|
9
|
-
name: "CapacitorHealthExtended",
|
|
10
|
-
targets: ["HealthPluginPlugin"])
|
|
11
|
-
],
|
|
12
|
-
dependencies: [
|
|
13
|
-
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", branch: "main")
|
|
14
|
-
],
|
|
15
|
-
targets: [
|
|
16
|
-
.target(
|
|
17
|
-
name: "HealthPluginPlugin",
|
|
18
|
-
dependencies: [
|
|
19
|
-
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
-
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
-
],
|
|
22
|
-
path: "ios/Sources/HealthPluginPlugin"),
|
|
23
|
-
.testTarget(
|
|
24
|
-
name: "HealthPluginPluginTests",
|
|
25
|
-
dependencies: ["HealthPluginPlugin"],
|
|
26
|
-
path: "ios/Tests/HealthPluginPluginTests")
|
|
27
|
-
]
|
|
28
|
-
)
|