@flomentumsolutions/capacitor-health-extended 0.0.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.
@@ -0,0 +1,671 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import HealthKit
4
+
5
+ /**
6
+ * Please read the Capacitor iOS Plugin Development Guide
7
+ * here: https://capacitorjs.com/docs/plugins/ios
8
+ */
9
+ @objc(HealthPlugin)
10
+ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
11
+ public let identifier = "HealthPlugin"
12
+ public let jsName = "HealthPlugin"
13
+ public let pluginMethods: [CAPPluginMethod] = [
14
+ CAPPluginMethod(name: "isHealthAvailable", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "checkHealthPermissions", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "requestHealthPermissions", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "openAppleHealthSettings", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "queryAggregated", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "queryLatestSample", returnType: CAPPluginReturnPromise)
21
+ ]
22
+
23
+ let healthStore = HKHealthStore()
24
+
25
+ @objc func isHealthAvailable(_ call: CAPPluginCall) {
26
+ let isAvailable = HKHealthStore.isHealthDataAvailable()
27
+ call.resolve(["available": isAvailable])
28
+ }
29
+
30
+ @objc func checkHealthPermissions(_ call: CAPPluginCall) {
31
+ guard let permissions = call.getArray("permissions") as? [String] else {
32
+ call.reject("Invalid permissions format")
33
+ return
34
+ }
35
+
36
+ var result: [String: String] = [:]
37
+
38
+ for permission in permissions {
39
+ let hkTypes = permissionToHKObjectType(permission)
40
+ for type in hkTypes {
41
+ let status = healthStore.authorizationStatus(for: type)
42
+
43
+ switch status {
44
+ case .notDetermined:
45
+ result[permission] = "notDetermined"
46
+ case .sharingDenied:
47
+ result[permission] = "denied"
48
+ case .sharingAuthorized:
49
+ result[permission] = "authorized"
50
+ @unknown default:
51
+ result[permission] = "unknown"
52
+ }
53
+ }
54
+ }
55
+
56
+ call.resolve(["permissions": result])
57
+ }
58
+
59
+ @objc func requestHealthPermissions(_ call: CAPPluginCall) {
60
+ guard let permissions = call.getArray("permissions") as? [String] else {
61
+ call.reject("Invalid permissions format")
62
+ return
63
+ }
64
+
65
+ let types: [HKObjectType] = permissions.flatMap { permissionToHKObjectType($0) }
66
+
67
+ healthStore.requestAuthorization(toShare: nil, read: Set(types)) { success, error in
68
+ if success {
69
+ //we don't know which actual permissions were granted, so we assume all
70
+ var result: [String: Bool] = [:]
71
+ permissions.forEach{ result[$0] = true }
72
+ call.resolve(["permissions": result])
73
+ } else if let error = error {
74
+ call.reject("Authorization failed: \(error.localizedDescription)")
75
+ } else {
76
+ //assume no permissions were granted. We can ask user to adjust them manually
77
+ var result: [String: Bool] = [:]
78
+ permissions.forEach{ result[$0] = false }
79
+ call.resolve(["permissions": result])
80
+ }
81
+ }
82
+ }
83
+
84
+ @objc func queryLatestSample(_ call: CAPPluginCall) {
85
+ guard let dataTypeString = call.getString("dataType") else {
86
+ call.reject("Missing data type")
87
+ return
88
+ }
89
+ guard aggregateTypeToHKQuantityType(dataTypeString) != nil else {
90
+ call.reject("Invalid data type")
91
+ return
92
+ }
93
+
94
+ let quantityType: HKQuantityType? = {
95
+ switch dataTypeString {
96
+ case "heart-rate":
97
+ return HKObjectType.quantityType(forIdentifier: .heartRate)
98
+ case "weight":
99
+ return HKObjectType.quantityType(forIdentifier: .bodyMass)
100
+ case "steps":
101
+ return HKObjectType.quantityType(forIdentifier: .stepCount)
102
+ default:
103
+ return nil
104
+ }
105
+ }()
106
+
107
+ guard let type = quantityType else {
108
+ call.reject("Invalid or unsupported data type")
109
+ return
110
+ }
111
+
112
+ let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
113
+ let predicate = HKQuery.predicateForSamples(withStart: Date.distantPast, end: Date(), options: .strictEndDate)
114
+
115
+ let query = HKSampleQuery(sampleType: type, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { _, samples, error in
116
+
117
+ print("Samples count for \(dataTypeString):", samples?.count ?? 0)
118
+
119
+ guard let quantitySample = samples?.first as? HKQuantitySample else {
120
+ if let error = error {
121
+ call.reject("Error fetching latest sample", "NO_SAMPLE", error)
122
+ } else {
123
+ call.reject("No sample found", "NO_SAMPLE")
124
+ }
125
+ return
126
+ }
127
+
128
+ var unit: HKUnit = .count()
129
+ if dataTypeString == "heart-rate" {
130
+ unit = HKUnit.count().unitDivided(by: HKUnit.minute())
131
+ } else if dataTypeString == "weight" {
132
+ unit = .gramUnit(with: .kilo)
133
+ }
134
+
135
+ let value = quantitySample.quantity.doubleValue(for: unit)
136
+ let timestamp = quantitySample.startDate.timeIntervalSince1970 * 1000
137
+
138
+ call.resolve([
139
+ "value": value,
140
+ "timestamp": timestamp,
141
+ "unit": unit.unitString
142
+ ])
143
+ }
144
+
145
+ healthStore.execute(query)
146
+ }
147
+
148
+ @objc func openAppleHealthSettings(_ call: CAPPluginCall) {
149
+ if let url = URL(string: UIApplication.openSettingsURLString) {
150
+ DispatchQueue.main.async {
151
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
152
+ call.resolve()
153
+ }
154
+ } else {
155
+ call.reject("Unable to open app-specific settings")
156
+ }
157
+ }
158
+
159
+ // Permission helpers
160
+ func permissionToHKObjectType(_ permission: String) -> [HKObjectType] {
161
+ switch permission {
162
+ case "READ_STEPS":
163
+ return [HKObjectType.quantityType(forIdentifier: .stepCount)].compactMap{$0}
164
+ case "READ_WEIGHT":
165
+ return [HKObjectType.quantityType(forIdentifier: .bodyMass)].compactMap{$0}
166
+ case "READ_ACTIVE_CALORIES":
167
+ return [HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)].compactMap{$0}
168
+ case "READ_WORKOUTS":
169
+ return [HKObjectType.workoutType()].compactMap{$0}
170
+ case "READ_HEART_RATE":
171
+ return [HKObjectType.quantityType(forIdentifier: .heartRate)].compactMap{$0}
172
+ case "READ_ROUTE":
173
+ return [HKSeriesType.workoutRoute()].compactMap{$0}
174
+ case "READ_DISTANCE":
175
+ return [
176
+ HKObjectType.quantityType(forIdentifier: .distanceCycling),
177
+ HKObjectType.quantityType(forIdentifier: .distanceSwimming),
178
+ HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning),
179
+ HKObjectType.quantityType(forIdentifier: .distanceDownhillSnowSports)
180
+ ].compactMap{$0}
181
+ case "READ_MINDFULNESS":
182
+ return [HKObjectType.categoryType(forIdentifier: .mindfulSession)!].compactMap{$0}
183
+ default:
184
+ return []
185
+ }
186
+ }
187
+
188
+ func aggregateTypeToHKQuantityType(_ dataType: String) -> HKQuantityType? {
189
+ switch dataType {
190
+ case "steps":
191
+ return HKObjectType.quantityType(forIdentifier: .stepCount)
192
+ case "active-calories":
193
+ return HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)
194
+ case "heart-rate":
195
+ return HKObjectType.quantityType(forIdentifier: .heartRate)
196
+ case "weight":
197
+ return HKObjectType.quantityType(forIdentifier: .bodyMass)
198
+ default:
199
+ return nil
200
+ }
201
+ }
202
+
203
+
204
+ @objc func queryAggregated(_ call: CAPPluginCall) {
205
+ guard let startDateString = call.getString("startDate"),
206
+ let endDateString = call.getString("endDate"),
207
+ let dataTypeString = call.getString("dataType"),
208
+ let bucket = call.getString("bucket"),
209
+ let startDate = self.isoDateFormatter.date(from: startDateString),
210
+ let endDate = self.isoDateFormatter.date(from: endDateString) else {
211
+ call.reject("Invalid parameters")
212
+ return
213
+ }
214
+
215
+ if(dataTypeString == "mindfulness") {
216
+ self.queryMindfulnessAggregated(startDate: startDate, endDate: endDate) {result, error in
217
+ if let error = error {
218
+ call.reject(error.localizedDescription)
219
+ } else if let result = result {
220
+ call.resolve(["aggregatedData": result])
221
+ }
222
+ }
223
+ } else {
224
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
225
+
226
+ guard let interval = calculateInterval(bucket: bucket) else {
227
+ call.reject("Invalid bucket")
228
+ return
229
+ }
230
+
231
+ guard let dataType = aggregateTypeToHKQuantityType(dataTypeString) else {
232
+ call.reject("Invalid data type")
233
+ return
234
+ }
235
+
236
+ let options: HKStatisticsOptions = {
237
+ switch dataType.identifier {
238
+ case HKQuantityTypeIdentifier.heartRate.rawValue,
239
+ HKQuantityTypeIdentifier.bodyMass.rawValue:
240
+ return .discreteAverage
241
+ default:
242
+ return .cumulativeSum
243
+ }
244
+ }()
245
+
246
+ let query = HKStatisticsCollectionQuery(
247
+ quantityType: dataType,
248
+ quantitySamplePredicate: predicate,
249
+ options: options,
250
+ anchorDate: startDate,
251
+ intervalComponents: interval
252
+ )
253
+
254
+ query.initialResultsHandler = { query, result, error in
255
+ if let error = error {
256
+ call.reject("Error fetching aggregated data: \(error.localizedDescription)")
257
+ return
258
+ }
259
+
260
+ var aggregatedSamples: [[String: Any]] = []
261
+
262
+ result?.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in
263
+ if let sum = statistics.sumQuantity() {
264
+ let startDate = statistics.startDate.timeIntervalSince1970 * 1000
265
+ let endDate = statistics.endDate.timeIntervalSince1970 * 1000
266
+
267
+ var value: Double = -1.0
268
+ if(dataTypeString == "steps" && dataType.is(compatibleWith: HKUnit.count())) {
269
+ value = sum.doubleValue(for: HKUnit.count())
270
+ } else if(dataTypeString == "active-calories" && dataType.is(compatibleWith: HKUnit.kilocalorie())) {
271
+ value = sum.doubleValue(for: HKUnit.kilocalorie())
272
+ } else if(dataTypeString == "mindfulness" && dataType.is(compatibleWith: HKUnit.second())) {
273
+ value = sum.doubleValue(for: HKUnit.second())
274
+ }
275
+
276
+
277
+ aggregatedSamples.append([
278
+ "startDate": startDate,
279
+ "endDate": endDate,
280
+ "value": value
281
+ ])
282
+ }
283
+ }
284
+
285
+ call.resolve(["aggregatedData": aggregatedSamples])
286
+ }
287
+
288
+ healthStore.execute(query)
289
+ }
290
+ }
291
+
292
+ func queryMindfulnessAggregated(startDate: Date, endDate: Date, completion: @escaping ([[String: Any]]?, Error?) -> Void) {
293
+ guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
294
+ completion(nil, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "MindfulSession type unavailable"]))
295
+ return
296
+ }
297
+
298
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
299
+ let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
300
+ guard let categorySamples = samples as? [HKCategorySample], error == nil else {
301
+ completion(nil, error)
302
+ return
303
+ }
304
+
305
+ // Aggregate total time per day
306
+
307
+ var dailyDurations: [Date: TimeInterval] = [:]
308
+ let calendar = Calendar.current
309
+
310
+ for sample in categorySamples {
311
+ let startOfDay = calendar.startOfDay(for: sample.startDate)
312
+ let duration = sample.endDate.timeIntervalSince(sample.startDate)
313
+
314
+ if let existingDuration = dailyDurations[startOfDay] {
315
+ dailyDurations[startOfDay] = existingDuration + duration
316
+ } else {
317
+ dailyDurations[startOfDay] = duration
318
+ }
319
+ }
320
+
321
+ var aggregatedSamples: [[String: Any]] = []
322
+ var dayComponent = DateComponents()
323
+ dayComponent.day = 1
324
+ dailyDurations.forEach { (dateAndDuration) in
325
+ aggregatedSamples.append([
326
+ "startDate": dateAndDuration.key as Any,
327
+ "endDate": calendar.date(byAdding: dayComponent, to: dateAndDuration.key) as Any,
328
+ "value": dateAndDuration.value
329
+ ])
330
+ }
331
+
332
+ completion(aggregatedSamples, nil)
333
+ }
334
+
335
+ healthStore.execute(query)
336
+ }
337
+
338
+
339
+
340
+ private func queryAggregated(for startDate: Date, for endDate: Date, for dataType: HKQuantityType?, completion: @escaping(Double?) -> Void) {
341
+
342
+
343
+ guard let quantityType = dataType else {
344
+ completion(nil)
345
+ return
346
+ }
347
+
348
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
349
+
350
+ let query = HKStatisticsQuery(
351
+ quantityType: quantityType,
352
+ quantitySamplePredicate: predicate,
353
+ options: .cumulativeSum
354
+ ) { _, result, _ in
355
+ guard let result = result, let sum = result.sumQuantity() else {
356
+ completion(0.0)
357
+ return
358
+ }
359
+ completion(sum.doubleValue(for: HKUnit.count()))
360
+ }
361
+
362
+ healthStore.execute(query)
363
+
364
+ }
365
+
366
+
367
+
368
+
369
+
370
+ func calculateInterval(bucket: String) -> DateComponents? {
371
+ switch bucket {
372
+ case "hour":
373
+ return DateComponents(hour: 1)
374
+ case "day":
375
+ return DateComponents(day: 1)
376
+ case "week":
377
+ return DateComponents(weekOfYear: 1)
378
+ default:
379
+ return nil
380
+ }
381
+ }
382
+
383
+ var isoDateFormatter: ISO8601DateFormatter = {
384
+ let f = ISO8601DateFormatter()
385
+ f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
386
+ return f
387
+ }()
388
+
389
+
390
+ @objc func queryWorkouts(_ call: CAPPluginCall) {
391
+ guard let startDateString = call.getString("startDate"),
392
+ let endDateString = call.getString("endDate"),
393
+ let includeHeartRate = call.getBool("includeHeartRate"),
394
+ let includeRoute = call.getBool("includeRoute"),
395
+ let includeSteps = call.getBool("includeSteps"),
396
+ let startDate = self.isoDateFormatter.date(from: startDateString),
397
+ let endDate = self.isoDateFormatter.date(from: endDateString) else {
398
+ call.reject("Invalid parameters")
399
+ return
400
+ }
401
+
402
+
403
+
404
+ // Create a predicate to filter workouts by date
405
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
406
+
407
+ let workoutQuery = HKSampleQuery(sampleType: HKObjectType.workoutType(), predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { query, samples, error in
408
+ if let error = error {
409
+ call.reject("Error querying workouts: \(error.localizedDescription)")
410
+ return
411
+ }
412
+
413
+ guard let workouts = samples as? [HKWorkout] else {
414
+ call.resolve(["workouts": []])
415
+ return
416
+ }
417
+
418
+ var workoutList: [[String: Any]] = []
419
+ var errors: [String: String] = [:]
420
+ let dispatchGroup = DispatchGroup()
421
+
422
+ // Process each workout
423
+ for workout in workouts {
424
+ var workoutDict: [String: Any] = [
425
+ "startDate": workout.startDate,
426
+ "endDate": workout.endDate,
427
+ "workoutType": self.workoutTypeMapping[workout.workoutActivityType.rawValue, default: "other"],
428
+ "sourceName": workout.sourceRevision.source.name,
429
+ "sourceBundleId": workout.sourceRevision.source.bundleIdentifier,
430
+ "id": workout.uuid.uuidString,
431
+ "duration": workout.duration,
432
+ "calories": workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? 0,
433
+ "distance": workout.totalDistance?.doubleValue(for: .meter()) ?? 0
434
+ ]
435
+
436
+
437
+ var heartRateSamples: [[String: Any]] = []
438
+ var routeSamples: [[String: Any]] = []
439
+
440
+ // Query heart rate data if requested
441
+ if includeHeartRate {
442
+ dispatchGroup.enter()
443
+ self.queryHeartRate(for: workout, completion: { (heartRates, error) in
444
+ if(error != nil) {
445
+ errors["heart-rate"] = error
446
+ }
447
+ heartRateSamples = heartRates
448
+ dispatchGroup.leave()
449
+ })
450
+ }
451
+
452
+ // Query route data if requested
453
+ if includeRoute {
454
+ dispatchGroup.enter()
455
+ self.queryRoute(for: workout, completion: { (routes, error) in
456
+ if(error != nil) {
457
+ errors["route"] = error
458
+ }
459
+ routeSamples = routes
460
+ dispatchGroup.leave()
461
+ })
462
+ }
463
+
464
+ if includeSteps {
465
+ dispatchGroup.enter()
466
+ self.queryAggregated(for: workout.startDate, for: workout.endDate, for: HKObjectType.quantityType(forIdentifier: .stepCount), completion:{ (steps) in
467
+ if(steps != nil) {
468
+ workoutDict["steps"] = steps
469
+ }
470
+ dispatchGroup.leave()
471
+ })
472
+ }
473
+
474
+ dispatchGroup.notify(queue: .main) {
475
+ workoutDict["heartRate"] = heartRateSamples
476
+ workoutDict["route"] = routeSamples
477
+ workoutList.append(workoutDict)
478
+ }
479
+
480
+
481
+ }
482
+
483
+ dispatchGroup.notify(queue: .main) {
484
+ call.resolve(["workouts": workoutList, "errors": errors])
485
+ }
486
+ }
487
+
488
+ healthStore.execute(workoutQuery)
489
+ }
490
+
491
+
492
+
493
+ // MARK: - Query Heart Rate Data
494
+ private func queryHeartRate(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
495
+ let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
496
+ let predicate = HKQuery.predicateForSamples(withStart: workout.startDate, end: workout.endDate, options: .strictStartDate)
497
+
498
+ let heartRateQuery = HKSampleQuery(sampleType: heartRateType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { query, samples, error in
499
+ guard let heartRateSamplesData = samples as? [HKQuantitySample], error == nil else {
500
+ completion([], error?.localizedDescription)
501
+ return
502
+ }
503
+
504
+ var heartRateSamples: [[String: Any]] = []
505
+
506
+ for sample in heartRateSamplesData {
507
+ let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
508
+
509
+ let sampleDict: [String: Any] = [
510
+ "timestamp": sample.startDate,
511
+ "bpm": sample.quantity.doubleValue(for: heartRateUnit)
512
+ ]
513
+
514
+ heartRateSamples.append(sampleDict)
515
+ }
516
+
517
+
518
+ completion(heartRateSamples, nil)
519
+ }
520
+
521
+ healthStore.execute(heartRateQuery)
522
+ }
523
+
524
+ // MARK: - Query Route Data
525
+ private func queryRoute(for workout: HKWorkout, completion: @escaping ([[String: Any]], String?) -> Void) {
526
+ let routeType = HKSeriesType.workoutRoute()
527
+ let predicate = HKQuery.predicateForObjects(from: workout)
528
+
529
+ let routeQuery = HKSampleQuery(sampleType: routeType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { query, samples, error in
530
+ guard let routes = samples as? [HKWorkoutRoute], error == nil else {
531
+ completion([], error?.localizedDescription)
532
+ return
533
+ }
534
+
535
+ var routeLocations: [[String: Any]] = []
536
+ let routeDispatchGroup = DispatchGroup()
537
+
538
+ // Query locations for each route
539
+ for route in routes {
540
+ routeDispatchGroup.enter()
541
+ self.queryLocations(for: route) { locations in
542
+ routeLocations.append(contentsOf: locations)
543
+ routeDispatchGroup.leave()
544
+ }
545
+ }
546
+
547
+ routeDispatchGroup.notify(queue: .main) {
548
+ completion(routeLocations, nil)
549
+ }
550
+ }
551
+
552
+ healthStore.execute(routeQuery)
553
+ }
554
+
555
+ // MARK: - Query Route Locations
556
+ private func queryLocations(for route: HKWorkoutRoute, completion: @escaping ([[String: Any]]) -> Void) {
557
+ var routeLocations: [[String: Any]] = []
558
+
559
+ let locationQuery = HKWorkoutRouteQuery(route: route) { query, locations, done, error in
560
+ guard let locations = locations, error == nil else {
561
+ completion([])
562
+ return
563
+ }
564
+
565
+ for location in locations {
566
+ let locationDict: [String: Any] = [
567
+ "timestamp": location.timestamp,
568
+ "lat": location.coordinate.latitude,
569
+ "lng": location.coordinate.longitude,
570
+ "alt": location.altitude
571
+ ]
572
+ routeLocations.append(locationDict)
573
+ }
574
+
575
+ if done {
576
+ completion(routeLocations)
577
+ }
578
+ }
579
+
580
+ healthStore.execute(locationQuery)
581
+ }
582
+
583
+
584
+ let workoutTypeMapping: [UInt : String] = [
585
+ 1 : "americanFootball" ,
586
+ 2 : "archery" ,
587
+ 3 : "australianFootball" ,
588
+ 4 : "badminton" ,
589
+ 5 : "baseball" ,
590
+ 6 : "basketball" ,
591
+ 7 : "bowling" ,
592
+ 8 : "boxing" ,
593
+ 9 : "climbing" ,
594
+ 10 : "cricket" ,
595
+ 11 : "crossTraining" ,
596
+ 12 : "curling" ,
597
+ 13 : "cycling" ,
598
+ 14 : "dance" ,
599
+ 15 : "danceInspiredTraining" ,
600
+ 16 : "elliptical" ,
601
+ 17 : "equestrianSports" ,
602
+ 18 : "fencing" ,
603
+ 19 : "fishing" ,
604
+ 20 : "functionalStrengthTraining" ,
605
+ 21 : "golf" ,
606
+ 22 : "gymnastics" ,
607
+ 23 : "handball" ,
608
+ 24 : "hiking" ,
609
+ 25 : "hockey" ,
610
+ 26 : "hunting" ,
611
+ 27 : "lacrosse" ,
612
+ 28 : "martialArts" ,
613
+ 29 : "mindAndBody" ,
614
+ 30 : "mixedMetabolicCardioTraining" ,
615
+ 31 : "paddleSports" ,
616
+ 32 : "play" ,
617
+ 33 : "preparationAndRecovery" ,
618
+ 34 : "racquetball" ,
619
+ 35 : "rowing" ,
620
+ 36 : "rugby" ,
621
+ 37 : "running" ,
622
+ 38 : "sailing" ,
623
+ 39 : "skatingSports" ,
624
+ 40 : "snowSports" ,
625
+ 41 : "soccer" ,
626
+ 42 : "softball" ,
627
+ 43 : "squash" ,
628
+ 44 : "stairClimbing" ,
629
+ 45 : "surfingSports" ,
630
+ 46 : "swimming" ,
631
+ 47 : "tableTennis" ,
632
+ 48 : "tennis" ,
633
+ 49 : "trackAndField" ,
634
+ 50 : "traditionalStrengthTraining" ,
635
+ 51 : "volleyball" ,
636
+ 52 : "walking" ,
637
+ 53 : "waterFitness" ,
638
+ 54 : "waterPolo" ,
639
+ 55 : "waterSports" ,
640
+ 56 : "wrestling" ,
641
+ 57 : "yoga" ,
642
+ 58 : "barre" ,
643
+ 59 : "coreTraining" ,
644
+ 60 : "crossCountrySkiing" ,
645
+ 61 : "downhillSkiing" ,
646
+ 62 : "flexibility" ,
647
+ 63 : "highIntensityIntervalTraining" ,
648
+ 64 : "jumpRope" ,
649
+ 65 : "kickboxing" ,
650
+ 66 : "pilates" ,
651
+ 67 : "snowboarding" ,
652
+ 68 : "stairs" ,
653
+ 69 : "stepTraining" ,
654
+ 70 : "wheelchairWalkPace" ,
655
+ 71 : "wheelchairRunPace" ,
656
+ 72 : "taiChi" ,
657
+ 73 : "mixedCardio" ,
658
+ 74 : "handCycling" ,
659
+ 75 : "discSports" ,
660
+ 76 : "fitnessGaming" ,
661
+ 77 : "cardioDance" ,
662
+ 78 : "socialDance" ,
663
+ 79 : "pickleball" ,
664
+ 80 : "cooldown" ,
665
+ 82 : "swimBikeRun" ,
666
+ 83 : "transition" ,
667
+ 84 : "underwaterDiving" ,
668
+ 3000 : "other"
669
+ ]
670
+
671
+ }