From 0b89660e5cf45a1cedbcec6f7af34010402f028a Mon Sep 17 00:00:00 2001 From: avargas-btf Date: Wed, 25 Sep 2024 18:09:04 -0500 Subject: [PATCH 01/36] Register WorkoutHealthValue --- packages/health/lib/health.json.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/health/lib/health.json.dart b/packages/health/lib/health.json.dart index 351098a28..2162869da 100644 --- a/packages/health/lib/health.json.dart +++ b/packages/health/lib/health.json.dart @@ -10,6 +10,7 @@ void _registerFromJsonFunctions() { FromJsonFactory().registerAll([ HealthValue(), NumericHealthValue(numericValue: 12), + WorkoutHealthValue(workoutActivityType: HealthWorkoutActivityType.RUNNING), AudiogramHealthValue( frequencies: [], leftEarSensitivities: [], From 75d0b038ad3d956aaa4a9f22c72bd4acef8c7920 Mon Sep 17 00:00:00 2001 From: Philipp Bauer Date: Sat, 5 Oct 2024 21:57:05 +0200 Subject: [PATCH 02/36] Remove automatically added workout permissions --- .../cachet/plugins/health/HealthPlugin.kt | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 70690126a..15c9a08c3 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -540,38 +540,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) } - // Workout also needs distance and total energy burned too - if (typeKey == WORKOUT) { - if (access == 0) { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } - } } scope.launch { result.success( @@ -625,38 +593,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) } - // Workout also needs distance and total energy burned too - if (typeKey == WORKOUT) { - if (access == 0) { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } - } } if (healthConnectRequestPermissionsLauncher == null) { result.success(false) From 2009d03434fbb541b0b06b64bb9b5892b66c64f5 Mon Sep 17 00:00:00 2001 From: Slava Ryabinin Date: Sat, 23 Nov 2024 08:51:31 +0300 Subject: [PATCH 03/36] Fix `HealthDataType was not aligned correctly` when adding SLEEP_LIGHT data --- packages/health/lib/src/health_plugin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 8868a519b..9223782b9 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1104,7 +1104,7 @@ class Health { HealthDataType.SLEEP_IN_BED => 0, HealthDataType.SLEEP_ASLEEP => 1, HealthDataType.SLEEP_AWAKE => 2, - HealthDataType.SLEEP_ASLEEP => 3, + HealthDataType.SLEEP_LIGHT => 3, HealthDataType.SLEEP_DEEP => 4, HealthDataType.SLEEP_REM => 5, HealthDataType.HEADACHE_UNSPECIFIED => 0, From 0c6aadf1fc7d190d07d9cd0f1bead463c3d5bc4b Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:39:04 +0100 Subject: [PATCH 04/36] ignore sdkman env --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f66dbae2d..8bb6a7a81 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,5 @@ app.*.symbols !/dev/ci/**/Gemfile.lock packages/app_usage/example/.flutter-plugins-dependencies packages/app_usage/example/.flutter-plugins-dependencies + +.sdkmanrc \ No newline at end of file From 028484610eed131e0d2675ea2af1dbcf00e341cb Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:47:08 +0100 Subject: [PATCH 05/36] Update AppDelegate.swift to use @main instead of @UIApplicationMain --- packages/health/example/ios/Runner/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/example/ios/Runner/AppDelegate.swift b/packages/health/example/ios/Runner/AppDelegate.swift index 70693e4a8..b63630348 100644 --- a/packages/health/example/ios/Runner/AppDelegate.swift +++ b/packages/health/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, From bc9f07cf9d57624aa74d66a8d2dc1ac8c3ca91dc Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:33:21 +0100 Subject: [PATCH 06/36] Comment out ECG-related HealthDataTypes in util.dart since iOS will throw error on WRITE --- packages/health/example/lib/util.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index 0a694b0a2..e1c49101d 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -46,13 +46,13 @@ const List dataTypesIOS = [ HealthDataType.HEADACHE_UNSPECIFIED, // note that a phone cannot write these ECG-based types - only read them - HealthDataType.ELECTROCARDIOGRAM, - HealthDataType.HIGH_HEART_RATE_EVENT, - HealthDataType.IRREGULAR_HEART_RATE_EVENT, - HealthDataType.LOW_HEART_RATE_EVENT, - HealthDataType.RESTING_HEART_RATE, - HealthDataType.WALKING_HEART_RATE, - HealthDataType.ATRIAL_FIBRILLATION_BURDEN, + // HealthDataType.ELECTROCARDIOGRAM, + // HealthDataType.HIGH_HEART_RATE_EVENT, + // HealthDataType.IRREGULAR_HEART_RATE_EVENT, + // HealthDataType.LOW_HEART_RATE_EVENT, + // HealthDataType.RESTING_HEART_RATE, + // HealthDataType.WALKING_HEART_RATE, + // HealthDataType.ATRIAL_FIBRILLATION_BURDEN, HealthDataType.NUTRITION, HealthDataType.GENDER, From 19aa01db47c11f999cc6b5dc968432ab795ef150 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:11:50 +0100 Subject: [PATCH 07/36] Refactor HealthApp widget and improve debug output formatting --- packages/health/example/lib/main.dart | 42 +++++++++++++++------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index db1069ddf..b77eae30e 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -10,6 +10,8 @@ import 'package:carp_serializable/carp_serializable.dart'; void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { + const HealthApp({super.key}); + @override _HealthAppState createState() => _HealthAppState(); } @@ -175,7 +177,9 @@ class _HealthAppState extends State { // filter out duplicates _healthDataList = Health().removeDuplicates(_healthDataList); - _healthDataList.forEach((data) => debugPrint(toJsonString(data))); + for (var data in _healthDataList) { + debugPrint(toJsonString(data)); + } // update the UI to display the results setState(() { @@ -662,7 +666,7 @@ class _HealthAppState extends State { if (p.value is AudiogramHealthValue) { return ListTile( title: Text("${p.typeString}: ${p.value}"), - trailing: Text('${p.unitString}'), + trailing: Text(p.unitString), subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } @@ -671,7 +675,7 @@ class _HealthAppState extends State { title: Text( "${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"), trailing: Text( - '${(p.value as WorkoutHealthValue).workoutActivityType.name}'), + (p.value as WorkoutHealthValue).workoutActivityType.name), subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } @@ -686,46 +690,46 @@ class _HealthAppState extends State { } return ListTile( title: Text("${p.typeString}: ${p.value}"), - trailing: Text('${p.unitString}'), + trailing: Text(p.unitString), subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); }); - Widget _contentNoData = const Text('No Data to show'); + final Widget _contentNoData = const Text('No Data to show'); - Widget _contentNotFetched = + final Widget _contentNotFetched = const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("Press 'Auth' to get permissions to access health data."), - const Text("Press 'Fetch Dat' to get health data."), - const Text("Press 'Add Data' to add some random health data."), - const Text("Press 'Delete Data' to remove some random health data."), + Text("Press 'Auth' to get permissions to access health data."), + Text("Press 'Fetch Dat' to get health data."), + Text("Press 'Add Data' to add some random health data."), + Text("Press 'Delete Data' to remove some random health data."), ]); - Widget _authorized = const Text('Authorization granted!'); + final Widget _authorized = const Text('Authorization granted!'); - Widget _authorizationNotGranted = const Column( + final Widget _authorizationNotGranted = const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('Authorization not given.'), - const Text( + Text('Authorization not given.'), + Text( 'For Google Health Connect please check if you have added the right permissions and services to the manifest file.'), - const Text('For Apple Health check your permissions in Apple Health.'), + Text('For Apple Health check your permissions in Apple Health.'), ], ); Widget _contentHealthConnectStatus = const Text( 'No status, click getHealthConnectSdkStatus to get the status.'); - Widget _dataAdded = const Text('Data points inserted successfully.'); + final Widget _dataAdded = const Text('Data points inserted successfully.'); - Widget _dataDeleted = const Text('Data points deleted successfully.'); + final Widget _dataDeleted = const Text('Data points deleted successfully.'); Widget get _stepsFetched => Text('Total number of steps: $_nofSteps.'); - Widget _dataNotAdded = + final Widget _dataNotAdded = const Text('Failed to add data.\nDo you have permissions to add data?'); - Widget _dataNotDeleted = const Text('Failed to delete data'); + final Widget _dataNotDeleted = const Text('Failed to delete data'); Widget get _content => switch (_state) { AppState.DATA_READY => _contentDataReady, From 5e39721e2497940d3e70c8baf07af9d49ed26407 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:04:57 +0100 Subject: [PATCH 08/36] Health data query to use HKStatisticsCollectionQuery instead of HKStatisticsQuery --- .../ios/Classes/SwiftHealthPlugin.swift | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index d7af83e05..fda57c54b 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -1171,33 +1171,36 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } - let query = HKStatisticsQuery( + // TODO: [NOTE] Computational heavy + let query = HKStatisticsCollectionQuery( quantityType: sampleType, quantitySamplePredicate: predicate, - options: .cumulativeSum - ) { query, queryResult, error in - - guard let queryResult = queryResult else { + options: .cumulativeSum, + anchorDate: dateFrom, + intervalComponents: DateComponents(day: 1) + ) + query.initialResultsHandler = { query, results, error in + guard let results = results else { let error = error! as NSError print("Error getting total steps in interval \(error.localizedDescription)") - + DispatchQueue.main.async { result(nil) } return - } - - var steps = 0.0 - - if let quantity = queryResult.sumQuantity() { - let unit = HKUnit.count() - steps = quantity.doubleValue(for: unit) - } - - let totalSteps = Int(steps) - DispatchQueue.main.async { - result(totalSteps) - } + } + + var totalSteps = 0.0 + results.enumerateStatistics(from: dateFrom, to: dateTo) { statistics, stop in + if let quantity = statistics.sumQuantity() { + let unit = HKUnit.count() + totalSteps += quantity.doubleValue(for: unit) + } + } + + DispatchQueue.main.async { + result(Int(totalSteps)) + } } HKHealthStore().execute(query) From c542556f5f32b2651f51c386480ae4c6c06f531c Mon Sep 17 00:00:00 2001 From: Iosif Futerman Date: Wed, 4 Dec 2024 12:01:19 +0200 Subject: [PATCH 09/36] fixed hasPermissions call. If types have permissions result should be true --- packages/health/ios/Classes/SwiftHealthPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index d7af83e05..0d7882f87 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -339,7 +339,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } - result(false) + result(true) } func hasPermission(type: HKObjectType, access: Int) -> Bool? { From cb2601800f03201de009d6aeb061f1c0d032901b Mon Sep 17 00:00:00 2001 From: PeteRyo0517 <> Date: Wed, 11 Dec 2024 14:39:20 +0900 Subject: [PATCH 10/36] Updated device_info_plus version dependency --- packages/health/CHANGELOG.md | 4 ++++ packages/health/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index d1e668fd7..cd792d435 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,7 @@ +## 11.1.2 + +* Updated `device_info_plus` version dependency + ## 11.1.1 * Fix of [#1059](https://github.com/cph-cachet/flutter-plugins/issues/1059) diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index a97aeaa6f..54fc3c4bf 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter intl: '>=0.18.0 <0.20.0' - device_info_plus: '>=9.0.0 <11.0.0' + device_info_plus: '>=9.0.0 <12.0.0' json_annotation: ^4.8.0 carp_serializable: ^2.0.0 # polymorphic json serialization From 079de1b2bf7761a9ebde8bb568b87b906e11f8fd Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Tue, 12 Nov 2024 16:31:59 +0100 Subject: [PATCH 11/36] [Health] Add lean mass data type Closes #1078 --- packages/health/CHANGELOG.md | 4 ++++ .../cachet/plugins/health/HealthPlugin.kt | 19 ++++++++++++++++++- .../ios/Classes/SwiftHealthPlugin.swift | 3 +++ packages/health/lib/src/heath_data_types.dart | 4 ++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index d1e668fd7..9a417c7ab 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,7 @@ +## 11.2.0 + +* Add lean mass data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) + ## 11.1.1 * Fix of [#1059](https://github.com/cph-cachet/flutter-plugins/issues/1059) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 216aff7ea..23f64dad7 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -48,6 +48,7 @@ const val BLOOD_OXYGEN = "BLOOD_OXYGEN" const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" const val BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" const val BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" +const val LEAN_BODY_MASS = "LEAN_BODY_MASS" const val BODY_TEMPERATURE = "BODY_TEMPERATURE" const val BODY_WATER_MASS = "BODY_WATER_MASS" const val DISTANCE_DELTA = "DISTANCE_DELTA" @@ -1596,6 +1597,22 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) + LEAN_BODY_MASS -> + LeanBodyMassRecord( + time = + Instant.ofEpochMilli( + startTime + ), + mass = + Mass.kilograms( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + HEIGHT -> HeightRecord( time = @@ -2398,6 +2415,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : private val mapToType = hashMapOf( BODY_FAT_PERCENTAGE to BodyFatRecord::class, + LEAN_BODY_MASS to LeanBodyMassRecord::class, HEIGHT to HeightRecord::class, WEIGHT to WeightRecord::class, STEPS to StepsRecord::class, @@ -2451,7 +2469,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // "HeartRate" to HeartRateRecord::class, // "Height" to HeightRecord::class, // "Hydration" to HydrationRecord::class, - // "LeanBodyMass" to LeanBodyMassRecord::class, // "MenstruationPeriod" to MenstruationPeriodRecord::class, // "Nutrition" to NutritionRecord::class, // "OvulationTest" to OvulationTestRecord::class, diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index d7af83e05..79da70922 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -35,6 +35,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" + let LEAN_BODY_MASS = "LEAN_BODY_MASS" let BODY_MASS_INDEX = "BODY_MASS_INDEX" let BODY_TEMPERATURE = "BODY_TEMPERATURE" // Nutrition @@ -1409,6 +1410,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .bloodPressureSystolic)! dataTypesDict[BODY_FAT_PERCENTAGE] = HKSampleType.quantityType( forIdentifier: .bodyFatPercentage)! + dataTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .bodyMass)! dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)! dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)! @@ -1507,6 +1509,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)! dataQuantityTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)! dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)! + dataQuantityTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .bodyMass)! dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)! dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)! diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index f50762721..0989381b9 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -11,6 +11,7 @@ enum HealthDataType { BLOOD_PRESSURE_DIASTOLIC, BLOOD_PRESSURE_SYSTOLIC, BODY_FAT_PERCENTAGE, + LEAN_BODY_MASS, BODY_MASS_INDEX, BODY_TEMPERATURE, BODY_WATER_MASS, @@ -125,6 +126,7 @@ const List dataTypeKeysIOS = [ HealthDataType.BLOOD_PRESSURE_DIASTOLIC, HealthDataType.BLOOD_PRESSURE_SYSTOLIC, HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.LEAN_BODY_MASS, HealthDataType.BODY_MASS_INDEX, HealthDataType.BODY_TEMPERATURE, HealthDataType.DIETARY_CARBS_CONSUMED, @@ -216,6 +218,7 @@ const List dataTypeKeysAndroid = [ HealthDataType.BLOOD_PRESSURE_DIASTOLIC, HealthDataType.BLOOD_PRESSURE_SYSTOLIC, HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.LEAN_BODY_MASS, HealthDataType.BODY_MASS_INDEX, HealthDataType.BODY_TEMPERATURE, HealthDataType.BODY_WATER_MASS, @@ -256,6 +259,7 @@ const Map dataTypeToUnit = { HealthDataType.BLOOD_PRESSURE_DIASTOLIC: HealthDataUnit.MILLIMETER_OF_MERCURY, HealthDataType.BLOOD_PRESSURE_SYSTOLIC: HealthDataUnit.MILLIMETER_OF_MERCURY, HealthDataType.BODY_FAT_PERCENTAGE: HealthDataUnit.PERCENT, + HealthDataType.LEAN_BODY_MASS: HealthDataUnit.KILOGRAM, HealthDataType.BODY_MASS_INDEX: HealthDataUnit.NO_UNIT, HealthDataType.BODY_TEMPERATURE: HealthDataUnit.DEGREE_CELSIUS, HealthDataType.BODY_WATER_MASS: HealthDataUnit.KILOGRAM, From 8950271e69c39486b43db7e0bf8c34960ad91206 Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Thu, 14 Nov 2024 17:45:46 +0100 Subject: [PATCH 12/36] Add missing conversion from LeanMassRecord --- .../cachet/plugins/health/HealthPlugin.kt | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 23f64dad7..846d5370e 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -1084,6 +1084,29 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) + is LeanMassRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.weight + .inKilograms, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) + is StepsRecord -> return listOf( mapOf( @@ -2456,7 +2479,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // "BasalMetabolicRate" to BasalMetabolicRateRecord::class, // "BloodGlucose" to BloodGlucoseRecord::class, // "BloodPressure" to BloodPressureRecord::class, - // "BodyFat" to BodyFatRecord::class, // "BodyTemperature" to BodyTemperatureRecord::class, // "BoneMass" to BoneMassRecord::class, // "CervicalMucus" to CervicalMucusRecord::class, From aebf51527a636863a62b761375263ee20808c55d Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Fri, 15 Nov 2024 17:18:53 +0100 Subject: [PATCH 13/36] Fix wrong field name in Android --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 846d5370e..002e824b3 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -1084,13 +1084,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) - is LeanMassRecord -> + is LeanBodyMassRecord -> return listOf( mapOf( "uuid" to metadata.id, "value" to - record.weight + record.mass .inKilograms, "date_from" to record.time From 26c15b6409b7dbbdc19bd882c773111e121cc017 Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Fri, 15 Nov 2024 17:19:24 +0100 Subject: [PATCH 14/36] Fix wrong type name in iOS --- packages/health/ios/Classes/SwiftHealthPlugin.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 79da70922..fcee46767 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -1410,7 +1410,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .bloodPressureSystolic)! dataTypesDict[BODY_FAT_PERCENTAGE] = HKSampleType.quantityType( forIdentifier: .bodyFatPercentage)! - dataTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .bodyMass)! + dataTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)! dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)! dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)! @@ -1471,7 +1471,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .waistCircumference)! dataTypesDict[WALKING_HEART_RATE] = HKSampleType.quantityType( forIdentifier: .walkingHeartRateAverage)! - dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)! + dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)! dataTypesDict[DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType( forIdentifier: .distanceWalkingRunning)! dataTypesDict[DISTANCE_SWIMMING] = HKSampleType.quantityType(forIdentifier: .distanceSwimming)! @@ -1509,7 +1509,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)! dataQuantityTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)! dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)! - dataQuantityTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .bodyMass)! + dataQuantityTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)! dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)! dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)! From a0de7bdeb64e7b168643ffe1bd9e92e65c561ac5 Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Sat, 16 Nov 2024 20:22:13 +0100 Subject: [PATCH 15/36] Fix weight type --- packages/health/ios/Classes/SwiftHealthPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index fcee46767..d2fce50c0 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -1471,7 +1471,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .waistCircumference)! dataTypesDict[WALKING_HEART_RATE] = HKSampleType.quantityType( forIdentifier: .walkingHeartRateAverage)! - dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)! + dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)! dataTypesDict[DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType( forIdentifier: .distanceWalkingRunning)! dataTypesDict[DISTANCE_SWIMMING] = HKSampleType.quantityType(forIdentifier: .distanceSwimming)! From 2578bc212c44415fcf756aaffa36f5a026929e3f Mon Sep 17 00:00:00 2001 From: Andrey Danilov Date: Tue, 24 Dec 2024 16:50:15 -0800 Subject: [PATCH 16/36] remove non-null check for name --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 216aff7ea..5c261f90f 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -1516,7 +1516,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "sugar" to record.sugar?.inGrams, "water" to null, "zinc" to record.zinc?.inGrams, - "name" to record.name!!, + "name" to record.name, "meal_type" to (mapTypeToMealType[ record.mealType] From 988cce86e3067fb7aa90196808113cd4aec0ea2e Mon Sep 17 00:00:00 2001 From: bernd70 Date: Sat, 28 Dec 2024 01:02:08 +0100 Subject: [PATCH 17/36] Deleting entries only selects own entries When deleting entries on iOS a compound predicate is used to limit the query to entries the calling app has created. Otherwise the call fails is the sample contains entries from other apps. --- .../ios/Classes/SwiftHealthPlugin.swift | 303 +++++++++--------- 1 file changed, 153 insertions(+), 150 deletions(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index d7af83e05..fb9e1fe49 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -10,7 +10,7 @@ enum RecordingMethod: Int { } public class SwiftHealthPlugin: NSObject, FlutterPlugin { - + let healthStore = HKHealthStore() var healthDataTypes = [HKSampleType]() var healthDataQuantityTypes = [HKQuantityType]() @@ -24,7 +24,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { var workoutActivityTypeMap: [String: HKWorkoutActivityType] = [:] var characteristicsTypesDict: [String: HKCharacteristicType] = [:] var nutritionList: [String] = [] - + // Health Data Type Keys let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" let ATRIAL_FIBRILLATION_BURDEN = "ATRIAL_FIBRILLATION_BURDEN" @@ -147,7 +147,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let SLEEP_IN_BED = "SLEEP_IN_BED" let SLEEP_LIGHT = "SLEEP_LIGHT" let SLEEP_REM = "SLEEP_REM" - + let EXERCISE_TIME = "EXERCISE_TIME" let WORKOUT = "WORKOUT" let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED" @@ -161,8 +161,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let GENDER = "GENDER" let BLOOD_TYPE = "BLOOD_TYPE" let MENSTRUATION_FLOW = "MENSTRUATION_FLOW" - - + + // Health Unit types // MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet // MOLE_UNIT_WITH_PREFIX_MOLAR_MASS, // requires molar mass & prefix input - not supported yet @@ -214,22 +214,22 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER" let UNKNOWN_UNIT = "UNKNOWN_UNIT" let NO_UNIT = "NO_UNIT" - + struct PluginError: Error { let message: String } - + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( name: "flutter_health", binaryMessenger: registrar.messenger()) let instance = SwiftHealthPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // Set up all data types initializeTypes() - + /// Handle checkIfHealthDataAvailable if call.method.elementsEqual("checkIfHealthDataAvailable") { checkIfHealthDataAvailable(call: call, result: result) @@ -237,72 +237,72 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else if call.method.elementsEqual("requestAuthorization") { try! requestAuthorization(call: call, result: result) } - + /// Handle getData else if call.method.elementsEqual("getData") { getData(call: call, result: result) } - + /// Handle getIntervalData else if (call.method.elementsEqual("getIntervalData")){ getIntervalData(call: call, result: result) } - + /// Handle getTotalStepsInInterval else if call.method.elementsEqual("getTotalStepsInInterval") { getTotalStepsInInterval(call: call, result: result) } - + /// Handle writeData else if call.method.elementsEqual("writeData") { try! writeData(call: call, result: result) } - + /// Handle writeAudiogram else if call.method.elementsEqual("writeAudiogram") { try! writeAudiogram(call: call, result: result) } - + /// Handle writeBloodPressure else if call.method.elementsEqual("writeBloodPressure") { try! writeBloodPressure(call: call, result: result) } - + /// Handle writeMeal else if (call.method.elementsEqual("writeMeal")){ try! writeMeal(call: call, result: result) } - + /// Handle writeInsulinDelivery else if (call.method.elementsEqual("writeInsulinDelivery")){ try! writeInsulinDelivery(call: call, result: result) } - + /// Handle writeWorkoutData else if call.method.elementsEqual("writeWorkoutData") { try! writeWorkoutData(call: call, result: result) } - + /// Handle writeMenstruationFlow else if call.method.elementsEqual("writeMenstruationFlow") { try! writeMenstruationFlow(call: call, result: result) } - + /// Handle hasPermission else if call.method.elementsEqual("hasPermissions") { try! hasPermissions(call: call, result: result) } - + /// Handle delete data else if call.method.elementsEqual("delete") { try! delete(call: call, result: result) } } - + func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { result(HKHealthStore.isHealthDataAvailable()) } - + func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws { let arguments = call.arguments as? NSDictionary guard var types = arguments?["types"] as? [String], @@ -311,18 +311,18 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments!") } - + if let nutritionIndex = types.firstIndex(of: NUTRITION) { types.remove(at: nutritionIndex) let nutritionPermission = permissions[nutritionIndex] permissions.remove(at: nutritionIndex) - + for nutritionType in nutritionList { types.append(nutritionType) permissions.append(nutritionPermission) } } - + for (index, type) in types.enumerated() { let sampleType = dataTypeLookUp(key: type) let success = hasPermission(type: sampleType, access: permissions[index]) @@ -338,12 +338,12 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + result(false) } - + func hasPermission(type: HKObjectType, access: Int) -> Bool? { - + if #available(iOS 13.0, *) { let status = healthStore.authorizationStatus(for: type) switch access { @@ -358,7 +358,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { return nil } } - + func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let types = arguments["types"] as? [String], @@ -367,7 +367,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments!") } - + var typesToRead = Set() var typesToWrite = Set() for (index, key) in types.enumerated() { @@ -401,7 +401,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + if #available(iOS 13.0, *) { healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { (success, error) in @@ -413,7 +413,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { result(false) // Handle the error here. } } - + func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let value = (arguments["value"] as? Double), @@ -425,7 +425,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) @@ -433,9 +433,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let metadata: [String: Any] = [ HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) ] - + let sample: HKObject - + if dataTypeLookUp(key: type).isKind(of: HKCategoryType.self) { sample = HKCategorySample( type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom, @@ -446,7 +446,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata) } - + HKHealthStore().save( sample, withCompletion: { (success, error) in @@ -458,7 +458,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let frequencies = (arguments["frequencies"] as? [Double]), @@ -469,12 +469,12 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + var sensitivityPoints = [HKAudiogramSensitivityPoint]() - + for index in 0...frequencies.count - 1 { let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index]) let dbUnit = HKUnit.decibelHearingLevel() @@ -484,23 +484,23 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right) sensitivityPoints.append(sensitivityPoint) } - + let audiogram: HKAudiogramSample let metadataReceived = (arguments["metadata"] as? [String: Any]?) - + if (metadataReceived) != nil { guard let deviceName = metadataReceived?!["HKDeviceName"] as? String else { return } guard let externalUUID = metadataReceived?!["HKExternalUUID"] as? String else { return } - + audiogram = HKAudiogramSample( sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: [HKMetadataKeyDeviceName: deviceName, HKMetadataKeyExternalUUID: externalUUID]) - + } else { audiogram = HKAudiogramSample( sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: nil) } - + HKHealthStore().save( audiogram, withCompletion: { (success, error) in @@ -512,7 +512,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeBloodPressure(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let systolic = (arguments["systolic"] as? Double), @@ -530,7 +530,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let metadata = [ HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) ] - + let systolic_sample = HKQuantitySample( type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), @@ -542,7 +542,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let bpCorrelationType = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)! let bpCorrelation = Set(arrayLiteral: systolic_sample, diastolic_sample) let blood_pressure_sample = HKCorrelation(type: bpCorrelationType , start: dateFrom, end: dateTo, objects: bpCorrelation) - + HKHealthStore().save( [blood_pressure_sample], withCompletion: { (success, error) in @@ -554,7 +554,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let name = (arguments["name"] as? String?), @@ -565,14 +565,14 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let mealTypeString = mealType ?? "UNKNOWN" let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue - + var metadata = ["HKFoodMeal": mealTypeString, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] if (name != nil) { metadata[HKMetadataKeyFoodType] = "\(name!)" @@ -587,11 +587,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { type: HKSampleType.quantityType(forIdentifier: identifier)!, quantity: HKQuantity(unit: unit, doubleValue: unwrappedValue), start: dateFrom, end: dateTo, metadata: metadata) nutrition.insert(nutritionSample) } - + if #available(iOS 15.0, *){ let type = HKCorrelationType.correlationType(forIdentifier: HKCorrelationTypeIdentifier.food)! let meal = HKCorrelation(type: type, start: dateFrom, end: dateTo, objects: nutrition, metadata: metadata) - + HKHealthStore().save(meal, withCompletion: { (success, error) in if let err = error { print("Error Saving Meal Sample: \(err.localizedDescription)") @@ -615,13 +615,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let type = HKSampleType.quantityType(forIdentifier: .insulinDelivery)! let quantity = HKQuantity(unit: HKUnit.internationalUnit(), doubleValue: units) let metadata = [HKMetadataKeyInsulinDeliveryReason: reason] - + let insulin_sample = HKQuantitySample(type: type, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata) - + HKHealthStore().save(insulin_sample, withCompletion: { (success, error) in if let err = error { print("Error Saving Insulin Delivery Sample: \(err.localizedDescription)") @@ -631,7 +631,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeMenstruationFlow(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let flow = (arguments["value"] as? Int), @@ -644,7 +644,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { guard let menstrualFlowType = HKCategoryValueMenstrualFlow(rawValue: flow) else { throw PluginError(message: "Invalid Menstrual Flow Type") } - + let dateTime = Date(timeIntervalSince1970: endTime.doubleValue / 1000) let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue @@ -654,15 +654,15 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } let metadata = [HKMetadataKeyMenstrualCycleStart: isStartOfCycle, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] - + let sample = HKCategorySample( type: categoryType, - value: menstrualFlowType.rawValue, - start: dateTime, + value: menstrualFlowType.rawValue, + start: dateTime, end: dateTime, metadata: metadata ) - + HKHealthStore().save( sample, withCompletion: { (success, error) in @@ -674,7 +674,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let activityType = (arguments["activityType"] as? String), @@ -684,10 +684,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid") } - + var totalEnergyBurned: HKQuantity? var totalDistance: HKQuantity? = nil - + // Handle optional arguments if let teb = (arguments["totalEnergyBurned"] as? Double) { totalEnergyBurned = HKQuantity( @@ -697,17 +697,17 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { totalDistance = HKQuantity( unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td) } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + var workout: HKWorkout - + workout = HKWorkout( activityType: ac, start: dateFrom, end: dateTo, duration: dateTo.timeIntervalSince(dateFrom), totalEnergyBurned: totalEnergyBurned ?? nil, totalDistance: totalDistance ?? nil, metadata: nil) - + HKHealthStore().save( workout, withCompletion: { (success, error) in @@ -719,33 +719,36 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func delete(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String)! let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let dataType = dataTypeLookUp(key: dataTypeKey) - - let predicate = HKQuery.predicateForSamples( + + let samplePredicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) + let ownerPredicate = HKQuery.predicateForObjects(from: HKSource.default()) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - + let deleteQuery = HKSampleQuery( - sampleType: dataType, predicate: predicate, limit: HKObjectQueryNoLimit, + sampleType: dataType, + predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [samplePredicate, ownerPredicate]), + limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in - + guard let samplesOrNil = samplesOrNil, error == nil else { // Handle the error if necessary print("Error deleting \(dataType)") return } - + // Delete the retrieved objects from the HealthKit store HKHealthStore().delete(samplesOrNil) { (success, error) in if let err = error { @@ -756,10 +759,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + HKHealthStore().execute(deleteQuery) } - + func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String)! @@ -769,20 +772,20 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) - + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let dataType = dataTypeLookUp(key: dataTypeKey) var unit: HKUnit? if let dataUnitKey = dataUnitKey { unit = unitDict[dataUnitKey] } - + let sourceIdForCharacteristic = "com.apple.Health" let sourceNameForCharacteristic = "Health" - + switch(dataTypeKey) { case "BIRTH_DATE": let dateOfBirth = getBirthDate() @@ -826,7 +829,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { default: break } - + var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) if (!includeManualEntry) { @@ -834,13 +837,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - + let query = HKSampleQuery( sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in - + switch samplesOrNil { case let (samples as [HKQuantitySample]) as Any: let dictionaries = samples.map { sample -> NSDictionary in @@ -860,9 +863,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { result(dictionaries) } - + case var (samplesCategory as [HKCategorySample]) as Any: - + if dataTypeKey == self.SLEEP_IN_BED { samplesCategory = samplesCategory.filter { $0.value == 0 } } @@ -898,13 +901,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } let categories = samplesCategory.map { sample -> NSDictionary in var metadata: [String: Any] = [:] - + if let sampleMetadata = sample.metadata { for (key, value) in sampleMetadata { metadata[key] = value } } - + return [ "uuid": "\(sample.uuid)", "value": sample.value, @@ -919,9 +922,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { result(categories) } - + case let (samplesWorkout as [HKWorkout]) as Any: - + let dictionaries = samplesWorkout.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", @@ -942,11 +945,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 ] } - + DispatchQueue.main.async { result(dictionaries) } - + case let (samplesAudiogram as [HKAudiogramSample]) as Any: let dictionaries = samplesAudiogram.map { sample -> NSDictionary in var frequencies = [Double]() @@ -973,7 +976,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { result(dictionaries) } - + case let (nutritionSample as [HKCorrelation]) as Any: var foods: [[String: Any?]] = [] for food in nutritionSample { @@ -1007,11 +1010,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { foods.append(sampleDict) } } - + DispatchQueue.main.async { result(foods) } - + default: if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { let dictionaries = ecgSamples.map(fetchEcgMeasurements) @@ -1026,10 +1029,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + HKHealthStore().execute(query) } - + @available(iOS 14.0, *) private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary { let semaphore = DispatchSemaphore(value: 0) @@ -1063,7 +1066,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "source_name": sample.sourceRevision.source.name, ] } - + func getIntervalData(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String) ?? "DEFAULT" @@ -1073,24 +1076,24 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let intervalInSecond = (arguments?["interval"] as? Int) ?? 1 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) - + // Set interval in seconds. var interval = DateComponents() interval.second = intervalInSecond - + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startDate.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endDate.doubleValue / 1000) - + let quantityType: HKQuantityType! = dataQuantityTypesDict[dataTypeKey] var predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: []) if (!includeManualEntry) { let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } - + let query = HKStatisticsCollectionQuery(quantityType: quantityType, quantitySamplePredicate: predicate, options: [.cumulativeSum, .separateBySource], anchorDate: dateFrom, intervalComponents: interval) - + query.initialResultsHandler = { [weak self] _, statisticCollectionOrNil, error in guard let self = self else { @@ -1101,7 +1104,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + // Error detected. if let error = error { print("Query error: \(error.localizedDescription)") @@ -1110,7 +1113,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + guard let collection = statisticCollectionOrNil as? HKStatisticsCollection else { print("Unexpected result from query") DispatchQueue.main.async { @@ -1118,7 +1121,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + var dictionaries = [[String: Any]]() collection.enumerateStatistics(from: dateFrom, to: dateTo) { [weak self] statisticData, _ in @@ -1127,7 +1130,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { print("Self is nil during enumeration") return } - + do { if let quantity = statisticData.sumQuantity(), let dataUnitKey = dataUnitKey, @@ -1151,18 +1154,18 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } HKHealthStore().execute(query) } - + func getTotalStepsInInterval(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) - + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)! var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) @@ -1170,53 +1173,53 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } - + let query = HKStatisticsQuery( quantityType: sampleType, quantitySamplePredicate: predicate, options: .cumulativeSum ) { query, queryResult, error in - + guard let queryResult = queryResult else { let error = error! as NSError print("Error getting total steps in interval \(error.localizedDescription)") - + DispatchQueue.main.async { result(nil) } return } - + var steps = 0.0 - + if let quantity = queryResult.sumQuantity() { let unit = HKUnit.count() steps = quantity.doubleValue(for: unit) } - + let totalSteps = Int(steps) DispatchQueue.main.async { result(totalSteps) } } - + HKHealthStore().execute(query) } - + func unitLookUp(key: String) -> HKUnit { guard let unit = unitDict[key] else { return HKUnit.count() } return unit } - + func dataTypeLookUp(key: String) -> HKSampleType { guard let dataType_ = dataTypesDict[key] else { return HKSampleType.quantityType(forIdentifier: .bodyMass)! } return dataType_ } - + func getGender() -> HKBiologicalSex? { var bioSex:HKBiologicalSex? do { @@ -1227,7 +1230,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return bioSex } - + func getBirthDate() -> Date? { var dob:Date? do { @@ -1238,7 +1241,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return dob } - + func getBloodType() -> HKBloodType? { var bloodType:HKBloodType? do { @@ -1249,7 +1252,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return bloodType } - + func initializeTypes() { // Initialize units unitDict[GRAM] = HKUnit.gram() @@ -1298,7 +1301,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { unitDict[MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL") unitDict[UNKNOWN_UNIT] = HKUnit.init(from: "") unitDict[NO_UNIT] = HKUnit.init(from: "") - + // Initialize workout types workoutActivityTypeMap["ARCHERY"] = .archery workoutActivityTypeMap["BOWLING"] = .bowling @@ -1402,7 +1405,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[RESPIRATORY_RATE] = HKSampleType.quantityType(forIdentifier: .respiratoryRate)! dataTypesDict[PERIPHERAL_PERFUSION_INDEX] = HKSampleType.quantityType( forIdentifier: .peripheralPerfusionIndex)! - + dataTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKSampleType.quantityType( forIdentifier: .bloodPressureDiastolic)! dataTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKSampleType.quantityType( @@ -1411,7 +1414,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .bodyFatPercentage)! dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)! dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)! - + // Nutrition dataTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)! dataTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)! @@ -1452,7 +1455,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)! dataTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)! dataTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)! - + dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType( forIdentifier: .electrodermalActivity)! dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType( @@ -1483,21 +1486,21 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! dataTypesDict[MENSTRUATION_FLOW] = HKSampleType.categoryType(forIdentifier: .menstrualFlow)! - - + + dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)! dataTypesDict[WORKOUT] = HKSampleType.workoutType() dataTypesDict[NUTRITION] = HKSampleType.correlationType( forIdentifier: .food)! - + healthDataTypes = Array(dataTypesDict.values) - + characteristicsTypesDict[BIRTH_DATE] = HKObjectType.characteristicType(forIdentifier: .dateOfBirth)! characteristicsTypesDict[GENDER] = HKObjectType.characteristicType(forIdentifier: .biologicalSex)! characteristicsTypesDict[BLOOD_TYPE] = HKObjectType.characteristicType(forIdentifier: .bloodType)! characteristicsDataTypes = Array(characteristicsTypesDict.values) } - + // Set up iOS 11 specific types (ordinary health data quantity types) if #available(iOS 11.0, *) { dataQuantityTypesDict[ACTIVE_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)! @@ -1509,7 +1512,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)! dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)! dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)! - + // Nutrition dataQuantityTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)! dataQuantityTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)! @@ -1550,7 +1553,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)! dataQuantityTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)! dataQuantityTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)! - + dataQuantityTypesDict[ELECTRODERMAL_ACTIVITY] = HKQuantityType.quantityType(forIdentifier: .electrodermalActivity)! dataQuantityTypesDict[FORCED_EXPIRATORY_VOLUME] = HKQuantityType.quantityType(forIdentifier: .forcedExpiratoryVolume1)! dataQuantityTypesDict[HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .heartRate)! @@ -1565,10 +1568,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[DISTANCE_SWIMMING] = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)! dataQuantityTypesDict[DISTANCE_CYCLING] = HKQuantityType.quantityType(forIdentifier: .distanceCycling)! dataQuantityTypesDict[FLIGHTS_CLIMBED] = HKQuantityType.quantityType(forIdentifier: .flightsClimbed)! - + healthDataQuantityTypes = Array(dataQuantityTypesDict.values) } - + // Set up heart rate data types specific to the apple watch, requires iOS 12 if #available(iOS 12.2, *) { dataTypesDict[HIGH_HEART_RATE_EVENT] = HKSampleType.categoryType( @@ -1577,32 +1580,32 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .lowHeartRateEvent)! dataTypesDict[IRREGULAR_HEART_RATE_EVENT] = HKSampleType.categoryType( forIdentifier: .irregularHeartRhythmEvent)! - + heartRateEventTypes = Set([ HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!, HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!, HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!, ]) } - + if #available(iOS 13.6, *) { dataTypesDict[HEADACHE_UNSPECIFIED] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_NOT_PRESENT] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_MILD] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_MODERATE] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_SEVERE] = HKSampleType.categoryType(forIdentifier: .headache)! - + headacheType = Set([ HKSampleType.categoryType(forIdentifier: .headache)! ]) } - + if #available(iOS 14.0, *) { dataTypesDict[ELECTROCARDIOGRAM] = HKSampleType.electrocardiogramType() - + unitDict[VOLT] = HKUnit.volt() unitDict[INCHES_OF_MERCURY] = HKUnit.inchesOfMercury() - + workoutActivityTypeMap["CARDIO_DANCE"] = HKWorkoutActivityType.cardioDance workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball @@ -1611,13 +1614,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { if #available(iOS 16.0, *) { dataTypesDict[ATRIAL_FIBRILLATION_BURDEN] = HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)! - } - + } + // Concatenate heart events, headache and health data types (both may be empty) allDataTypes = Set(heartRateEventTypes + healthDataTypes) allDataTypes = allDataTypes.union(headacheType) } - + func getWorkoutType(type: HKWorkoutActivityType) -> String { switch type { case .americanFootball: From c1ddc2c035e4476cca4f96fcacaa2228f4259eb8 Mon Sep 17 00:00:00 2001 From: bernd70 Date: Sat, 28 Dec 2024 01:27:30 +0100 Subject: [PATCH 18/36] Undid whitespace changes --- .../ios/Classes/SwiftHealthPlugin.swift | 296 +++++++++--------- 1 file changed, 148 insertions(+), 148 deletions(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index fb9e1fe49..7bb71ca2b 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -10,7 +10,7 @@ enum RecordingMethod: Int { } public class SwiftHealthPlugin: NSObject, FlutterPlugin { - + let healthStore = HKHealthStore() var healthDataTypes = [HKSampleType]() var healthDataQuantityTypes = [HKQuantityType]() @@ -24,7 +24,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { var workoutActivityTypeMap: [String: HKWorkoutActivityType] = [:] var characteristicsTypesDict: [String: HKCharacteristicType] = [:] var nutritionList: [String] = [] - + // Health Data Type Keys let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" let ATRIAL_FIBRILLATION_BURDEN = "ATRIAL_FIBRILLATION_BURDEN" @@ -147,7 +147,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let SLEEP_IN_BED = "SLEEP_IN_BED" let SLEEP_LIGHT = "SLEEP_LIGHT" let SLEEP_REM = "SLEEP_REM" - + let EXERCISE_TIME = "EXERCISE_TIME" let WORKOUT = "WORKOUT" let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED" @@ -161,8 +161,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let GENDER = "GENDER" let BLOOD_TYPE = "BLOOD_TYPE" let MENSTRUATION_FLOW = "MENSTRUATION_FLOW" - - + + // Health Unit types // MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet // MOLE_UNIT_WITH_PREFIX_MOLAR_MASS, // requires molar mass & prefix input - not supported yet @@ -214,22 +214,22 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER" let UNKNOWN_UNIT = "UNKNOWN_UNIT" let NO_UNIT = "NO_UNIT" - + struct PluginError: Error { let message: String } - + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( name: "flutter_health", binaryMessenger: registrar.messenger()) let instance = SwiftHealthPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // Set up all data types initializeTypes() - + /// Handle checkIfHealthDataAvailable if call.method.elementsEqual("checkIfHealthDataAvailable") { checkIfHealthDataAvailable(call: call, result: result) @@ -237,72 +237,72 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else if call.method.elementsEqual("requestAuthorization") { try! requestAuthorization(call: call, result: result) } - + /// Handle getData else if call.method.elementsEqual("getData") { getData(call: call, result: result) } - + /// Handle getIntervalData else if (call.method.elementsEqual("getIntervalData")){ getIntervalData(call: call, result: result) } - + /// Handle getTotalStepsInInterval else if call.method.elementsEqual("getTotalStepsInInterval") { getTotalStepsInInterval(call: call, result: result) } - + /// Handle writeData else if call.method.elementsEqual("writeData") { try! writeData(call: call, result: result) } - + /// Handle writeAudiogram else if call.method.elementsEqual("writeAudiogram") { try! writeAudiogram(call: call, result: result) } - + /// Handle writeBloodPressure else if call.method.elementsEqual("writeBloodPressure") { try! writeBloodPressure(call: call, result: result) } - + /// Handle writeMeal else if (call.method.elementsEqual("writeMeal")){ try! writeMeal(call: call, result: result) } - + /// Handle writeInsulinDelivery else if (call.method.elementsEqual("writeInsulinDelivery")){ try! writeInsulinDelivery(call: call, result: result) } - + /// Handle writeWorkoutData else if call.method.elementsEqual("writeWorkoutData") { try! writeWorkoutData(call: call, result: result) } - + /// Handle writeMenstruationFlow else if call.method.elementsEqual("writeMenstruationFlow") { try! writeMenstruationFlow(call: call, result: result) } - + /// Handle hasPermission else if call.method.elementsEqual("hasPermissions") { try! hasPermissions(call: call, result: result) } - + /// Handle delete data else if call.method.elementsEqual("delete") { try! delete(call: call, result: result) } } - + func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { result(HKHealthStore.isHealthDataAvailable()) } - + func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws { let arguments = call.arguments as? NSDictionary guard var types = arguments?["types"] as? [String], @@ -311,18 +311,18 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments!") } - + if let nutritionIndex = types.firstIndex(of: NUTRITION) { types.remove(at: nutritionIndex) let nutritionPermission = permissions[nutritionIndex] permissions.remove(at: nutritionIndex) - + for nutritionType in nutritionList { types.append(nutritionType) permissions.append(nutritionPermission) } } - + for (index, type) in types.enumerated() { let sampleType = dataTypeLookUp(key: type) let success = hasPermission(type: sampleType, access: permissions[index]) @@ -338,12 +338,12 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + result(false) } - + func hasPermission(type: HKObjectType, access: Int) -> Bool? { - + if #available(iOS 13.0, *) { let status = healthStore.authorizationStatus(for: type) switch access { @@ -358,7 +358,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { return nil } } - + func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let types = arguments["types"] as? [String], @@ -367,7 +367,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments!") } - + var typesToRead = Set() var typesToWrite = Set() for (index, key) in types.enumerated() { @@ -401,7 +401,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + if #available(iOS 13.0, *) { healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { (success, error) in @@ -413,7 +413,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { result(false) // Handle the error here. } } - + func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let value = (arguments["value"] as? Double), @@ -425,7 +425,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) @@ -433,9 +433,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let metadata: [String: Any] = [ HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) ] - + let sample: HKObject - + if dataTypeLookUp(key: type).isKind(of: HKCategoryType.self) { sample = HKCategorySample( type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom, @@ -446,7 +446,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata) } - + HKHealthStore().save( sample, withCompletion: { (success, error) in @@ -458,7 +458,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let frequencies = (arguments["frequencies"] as? [Double]), @@ -469,12 +469,12 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + var sensitivityPoints = [HKAudiogramSensitivityPoint]() - + for index in 0...frequencies.count - 1 { let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index]) let dbUnit = HKUnit.decibelHearingLevel() @@ -484,23 +484,23 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right) sensitivityPoints.append(sensitivityPoint) } - + let audiogram: HKAudiogramSample let metadataReceived = (arguments["metadata"] as? [String: Any]?) - + if (metadataReceived) != nil { guard let deviceName = metadataReceived?!["HKDeviceName"] as? String else { return } guard let externalUUID = metadataReceived?!["HKExternalUUID"] as? String else { return } - + audiogram = HKAudiogramSample( sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: [HKMetadataKeyDeviceName: deviceName, HKMetadataKeyExternalUUID: externalUUID]) - + } else { audiogram = HKAudiogramSample( sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: nil) } - + HKHealthStore().save( audiogram, withCompletion: { (success, error) in @@ -512,7 +512,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeBloodPressure(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let systolic = (arguments["systolic"] as? Double), @@ -530,7 +530,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let metadata = [ HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) ] - + let systolic_sample = HKQuantitySample( type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), @@ -542,7 +542,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let bpCorrelationType = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)! let bpCorrelation = Set(arrayLiteral: systolic_sample, diastolic_sample) let blood_pressure_sample = HKCorrelation(type: bpCorrelationType , start: dateFrom, end: dateTo, objects: bpCorrelation) - + HKHealthStore().save( [blood_pressure_sample], withCompletion: { (success, error) in @@ -554,7 +554,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let name = (arguments["name"] as? String?), @@ -565,14 +565,14 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let mealTypeString = mealType ?? "UNKNOWN" let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue - + var metadata = ["HKFoodMeal": mealTypeString, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] if (name != nil) { metadata[HKMetadataKeyFoodType] = "\(name!)" @@ -587,11 +587,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { type: HKSampleType.quantityType(forIdentifier: identifier)!, quantity: HKQuantity(unit: unit, doubleValue: unwrappedValue), start: dateFrom, end: dateTo, metadata: metadata) nutrition.insert(nutritionSample) } - + if #available(iOS 15.0, *){ let type = HKCorrelationType.correlationType(forIdentifier: HKCorrelationTypeIdentifier.food)! let meal = HKCorrelation(type: type, start: dateFrom, end: dateTo, objects: nutrition, metadata: metadata) - + HKHealthStore().save(meal, withCompletion: { (success, error) in if let err = error { print("Error Saving Meal Sample: \(err.localizedDescription)") @@ -615,13 +615,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let type = HKSampleType.quantityType(forIdentifier: .insulinDelivery)! let quantity = HKQuantity(unit: HKUnit.internationalUnit(), doubleValue: units) let metadata = [HKMetadataKeyInsulinDeliveryReason: reason] - + let insulin_sample = HKQuantitySample(type: type, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata) - + HKHealthStore().save(insulin_sample, withCompletion: { (success, error) in if let err = error { print("Error Saving Insulin Delivery Sample: \(err.localizedDescription)") @@ -631,7 +631,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeMenstruationFlow(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let flow = (arguments["value"] as? Int), @@ -644,7 +644,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { guard let menstrualFlowType = HKCategoryValueMenstrualFlow(rawValue: flow) else { throw PluginError(message: "Invalid Menstrual Flow Type") } - + let dateTime = Date(timeIntervalSince1970: endTime.doubleValue / 1000) let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue @@ -654,15 +654,15 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } let metadata = [HKMetadataKeyMenstrualCycleStart: isStartOfCycle, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] - + let sample = HKCategorySample( type: categoryType, - value: menstrualFlowType.rawValue, - start: dateTime, + value: menstrualFlowType.rawValue, + start: dateTime, end: dateTime, metadata: metadata ) - + HKHealthStore().save( sample, withCompletion: { (success, error) in @@ -674,7 +674,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let activityType = (arguments["activityType"] as? String), @@ -684,10 +684,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid") } - + var totalEnergyBurned: HKQuantity? var totalDistance: HKQuantity? = nil - + // Handle optional arguments if let teb = (arguments["totalEnergyBurned"] as? Double) { totalEnergyBurned = HKQuantity( @@ -697,17 +697,17 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { totalDistance = HKQuantity( unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td) } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + var workout: HKWorkout - + workout = HKWorkout( activityType: ac, start: dateFrom, end: dateTo, duration: dateTo.timeIntervalSince(dateFrom), totalEnergyBurned: totalEnergyBurned ?? nil, totalDistance: totalDistance ?? nil, metadata: nil) - + HKHealthStore().save( workout, withCompletion: { (success, error) in @@ -719,36 +719,36 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func delete(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String)! let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let dataType = dataTypeLookUp(key: dataTypeKey) - + let samplePredicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) let ownerPredicate = HKQuery.predicateForObjects(from: HKSource.default()) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - + let deleteQuery = HKSampleQuery( sampleType: dataType, predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [samplePredicate, ownerPredicate]), limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in - + guard let samplesOrNil = samplesOrNil, error == nil else { // Handle the error if necessary print("Error deleting \(dataType)") return } - + // Delete the retrieved objects from the HealthKit store HKHealthStore().delete(samplesOrNil) { (success, error) in if let err = error { @@ -759,10 +759,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + HKHealthStore().execute(deleteQuery) } - + func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String)! @@ -772,20 +772,20 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) - + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let dataType = dataTypeLookUp(key: dataTypeKey) var unit: HKUnit? if let dataUnitKey = dataUnitKey { unit = unitDict[dataUnitKey] } - + let sourceIdForCharacteristic = "com.apple.Health" let sourceNameForCharacteristic = "Health" - + switch(dataTypeKey) { case "BIRTH_DATE": let dateOfBirth = getBirthDate() @@ -829,7 +829,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { default: break } - + var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) if (!includeManualEntry) { @@ -837,13 +837,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - + let query = HKSampleQuery( sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in - + switch samplesOrNil { case let (samples as [HKQuantitySample]) as Any: let dictionaries = samples.map { sample -> NSDictionary in @@ -863,9 +863,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { result(dictionaries) } - + case var (samplesCategory as [HKCategorySample]) as Any: - + if dataTypeKey == self.SLEEP_IN_BED { samplesCategory = samplesCategory.filter { $0.value == 0 } } @@ -901,13 +901,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } let categories = samplesCategory.map { sample -> NSDictionary in var metadata: [String: Any] = [:] - + if let sampleMetadata = sample.metadata { for (key, value) in sampleMetadata { metadata[key] = value } } - + return [ "uuid": "\(sample.uuid)", "value": sample.value, @@ -922,9 +922,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { result(categories) } - + case let (samplesWorkout as [HKWorkout]) as Any: - + let dictionaries = samplesWorkout.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", @@ -945,11 +945,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 ] } - + DispatchQueue.main.async { result(dictionaries) } - + case let (samplesAudiogram as [HKAudiogramSample]) as Any: let dictionaries = samplesAudiogram.map { sample -> NSDictionary in var frequencies = [Double]() @@ -976,7 +976,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { result(dictionaries) } - + case let (nutritionSample as [HKCorrelation]) as Any: var foods: [[String: Any?]] = [] for food in nutritionSample { @@ -1010,11 +1010,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { foods.append(sampleDict) } } - + DispatchQueue.main.async { result(foods) } - + default: if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { let dictionaries = ecgSamples.map(fetchEcgMeasurements) @@ -1029,10 +1029,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + HKHealthStore().execute(query) } - + @available(iOS 14.0, *) private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary { let semaphore = DispatchSemaphore(value: 0) @@ -1066,7 +1066,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "source_name": sample.sourceRevision.source.name, ] } - + func getIntervalData(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String) ?? "DEFAULT" @@ -1076,24 +1076,24 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let intervalInSecond = (arguments?["interval"] as? Int) ?? 1 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) - + // Set interval in seconds. var interval = DateComponents() interval.second = intervalInSecond - + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startDate.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endDate.doubleValue / 1000) - + let quantityType: HKQuantityType! = dataQuantityTypesDict[dataTypeKey] var predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: []) if (!includeManualEntry) { let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } - + let query = HKStatisticsCollectionQuery(quantityType: quantityType, quantitySamplePredicate: predicate, options: [.cumulativeSum, .separateBySource], anchorDate: dateFrom, intervalComponents: interval) - + query.initialResultsHandler = { [weak self] _, statisticCollectionOrNil, error in guard let self = self else { @@ -1104,7 +1104,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + // Error detected. if let error = error { print("Query error: \(error.localizedDescription)") @@ -1113,7 +1113,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + guard let collection = statisticCollectionOrNil as? HKStatisticsCollection else { print("Unexpected result from query") DispatchQueue.main.async { @@ -1121,7 +1121,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + var dictionaries = [[String: Any]]() collection.enumerateStatistics(from: dateFrom, to: dateTo) { [weak self] statisticData, _ in @@ -1130,7 +1130,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { print("Self is nil during enumeration") return } - + do { if let quantity = statisticData.sumQuantity(), let dataUnitKey = dataUnitKey, @@ -1154,18 +1154,18 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } HKHealthStore().execute(query) } - + func getTotalStepsInInterval(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) - + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)! var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) @@ -1173,53 +1173,53 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } - + let query = HKStatisticsQuery( quantityType: sampleType, quantitySamplePredicate: predicate, options: .cumulativeSum ) { query, queryResult, error in - + guard let queryResult = queryResult else { let error = error! as NSError print("Error getting total steps in interval \(error.localizedDescription)") - + DispatchQueue.main.async { result(nil) } return } - + var steps = 0.0 - + if let quantity = queryResult.sumQuantity() { let unit = HKUnit.count() steps = quantity.doubleValue(for: unit) } - + let totalSteps = Int(steps) DispatchQueue.main.async { result(totalSteps) } } - + HKHealthStore().execute(query) } - + func unitLookUp(key: String) -> HKUnit { guard let unit = unitDict[key] else { return HKUnit.count() } return unit } - + func dataTypeLookUp(key: String) -> HKSampleType { guard let dataType_ = dataTypesDict[key] else { return HKSampleType.quantityType(forIdentifier: .bodyMass)! } return dataType_ } - + func getGender() -> HKBiologicalSex? { var bioSex:HKBiologicalSex? do { @@ -1230,7 +1230,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return bioSex } - + func getBirthDate() -> Date? { var dob:Date? do { @@ -1241,7 +1241,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return dob } - + func getBloodType() -> HKBloodType? { var bloodType:HKBloodType? do { @@ -1252,7 +1252,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return bloodType } - + func initializeTypes() { // Initialize units unitDict[GRAM] = HKUnit.gram() @@ -1301,7 +1301,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { unitDict[MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL") unitDict[UNKNOWN_UNIT] = HKUnit.init(from: "") unitDict[NO_UNIT] = HKUnit.init(from: "") - + // Initialize workout types workoutActivityTypeMap["ARCHERY"] = .archery workoutActivityTypeMap["BOWLING"] = .bowling @@ -1405,7 +1405,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[RESPIRATORY_RATE] = HKSampleType.quantityType(forIdentifier: .respiratoryRate)! dataTypesDict[PERIPHERAL_PERFUSION_INDEX] = HKSampleType.quantityType( forIdentifier: .peripheralPerfusionIndex)! - + dataTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKSampleType.quantityType( forIdentifier: .bloodPressureDiastolic)! dataTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKSampleType.quantityType( @@ -1414,7 +1414,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .bodyFatPercentage)! dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)! dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)! - + // Nutrition dataTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)! dataTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)! @@ -1455,7 +1455,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)! dataTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)! dataTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)! - + dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType( forIdentifier: .electrodermalActivity)! dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType( @@ -1486,21 +1486,21 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! dataTypesDict[MENSTRUATION_FLOW] = HKSampleType.categoryType(forIdentifier: .menstrualFlow)! - - + + dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)! dataTypesDict[WORKOUT] = HKSampleType.workoutType() dataTypesDict[NUTRITION] = HKSampleType.correlationType( forIdentifier: .food)! - + healthDataTypes = Array(dataTypesDict.values) - + characteristicsTypesDict[BIRTH_DATE] = HKObjectType.characteristicType(forIdentifier: .dateOfBirth)! characteristicsTypesDict[GENDER] = HKObjectType.characteristicType(forIdentifier: .biologicalSex)! characteristicsTypesDict[BLOOD_TYPE] = HKObjectType.characteristicType(forIdentifier: .bloodType)! characteristicsDataTypes = Array(characteristicsTypesDict.values) } - + // Set up iOS 11 specific types (ordinary health data quantity types) if #available(iOS 11.0, *) { dataQuantityTypesDict[ACTIVE_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)! @@ -1512,7 +1512,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)! dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)! dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)! - + // Nutrition dataQuantityTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)! dataQuantityTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)! @@ -1553,7 +1553,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)! dataQuantityTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)! dataQuantityTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)! - + dataQuantityTypesDict[ELECTRODERMAL_ACTIVITY] = HKQuantityType.quantityType(forIdentifier: .electrodermalActivity)! dataQuantityTypesDict[FORCED_EXPIRATORY_VOLUME] = HKQuantityType.quantityType(forIdentifier: .forcedExpiratoryVolume1)! dataQuantityTypesDict[HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .heartRate)! @@ -1568,10 +1568,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[DISTANCE_SWIMMING] = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)! dataQuantityTypesDict[DISTANCE_CYCLING] = HKQuantityType.quantityType(forIdentifier: .distanceCycling)! dataQuantityTypesDict[FLIGHTS_CLIMBED] = HKQuantityType.quantityType(forIdentifier: .flightsClimbed)! - + healthDataQuantityTypes = Array(dataQuantityTypesDict.values) } - + // Set up heart rate data types specific to the apple watch, requires iOS 12 if #available(iOS 12.2, *) { dataTypesDict[HIGH_HEART_RATE_EVENT] = HKSampleType.categoryType( @@ -1580,32 +1580,32 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .lowHeartRateEvent)! dataTypesDict[IRREGULAR_HEART_RATE_EVENT] = HKSampleType.categoryType( forIdentifier: .irregularHeartRhythmEvent)! - + heartRateEventTypes = Set([ HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!, HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!, HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!, ]) } - + if #available(iOS 13.6, *) { dataTypesDict[HEADACHE_UNSPECIFIED] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_NOT_PRESENT] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_MILD] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_MODERATE] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_SEVERE] = HKSampleType.categoryType(forIdentifier: .headache)! - + headacheType = Set([ HKSampleType.categoryType(forIdentifier: .headache)! ]) } - + if #available(iOS 14.0, *) { dataTypesDict[ELECTROCARDIOGRAM] = HKSampleType.electrocardiogramType() - + unitDict[VOLT] = HKUnit.volt() unitDict[INCHES_OF_MERCURY] = HKUnit.inchesOfMercury() - + workoutActivityTypeMap["CARDIO_DANCE"] = HKWorkoutActivityType.cardioDance workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball @@ -1614,13 +1614,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { if #available(iOS 16.0, *) { dataTypesDict[ATRIAL_FIBRILLATION_BURDEN] = HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)! - } - + } + // Concatenate heart events, headache and health data types (both may be empty) allDataTypes = Set(heartRateEventTypes + healthDataTypes) allDataTypes = allDataTypes.union(headacheType) } - + func getWorkoutType(type: HKWorkoutActivityType) -> String { switch type { case .americanFootball: From 289646bfb51771474dd2ae62154783f0310ef8a4 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:51:21 +0100 Subject: [PATCH 19/36] Update CHANGELOG for version 12.0.0 --- packages/health/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index d1e668fd7..3ebf9db01 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,8 @@ +## 12.0.0 + +* Fix of [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) +* Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) + ## 11.1.1 * Fix of [#1059](https://github.com/cph-cachet/flutter-plugins/issues/1059) From dcdba9e72632873332d572b6db507516097f8fcb Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 4 Jan 2025 22:01:42 +0100 Subject: [PATCH 20/36] Update dependencies: intl to ^0.20.1, device_info_plus to ^11.2.0, and permission_handler to ^11.3.1 --- packages/health/CHANGELOG.md | 3 +++ packages/health/example/pubspec.yaml | 2 +- packages/health/pubspec.yaml | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 3ebf9db01..9d1a0c4e7 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -2,6 +2,9 @@ * Fix of [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) * Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) +* Updated `intl` to ^0.20.1 - Closes [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) +* Updated `device_info_plus` to ^11.2.0 +* Example app: Updated `permission_handler` to ^11.3.1 ## 11.1.1 diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml index d472b3f03..031bcc6c2 100644 --- a/packages/health/example/pubspec.yaml +++ b/packages/health/example/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 - permission_handler: ^10.2.0 + permission_handler: ^11.3.1 carp_serializable: ^2.0.0 # polymorphic json serialization health: path: ../ diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index a97aeaa6f..30318d95d 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -10,8 +10,8 @@ environment: dependencies: flutter: sdk: flutter - intl: '>=0.18.0 <0.20.0' - device_info_plus: '>=9.0.0 <11.0.0' + intl: ^0.20.1 + device_info_plus: ^11.2.0 json_annotation: ^4.8.0 carp_serializable: ^2.0.0 # polymorphic json serialization From dd5d5f905481b77a3899000520d977b763530423 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 5 Jan 2025 03:41:18 +0100 Subject: [PATCH 21/36] Fix null handling for record name in HealthPlugin --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 5c261f90f..94c0fce99 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -1516,7 +1516,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "sugar" to record.sugar?.inGrams, "water" to null, "zinc" to record.zinc?.inGrams, - "name" to record.name, + "name" to (record.name ?: ""), "meal_type" to (mapTypeToMealType[ record.mealType] From 7a98254dbedcbe9237308556bfd9ef69f84bc4b3 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 5 Jan 2025 03:48:13 +0100 Subject: [PATCH 22/36] Update CHANGELOG.md to include fixes for issues #950 and #1104 --- packages/health/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 9d1a0c4e7..1917bf296 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -2,6 +2,7 @@ * Fix of [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) * Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) +* Fix of [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) * Updated `intl` to ^0.20.1 - Closes [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) * Updated `device_info_plus` to ^11.2.0 * Example app: Updated `permission_handler` to ^11.3.1 From f9e86e88d1168f56aaf896b76f7e98a3a404c45d Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:36:42 +0100 Subject: [PATCH 23/36] Add lean body mass data type and permissions; update example app --- packages/health/CHANGELOG.md | 2 +- .../android/app/src/main/AndroidManifest.xml | 2 ++ packages/health/example/lib/main.dart | 16 +++++++++++++++- packages/health/example/lib/util.dart | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 96adb741a..8164c7d4d 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,6 +1,6 @@ ## 12.0.0 -* Add lean mass data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) +* Add lean mass data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) - PR [#1097](https://github.com/cph-cachet/flutter-plugins/pull/1097) * Fix of [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) * Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) * Fix of [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml index f6d185ace..996adaf97 100644 --- a/packages/health/example/android/app/src/main/AndroidManifest.xml +++ b/packages/health/example/android/app/src/main/AndroidManifest.xml @@ -65,6 +65,8 @@ + + diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index b77eae30e..00ac1123a 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -178,7 +178,15 @@ class _HealthAppState extends State { _healthDataList = Health().removeDuplicates(_healthDataList); for (var data in _healthDataList) { - debugPrint(toJsonString(data)); + try { + // print the data points to the console + debugPrint(toJsonString(data)); + } catch (error) { + // FIXME: Getting json failed since its instance of 'HealthDataPoint' + debugPrint("Exception in printDataPoint: $error"); + // raise the error to stop the app from crashing + rethrow; + } } // update the UI to display the results @@ -285,6 +293,12 @@ class _HealthAppState extends State { type: HealthDataType.SLEEP_DEEP, startTime: earlier, endTime: now); + success &= await Health().writeHealthData( + value: 22, + type: HealthDataType.LEAN_BODY_MASS, + startTime: earlier, + endTime: now, + ); // specialized write methods success &= await Health().writeBloodOxygen( diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index e1c49101d..da7e28089 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -44,6 +44,7 @@ const List dataTypesIOS = [ HealthDataType.HEADACHE_MODERATE, HealthDataType.HEADACHE_SEVERE, HealthDataType.HEADACHE_UNSPECIFIED, + HealthDataType.LEAN_BODY_MASS, // note that a phone cannot write these ECG-based types - only read them // HealthDataType.ELECTROCARDIOGRAM, @@ -76,6 +77,7 @@ const List dataTypesAndroid = [ HealthDataType.BODY_FAT_PERCENTAGE, HealthDataType.HEIGHT, HealthDataType.WEIGHT, + HealthDataType.LEAN_BODY_MASS, // HealthDataType.BODY_MASS_INDEX, HealthDataType.BODY_TEMPERATURE, HealthDataType.HEART_RATE, From b6a9a4f3dfafdf61a7e25a2d79b8b9b0e47ef9c8 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:48:30 +0100 Subject: [PATCH 24/36] Update CHANGELOG.md with AndroidManifest permissions for LEAN_BODY_MASS --- packages/health/CHANGELOG.md | 5 +++ .../ios/Runner.xcodeproj/project.pbxproj | 34 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 8164c7d4d..db6c8fbfa 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,6 +1,11 @@ ## 12.0.0 * Add lean mass data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) - PR [#1097](https://github.com/cph-cachet/flutter-plugins/pull/1097) + * The following AndroidManifest values are required to READ/WRITE `LEAN_BODY_MASS`: + ```XML + + + ``` * Fix of [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) * Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) * Fix of [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index 4ce55d34f..794fdbba4 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 8AB8966E9F27B6C816D51EA9 /* [CP] Embed Pods Frameworks */, + FEE6262278064D703A8D8D21 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -293,6 +294,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + FEE6262278064D703A8D8D21 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -394,7 +413,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -531,7 +553,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -564,7 +589,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", From 54b8fbff949e40b15b2cfdd0119844d1b957e8b9 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:56:44 +0100 Subject: [PATCH 25/36] Add support for WaterTemperature and UnderwaterDepth data types and UnderwaterDiving workout in iOS --- packages/health/CHANGELOG.md | 2 + packages/health/README.md | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 34 +- packages/health/example/lib/main.dart | 19 + packages/health/example/lib/util.dart | 2 + .../ios/Classes/SwiftHealthPlugin.swift | 14 +- packages/health/ios/health.podspec | 4 +- packages/health/lib/health.g.dart | 343 +++++++----------- packages/health/lib/src/health_plugin.dart | 1 + packages/health/lib/src/heath_data_types.dart | 7 + 10 files changed, 219 insertions(+), 211 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 1917bf296..c07bafbf5 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -6,6 +6,8 @@ * Updated `intl` to ^0.20.1 - Closes [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) * Updated `device_info_plus` to ^11.2.0 * Example app: Updated `permission_handler` to ^11.3.1 +* iOS: Add `WATER_TEMPERATURE` and `UNDERWATER_DEPTH` health values - Closes [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) +* iOS: Add support for `Underwater Diving` workout - Closes [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) ## 11.1.1 diff --git a/packages/health/README.md b/packages/health/README.md index 68ec291bc..e10b90ab5 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -355,6 +355,9 @@ The plugin supports the following [`HealthDataType`](https://pub.dev/documentati | ELECTROCARDIOGRAM | VOLT | yes | | Requires Apple Watch to write the data | | NUTRITION | NO_UNIT | yes | yes | | | INSULIN_DELIVERY | INTERNATIONAL_UNIT | yes | | | +| MENSTRUATION_FLOW | NO_UNIT | yes | yes | | +| WATER_TEMPERATURE | DEGREE_CELSIUS | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout | +| UNDERWATER_DEPTH | METER | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout | ## Workout Types @@ -443,6 +446,7 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | TENNIS | yes | yes | | | TRACK_AND_FIELD | yes | | | | TRADITIONAL_STRENGTH_TRAINING | yes | (yes) | on Android this will be stored as STRENGTH_TRAINING | +| UNDERWATER_DIVING | yes | | | | VOLLEYBALL | yes | yes | | | WALKING | yes | yes | | | WATER_FITNESS | yes | | | diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index 4ce55d34f..a64ef83b6 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 8AB8966E9F27B6C816D51EA9 /* [CP] Embed Pods Frameworks */, + 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -271,6 +272,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; AFF7CCF5217A091E1625CD54 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -394,7 +413,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -531,7 +553,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -564,7 +589,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index b77eae30e..e02c1e037 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -376,6 +376,25 @@ class _HealthAppState extends State { endTime: now, ); + // Available on iOS 16.0+ only + if (Platform.isIOS) { + success &= await Health().writeHealthData( + value: 22, + type: HealthDataType.WATER_TEMPERATURE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual + ); + + success &= await Health().writeHealthData( + value: 55, + type: HealthDataType.UNDERWATER_DEPTH, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual + ); + } + setState(() { _state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED; }); diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index e1c49101d..24168041f 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -59,6 +59,8 @@ const List dataTypesIOS = [ HealthDataType.BLOOD_TYPE, HealthDataType.BIRTH_DATE, HealthDataType.MENSTRUATION_FLOW, + HealthDataType.WATER_TEMPERATURE, + HealthDataType.UNDERWATER_DEPTH, ]; /// List of data types available on Android. diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index a0bb9d4fd..ebb1f5d40 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -161,6 +161,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let GENDER = "GENDER" let BLOOD_TYPE = "BLOOD_TYPE" let MENSTRUATION_FLOW = "MENSTRUATION_FLOW" + let WATER_TEMPERATURE = "WATER_TEMPERATURE" + let UNDERWATER_DEPTH = "UNDERWATER_DEPTH" // Health Unit types @@ -857,7 +859,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, - "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil + "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil, + "dataUnitKey": unit?.unitString ] } DispatchQueue.main.async { @@ -1384,6 +1387,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["TAI_CHI"] = .taiChi workoutActivityTypeMap["WRESTLING"] = .wrestling workoutActivityTypeMap["OTHER"] = .other + if #available(iOS 17.0, *) { + workoutActivityTypeMap["UNDERWATER_DIVING"] = .underwaterDiving + } + nutritionList = [ DIETARY_ENERGY_CONSUMED, DIETARY_CARBS_CONSUMED, DIETARY_PROTEIN_CONSUMED, DIETARY_FATS_CONSUMED, DIETARY_CAFFEINE, DIETARY_FIBER, DIETARY_SUGAR, @@ -1617,6 +1624,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { if #available(iOS 16.0, *) { dataTypesDict[ATRIAL_FIBRILLATION_BURDEN] = HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)! + + dataTypesDict[WATER_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .waterTemperature)! + dataTypesDict[UNDERWATER_DEPTH] = HKQuantityType.quantityType(forIdentifier: .underwaterDepth)! } // Concatenate heart events, headache and health data types (both may be empty) @@ -1774,6 +1784,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { return "mixedCardio" case .handCycling: return "handCycling" + case .underwaterDiving: + return "underwaterDiving" default: return "other" } diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec index 3a8d89370..96254602b 100644 --- a/packages/health/ios/health.podspec +++ b/packages/health/ios/health.podspec @@ -3,14 +3,14 @@ # Pod::Spec.new do |s| s.name = 'health' - s.version = '1.0.4' + s.version = '12.0.0' s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.' s.description = <<-DESC Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. DESC s.homepage = 'https://pub.dev/packages/health' s.license = { :file => '../LICENSE' } - s.author = { 'Copenhagen Center for Health Technology' => 'cph.cachet@gmail.com' } + s.author = { 'Copenhagen Research Platform at DTU' => 'support@carp.dk' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index fa4423446..c61f0d2e5 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -29,31 +29,23 @@ HealthDataPoint _$HealthDataPointFromJson(Map json) => metadata: json['metadata'] as Map?, ); -Map _$HealthDataPointToJson(HealthDataPoint instance) { - final val = { - 'uuid': instance.uuid, - 'value': instance.value.toJson(), - 'type': _$HealthDataTypeEnumMap[instance.type]!, - 'unit': _$HealthDataUnitEnumMap[instance.unit]!, - 'dateFrom': instance.dateFrom.toIso8601String(), - 'dateTo': instance.dateTo.toIso8601String(), - 'sourcePlatform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, - 'sourceDeviceId': instance.sourceDeviceId, - 'sourceId': instance.sourceId, - 'sourceName': instance.sourceName, - 'recordingMethod': _$RecordingMethodEnumMap[instance.recordingMethod]!, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('workoutSummary', instance.workoutSummary?.toJson()); - writeNotNull('metadata', instance.metadata); - return val; -} +Map _$HealthDataPointToJson(HealthDataPoint instance) => + { + 'uuid': instance.uuid, + 'value': instance.value.toJson(), + 'type': _$HealthDataTypeEnumMap[instance.type]!, + 'unit': _$HealthDataUnitEnumMap[instance.unit]!, + 'dateFrom': instance.dateFrom.toIso8601String(), + 'dateTo': instance.dateTo.toIso8601String(), + 'sourcePlatform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, + 'sourceDeviceId': instance.sourceDeviceId, + 'sourceId': instance.sourceId, + 'sourceName': instance.sourceName, + 'recordingMethod': _$RecordingMethodEnumMap[instance.recordingMethod]!, + if (instance.workoutSummary?.toJson() case final value?) + 'workoutSummary': value, + if (instance.metadata case final value?) 'metadata': value, + }; const _$HealthDataTypeEnumMap = { HealthDataType.ACTIVE_ENERGY_BURNED: 'ACTIVE_ENERGY_BURNED', @@ -148,6 +140,8 @@ const _$HealthDataTypeEnumMap = { HealthDataType.BIRTH_DATE: 'BIRTH_DATE', HealthDataType.BLOOD_TYPE: 'BLOOD_TYPE', HealthDataType.MENSTRUATION_FLOW: 'MENSTRUATION_FLOW', + HealthDataType.WATER_TEMPERATURE: 'WATER_TEMPERATURE', + HealthDataType.UNDERWATER_DEPTH: 'UNDERWATER_DEPTH', HealthDataType.HIGH_HEART_RATE_EVENT: 'HIGH_HEART_RATE_EVENT', HealthDataType.LOW_HEART_RATE_EVENT: 'LOW_HEART_RATE_EVENT', HealthDataType.IRREGULAR_HEART_RATE_EVENT: 'IRREGULAR_HEART_RATE_EVENT', @@ -223,37 +217,21 @@ const _$RecordingMethodEnumMap = { HealthValue _$HealthValueFromJson(Map json) => HealthValue()..$type = json['__type'] as String?; -Map _$HealthValueToJson(HealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - return val; -} +Map _$HealthValueToJson(HealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + }; NumericHealthValue _$NumericHealthValueFromJson(Map json) => NumericHealthValue( numericValue: json['numericValue'] as num, )..$type = json['__type'] as String?; -Map _$NumericHealthValueToJson(NumericHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['numericValue'] = instance.numericValue; - return val; -} +Map _$NumericHealthValueToJson(NumericHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'numericValue': instance.numericValue, + }; AudiogramHealthValue _$AudiogramHealthValueFromJson( Map json) => @@ -269,21 +247,13 @@ AudiogramHealthValue _$AudiogramHealthValueFromJson( )..$type = json['__type'] as String?; Map _$AudiogramHealthValueToJson( - AudiogramHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['frequencies'] = instance.frequencies; - val['leftEarSensitivities'] = instance.leftEarSensitivities; - val['rightEarSensitivities'] = instance.rightEarSensitivities; - return val; -} + AudiogramHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'frequencies': instance.frequencies, + 'leftEarSensitivities': instance.leftEarSensitivities, + 'rightEarSensitivities': instance.rightEarSensitivities, + }; WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) => WorkoutHealthValue( @@ -300,29 +270,23 @@ WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) => $enumDecodeNullable(_$HealthDataUnitEnumMap, json['totalStepsUnit']), )..$type = json['__type'] as String?; -Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['workoutActivityType'] = - _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!; - writeNotNull('totalEnergyBurned', instance.totalEnergyBurned); - writeNotNull('totalEnergyBurnedUnit', - _$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit]); - writeNotNull('totalDistance', instance.totalDistance); - writeNotNull( - 'totalDistanceUnit', _$HealthDataUnitEnumMap[instance.totalDistanceUnit]); - writeNotNull('totalSteps', instance.totalSteps); - writeNotNull( - 'totalStepsUnit', _$HealthDataUnitEnumMap[instance.totalStepsUnit]); - return val; -} +Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'workoutActivityType': + _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!, + if (instance.totalEnergyBurned case final value?) + 'totalEnergyBurned': value, + if (_$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit] + case final value?) + 'totalEnergyBurnedUnit': value, + if (instance.totalDistance case final value?) 'totalDistance': value, + if (_$HealthDataUnitEnumMap[instance.totalDistanceUnit] case final value?) + 'totalDistanceUnit': value, + if (instance.totalSteps case final value?) 'totalSteps': value, + if (_$HealthDataUnitEnumMap[instance.totalStepsUnit] case final value?) + 'totalStepsUnit': value, + }; const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', @@ -406,6 +370,7 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', HealthWorkoutActivityType.WRESTLING: 'WRESTLING', + HealthWorkoutActivityType.UNDERWATER_DIVING: 'UNDERWATER_DIVING', HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', HealthWorkoutActivityType.DANCING: 'DANCING', @@ -443,23 +408,18 @@ ElectrocardiogramHealthValue _$ElectrocardiogramHealthValueFromJson( )..$type = json['__type'] as String?; Map _$ElectrocardiogramHealthValueToJson( - ElectrocardiogramHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['voltageValues'] = instance.voltageValues.map((e) => e.toJson()).toList(); - writeNotNull('averageHeartRate', instance.averageHeartRate); - writeNotNull('samplingFrequency', instance.samplingFrequency); - writeNotNull('classification', - _$ElectrocardiogramClassificationEnumMap[instance.classification]); - return val; -} + ElectrocardiogramHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'voltageValues': instance.voltageValues.map((e) => e.toJson()).toList(), + if (instance.averageHeartRate case final value?) + 'averageHeartRate': value, + if (instance.samplingFrequency case final value?) + 'samplingFrequency': value, + if (_$ElectrocardiogramClassificationEnumMap[instance.classification] + case final value?) + 'classification': value, + }; const _$ElectrocardiogramClassificationEnumMap = { ElectrocardiogramClassification.NOT_SET: 'NOT_SET', @@ -483,20 +443,12 @@ ElectrocardiogramVoltageValue _$ElectrocardiogramVoltageValueFromJson( )..$type = json['__type'] as String?; Map _$ElectrocardiogramVoltageValueToJson( - ElectrocardiogramVoltageValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['voltage'] = instance.voltage; - val['timeSinceSampleStart'] = instance.timeSinceSampleStart; - return val; -} + ElectrocardiogramVoltageValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'voltage': instance.voltage, + 'timeSinceSampleStart': instance.timeSinceSampleStart, + }; InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson( Map json) => @@ -506,20 +458,12 @@ InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson( )..$type = json['__type'] as String?; Map _$InsulinDeliveryHealthValueToJson( - InsulinDeliveryHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['units'] = instance.units; - val['reason'] = _$InsulinDeliveryReasonEnumMap[instance.reason]!; - return val; -} + InsulinDeliveryHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'units': instance.units, + 'reason': _$InsulinDeliveryReasonEnumMap[instance.reason]!, + }; const _$InsulinDeliveryReasonEnumMap = { InsulinDeliveryReason.NOT_SET: 'NOT_SET', @@ -577,62 +521,58 @@ NutritionHealthValue _$NutritionHealthValueFromJson( )..$type = json['__type'] as String?; Map _$NutritionHealthValueToJson( - NutritionHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - writeNotNull('name', instance.name); - writeNotNull('mealType', instance.mealType); - writeNotNull('calories', instance.calories); - writeNotNull('protein', instance.protein); - writeNotNull('fat', instance.fat); - writeNotNull('carbs', instance.carbs); - writeNotNull('caffeine', instance.caffeine); - writeNotNull('vitaminA', instance.vitaminA); - writeNotNull('b1Thiamine', instance.b1Thiamine); - writeNotNull('b2Riboflavin', instance.b2Riboflavin); - writeNotNull('b3Niacin', instance.b3Niacin); - writeNotNull('b5PantothenicAcid', instance.b5PantothenicAcid); - writeNotNull('b6Pyridoxine', instance.b6Pyridoxine); - writeNotNull('b7Biotin', instance.b7Biotin); - writeNotNull('b9Folate', instance.b9Folate); - writeNotNull('b12Cobalamin', instance.b12Cobalamin); - writeNotNull('vitaminC', instance.vitaminC); - writeNotNull('vitaminD', instance.vitaminD); - writeNotNull('vitaminE', instance.vitaminE); - writeNotNull('vitaminK', instance.vitaminK); - writeNotNull('calcium', instance.calcium); - writeNotNull('chloride', instance.chloride); - writeNotNull('cholesterol', instance.cholesterol); - writeNotNull('choline', instance.choline); - writeNotNull('chromium', instance.chromium); - writeNotNull('copper', instance.copper); - writeNotNull('fatUnsaturated', instance.fatUnsaturated); - writeNotNull('fatMonounsaturated', instance.fatMonounsaturated); - writeNotNull('fatPolyunsaturated', instance.fatPolyunsaturated); - writeNotNull('fatSaturated', instance.fatSaturated); - writeNotNull('fatTransMonoenoic', instance.fatTransMonoenoic); - writeNotNull('fiber', instance.fiber); - writeNotNull('iodine', instance.iodine); - writeNotNull('iron', instance.iron); - writeNotNull('magnesium', instance.magnesium); - writeNotNull('manganese', instance.manganese); - writeNotNull('molybdenum', instance.molybdenum); - writeNotNull('phosphorus', instance.phosphorus); - writeNotNull('potassium', instance.potassium); - writeNotNull('selenium', instance.selenium); - writeNotNull('sodium', instance.sodium); - writeNotNull('sugar', instance.sugar); - writeNotNull('water', instance.water); - writeNotNull('zinc', instance.zinc); - return val; -} + NutritionHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + if (instance.name case final value?) 'name': value, + if (instance.mealType case final value?) 'mealType': value, + if (instance.calories case final value?) 'calories': value, + if (instance.protein case final value?) 'protein': value, + if (instance.fat case final value?) 'fat': value, + if (instance.carbs case final value?) 'carbs': value, + if (instance.caffeine case final value?) 'caffeine': value, + if (instance.vitaminA case final value?) 'vitaminA': value, + if (instance.b1Thiamine case final value?) 'b1Thiamine': value, + if (instance.b2Riboflavin case final value?) 'b2Riboflavin': value, + if (instance.b3Niacin case final value?) 'b3Niacin': value, + if (instance.b5PantothenicAcid case final value?) + 'b5PantothenicAcid': value, + if (instance.b6Pyridoxine case final value?) 'b6Pyridoxine': value, + if (instance.b7Biotin case final value?) 'b7Biotin': value, + if (instance.b9Folate case final value?) 'b9Folate': value, + if (instance.b12Cobalamin case final value?) 'b12Cobalamin': value, + if (instance.vitaminC case final value?) 'vitaminC': value, + if (instance.vitaminD case final value?) 'vitaminD': value, + if (instance.vitaminE case final value?) 'vitaminE': value, + if (instance.vitaminK case final value?) 'vitaminK': value, + if (instance.calcium case final value?) 'calcium': value, + if (instance.chloride case final value?) 'chloride': value, + if (instance.cholesterol case final value?) 'cholesterol': value, + if (instance.choline case final value?) 'choline': value, + if (instance.chromium case final value?) 'chromium': value, + if (instance.copper case final value?) 'copper': value, + if (instance.fatUnsaturated case final value?) 'fatUnsaturated': value, + if (instance.fatMonounsaturated case final value?) + 'fatMonounsaturated': value, + if (instance.fatPolyunsaturated case final value?) + 'fatPolyunsaturated': value, + if (instance.fatSaturated case final value?) 'fatSaturated': value, + if (instance.fatTransMonoenoic case final value?) + 'fatTransMonoenoic': value, + if (instance.fiber case final value?) 'fiber': value, + if (instance.iodine case final value?) 'iodine': value, + if (instance.iron case final value?) 'iron': value, + if (instance.magnesium case final value?) 'magnesium': value, + if (instance.manganese case final value?) 'manganese': value, + if (instance.molybdenum case final value?) 'molybdenum': value, + if (instance.phosphorus case final value?) 'phosphorus': value, + if (instance.potassium case final value?) 'potassium': value, + if (instance.selenium case final value?) 'selenium': value, + if (instance.sodium case final value?) 'sodium': value, + if (instance.sugar case final value?) 'sugar': value, + if (instance.water case final value?) 'water': value, + if (instance.zinc case final value?) 'zinc': value, + }; MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson( Map json) => @@ -644,22 +584,15 @@ MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson( )..$type = json['__type'] as String?; Map _$MenstruationFlowHealthValueToJson( - MenstruationFlowHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - writeNotNull('flow', _$MenstrualFlowEnumMap[instance.flow]); - writeNotNull('isStartOfCycle', instance.isStartOfCycle); - writeNotNull('wasUserEntered', instance.wasUserEntered); - val['dateTime'] = instance.dateTime.toIso8601String(); - return val; -} + MenstruationFlowHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + if (_$MenstrualFlowEnumMap[instance.flow] case final value?) + 'flow': value, + if (instance.isStartOfCycle case final value?) 'isStartOfCycle': value, + if (instance.wasUserEntered case final value?) 'wasUserEntered': value, + 'dateTime': instance.dateTime.toIso8601String(), + }; const _$MenstrualFlowEnumMap = { MenstrualFlow.unspecified: 'unspecified', diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 8868a519b..6f513b0ac 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1257,6 +1257,7 @@ class Health { HealthWorkoutActivityType.YOGA, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, + HealthWorkoutActivityType.UNDERWATER_DIVING, }.contains(type); } diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index f50762721..878bbc00f 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -95,6 +95,8 @@ enum HealthDataType { BIRTH_DATE, BLOOD_TYPE, MENSTRUATION_FLOW, + WATER_TEMPERATURE, + UNDERWATER_DEPTH, // Heart Rate events (specific to Apple Watch) HIGH_HEART_RATE_EVENT, @@ -206,6 +208,8 @@ const List dataTypeKeysIOS = [ HealthDataType.BIRTH_DATE, HealthDataType.BLOOD_TYPE, HealthDataType.MENSTRUATION_FLOW, + HealthDataType.WATER_TEMPERATURE, + HealthDataType.UNDERWATER_DEPTH, ]; /// List of data types available on Android @@ -352,6 +356,8 @@ const Map dataTypeToUnit = { HealthDataType.NUTRITION: HealthDataUnit.NO_UNIT, HealthDataType.MENSTRUATION_FLOW: HealthDataUnit.NO_UNIT, + HealthDataType.WATER_TEMPERATURE: HealthDataUnit.DEGREE_CELSIUS, + HealthDataType.UNDERWATER_DEPTH: HealthDataUnit.METER, // Health Connect HealthDataType.TOTAL_CALORIES_BURNED: HealthDataUnit.KILOCALORIE, @@ -527,6 +533,7 @@ enum HealthWorkoutActivityType { WHEELCHAIR_RUN_PACE, WHEELCHAIR_WALK_PACE, WRESTLING, + UNDERWATER_DIVING, // Android only BIKING_STATIONARY, From ecd36d41c9f3d270b94ca82077ca9234a3da6c11 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:42:13 +0100 Subject: [PATCH 26/36] Update build for LEAN_BODY_MASS data type --- packages/health/CHANGELOG.md | 12 ++++++------ packages/health/README.md | 1 + packages/health/example/lib/main.dart | 10 +--------- packages/health/lib/health.g.dart | 1 + 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 6dd9d146b..093d10da9 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,19 +1,19 @@ ## 12.0.0 -* Add lean mass data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) - PR [#1097](https://github.com/cph-cachet/flutter-plugins/pull/1097) +* Add `LEAN_BODY_MASS` data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) - PR [#1097](https://github.com/cph-cachet/flutter-plugins/pull/1097) * The following AndroidManifest values are required to READ/WRITE `LEAN_BODY_MASS`: ```XML ``` -* Fix of [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) +* iOS: Add `WATER_TEMPERATURE` and `UNDERWATER_DEPTH` health values [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) +* iOS: Add support for `Underwater Diving` workout [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) +* Fix [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) * Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) -* Fix of [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) -* Updated `intl` to ^0.20.1 - Closes [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) +* Fix [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) +* Updated `intl` to ^0.20.1 [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) * Updated `device_info_plus` to ^11.2.0 * Example app: Updated `permission_handler` to ^11.3.1 -* iOS: Add `WATER_TEMPERATURE` and `UNDERWATER_DEPTH` health values - Closes [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) -* iOS: Add support for `Underwater Diving` workout - Closes [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) ## 11.1.1 diff --git a/packages/health/README.md b/packages/health/README.md index e10b90ab5..eea60737c 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -358,6 +358,7 @@ The plugin supports the following [`HealthDataType`](https://pub.dev/documentati | MENSTRUATION_FLOW | NO_UNIT | yes | yes | | | WATER_TEMPERATURE | DEGREE_CELSIUS | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout | | UNDERWATER_DEPTH | METER | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout | +| LEAN_BODY_MASS | KILOGRAMS | yes | yes | | ## Workout Types diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 95b964061..e86985d6c 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -178,15 +178,7 @@ class _HealthAppState extends State { _healthDataList = Health().removeDuplicates(_healthDataList); for (var data in _healthDataList) { - try { - // print the data points to the console - debugPrint(toJsonString(data)); - } catch (error) { - // FIXME: Getting json failed since its instance of 'HealthDataPoint' - debugPrint("Exception in printDataPoint: $error"); - // raise the error to stop the app from crashing - rethrow; - } + debugPrint(toJsonString(data)); } // update the UI to display the results diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index c61f0d2e5..70feb2de1 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -57,6 +57,7 @@ const _$HealthDataTypeEnumMap = { HealthDataType.BLOOD_PRESSURE_DIASTOLIC: 'BLOOD_PRESSURE_DIASTOLIC', HealthDataType.BLOOD_PRESSURE_SYSTOLIC: 'BLOOD_PRESSURE_SYSTOLIC', HealthDataType.BODY_FAT_PERCENTAGE: 'BODY_FAT_PERCENTAGE', + HealthDataType.LEAN_BODY_MASS: 'LEAN_BODY_MASS', HealthDataType.BODY_MASS_INDEX: 'BODY_MASS_INDEX', HealthDataType.BODY_TEMPERATURE: 'BODY_TEMPERATURE', HealthDataType.BODY_WATER_MASS: 'BODY_WATER_MASS', From a1ee70e854c690db56a10fe4b1250ecc5252db53 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:06:23 +0100 Subject: [PATCH 27/36] Update CHANGELOG.md to include fixes for issues #1047 and #939 --- packages/health/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 093d10da9..27cfdc1d6 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -11,6 +11,7 @@ * Fix [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) * Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) * Fix [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) +* Fix [#1047](https://github.com/cph-cachet/flutter-plugins/issues/1047) and [#939](https://github.com/cph-cachet/flutter-plugins/issues/939) - PR [#1091](https://github.com/cph-cachet/flutter-plugins/pull/1091) * Updated `intl` to ^0.20.1 [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) * Updated `device_info_plus` to ^11.2.0 * Example app: Updated `permission_handler` to ^11.3.1 From 80af770feb0a7de676d0b2007e316366f3a629cc Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:38:55 +0100 Subject: [PATCH 28/36] chore: update CHANGELOG.md for breaking change in permission requests for WORKOUT data type --- packages/health/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 27cfdc1d6..80d850a8a 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,5 +1,8 @@ ## 12.0.0 +* **BREAKING** (Android) Remove automatic permission request of `DISTANCE_DELTA` and `TOTAL_CALORIES_BURNED` data types when requesting permission for `WORKOUT` health data type. + * For `WORKOUT`s that require above permissions, now those need to be requested manually. + * Fix [#984](https://github.com/cph-cachet/flutter-plugins/issues/984) - PR [#1055](https://github.com/cph-cachet/flutter-plugins/pull/1055) * Add `LEAN_BODY_MASS` data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) - PR [#1097](https://github.com/cph-cachet/flutter-plugins/pull/1097) * The following AndroidManifest values are required to READ/WRITE `LEAN_BODY_MASS`: ```XML From 86a8fac377b374b09ac62004984a440aee59e355 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:49:45 +0100 Subject: [PATCH 29/36] chore: update CHANGELOG.md to include fix for SLEEP_LIGHT type alignment issue --- packages/health/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 80d850a8a..e6efc7c4e 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -15,6 +15,7 @@ * Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) * Fix [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) * Fix [#1047](https://github.com/cph-cachet/flutter-plugins/issues/1047) and [#939](https://github.com/cph-cachet/flutter-plugins/issues/939) - PR [#1091](https://github.com/cph-cachet/flutter-plugins/pull/1091) +* Fix issue where `SLEEP_LIGHT` type was not aligned correctly - PR [#1086](https://github.com/cph-cachet/flutter-plugins/pull/1086) * Updated `intl` to ^0.20.1 [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) * Updated `device_info_plus` to ^11.2.0 * Example app: Updated `permission_handler` to ^11.3.1 From 35cd46c75b582881f0500d78450c6b4d098757e2 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:17:32 +0100 Subject: [PATCH 30/36] feat: refactor Health class to remove singleton pattern and enable dependency injection for DeviceInfoPlugin --- packages/health/CHANGELOG.md | 10 + packages/health/example/lib/main.dart | 85 ++--- packages/health/lib/src/health_plugin.dart | 16 +- packages/health/pubspec.yaml | 1 + packages/health/test/health_test.dart | 341 ++++++++++++++++++ .../health/test/mocks/device_info_mock.dart | 60 +++ 6 files changed, 462 insertions(+), 51 deletions(-) create mode 100644 packages/health/test/mocks/device_info_mock.dart diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index e6efc7c4e..994906485 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,5 +1,15 @@ ## 12.0.0 +* **BREAKING** This release introduces a significant architectural change to the `health` plugin by removing the `singleton` pattern. + * **Dependency Injection for `DeviceInfoPlugin`**: + - The `Health` class is no longer a singleton. + - The `Health()` factory constructor is removed. + - The `Health` class now accepts an (optional) `DeviceInfoPlugin` dependency through its constructor, this change was introduced to provide easy mocking of the `DeviceInfo` class during unit tests. + - This architectural change means that, for the application to work correctly, the `Health` class *MUST* be initialized correctly as a global instance, as explained in the **Initialization** section below. + - Previously, the `Health` class directly instantiated the `DeviceInfoPlugin` internally, which was not ideal when trying to mock this functionality in unit tests, and could lead to problems with state management. + * **Impact**: + - For most users, **no immediate code changes are required** but it is paramount to initialize the `Health` class as a global instance (i.e. do not call `Health()` every time but rather define an instance `final health = Health();`). + * **BREAKING** (Android) Remove automatic permission request of `DISTANCE_DELTA` and `TOTAL_CALORIES_BURNED` data types when requesting permission for `WORKOUT` health data type. * For `WORKOUT`s that require above permissions, now those need to be requested manually. * Fix [#984](https://github.com/cph-cachet/flutter-plugins/issues/984) - PR [#1055](https://github.com/cph-cachet/flutter-plugins/pull/1055) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index e86985d6c..f67f860a0 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -7,6 +7,9 @@ import 'package:health_example/util.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:carp_serializable/carp_serializable.dart'; +// Global Health instance +final health = Health(); + void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { @@ -85,15 +88,15 @@ class _HealthAppState extends State { @override void initState() { // configure the health plugin before use and check the Health Connect status - Health().configure(); - Health().getHealthConnectSdkStatus(); + health.configure(); + health.getHealthConnectSdkStatus(); super.initState(); } /// Install Google Health Connect on this phone. Future installHealthConnect() async => - await Health().installHealthConnect(); + await health.installHealthConnect(); /// Authorize, i.e. get permissions to access relevant health data. Future authorize() async { @@ -107,7 +110,7 @@ class _HealthAppState extends State { // Check if we have health permissions bool? hasPermissions = - await Health().hasPermissions(types, permissions: permissions); + await health.hasPermissions(types, permissions: permissions); // hasPermissions = false because the hasPermission cannot disclose if WRITE access exists. // Hence, we have to request with WRITE as well. @@ -117,8 +120,8 @@ class _HealthAppState extends State { if (!hasPermissions) { // requesting access to the data types before reading them try { - authorized = await Health() - .requestAuthorization(types, permissions: permissions); + authorized = + await health.requestAuthorization(types, permissions: permissions); } catch (error) { debugPrint("Exception in authorize: $error"); } @@ -132,7 +135,7 @@ class _HealthAppState extends State { Future getHealthConnectSdkStatus() async { assert(Platform.isAndroid, "This is only available on Android"); - final status = await Health().getHealthConnectSdkStatus(); + final status = await health.getHealthConnectSdkStatus(); setState(() { _contentHealthConnectStatus = @@ -154,7 +157,7 @@ class _HealthAppState extends State { try { // fetch health data - List healthData = await Health().getHealthDataFromTypes( + List healthData = await health.getHealthDataFromTypes( types: types, startTime: yesterday, endTime: now, @@ -175,7 +178,7 @@ class _HealthAppState extends State { } // filter out duplicates - _healthDataList = Health().removeDuplicates(_healthDataList); + _healthDataList = health.removeDuplicates(_healthDataList); for (var data in _healthDataList) { debugPrint(toJsonString(data)); @@ -201,91 +204,91 @@ class _HealthAppState extends State { bool success = true; // misc. health data examples using the writeHealthData() method - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 1.925, type: HealthDataType.HEIGHT, startTime: earlier, endTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 90, type: HealthDataType.WEIGHT, startTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 90, type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 90, type: HealthDataType.STEPS, startTime: earlier, endTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 200, type: HealthDataType.ACTIVE_ENERGY_BURNED, startTime: earlier, endTime: now, ); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 70, type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now); if (Platform.isIOS) { - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 30, type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, startTime: earlier, endTime: now); } else { - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 30, type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, startTime: earlier, endTime: now); } - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 37, type: HealthDataType.BODY_TEMPERATURE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 105, type: HealthDataType.BLOOD_GLUCOSE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 1.8, type: HealthDataType.WATER, startTime: earlier, endTime: now); // different types of sleep - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_REM, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_ASLEEP, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_AWAKE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_DEEP, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 22, type: HealthDataType.LEAN_BODY_MASS, startTime: earlier, @@ -293,12 +296,12 @@ class _HealthAppState extends State { ); // specialized write methods - success &= await Health().writeBloodOxygen( + success &= await health.writeBloodOxygen( saturation: 98, startTime: earlier, endTime: now, ); - success &= await Health().writeWorkoutData( + success &= await health.writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, title: "Random workout name that shows up in Health Connect", start: now.subtract(const Duration(minutes: 15)), @@ -306,12 +309,12 @@ class _HealthAppState extends State { totalDistance: 2430, totalEnergyBurned: 400, ); - success &= await Health().writeBloodPressure( + success &= await health.writeBloodPressure( systolic: 90, diastolic: 80, startTime: now, ); - success &= await Health().writeMeal( + success &= await health.writeMeal( mealType: MealType.SNACK, startTime: earlier, endTime: now, @@ -363,7 +366,7 @@ class _HealthAppState extends State { // const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]; // const leftEarSensitivities = [49.0, 54.0, 89.0, 52.0, 77.0, 35.0]; // const rightEarSensitivities = [76.0, 66.0, 90.0, 22.0, 85.0, 44.5]; - // success &= await Health().writeAudiogram( + // success &= await health.writeAudiogram( // frequencies, // leftEarSensitivities, // rightEarSensitivities, @@ -375,7 +378,7 @@ class _HealthAppState extends State { // }, // ); - success &= await Health().writeMenstruationFlow( + success &= await health.writeMenstruationFlow( flow: MenstrualFlow.medium, isStartOfCycle: true, startTime: earlier, @@ -383,8 +386,8 @@ class _HealthAppState extends State { ); // Available on iOS 16.0+ only - if (Platform.isIOS) { - success &= await Health().writeHealthData( + if (Platform.isIOS) { + success &= await health.writeHealthData( value: 22, type: HealthDataType.WATER_TEMPERATURE, startTime: earlier, @@ -392,7 +395,7 @@ class _HealthAppState extends State { recordingMethod: RecordingMethod.manual ); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 55, type: HealthDataType.UNDERWATER_DEPTH, startTime: earlier, @@ -413,7 +416,7 @@ class _HealthAppState extends State { bool success = true; for (HealthDataType type in types) { - success &= await Health().delete( + success &= await health.delete( type: type, startTime: earlier, endTime: now, @@ -434,15 +437,15 @@ class _HealthAppState extends State { final midnight = DateTime(now.year, now.month, now.day); bool stepsPermission = - await Health().hasPermissions([HealthDataType.STEPS]) ?? false; + await health.hasPermissions([HealthDataType.STEPS]) ?? false; if (!stepsPermission) { stepsPermission = - await Health().requestAuthorization([HealthDataType.STEPS]); + await health.requestAuthorization([HealthDataType.STEPS]); } if (stepsPermission) { try { - steps = await Health().getTotalStepsInInterval(midnight, now, + steps = await health.getTotalStepsInInterval(midnight, now, includeManualEntry: !recordingMethodsToFilter.contains(RecordingMethod.manual)); } catch (error) { @@ -468,7 +471,7 @@ class _HealthAppState extends State { bool success = false; try { - await Health().revokePermissions(); + await health.revokePermissions(); success = true; } catch (error) { debugPrint("Exception in revokeAccess: $error"); @@ -503,7 +506,7 @@ class _HealthAppState extends State { child: const Text("Check Health Connect Status", style: TextStyle(color: Colors.white))), if (Platform.isAndroid && - Health().healthConnectSdkStatus != + health.healthConnectSdkStatus != HealthConnectSdkStatus.sdkAvailable) TextButton( onPressed: installHealthConnect, @@ -513,7 +516,7 @@ class _HealthAppState extends State { style: TextStyle(color: Colors.white))), if (Platform.isIOS || Platform.isAndroid && - Health().healthConnectSdkStatus == + health.healthConnectSdkStatus == HealthConnectSdkStatus.sdkAvailable) Wrap(spacing: 10, children: [ TextButton( diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 8df652e4e..06b76bb52 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -34,19 +34,15 @@ part of '../health.dart'; /// or getter methods. Otherwise, the plugin will throw an exception. class Health { static const MethodChannel _channel = MethodChannel('flutter_health'); - static final _instance = Health._(); String? _deviceId; - final _deviceInfo = DeviceInfoPlugin(); - HealthConnectSdkStatus _healthConnectSdkStatus = - HealthConnectSdkStatus.sdkUnavailable; + final DeviceInfoPlugin _deviceInfo; + HealthConnectSdkStatus _healthConnectSdkStatus = + HealthConnectSdkStatus.sdkUnavailable; - Health._() { - _registerFromJsonFunctions(); - } - - /// The singleton [Health] instance. - factory Health() => _instance; + Health({DeviceInfoPlugin? deviceInfo}) : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { + _registerFromJsonFunctions(); + } /// The latest status on availability of Health Connect SDK on this phone. HealthConnectSdkStatus get healthConnectSdkStatus => _healthConnectSdkStatus; diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index 8a1cc79f5..16c5823af 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -26,6 +26,7 @@ dev_dependencies: # dart run build_runner build --delete-conflicting-outputs build_runner: any json_serializable: any + mocktail: ^1.0.4 flutter: plugin: diff --git a/packages/health/test/health_test.dart b/packages/health/test/health_test.dart index 8b1378917..9e6759973 100644 --- a/packages/health/test/health_test.dart +++ b/packages/health/test/health_test.dart @@ -1 +1,342 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:health/health.dart'; +import 'dart:convert'; +import 'package:carp_serializable/carp_serializable.dart'; +import 'mocks/device_info_mock.dart'; // Import the mock file + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('HealthDataPoint fromJson Tests', () { + // Helper function to print the toJsonString, + // useful for debugging failed tests + String toJsonString(HealthDataPoint hdp) { + return jsonEncode(hdp.toJson()); + } + + //Instantiate Health class with the Mock + final health = Health(deviceInfo: MockDeviceInfoPlugin()); + setUpAll(() async { + await health.configure(); + }); + test('Test WorkoutHealthValue', () async { + var entry = { + "uuid": "A91A2F10-3D7B-486A-B140-5ADCD3C9C6D0", + "value": { + "__type": "WorkoutHealthValue", + "workoutActivityType": "AMERICAN_FOOTBALL", + "totalEnergyBurned": 100, + "totalEnergyBurnedUnit": "KILOCALORIE", + "totalDistance": 2000, + "totalDistanceUnit": "METER" + }, + "type": "WORKOUT", + "unit": "NO_UNIT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "756B1A7A-C972-4BDB-9748-0D4749CF299C", + "sourceId": "com.apple.Health", + "sourceName": "Salud", + "recordingMethod": "manual", + "workoutSummary": { + "workoutType": "AMERICAN_FOOTBALL", + "totalDistance": 2000, + "totalEnergyBurned": 100, + "totalSteps": 0 + } + }; + + var hdp = HealthDataPoint.fromJson(entry); + + expect(hdp.uuid, "A91A2F10-3D7B-486A-B140-5ADCD3C9C6D0"); + expect(hdp.type, HealthDataType.WORKOUT); + expect(hdp.unit, HealthDataUnit.NO_UNIT); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "756B1A7A-C972-4BDB-9748-0D4749CF299C"); + expect(hdp.sourceId, "com.apple.Health"); + expect(hdp.sourceName, "Salud"); + expect(hdp.recordingMethod, RecordingMethod.manual); + + expect(hdp.value, isA()); + expect((hdp.value as WorkoutHealthValue).workoutActivityType, + HealthWorkoutActivityType.AMERICAN_FOOTBALL); + expect((hdp.value as WorkoutHealthValue).totalEnergyBurned, 100); + expect((hdp.value as WorkoutHealthValue).totalEnergyBurnedUnit, + HealthDataUnit.KILOCALORIE); + expect((hdp.value as WorkoutHealthValue).totalDistance, 2000); + expect((hdp.value as WorkoutHealthValue).totalDistanceUnit, + HealthDataUnit.METER); + + // debugPrint(toJsonString(hdp)); + // don't print try to see if toJsonString works + expect(toJsonString(hdp), isA()); + + + }); + test('Test NumericHealthValue', () { + final json = { + "uuid": "some-uuid-1", + "value": {"__type": "NumericHealthValue", "numericValue": 123.45}, + "type": "HEART_RATE", + "unit": "COUNT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "automatic" + }; + + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-1"); + expect(hdp.type, HealthDataType.HEART_RATE); + expect(hdp.unit, HealthDataUnit.COUNT); + expect(hdp.sourcePlatform, HealthPlatformType.googleHealthConnect); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.automatic); + + expect(hdp.value, isA()); + expect((hdp.value as NumericHealthValue).numericValue, 123.45); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test AudiogramHealthValue', () { + final json = { + "uuid": "some-uuid-2", + "value": { + "__type": "AudiogramHealthValue", + "frequencies": [1000.0, 2000.0, 3000.0], + "leftEarSensitivities": [20.0, 25.0, 30.0], + "rightEarSensitivities": [15.0, 20.0, 25.0] + }, + "type": "AUDIOGRAM", + "unit": "DECIBEL_HEARING_LEVEL", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "manual" + }; + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-2"); + expect(hdp.type, HealthDataType.AUDIOGRAM); + expect(hdp.unit, HealthDataUnit.DECIBEL_HEARING_LEVEL); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.manual); + expect(hdp.value, isA()); + + final audiogramValue = hdp.value as AudiogramHealthValue; + expect(audiogramValue.frequencies, [1000.0, 2000.0, 3000.0]); + expect(audiogramValue.leftEarSensitivities, [20.0, 25.0, 30.0]); + expect(audiogramValue.rightEarSensitivities, [15.0, 20.0, 25.0]); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test ElectrocardiogramHealthValue', () { + final json = { + "uuid": "some-uuid-3", + "value": { + "__type": "ElectrocardiogramHealthValue", + "voltageValues": [ + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.1, + "timeSinceSampleStart": 0.01 + }, + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.2, + "timeSinceSampleStart": 0.02 + }, + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.3, + "timeSinceSampleStart": 0.03 + } + ], + }, + "type": "ELECTROCARDIOGRAM", + "unit": "VOLT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "active" + }; + + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-3"); + expect(hdp.type, HealthDataType.ELECTROCARDIOGRAM); + expect(hdp.unit, HealthDataUnit.VOLT); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.active); + expect(hdp.value, isA()); + + final ecgValue = hdp.value as ElectrocardiogramHealthValue; + expect(ecgValue.voltageValues.length, 3); + expect(ecgValue.voltageValues[0], isA()); + expect(ecgValue.voltageValues[0].voltage, 0.1); + expect(ecgValue.voltageValues[0].timeSinceSampleStart, 0.01); + expect(ecgValue.voltageValues[1].voltage, 0.2); + expect(ecgValue.voltageValues[1].timeSinceSampleStart, 0.02); + expect(ecgValue.voltageValues[2].voltage, 0.3); + expect(ecgValue.voltageValues[2].timeSinceSampleStart, 0.03); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test NutritionHealthValue', () { + final json = { + "uuid": "some-uuid-4", + "value": { + "__type": "NutritionHealthValue", + "calories": 500.0, + "carbs": 60.0, + "protein": 20.0, + "fat": 30.0, + "caffeine": 100.0, + "vitaminA": 20.0, + "b1Thiamine": 20.0, + "b2Riboflavin": 20.0, + "b3Niacin": 20.0, + "b5PantothenicAcid": 20.0, + "b6Pyridoxine": 20.0, + "b7Biotin": 20.0, + "b9Folate": 20.0, + "b12Cobalamin": 20.0, + "vitaminC": 20.0, + "vitaminD": 20.0, + "vitaminE": 20.0, + "vitaminK": 20.0, + "calcium": 20.0, + "cholesterol": 20.0, + "chloride": 20.0, + "chromium": 20.0, + "copper": 20.0, + "fatUnsaturated": 20.0, + "fatMonounsaturated": 20.0, + "fatPolyunsaturated": 20.0, + "fatSaturated": 20.0, + "fatTransMonoenoic": 20.0, + "fiber": 20.0, + "iodine": 20.0, + "iron": 20.0, + "magnesium": 20.0, + "manganese": 20.0, + "molybdenum": 20.0, + "phosphorus": 20.0, + "potassium": 20.0, + "selenium": 20.0, + "sodium": 20.0, + "sugar": 20.0, + "water": 20.0, + "zinc": 20.0 + }, + "type": "NUTRITION", + "unit": "NO_UNIT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "manual" + }; + + final hdp = HealthDataPoint.fromJson(json); + expect(hdp.uuid, "some-uuid-4"); + expect(hdp.type, HealthDataType.NUTRITION); + expect(hdp.unit, HealthDataUnit.NO_UNIT); + expect(hdp.sourcePlatform, HealthPlatformType.googleHealthConnect); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.manual); + expect(hdp.value, isA()); + + final nutritionValue = hdp.value as NutritionHealthValue; + expect(nutritionValue.calories, 500.0); + expect(nutritionValue.carbs, 60.0); + expect(nutritionValue.protein, 20.0); + expect(nutritionValue.fat, 30.0); + expect(nutritionValue.caffeine, 100.0); + expect(nutritionValue.vitaminA, 20.0); + expect(nutritionValue.b1Thiamine, 20.0); + expect(nutritionValue.b2Riboflavin, 20.0); + expect(nutritionValue.b3Niacin, 20.0); + expect(nutritionValue.b5PantothenicAcid, 20.0); + expect(nutritionValue.b6Pyridoxine, 20.0); + expect(nutritionValue.b7Biotin, 20.0); + expect(nutritionValue.b9Folate, 20.0); + expect(nutritionValue.b12Cobalamin, 20.0); + expect(nutritionValue.vitaminC, 20.0); + expect(nutritionValue.vitaminD, 20.0); + expect(nutritionValue.vitaminE, 20.0); + expect(nutritionValue.vitaminK, 20.0); + expect(nutritionValue.calcium, 20.0); + expect(nutritionValue.cholesterol, 20.0); + expect(nutritionValue.chloride, 20.0); + expect(nutritionValue.chromium, 20.0); + expect(nutritionValue.copper, 20.0); + expect(nutritionValue.fatUnsaturated, 20.0); + expect(nutritionValue.fatMonounsaturated, 20.0); + expect(nutritionValue.fatPolyunsaturated, 20.0); + expect(nutritionValue.fatSaturated, 20.0); + expect(nutritionValue.fatTransMonoenoic, 20.0); + expect(nutritionValue.fiber, 20.0); + expect(nutritionValue.iodine, 20.0); + expect(nutritionValue.iron, 20.0); + expect(nutritionValue.magnesium, 20.0); + expect(nutritionValue.manganese, 20.0); + expect(nutritionValue.molybdenum, 20.0); + expect(nutritionValue.phosphorus, 20.0); + expect(nutritionValue.potassium, 20.0); + expect(nutritionValue.selenium, 20.0); + expect(nutritionValue.sodium, 20.0); + expect(nutritionValue.sugar, 20.0); + expect(nutritionValue.water, 20.0); + expect(nutritionValue.zinc, 20.0); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test HealthValue error handling', () { + final json = { + "uuid": "some-uuid-error", + "value": { + "__type": "UnknownHealthValue", // This should throw an error + "numericValue": 123.45 + }, + "type": "HEART_RATE", + "unit": "COUNT_PER_MINUTE", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "automatic" + }; + expect( + () => HealthDataPoint.fromJson(json), + throwsA( + isA())); //Expect SerializationException + }); + }); +} diff --git a/packages/health/test/mocks/device_info_mock.dart b/packages/health/test/mocks/device_info_mock.dart new file mode 100644 index 000000000..019a36012 --- /dev/null +++ b/packages/health/test/mocks/device_info_mock.dart @@ -0,0 +1,60 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +class MockDeviceInfoPlugin extends Mock implements DeviceInfoPlugin { + @override + Future get androidInfo => + Future.value(AndroidDeviceInfo.fromMap({ + 'id': 'mock-android-id', + 'version': { + 'baseOS': 'mock-baseOS', + 'codename': 'mock-codename', + 'incremental': 'mock-incremental', + 'previewSdkInt': 23, + 'release': 'mock-release', + 'sdkInt': 30, + 'securityPatch': 'mock-securityPatch', + }, + 'board': 'mock-board', + 'bootloader': 'mock-bootloader', + 'brand': 'mock-brand', + 'device': 'mock-device', + 'display': 'mock-display', + 'fingerprint': 'mock-fingerprint', + 'hardware': 'mock-hardware', + 'host': 'mock-host', + 'manufacturer': 'mock-manufacturer', + 'model': 'mock-model', + 'product': 'mock-product', + 'supported32BitAbis': [], + 'supported64BitAbis': [], + 'supportedAbis': [], + 'tags': 'mock-tags', + 'type': 'mock-type', + 'isPhysicalDevice': true, + 'systemFeatures': [], + 'serialNumber': 'mock-serial', + 'isLowRamDevice': false, + })); + + + @override + Future get iosInfo => Future.value(IosDeviceInfo.fromMap({ + 'name': 'mock-ios-name', + 'systemName': 'mock-ios-systemName', + 'systemVersion': '16.0', + 'model': 'mock-ios-model', + 'modelName': 'mock-ios-modelName', + 'localizedModel': 'mock-ios-localizedModel', + 'identifierForVendor': 'mock-ios-id', + 'isPhysicalDevice': true, + 'isiOSAppOnMac': false, + 'utsname': { + 'sysname': 'mock-ios-sysname', + 'nodename': 'mock-ios-nodename', + 'release': 'mock-ios-release', + 'version': 'mock-ios-version', + 'machine': 'mock-ios-machine', + }, + })); +} \ No newline at end of file From 8b34d36d00de1a4fd2f97488af70be3716ad8126 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:24:28 +0100 Subject: [PATCH 31/36] test: clean up health_test.dart --- packages/health/test/health_test.dart | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/health/test/health_test.dart b/packages/health/test/health_test.dart index 9e6759973..dc7c88cbc 100644 --- a/packages/health/test/health_test.dart +++ b/packages/health/test/health_test.dart @@ -1,19 +1,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:health/health.dart'; -import 'dart:convert'; import 'package:carp_serializable/carp_serializable.dart'; -import 'mocks/device_info_mock.dart'; // Import the mock file +import 'mocks/device_info_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('HealthDataPoint fromJson Tests', () { - // Helper function to print the toJsonString, - // useful for debugging failed tests - String toJsonString(HealthDataPoint hdp) { - return jsonEncode(hdp.toJson()); - } //Instantiate Health class with the Mock final health = Health(deviceInfo: MockDeviceInfoPlugin()); @@ -70,7 +64,6 @@ void main() { HealthDataUnit.METER); // debugPrint(toJsonString(hdp)); - // don't print try to see if toJsonString works expect(toJsonString(hdp), isA()); @@ -103,6 +96,7 @@ void main() { expect(hdp.value, isA()); expect((hdp.value as NumericHealthValue).numericValue, 123.45); + // debugPrint(toJsonString(hdp)); expect(toJsonString(hdp), isA()); }); @@ -141,6 +135,7 @@ void main() { expect(audiogramValue.frequencies, [1000.0, 2000.0, 3000.0]); expect(audiogramValue.leftEarSensitivities, [20.0, 25.0, 30.0]); expect(audiogramValue.rightEarSensitivities, [15.0, 20.0, 25.0]); + // debugPrint(toJsonString(hdp)); expect(toJsonString(hdp), isA()); }); From 87745f3366f10dec1faf2653a6fec12229b40197 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:35:13 +0100 Subject: [PATCH 32/36] chore: update CHANGELOG.md to include fix for issue #1051 --- packages/health/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index e6efc7c4e..5b1f02d8f 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -16,6 +16,7 @@ * Fix [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) * Fix [#1047](https://github.com/cph-cachet/flutter-plugins/issues/1047) and [#939](https://github.com/cph-cachet/flutter-plugins/issues/939) - PR [#1091](https://github.com/cph-cachet/flutter-plugins/pull/1091) * Fix issue where `SLEEP_LIGHT` type was not aligned correctly - PR [#1086](https://github.com/cph-cachet/flutter-plugins/pull/1086) +* Fix [#1051](https://github.com/cph-cachet/flutter-plugins/issues/1051) - PR [#1052](https://github.com/cph-cachet/flutter-plugins/pull/1052) * Updated `intl` to ^0.20.1 [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) * Updated `device_info_plus` to ^11.2.0 * Example app: Updated `permission_handler` to ^11.3.1 From 8f7884583afd3035b76770a44f3a8b320658a4a4 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:45:20 +0100 Subject: [PATCH 33/36] chore: update CHANGELOG.md --- packages/health/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index a838ae0b2..40f8b6f04 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -9,7 +9,6 @@ - Previously, the `Health` class directly instantiated the `DeviceInfoPlugin` internally, which was not ideal when trying to mock this functionality in unit tests, and could lead to problems with state management. * **Impact**: - For most users, **no immediate code changes are required** but it is paramount to initialize the `Health` class as a global instance (i.e. do not call `Health()` every time but rather define an instance `final health = Health();`). - * **BREAKING** (Android) Remove automatic permission request of `DISTANCE_DELTA` and `TOTAL_CALORIES_BURNED` data types when requesting permission for `WORKOUT` health data type. * For `WORKOUT`s that require above permissions, now those need to be requested manually. * Fix [#984](https://github.com/cph-cachet/flutter-plugins/issues/984) - PR [#1055](https://github.com/cph-cachet/flutter-plugins/pull/1055) From 90d950232b98e56d16eef884890d7ffcd326b145 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:46:56 +0100 Subject: [PATCH 34/36] chore: update CHANGELOG.md --- packages/health/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 40f8b6f04..7c6d28359 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -5,7 +5,7 @@ - The `Health` class is no longer a singleton. - The `Health()` factory constructor is removed. - The `Health` class now accepts an (optional) `DeviceInfoPlugin` dependency through its constructor, this change was introduced to provide easy mocking of the `DeviceInfo` class during unit tests. - - This architectural change means that, for the application to work correctly, the `Health` class *MUST* be initialized correctly as a global instance, as explained in the **Initialization** section below. + - This architectural change means that, for the application to work correctly, the `Health` class *MUST* be initialized correctly as a global instance. - Previously, the `Health` class directly instantiated the `DeviceInfoPlugin` internally, which was not ideal when trying to mock this functionality in unit tests, and could lead to problems with state management. * **Impact**: - For most users, **no immediate code changes are required** but it is paramount to initialize the `Health` class as a global instance (i.e. do not call `Health()` every time but rather define an instance `final health = Health();`). From 028b74b0e1f4d3d52755b079f94d76abd0fc41c3 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:49:06 +0100 Subject: [PATCH 35/36] chore: update CHANGELOG.md --- packages/health/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 7c6d28359..7753e1d0e 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -6,7 +6,6 @@ - The `Health()` factory constructor is removed. - The `Health` class now accepts an (optional) `DeviceInfoPlugin` dependency through its constructor, this change was introduced to provide easy mocking of the `DeviceInfo` class during unit tests. - This architectural change means that, for the application to work correctly, the `Health` class *MUST* be initialized correctly as a global instance. - - Previously, the `Health` class directly instantiated the `DeviceInfoPlugin` internally, which was not ideal when trying to mock this functionality in unit tests, and could lead to problems with state management. * **Impact**: - For most users, **no immediate code changes are required** but it is paramount to initialize the `Health` class as a global instance (i.e. do not call `Health()` every time but rather define an instance `final health = Health();`). * **BREAKING** (Android) Remove automatic permission request of `DISTANCE_DELTA` and `TOTAL_CALORIES_BURNED` data types when requesting permission for `WORKOUT` health data type. From 7c332751527677cdafe30f4552128565125a3693 Mon Sep 17 00:00:00 2001 From: bardram Date: Wed, 15 Jan 2025 18:07:20 +0100 Subject: [PATCH 36/36] Small updates to documentation and improving on pub.dev scores --- packages/health/example/lib/main.dart | 42 +++++++++---------- packages/health/lib/health.dart | 2 +- packages/health/lib/src/health_plugin.dart | 22 ++++++---- .../health/lib/src/health_value_types.dart | 2 - 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index f67f860a0..59c963dac 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -16,7 +16,7 @@ class HealthApp extends StatefulWidget { const HealthApp({super.key}); @override - _HealthAppState createState() => _HealthAppState(); + HealthAppState createState() => HealthAppState(); } enum AppState { @@ -37,7 +37,7 @@ enum AppState { PERMISSIONS_NOT_REVOKED, } -class _HealthAppState extends State { +class HealthAppState extends State { List _healthDataList = []; AppState _state = AppState.DATA_NOT_FETCHED; int _nofSteps = 0; @@ -288,12 +288,12 @@ class _HealthAppState extends State { type: HealthDataType.SLEEP_DEEP, startTime: earlier, endTime: now); - success &= await health.writeHealthData( - value: 22, - type: HealthDataType.LEAN_BODY_MASS, + success &= await health.writeHealthData( + value: 22, + type: HealthDataType.LEAN_BODY_MASS, startTime: earlier, endTime: now, - ); + ); // specialized write methods success &= await health.writeBloodOxygen( @@ -386,22 +386,20 @@ class _HealthAppState extends State { ); // Available on iOS 16.0+ only - if (Platform.isIOS) { + if (Platform.isIOS) { success &= await health.writeHealthData( - value: 22, - type: HealthDataType.WATER_TEMPERATURE, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual - ); + value: 22, + type: HealthDataType.WATER_TEMPERATURE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); - success &= await health.writeHealthData( - value: 55, - type: HealthDataType.UNDERWATER_DEPTH, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual - ); + success &= await health.writeHealthData( + value: 55, + type: HealthDataType.UNDERWATER_DEPTH, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); } setState(() { @@ -702,8 +700,8 @@ class _HealthAppState extends State { return ListTile( title: Text( "${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"), - trailing: Text( - (p.value as WorkoutHealthValue).workoutActivityType.name), + trailing: + Text((p.value as WorkoutHealthValue).workoutActivityType.name), subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } diff --git a/packages/health/lib/health.dart b/packages/health/lib/health.dart index 1c960d54f..af94d80e7 100644 --- a/packages/health/lib/health.dart +++ b/packages/health/lib/health.dart @@ -1,4 +1,4 @@ -library health; +library; import 'dart:async'; import 'dart:collection'; diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 06b76bb52..cc62e3ac7 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1,8 +1,12 @@ part of '../health.dart'; -/// Main class for the Plugin. This class works as a singleton and should be -/// accessed via `Health()` factory method. The plugin must be configured using -/// the [configure] method before used. +/// Main class for the Plugin. +/// +/// Use this class to get an instance of the Health plugin, like this: +/// +/// final health = Health(); +/// +/// The plugin must be configured using the [configure] method before used. /// /// Overall, the plugin supports: /// @@ -37,12 +41,14 @@ class Health { String? _deviceId; final DeviceInfoPlugin _deviceInfo; - HealthConnectSdkStatus _healthConnectSdkStatus = - HealthConnectSdkStatus.sdkUnavailable; + HealthConnectSdkStatus _healthConnectSdkStatus = + HealthConnectSdkStatus.sdkUnavailable; - Health({DeviceInfoPlugin? deviceInfo}) : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { - _registerFromJsonFunctions(); - } + /// Get an instance of the health plugin. + Health({DeviceInfoPlugin? deviceInfo}) + : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { + _registerFromJsonFunctions(); + } /// The latest status on availability of Health Connect SDK on this phone. HealthConnectSdkStatus get healthConnectSdkStatus => _healthConnectSdkStatus; diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index 4b88adc50..af13e1892 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -608,8 +608,6 @@ class NutritionHealthValue extends HealthValue { @override Map toJson() => _$NutritionHealthValueToJson(this); - static double? _toDoubleOrNull(num? value) => value?.toDouble(); - /// Create a [NutritionHealthValue] based on a health data point from native data format. factory NutritionHealthValue.fromHealthDataPoint(dynamic dataPoint) { dataPoint = dataPoint as Map;