From dd6a32a5af51ffee1c4b2ef0f3e932bb6ed14914 Mon Sep 17 00:00:00 2001 From: bruce3x Date: Tue, 26 Dec 2023 17:10:13 +0800 Subject: [PATCH 01/11] feat: add `STAND_TIME` data type --- packages/health/README.md | 7 ++++--- packages/health/ios/Classes/SwiftHealthPlugin.swift | 4 +++- packages/health/lib/src/data_types.dart | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index f3d47d36a..c2b583427 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -23,7 +23,7 @@ Note that for Android, the target phone **needs** to have [Google Fit](https://w ## Data Types | **Data Type** | **Unit** | **iOS** | **Android (Google Fit)** | **Android (Health Connect)** | **Comments** | -| --------------------------- | ----------------------- | ------- | ------------------------ |------------------------------| -------------------------------------- | +|-----------------------------|-------------------------| ------- |--------------------------|------------------------------| -------------------------------------- | | ACTIVE_ENERGY_BURNED | CALORIES | yes | yes | yes | | | BASAL_ENERGY_BURNED | CALORIES | yes | | yes | | | BLOOD_GLUCOSE | MILLIGRAM_PER_DECILITER | yes | yes | yes | | @@ -37,8 +37,8 @@ Note that for Android, the target phone **needs** to have [Google Fit](https://w | HEART_RATE | BEATS_PER_MINUTE | yes | yes | yes | | | HEIGHT | METERS | yes | yes | yes | | | RESTING_HEART_RATE | BEATS_PER_MINUTE | yes | | yes | | -| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | | yes | -| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | +| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | | yes | +| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | | STEPS | COUNT | yes | yes | yes | | | WAIST_CIRCUMFERENCE | METERS | yes | | | | | WALKING_HEART_RATE | BEATS_PER_MINUTE | yes | | | | @@ -71,6 +71,7 @@ Note that for Android, the target phone **needs** to have [Google Fit](https://w | AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | | | ELECTROCARDIOGRAM | VOLT | yes | | | Requires Apple Watch to write the data | | NUTRITION | NO_UNIT | yes | yes | yes | | +| STAND_TIME | MINUTES | yes | | | | ## Setup diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 36d560010..89f18882a 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -62,7 +62,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let HEADACHE_SEVERE = "HEADACHE_SEVERE" let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM" let NUTRITION = "NUTRITION" - + let STAND_TIME = "STAND_TIME" + // 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 @@ -1052,6 +1053,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[WORKOUT] = HKSampleType.workoutType() dataTypesDict[NUTRITION] = HKSampleType.correlationType( forIdentifier: .food)! + dataTypesDict[STAND_TIME] = HKSampleType.quantityType(forIdentifier: .appleStandTime) healthDataTypes = Array(dataTypesDict.values) } diff --git a/packages/health/lib/src/data_types.dart b/packages/health/lib/src/data_types.dart index bca127cd0..1c2372834 100644 --- a/packages/health/lib/src/data_types.dart +++ b/packages/health/lib/src/data_types.dart @@ -49,6 +49,7 @@ enum HealthDataType { HEADACHE_SEVERE, HEADACHE_UNSPECIFIED, NUTRITION, + STAND_TIME, // Heart Rate events (specific to Apple Watch) HIGH_HEART_RATE_EVENT, @@ -114,6 +115,7 @@ const List _dataTypeKeysIOS = [ HealthDataType.HEADACHE_UNSPECIFIED, HealthDataType.ELECTROCARDIOGRAM, HealthDataType.NUTRITION, + HealthDataType.STAND_TIME, ]; /// List of data types available on Android @@ -208,6 +210,7 @@ const Map _dataTypeToUnit = { HealthDataType.ELECTROCARDIOGRAM: HealthDataUnit.VOLT, HealthDataType.NUTRITION: HealthDataUnit.NO_UNIT, + HealthDataType.STAND_TIME: HealthDataUnit.MINUTE, }; const PlatformTypeJsonValue = { From b6978abbba4ab6cf54a94ec34ebc4f8348d6bb50 Mon Sep 17 00:00:00 2001 From: bruce3x Date: Mon, 8 Jan 2024 18:56:52 +0800 Subject: [PATCH 02/11] feat: get data only from watch --- packages/health/ios/Classes/SwiftHealthPlugin.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 89f18882a..280e5e3dd 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -582,10 +582,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) + let watchPredicate = HKQuery.predicateForObjects(withDeviceProperty: HKDevicePropertyKeyModel, allowedValues: ["Watch"]) + let combinedPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, watchPredicate]) + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery( - sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] + sampleType: dataType, predicate: combinedPredicate, limit: limit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in From 23063455a84941b872fb56fd4bf440b85ecd6655 Mon Sep 17 00:00:00 2001 From: ikoamu Date: Sat, 24 Feb 2024 18:13:55 +0900 Subject: [PATCH 03/11] Add support for saving blood pressure as a correlation(ios) --- packages/health/ios/Classes/SwiftHealthPlugin.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 36d560010..070968760 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -404,9 +404,12 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic), start: dateFrom, end: dateTo) + 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( - [systolic_sample, diastolic_sample], + [blood_pressure_sample], withCompletion: { (success, error) in if let err = error { print("Error Saving Blood Pressure Sample: \(err.localizedDescription)") From 42f7eef7ceef85adb6e55d6627431c58155b4bd6 Mon Sep 17 00:00:00 2001 From: tm-hirai Date: Thu, 14 Mar 2024 21:46:29 +0900 Subject: [PATCH 04/11] add body water mass to health connect --- packages/health/README.md | 1 + .../cachet/plugins/health/HealthPlugin.kt | 18 ++++++++++++++++++ packages/health/lib/src/data_types.dart | 3 +++ 3 files changed, 22 insertions(+) diff --git a/packages/health/README.md b/packages/health/README.md index 800e245fc..7d6a63ca2 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -33,6 +33,7 @@ Note that for Android, the target phone **needs** to have [Google Fit](https://w | BODY_FAT_PERCENTAGE | PERCENTAGE | yes | yes | yes | | | BODY_MASS_INDEX | NO_UNIT | yes | yes | yes | | | BODY_TEMPERATURE | DEGREE_CELSIUS | yes | yes | yes | | +| BODY_WATER_MASS | KILOGRAMS | | | yes | | | ELECTRODERMAL_ACTIVITY | SIEMENS | yes | | | | | HEART_RATE | BEATS_PER_MINUTE | yes | yes | yes | | | HEIGHT | METERS | yes | yes | yes | | 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 424f0afea..77014f78d 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 @@ -85,6 +85,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : private var ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" private var HEART_RATE = "HEART_RATE" private var BODY_TEMPERATURE = "BODY_TEMPERATURE" + private var BODY_WATER_MASS = "BODY_WATER_MASS" private var BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" private var BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" private var BLOOD_OXYGEN = "BLOOD_OXYGEN" @@ -1940,6 +1941,16 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) + is BodyWaterMassRecord -> return listOf( + mapOf( + "value" to record.mass.inKilograms, + "date_from" to record.time.toEpochMilli(), + "date_to" to record.time.toEpochMilli(), + "source_id" to "", + "source_name" to metadata.dataOrigin.packageName, + ), + ) + is BloodPressureRecord -> return listOf( mapOf( "value" to if (dataType == BLOOD_PRESSURE_DIASTOLIC) record.diastolic.inMillimetersOfMercury else record.systolic.inMillimetersOfMercury, @@ -2122,6 +2133,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : zoneOffset = null, ) + BODY_WATER_MASS -> BodyWaterMassRecord( + time = Instant.ofEpochMilli(startTime), + mass = Mass.kilograms(value), + zoneOffset = null, + ) + BLOOD_OXYGEN -> OxygenSaturationRecord( time = Instant.ofEpochMilli(startTime), percentage = Percentage(value), @@ -2406,6 +2423,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ACTIVE_ENERGY_BURNED to ActiveCaloriesBurnedRecord::class, HEART_RATE to HeartRateRecord::class, BODY_TEMPERATURE to BodyTemperatureRecord::class, + BODY_WATER_MASS to BodyWaterMassRecord::class, BLOOD_PRESSURE_SYSTOLIC to BloodPressureRecord::class, BLOOD_PRESSURE_DIASTOLIC to BloodPressureRecord::class, BLOOD_OXYGEN to OxygenSaturationRecord::class, diff --git a/packages/health/lib/src/data_types.dart b/packages/health/lib/src/data_types.dart index bca127cd0..ffda58380 100644 --- a/packages/health/lib/src/data_types.dart +++ b/packages/health/lib/src/data_types.dart @@ -12,6 +12,7 @@ enum HealthDataType { BODY_FAT_PERCENTAGE, BODY_MASS_INDEX, BODY_TEMPERATURE, + BODY_WATER_MASS, DIETARY_CARBS_CONSUMED, DIETARY_ENERGY_CONSUMED, DIETARY_FATS_CONSUMED, @@ -126,6 +127,7 @@ const List _dataTypeKeysAndroid = [ HealthDataType.BODY_FAT_PERCENTAGE, HealthDataType.BODY_MASS_INDEX, HealthDataType.BODY_TEMPERATURE, + HealthDataType.BODY_WATER_MASS, HealthDataType.HEART_RATE, HealthDataType.HEIGHT, HealthDataType.STEPS, @@ -160,6 +162,7 @@ const Map _dataTypeToUnit = { HealthDataType.BODY_FAT_PERCENTAGE: HealthDataUnit.PERCENT, HealthDataType.BODY_MASS_INDEX: HealthDataUnit.NO_UNIT, HealthDataType.BODY_TEMPERATURE: HealthDataUnit.DEGREE_CELSIUS, + HealthDataType.BODY_WATER_MASS: HealthDataUnit.KILOGRAM, HealthDataType.DIETARY_CARBS_CONSUMED: HealthDataUnit.GRAM, HealthDataType.DIETARY_ENERGY_CONSUMED: HealthDataUnit.KILOCALORIE, HealthDataType.DIETARY_FATS_CONSUMED: HealthDataUnit.GRAM, From 532cf5d0e01c4e40b741d5d204e026eee2090f86 Mon Sep 17 00:00:00 2001 From: Philipp Bauer Date: Sat, 30 Mar 2024 16:37:04 +0100 Subject: [PATCH 05/11] writeWorkoutData: Add title --- .../kotlin/cachet/plugins/health/HealthPlugin.kt | 3 ++- packages/health/example/lib/main.dart | 12 +++++++----- packages/health/lib/src/health_plugin.dart | 3 +++ 3 files changed, 12 insertions(+), 6 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 7d156be57..7d482f49a 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 @@ -3765,6 +3765,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : return } val workoutType = workoutTypeMapHealthConnect[type]!! + val title = call.argument("title") ?: type scope.launch { try { @@ -3776,7 +3777,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endTime = endTime, endZoneOffset = null, exerciseType = workoutType, - title = type, + title = title, ), ) if (totalDistance != null) { diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 4da7b0c7c..31ca955e1 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -185,11 +185,13 @@ class _HealthAppState extends State { // specialized write methods success &= await Health().writeWorkoutData( - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - now.subtract(Duration(minutes: 15)), - now, - totalDistance: 2430, - totalEnergyBurned: 400); + HealthWorkoutActivityType.AMERICAN_FOOTBALL, + now.subtract(Duration(minutes: 15)), + now, + totalDistance: 2430, + totalEnergyBurned: 400, + title: "Random workout name that shows up in Health Connect", + ); success &= await Health().writeBloodPressure(90, 80, earlier, now); success &= await Health().writeMeal( earlier, now, 1000, 50, 25, 50, "Banana", 0.002, MealType.SNACK); diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index fb8e1b763..8038c5715 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -834,6 +834,7 @@ class Health { /// - [totalEnergyBurnedUnit] The UNIT used to measure [totalEnergyBurned] *ONLY FOR IOS* Default value is KILOCALORIE. /// - [totalDistance] The total distance traveled during the workout /// - [totalDistanceUnit] The UNIT used to measure [totalDistance] *ONLY FOR IOS* Default value is METER. + /// - [title] The title of the workout. *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING" Future writeWorkoutData( HealthWorkoutActivityType activityType, DateTime start, @@ -842,6 +843,7 @@ class Health { HealthDataUnit totalEnergyBurnedUnit = HealthDataUnit.KILOCALORIE, int? totalDistance, HealthDataUnit totalDistanceUnit = HealthDataUnit.METER, + String? title, }) async { // Check that value is on the current Platform if (platformType == PlatformType.IOS && !_isOnIOS(activityType)) { @@ -860,6 +862,7 @@ class Health { 'totalEnergyBurnedUnit': totalEnergyBurnedUnit.name, 'totalDistance': totalDistance, 'totalDistanceUnit': totalDistanceUnit.name, + 'title': title, }; return await _channel.invokeMethod('writeWorkoutData', args) == true; } From 5342c0d0ff2e860db3a63d4d99c4ce975b06d609 Mon Sep 17 00:00:00 2001 From: ikoamu Date: Mon, 1 Apr 2024 00:18:29 +0900 Subject: [PATCH 06/11] resolve conflict --- packages/health/README.md | 376 +++++++++--------- .../ios/Classes/SwiftHealthPlugin.swift | 9 +- packages/health/lib/src/heath_data_types.dart | 3 - 3 files changed, 191 insertions(+), 197 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index ec4463048..e37d43a7e 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -191,7 +191,7 @@ Below is a simplified flow of how to use the plugin. ```dart // configure the health plugin before use. Health().configure(useHealthConnectIfAvailable: true); - + // define the types to get var types = [ HealthDataType.STEPS, @@ -281,196 +281,196 @@ points = Health().removeDuplicates(points); The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html). | **Data Type** | **Unit** | **Apple Health** | **Google Fit** | **Google Health Connect** | **Comments** | -| --------------------------- | ----------------------- | ---------------- | -------------- | ------------------------- | -------------------------------------- | -| ACTIVE_ENERGY_BURNED | CALORIES | yes | yes | yes | | -| BASAL_ENERGY_BURNED | CALORIES | yes | | yes | | -| BLOOD_GLUCOSE | MILLIGRAM_PER_DECILITER | yes | yes | yes | | -| BLOOD_OXYGEN | PERCENTAGE | yes | yes | yes | | -| BLOOD_PRESSURE_DIASTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | -| BLOOD_PRESSURE_SYSTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | -| BODY_FAT_PERCENTAGE | PERCENTAGE | yes | yes | yes | | -| BODY_MASS_INDEX | NO_UNIT | yes | yes | yes | | -| BODY_TEMPERATURE | DEGREE_CELSIUS | yes | yes | yes | | -| BODY_WATER_MASS | KILOGRAMS | | | yes | | -| ELECTRODERMAL_ACTIVITY | SIEMENS | yes | | | | -| HEART_RATE | BEATS_PER_MINUTE | yes | yes | yes | | -| HEIGHT | METERS | yes | yes | yes | | -| RESTING_HEART_RATE | BEATS_PER_MINUTE | yes | | yes | | -| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | | yes | | -| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | | -| STEPS | COUNT | yes | yes | yes | | -| WAIST_CIRCUMFERENCE | METERS | yes | | | | -| WALKING_HEART_RATE | BEATS_PER_MINUTE | yes | | | | -| WEIGHT | KILOGRAMS | yes | yes | yes | | -| DISTANCE_WALKING_RUNNING | METERS | yes | | | | -| FLIGHTS_CLIMBED | COUNT | yes | | yes | | -| MOVE_MINUTES | MINUTES | | yes | | | -| DISTANCE_DELTA | METERS | | yes | yes | | -| MINDFULNESS | MINUTES | yes | | | | -| SLEEP_IN_BED | MINUTES | yes | | | | -| SLEEP_ASLEEP | MINUTES | yes | | yes | | -| SLEEP_AWAKE | MINUTES | yes | | yes | | -| SLEEP_DEEP | MINUTES | yes | | yes | | -| SLEEP_LIGHT | MINUTES | | | yes | | -| SLEEP_REM | MINUTES | yes | | yes | | -| SLEEP_OUT_OF_BED | MINUTES | | | yes | | -| SLEEP_SESSION | MINUTES | | | yes | | -| WATER | LITER | yes | yes | yes | | -| EXERCISE_TIME | MINUTES | yes | | | | -| WORKOUT | NO_UNIT | yes | yes | yes | See table below | -| HIGH_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| LOW_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| IRREGULAR_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| HEART_RATE_VARIABILITY_SDNN | MILLISECONDS | yes | | | Requires Apple Watch to write the data | -| HEADACHE_NOT_PRESENT | MINUTES | yes | | | | -| HEADACHE_MILD | MINUTES | yes | | | | -| HEADACHE_MODERATE | MINUTES | yes | | | | -| HEADACHE_SEVERE | MINUTES | yes | | | | -| HEADACHE_UNSPECIFIED | MINUTES | yes | | | | -| AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | | -| ELECTROCARDIOGRAM | VOLT | yes | | | Requires Apple Watch to write the data | -| NUTRITION | NO_UNIT | yes | yes | yes | | +| --------------------------- | ----------------------- | ------- | ----------------------- |---------------------------| -------------------------------------- | +| ACTIVE_ENERGY_BURNED | CALORIES | yes | yes | yes | | +| BASAL_ENERGY_BURNED | CALORIES | yes | | yes | | +| BLOOD_GLUCOSE | MILLIGRAM_PER_DECILITER | yes | yes | yes | | +| BLOOD_OXYGEN | PERCENTAGE | yes | yes | yes | | +| BLOOD_PRESSURE_DIASTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | +| BLOOD_PRESSURE_SYSTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | +| BODY_FAT_PERCENTAGE | PERCENTAGE | yes | yes | yes | | +| BODY_MASS_INDEX | NO_UNIT | yes | yes | yes | | +| BODY_TEMPERATURE | DEGREE_CELSIUS | yes | yes | yes | | +| BODY_WATER_MASS | KILOGRAMS | | | yes | | +| ELECTRODERMAL_ACTIVITY | SIEMENS | yes | | | | +| HEART_RATE | BEATS_PER_MINUTE | yes | yes | yes | | +| HEIGHT | METERS | yes | yes | yes | | +| RESTING_HEART_RATE | BEATS_PER_MINUTE | yes | | yes | | +| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | | yes | | +| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | | +| STEPS | COUNT | yes | yes | yes | | +| WAIST_CIRCUMFERENCE | METERS | yes | | | | +| WALKING_HEART_RATE | BEATS_PER_MINUTE | yes | | | | +| WEIGHT | KILOGRAMS | yes | yes | yes | | +| DISTANCE_WALKING_RUNNING | METERS | yes | | | | +| FLIGHTS_CLIMBED | COUNT | yes | | yes | | +| MOVE_MINUTES | MINUTES | | yes | | | +| DISTANCE_DELTA | METERS | | yes | yes | | +| MINDFULNESS | MINUTES | yes | | | | +| SLEEP_IN_BED | MINUTES | yes | | | | +| SLEEP_ASLEEP | MINUTES | yes | | yes | | +| SLEEP_AWAKE | MINUTES | yes | | yes | | +| SLEEP_DEEP | MINUTES | yes | | yes | | +| SLEEP_LIGHT | MINUTES | | | yes | | +| SLEEP_REM | MINUTES | yes | | yes | | +| SLEEP_OUT_OF_BED | MINUTES | | | yes | | +| SLEEP_SESSION | MINUTES | | | yes | | +| WATER | LITER | yes | yes | yes | | +| EXERCISE_TIME | MINUTES | yes | | | | +| WORKOUT | NO_UNIT | yes | yes | yes | See table below | +| HIGH_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | +| LOW_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | +| IRREGULAR_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | +| HEART_RATE_VARIABILITY_SDNN | MILLISECONDS | yes | | | Requires Apple Watch to write the data | +| HEADACHE_NOT_PRESENT | MINUTES | yes | | | | +| HEADACHE_MILD | MINUTES | yes | | | | +| HEADACHE_MODERATE | MINUTES | yes | | | | +| HEADACHE_SEVERE | MINUTES | yes | | | | +| HEADACHE_UNSPECIFIED | MINUTES | yes | | | | +| AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | | +| ELECTROCARDIOGRAM | VOLT | yes | | | Requires Apple Watch to write the data | +| NUTRITION | NO_UNIT | yes | yes | yes | | ## Workout Types The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/documentation/health/latest/health/HealthWorkoutActivityType.html). | **Workout Type** | **Apple Health** | **Google Fit** | **Google Health Connect** | **Comments** | -| -------------------------------- | ---------------- | -------------- | ------------------------- | ----------------------------------------------------------------- | -| ARCHERY | yes | yes | | | -| BADMINTON | yes | yes | yes | | -| BASEBALL | yes | yes | yes | | -| BASKETBALL | yes | yes | yes | | -| BIKING | yes | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | -| BOXING | yes | yes | yes | | -| CRICKET | yes | yes | yes | | -| CURLING | yes | yes | | | -| ELLIPTICAL | yes | yes | yes | | -| FENCING | yes | yes | yes | | -| AMERICAN_FOOTBALL | yes | yes | yes | | -| AUSTRALIAN_FOOTBALL | yes | yes | yes | | -| SOCCER | yes | yes | | | -| GOLF | yes | yes | yes | | -| GYMNASTICS | yes | yes | yes | | -| HANDBALL | yes | yes | yes | | -| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | yes | | -| HIKING | yes | yes | yes | | -| HOCKEY | yes | yes | | | -| SKATING | yes | yes | yes | On iOS this is skating_sports | -| JUMP_ROPE | yes | yes | | | -| KICKBOXING | yes | yes | | | -| MARTIAL_ARTS | yes | yes | yes | | -| PILATES | yes | yes | yes | | -| RACQUETBALL | yes | yes | yes | | -| RUGBY | yes | yes | yes | | -| RUNNING | yes | yes | yes | | -| ROWING | yes | yes | yes | | -| SAILING | yes | yes | yes | | -| CROSS_COUNTRY_SKIING | yes | yes | | | -| DOWNHILL_SKIING | yes | yes | | | -| SNOWBOARDING | yes | yes | yes | | -| SOFTBALL | yes | yes | yes | | -| SQUASH | yes | yes | yes | | -| STAIR_CLIMBING | yes | yes | yes | | -| SWIMMING | yes | yes | | | -| TABLE_TENNIS | yes | yes | yes | | -| TENNIS | yes | yes | yes | | -| VOLLEYBALL | yes | yes | yes | | -| WALKING | yes | yes | yes | | -| WATER_POLO | yes | yes | yes | | -| YOGA | yes | yes | yes | | -| BOWLING | yes | | | | -| CROSS_TRAINING | yes | | | | -| TRACK_AND_FIELD | yes | | | | -| DISC_SPORTS | yes | | | | -| LACROSSE | yes | | | | -| PREPARATION_AND_RECOVERY | yes | | | | -| FLEXIBILITY | yes | | | | -| COOLDOWN | yes | | | | -| WHEELCHAIR_WALK_PACE | yes | | | | -| WHEELCHAIR_RUN_PACE | yes | | | | -| HAND_CYCLING | yes | | | | -| CORE_TRAINING | yes | | | | -| FUNCTIONAL_STRENGTH_TRAINING | yes | | | | -| TRADITIONAL_STRENGTH_TRAINING | yes | | | | -| MIXED_CARDIO | yes | | | | -| STAIRS | yes | | | | -| STEP_TRAINING | yes | | | | -| FITNESS_GAMING | yes | | | | -| BARRE | yes | | | | -| CARDIO_DANCE | yes | | | | -| SOCIAL_DANCE | yes | | | | -| MIND_AND_BODY | yes | | | | -| PICKLEBALL | yes | | | | -| CLIMBING | yes | | | | -| EQUESTRIAN_SPORTS | yes | | | | -| FISHING | yes | | | | -| HUNTING | yes | | | | -| PLAY | yes | | | | -| SNOW_SPORTS | yes | | | | -| PADDLE_SPORTS | yes | | | | -| SURFING_SPORTS | yes | | | | -| WATER_FITNESS | yes | | | | -| WATER_SPORTS | yes | | | | -| TAI_CHI | yes | | | | -| WRESTLING | yes | | | | -| AEROBICS | | yes | | | -| BIATHLON | | yes | | | -| CALISTHENICS | | yes | yes | | -| CIRCUIT_TRAINING | | yes | | | -| CROSS_FIT | | yes | | | -| DANCING | | yes | yes | | -| DIVING | | yes | | | -| ELEVATOR | | yes | | | -| ERGOMETER | | yes | | | -| ESCALATOR | | yes | | | -| FRISBEE_DISC | | yes | yes | | -| GARDENING | | yes | | | -| GUIDED_BREATHING | | yes | yes | | -| HORSEBACK_RIDING | | yes | | | -| HOUSEWORK | | yes | | | -| INTERVAL_TRAINING | | yes | | | -| IN_VEHICLE | | yes | | | -| KAYAKING | | yes | | | -| KETTLEBELL_TRAINING | | yes | | | -| KICK_SCOOTER | | yes | | | -| KITE_SURFING | | yes | | | -| MEDITATION | | yes | | | -| MIXED_MARTIAL_ARTS | | yes | | | -| P90X | | yes | | | -| PARAGLIDING | | yes | yes | | -| POLO | | yes | | | -| ROCK_CLIMBING | (yes) | yes | yes | on iOS this will be stored as CLIMBING | -| RUNNING_JOGGING | (yes) | yes | | on iOS this will be stored as RUNNING | -| RUNNING_SAND | (yes) | yes | | on iOS this will be stored as RUNNING | -| RUNNING_TREADMILL | (yes) | yes | yes | on iOS this will be stored as RUNNING | -| SCUBA_DIVING | | yes | yes | | -| SKATING_CROSS | (yes) | yes | | on iOS this will be stored as SKATING | -| SKATING_INDOOR | (yes) | yes | | on iOS this will be stored as SKATING | -| SKATING_INLINE | (yes) | yes | | on iOS this will be stored as SKATING | -| SKIING_BACK_COUNTRY | | yes | | | -| SKIING_KITE | | yes | | | -| SKIING_ROLLER | | yes | | | -| SLEDDING | | yes | | | -| STAIR_CLIMBING_MACHINE | | yes | yes | | -| STANDUP_PADDLEBOARDING | | yes | | | -| STILL | | yes | | | -| STRENGTH_TRAINING | | yes | yes | | -| SURFING | | yes | yes | | -| SWIMMING_OPEN_WATER | | yes | yes | | -| SWIMMING_POOL | | yes | yes | | -| TEAM_SPORTS | | yes | | | -| TILTING | | yes | | | -| TREADMILL | | yes | | | -| VOLLEYBALL_BEACH | | yes | | | -| VOLLEYBALL_INDOOR | | yes | | | -| WAKEBOARDING | | yes | | | -| WALKING_FITNESS | | yes | | | -| WALKING_NORDIC | | yes | | | -| WALKING_STROLLER | | yes | | | -| WALKING_TREADMILL | | yes | | | -| WEIGHTLIFTING | | yes | yes | | -| WHEELCHAIR | | yes | yes | | -| WINDSURFING | | yes | | | -| ZUMBA | | yes | | | -| OTHER | yes | yes | | | +| -------------------------------- | ------- | ----------------------- | ---------------------------- | ----------------------------------------------------------------- | +| ARCHERY | yes | yes | | | +| BADMINTON | yes | yes | yes | | +| BASEBALL | yes | yes | yes | | +| BASKETBALL | yes | yes | yes | | +| BIKING | yes | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | +| BOXING | yes | yes | yes | | +| CRICKET | yes | yes | yes | | +| CURLING | yes | yes | | | +| ELLIPTICAL | yes | yes | yes | | +| FENCING | yes | yes | yes | | +| AMERICAN_FOOTBALL | yes | yes | yes | | +| AUSTRALIAN_FOOTBALL | yes | yes | yes | | +| SOCCER | yes | yes | | | +| GOLF | yes | yes | yes | | +| GYMNASTICS | yes | yes | yes | | +| HANDBALL | yes | yes | yes | | +| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | yes | | +| HIKING | yes | yes | yes | | +| HOCKEY | yes | yes | | | +| SKATING | yes | yes | yes | On iOS this is skating_sports | +| JUMP_ROPE | yes | yes | | | +| KICKBOXING | yes | yes | | | +| MARTIAL_ARTS | yes | yes | yes | | +| PILATES | yes | yes | yes | | +| RACQUETBALL | yes | yes | yes | | +| RUGBY | yes | yes | yes | | +| RUNNING | yes | yes | yes | | +| ROWING | yes | yes | yes | | +| SAILING | yes | yes | yes | | +| CROSS_COUNTRY_SKIING | yes | yes | | | +| DOWNHILL_SKIING | yes | yes | | | +| SNOWBOARDING | yes | yes | yes | | +| SOFTBALL | yes | yes | yes | | +| SQUASH | yes | yes | yes | | +| STAIR_CLIMBING | yes | yes | yes | | +| SWIMMING | yes | yes | | | +| TABLE_TENNIS | yes | yes | yes | | +| TENNIS | yes | yes | yes | | +| VOLLEYBALL | yes | yes | yes | | +| WALKING | yes | yes | yes | | +| WATER_POLO | yes | yes | yes | | +| YOGA | yes | yes | yes | | +| BOWLING | yes | | | | +| CROSS_TRAINING | yes | | | | +| TRACK_AND_FIELD | yes | | | | +| DISC_SPORTS | yes | | | | +| LACROSSE | yes | | | | +| PREPARATION_AND_RECOVERY | yes | | | | +| FLEXIBILITY | yes | | | | +| COOLDOWN | yes | | | | +| WHEELCHAIR_WALK_PACE | yes | | | | +| WHEELCHAIR_RUN_PACE | yes | | | | +| HAND_CYCLING | yes | | | | +| CORE_TRAINING | yes | | | | +| FUNCTIONAL_STRENGTH_TRAINING | yes | | | | +| TRADITIONAL_STRENGTH_TRAINING | yes | | | | +| MIXED_CARDIO | yes | | | | +| STAIRS | yes | | | | +| STEP_TRAINING | yes | | | | +| FITNESS_GAMING | yes | | | | +| BARRE | yes | | | | +| CARDIO_DANCE | yes | | | | +| SOCIAL_DANCE | yes | | | | +| MIND_AND_BODY | yes | | | | +| PICKLEBALL | yes | | | | +| CLIMBING | yes | | | | +| EQUESTRIAN_SPORTS | yes | | | | +| FISHING | yes | | | | +| HUNTING | yes | | | | +| PLAY | yes | | | | +| SNOW_SPORTS | yes | | | | +| PADDLE_SPORTS | yes | | | | +| SURFING_SPORTS | yes | | | | +| WATER_FITNESS | yes | | | | +| WATER_SPORTS | yes | | | | +| TAI_CHI | yes | | | | +| WRESTLING | yes | | | | +| AEROBICS | | yes | | | +| BIATHLON | | yes | | | +| CALISTHENICS | | yes | yes | | +| CIRCUIT_TRAINING | | yes | | | +| CROSS_FIT | | yes | | | +| DANCING | | yes | yes | | +| DIVING | | yes | | | +| ELEVATOR | | yes | | | +| ERGOMETER | | yes | | | +| ESCALATOR | | yes | | | +| FRISBEE_DISC | | yes | yes | | +| GARDENING | | yes | | | +| GUIDED_BREATHING | | yes | yes | | +| HORSEBACK_RIDING | | yes | | | +| HOUSEWORK | | yes | | | +| INTERVAL_TRAINING | | yes | | | +| IN_VEHICLE | | yes | | | +| KAYAKING | | yes | | | +| KETTLEBELL_TRAINING | | yes | | | +| KICK_SCOOTER | | yes | | | +| KITE_SURFING | | yes | | | +| MEDITATION | | yes | | | +| MIXED_MARTIAL_ARTS | | yes | | | +| P90X | | yes | | | +| PARAGLIDING | | yes | yes | | +| POLO | | yes | | | +| ROCK_CLIMBING | (yes) | yes | yes | on iOS this will be stored as CLIMBING | +| RUNNING_JOGGING | (yes) | yes | | on iOS this will be stored as RUNNING | +| RUNNING_SAND | (yes) | yes | | on iOS this will be stored as RUNNING | +| RUNNING_TREADMILL | (yes) | yes | yes | on iOS this will be stored as RUNNING | +| SCUBA_DIVING | | yes | yes | | +| SKATING_CROSS | (yes) | yes | | on iOS this will be stored as SKATING | +| SKATING_INDOOR | (yes) | yes | | on iOS this will be stored as SKATING | +| SKATING_INLINE | (yes) | yes | | on iOS this will be stored as SKATING | +| SKIING_BACK_COUNTRY | | yes | | | +| SKIING_KITE | | yes | | | +| SKIING_ROLLER | | yes | | | +| SLEDDING | | yes | | | +| STAIR_CLIMBING_MACHINE | | yes | yes | | +| STANDUP_PADDLEBOARDING | | yes | | | +| STILL | | yes | | | +| STRENGTH_TRAINING | | yes | yes | | +| SURFING | | yes | yes | | +| SWIMMING_OPEN_WATER | | yes | yes | | +| SWIMMING_POOL | | yes | yes | | +| TEAM_SPORTS | | yes | | | +| TILTING | | yes | | | +| TREADMILL | | yes | | | +| VOLLEYBALL_BEACH | | yes | | | +| VOLLEYBALL_INDOOR | | yes | | | +| WAKEBOARDING | | yes | | | +| WALKING_FITNESS | | yes | | | +| WALKING_NORDIC | | yes | | | +| WALKING_STROLLER | | yes | | | +| WALKING_TREADMILL | | yes | | | +| WEIGHTLIFTING | | yes | yes | | +| WHEELCHAIR | | yes | yes | | +| WINDSURFING | | yes | | | +| ZUMBA | | yes | | | +| OTHER | yes | yes | | | \ No newline at end of file diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 8e7e9845e..efc83f371 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -70,7 +70,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let HEADACHE_SEVERE = "HEADACHE_SEVERE" let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM" let NUTRITION = "NUTRITION" - let STAND_TIME = "STAND_TIME" // Health Unit types // MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet @@ -430,7 +429,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 @@ -616,8 +615,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) - let watchPredicate = HKQuery.predicateForObjects(withDeviceProperty: HKDevicePropertyKeyModel, allowedValues: ["Watch"]) - let combinedPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, watchPredicate]) if (!includeManualEntry) { let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) @@ -625,7 +622,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery( - sampleType: dataType, predicate: combinedPredicate, limit: limit, sortDescriptors: [sortDescriptor] + sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in @@ -1202,7 +1199,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[WORKOUT] = HKSampleType.workoutType() dataTypesDict[NUTRITION] = HKSampleType.correlationType( forIdentifier: .food)! - dataTypesDict[STAND_TIME] = HKSampleType.quantityType(forIdentifier: .appleStandTime) + healthDataTypes = Array(dataTypesDict.values) } diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index 4e683dfff..37ca9b5cd 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -56,7 +56,6 @@ enum HealthDataType { HEADACHE_SEVERE, HEADACHE_UNSPECIFIED, NUTRITION, - STAND_TIME, // Heart Rate events (specific to Apple Watch) HIGH_HEART_RATE_EVENT, @@ -131,7 +130,6 @@ const List dataTypeKeysIOS = [ HealthDataType.HEADACHE_UNSPECIFIED, HealthDataType.ELECTROCARDIOGRAM, HealthDataType.NUTRITION, - HealthDataType.STAND_TIME, ]; /// List of data types available on Android @@ -236,7 +234,6 @@ const Map dataTypeToUnit = { HealthDataType.ELECTROCARDIOGRAM: HealthDataUnit.VOLT, HealthDataType.NUTRITION: HealthDataUnit.NO_UNIT, - HealthDataType.STAND_TIME: HealthDataUnit.MINUTE, // Health Connect HealthDataType.TOTAL_CALORIES_BURNED: HealthDataUnit.KILOCALORIE, From f40f30a6ba9ea6d824c5cd4b4f1961e55485b7dd Mon Sep 17 00:00:00 2001 From: ikoamu Date: Mon, 1 Apr 2024 00:21:03 +0900 Subject: [PATCH 07/11] Add missing newline at end of file --- packages/health/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/README.md b/packages/health/README.md index e37d43a7e..9a6ebc06c 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -473,4 +473,4 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | WHEELCHAIR | | yes | yes | | | WINDSURFING | | yes | | | | ZUMBA | | yes | | | -| OTHER | yes | yes | | | \ No newline at end of file +| OTHER | yes | yes | | | From 6fcf0bd3a821099e007f2161b4708d991187da06 Mon Sep 17 00:00:00 2001 From: Philipp Bauer Date: Tue, 2 Apr 2024 15:45:18 +0200 Subject: [PATCH 08/11] Add installHealthConnect --- .../cachet/plugins/health/HealthPlugin.kt | 17 +++++++++++++++++ packages/health/example/lib/main.dart | 13 +++++++++++++ packages/health/lib/src/health_plugin.dart | 14 ++++++++++++++ 3 files changed, 44 insertions(+) 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 7d156be57..a4d954c6f 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 @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.os.Handler import android.util.Log @@ -2382,6 +2383,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : /** Handle calls from the MethodChannel */ override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { + "installHealthConnect" -> installHealthConnect(call, result) "useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result) "hasPermissions" -> hasPermissions(call, result) "requestAuthorization" -> requestAuthorization(call, result) @@ -2444,6 +2446,21 @@ class HealthPlugin(private var channel: MethodChannel? = null) : healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE } + private fun installHealthConnect(call: MethodCall, result: Result) { + val uriString = + "market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding" + context!!.startActivity( + Intent(Intent.ACTION_VIEW).apply { + setPackage("com.android.vending") + data = Uri.parse(uriString) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + putExtra("overlay", true) + putExtra("callerId", context!!.packageName) + } + ) + result.success(null) + } + fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { useHealthConnectIfAvailable = true result.success(null) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 4da7b0c7c..a1767cb3a 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:health/health.dart'; @@ -69,6 +70,10 @@ class _HealthAppState extends State { super.initState(); } + Future installHealthConnect() async { + await Health().installHealthConnect(); + } + /// Authorize, i.e. get permissions to access relevant health data. Future authorize() async { // If we are trying to read Step Count, Workout, Sleep or other data that requires @@ -330,6 +335,14 @@ class _HealthAppState extends State { style: ButtonStyle( backgroundColor: MaterialStatePropertyAll(Colors.blue))), + if (Platform.isAndroid) + TextButton( + onPressed: installHealthConnect, + child: Text("Install Health Connect", + style: TextStyle(color: Colors.white)), + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue))), ], ), Divider(thickness: 3), diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index fb8e1b763..4ee6eb1c5 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -61,6 +61,20 @@ class Health { } } + /// ANDROID ONLY: + /// Prompt the user to install the Health Connect app via the installed store (most likely Play Store). + Future installHealthConnect() async { + try { + if (platformType != PlatformType.ANDROID) { + throw UnsupportedError( + 'installHealthConnect is only available on Android'); + } + await _channel.invokeMethod('installHealthConnect'); + } catch (e) { + debugPrint('$runtimeType - Exception in revokePermissions(): $e'); + } + } + /// Is this plugin using Health Connect (true) or Google Fit (false)? /// /// This is set in the [configure] method. From ddbb06d30218135df116c297e45bbd19d4a61c98 Mon Sep 17 00:00:00 2001 From: Philipp Bauer Date: Tue, 2 Apr 2024 18:25:21 +0200 Subject: [PATCH 09/11] Add getHealthConnectSdkStatus --- .../cachet/plugins/health/HealthPlugin.kt | 26 ++++++++++++----- packages/health/example/lib/main.dart | 26 +++++++++++++++++ packages/health/lib/src/functions.dart | 29 +++++++++++++++++++ packages/health/lib/src/health_plugin.dart | 20 +++++++++++++ 4 files changed, 93 insertions(+), 8 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 7d156be57..385097fa1 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 @@ -2383,6 +2383,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result) + "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) "hasPermissions" -> hasPermissions(call, result) "requestAuthorization" -> requestAuthorization(call, result) "revokePermissions" -> revokePermissions(call, result) @@ -2408,15 +2409,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : binding.addActivityResultListener(this) activity = binding.activity - if (healthConnectAvailable) { - val requestPermissionActivityContract = - PermissionController.createRequestPermissionResultContract() + val requestPermissionActivityContract = + PermissionController.createRequestPermissionResultContract() - healthConnectRequestPermissionsLauncher = - (activity as ComponentActivity).registerForActivityResult( - requestPermissionActivityContract - ) { granted -> onHealthConnectPermissionCallback(granted) } - } + healthConnectRequestPermissionsLauncher = + (activity as ComponentActivity).registerForActivityResult( + requestPermissionActivityContract + ) { granted -> onHealthConnectPermissionCallback(granted) } } override fun onDetachedFromActivityForConfigChanges() { @@ -2449,6 +2448,17 @@ class HealthPlugin(private var channel: MethodChannel? = null) : result.success(null) } + private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { + checkAvailability() + if (healthConnectAvailable) { + healthConnectClient = + HealthConnectClient.getOrCreate( + context!! + ) + } + result.success(healthConnectStatus) + } + private fun hasPermissionsHC(call: MethodCall, result: Result) { val args = call.arguments as HashMap<*, *> val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 4da7b0c7c..0e282c133 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:health/health.dart'; @@ -29,6 +30,7 @@ enum AppState { DATA_NOT_ADDED, DATA_NOT_DELETED, STEPS_READY, + HEALTH_CONNECT_STATUS, } class _HealthAppState extends State { @@ -102,6 +104,18 @@ class _HealthAppState extends State { (authorized) ? AppState.AUTHORIZED : AppState.AUTH_NOT_GRANTED); } + /// Gets the Health Connect status on Android. + Future getHealthConnectSdkStatus() async { + assert(Platform.isAndroid, "This is only available on Android"); + + final status = await Health().getHealthConnectSdkStatus(); + + setState(() { + _contentHealthConnectStatus = Text('Health Connect Status: $status'); + _state = AppState.HEALTH_CONNECT_STATUS; + }); + } + /// Fetch data points from the health plugin and show them in the app. Future fetchData() async { setState(() => _state = AppState.FETCHING_DATA); @@ -295,6 +309,14 @@ class _HealthAppState extends State { style: ButtonStyle( backgroundColor: MaterialStatePropertyAll(Colors.blue))), + if (Platform.isAndroid) + TextButton( + onPressed: getHealthConnectSdkStatus, + child: Text("getHealthConnectSdkStatus", + style: TextStyle(color: Colors.white)), + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue))), TextButton( onPressed: fetchData, child: Text("Fetch Data", @@ -412,6 +434,9 @@ class _HealthAppState extends State { mainAxisAlignment: MainAxisAlignment.center, ); + Widget _contentHealthConnectStatus = const Text( + 'No status, click getHealthConnectSdkStatus to get the status.'); + Widget _dataAdded = const Text('Data points inserted successfully.'); Widget _dataDeleted = const Text('Data points deleted successfully.'); @@ -435,5 +460,6 @@ class _HealthAppState extends State { AppState.DATA_NOT_ADDED => _dataNotAdded, AppState.DATA_NOT_DELETED => _dataNotDeleted, AppState.STEPS_READY => _stepsFetched, + AppState.HEALTH_CONNECT_STATUS => _contentHealthConnectStatus, }; } diff --git a/packages/health/lib/src/functions.dart b/packages/health/lib/src/functions.dart index 7d1d8a80e..0b4cbdc08 100644 --- a/packages/health/lib/src/functions.dart +++ b/packages/health/lib/src/functions.dart @@ -18,3 +18,32 @@ class HealthException implements Exception { /// A list of supported platforms. enum PlatformType { IOS, ANDROID } + +/// The status of Health Connect. +/// +/// NOTE: +/// The enum order is arbitrary. If you need the native value, use [nativeValue] and not the index. +/// +/// Reference: +/// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#constants_1 +enum HealthConnectSdkStatus { + /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#SDK_UNAVAILABLE() + sdkUnavailable(1), + + /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED() + sdkUnavailableProviderUpdateRequired(2), + + /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#SDK_AVAILABLE() + sdkAvailable(3); + + const HealthConnectSdkStatus(this.nativeValue); + + /// The native value that matches the value in the Android SDK. + final int nativeValue; + + factory HealthConnectSdkStatus.fromNativeValue(int value) { + return HealthConnectSdkStatus.values.firstWhere( + (e) => e.nativeValue == value, + orElse: () => HealthConnectSdkStatus.sdkUnavailable); + } +} diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index fb8e1b763..61c831bea 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -120,6 +120,26 @@ class Health { }); } + /// Returns the current status of Health Connect availability. + /// + /// See this for more info: + /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#getSdkStatus(android.content.Context,kotlin.String) + /// + /// Not implemented on iOS as Health Connect doesn't exist at all there. + Future getHealthConnectSdkStatus() async { + try { + if (platformType == PlatformType.IOS) { + throw UnsupportedError('Health Connect is not available on iOS.'); + } + final int status = + (await _channel.invokeMethod('getHealthConnectSdkStatus'))!; + return HealthConnectSdkStatus.fromNativeValue(status); + } catch (e) { + debugPrint('$runtimeType - Exception in getHealthConnectSdkStatus(): $e'); + return null; + } + } + /// Revokes permissions of all types. /// /// Uses `disableFit()` on Google Fit. From d96cf6a00591af4e42b38c854da6716c9e868c3f Mon Sep 17 00:00:00 2001 From: bardram Date: Wed, 3 Apr 2024 13:02:56 +0200 Subject: [PATCH 10/11] PR #941 #943 #938 merged --- packages/health/CHANGELOG.md | 14 +- packages/health/example/lib/main.dart | 155 ++++++++---- packages/health/lib/src/functions.dart | 6 +- packages/health/lib/src/health_plugin.dart | 263 +++++++++++---------- packages/health/pubspec.yaml | 2 +- 5 files changed, 266 insertions(+), 174 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index eda60a7cf..da6965245 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,8 +1,16 @@ +## 10.2.0 + +* Using named parameters in most methods for consistency. +* Improved support for Google Health Connect + * getHealthConnectSdkStatus, PR [#941](https://github.com/cph-cachet/flutter-plugins/pull/941) + * installHealthConnect, PR [#943](https://github.com/cph-cachet/flutter-plugins/pull/943) + * workout title, PR [#938](https://github.com/cph-cachet/flutter-plugins/pull/938) + ## 10.1.1 -* fix of error in `WorkoutSummary` JSON serialization. -* fix of [#934](https://github.com/cph-cachet/flutter-plugins/issues/934) -* empty value check for calories nutrition, PR [#926](https://github.com/cph-cachet/flutter-plugins/pull/926) +* Fix of error in `WorkoutSummary` JSON serialization. +* Fix of [#934](https://github.com/cph-cachet/flutter-plugins/issues/934) +* Empty value check for calories nutrition, PR [#926](https://github.com/cph-cachet/flutter-plugins/pull/926) ## 10.0.0 diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 1ce79c152..1dd7d8133 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -38,11 +38,12 @@ class _HealthAppState extends State { AppState _state = AppState.DATA_NOT_FETCHED; int _nofSteps = 0; - // Define the types to get. - - // Use the entire list on e.g. Android. - static final types = dataTypesIOS; - // static final types = dataTypesAndroid; + // All types available depending on platform (iOS ot Android). + List get types => (Platform.isAndroid) + ? dataTypesAndroid + : (Platform.isIOS) + ? dataTypesIOS + : []; // // Or specify specific types // static final types = [ @@ -59,10 +60,12 @@ class _HealthAppState extends State { // Set up corresponding permissions // READ only - final permissions = types.map((e) => HealthDataAccess.READ).toList(); + List get permissions => + types.map((e) => HealthDataAccess.READ).toList(); // Or both READ and WRITE - // final permissions = types.map((e) => HealthDataAccess.READ_WRITE).toList(); + // List get permissions => + // types.map((e) => HealthDataAccess.READ_WRITE).toList(); void initState() { // configure the health plugin before use. @@ -71,6 +74,7 @@ class _HealthAppState extends State { super.initState(); } + /// Install Google Health Connect on this phone. Future installHealthConnect() async { await Health().installHealthConnect(); } @@ -133,8 +137,11 @@ class _HealthAppState extends State { try { // fetch health data - List healthData = - await Health().getHealthDataFromTypes(yesterday, now, types); + List healthData = await Health().getHealthDataFromTypes( + types: types, + startTime: yesterday, + endTime: now, + ); debugPrint('Total number of data points: ${healthData.length}. ' '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); @@ -171,48 +178,102 @@ class _HealthAppState extends State { bool success = true; // misc. health data examples using the writeHealthData() method - success &= await Health() - .writeHealthData(1.925, HealthDataType.HEIGHT, earlier, now); - success &= - await Health().writeHealthData(90, HealthDataType.WEIGHT, now, now); - success &= await Health() - .writeHealthData(90, HealthDataType.HEART_RATE, earlier, now); - success &= - await Health().writeHealthData(90, HealthDataType.STEPS, earlier, now); success &= await Health().writeHealthData( - 200, HealthDataType.ACTIVE_ENERGY_BURNED, earlier, now); - success &= await Health() - .writeHealthData(70, HealthDataType.HEART_RATE, earlier, now); - success &= await Health() - .writeHealthData(37, HealthDataType.BODY_TEMPERATURE, earlier, now); - success &= await Health().writeBloodOxygen(98, earlier, now, flowRate: 1.0); - success &= await Health() - .writeHealthData(105, HealthDataType.BLOOD_GLUCOSE, earlier, now); - success &= - await Health().writeHealthData(1.8, HealthDataType.WATER, earlier, now); + value: 1.925, + type: HealthDataType.HEIGHT, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 90, type: HealthDataType.WEIGHT, startTime: now); + success &= await Health().writeHealthData( + value: 90, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 90, + type: HealthDataType.STEPS, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 70, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 37, + type: HealthDataType.BODY_TEMPERATURE, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 105, + type: HealthDataType.BLOOD_GLUCOSE, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 1.8, + type: HealthDataType.WATER, + startTime: earlier, + endTime: now); // different types of sleep - success &= await Health() - .writeHealthData(0.0, HealthDataType.SLEEP_REM, earlier, now); - success &= await Health() - .writeHealthData(0.0, HealthDataType.SLEEP_ASLEEP, earlier, now); - success &= await Health() - .writeHealthData(0.0, HealthDataType.SLEEP_AWAKE, earlier, now); - success &= await Health() - .writeHealthData(0.0, HealthDataType.SLEEP_DEEP, earlier, now); + success &= await Health().writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_REM, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_ASLEEP, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_AWAKE, + startTime: earlier, + endTime: now); + success &= await Health().writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_DEEP, + startTime: earlier, + endTime: now); // specialized write methods + success &= await Health().writeBloodOxygen( + saturation: 98, + startTime: earlier, + endTime: now, + flowRate: 1.0, + ); success &= await Health().writeWorkoutData( - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - now.subtract(Duration(minutes: 15)), - now, + activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, + title: "Random workout name that shows up in Health Connect", + start: now.subtract(Duration(minutes: 15)), + end: now, totalDistance: 2430, totalEnergyBurned: 400, - title: "Random workout name that shows up in Health Connect", ); - success &= await Health().writeBloodPressure(90, 80, earlier, now); + success &= await Health().writeBloodPressure( + systolic: 90, + diastolic: 80, + startTime: now, + ); success &= await Health().writeMeal( - earlier, now, 1000, 50, 25, 50, "Banana", 0.002, MealType.SNACK); + mealType: MealType.SNACK, + startTime: earlier, + endTime: now, + caloriesConsumed: 1000, + carbohydrates: 50, + protein: 25, + fatTotal: 50, + name: "Banana", + caffeine: 0.002, + ); // Store an Audiogram - only available on iOS // const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]; @@ -242,7 +303,11 @@ class _HealthAppState extends State { bool success = true; for (HealthDataType type in types) { - success &= await Health().delete(type, earlier, now); + success &= await Health().delete( + type: type, + startTime: earlier, + endTime: now, + ); } setState(() { @@ -310,15 +375,15 @@ class _HealthAppState extends State { children: [ TextButton( onPressed: authorize, - child: - Text("Auth", style: TextStyle(color: Colors.white)), + child: Text("Authenticate", + style: TextStyle(color: Colors.white)), style: ButtonStyle( backgroundColor: MaterialStatePropertyAll(Colors.blue))), if (Platform.isAndroid) TextButton( onPressed: getHealthConnectSdkStatus, - child: Text("getHealthConnectSdkStatus", + child: Text("Check Health Connect Status", style: TextStyle(color: Colors.white)), style: ButtonStyle( backgroundColor: diff --git a/packages/health/lib/src/functions.dart b/packages/health/lib/src/functions.dart index 0b4cbdc08..4cb6a2b1c 100644 --- a/packages/health/lib/src/functions.dart +++ b/packages/health/lib/src/functions.dart @@ -19,10 +19,10 @@ class HealthException implements Exception { /// A list of supported platforms. enum PlatformType { IOS, ANDROID } -/// The status of Health Connect. +/// The status of Google Health Connect. /// -/// NOTE: -/// The enum order is arbitrary. If you need the native value, use [nativeValue] and not the index. +/// **NOTE** - The enum order is arbitrary. If you need the native value, +/// use [nativeValue] and not the index. /// /// Reference: /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#constants_1 diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 50a08a0d9..317e9db4e 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -27,7 +27,7 @@ class Health { String? _deviceId; final _deviceInfo = DeviceInfoPlugin(); - late bool _useHealthConnectIfAvailable; + bool _useHealthConnectIfAvailable = false; Health._() { _registerFromJsonFunctions(); @@ -61,20 +61,6 @@ class Health { } } - /// ANDROID ONLY: - /// Prompt the user to install the Health Connect app via the installed store (most likely Play Store). - Future installHealthConnect() async { - try { - if (platformType != PlatformType.ANDROID) { - throw UnsupportedError( - 'installHealthConnect is only available on Android'); - } - await _channel.invokeMethod('installHealthConnect'); - } catch (e) { - debugPrint('$runtimeType - Exception in revokePermissions(): $e'); - } - } - /// Is this plugin using Health Connect (true) or Google Fit (false)? /// /// This is set in the [configure] method. @@ -134,12 +120,30 @@ class Health { }); } + /// Revokes permissions of all types. + /// + /// Uses `disableFit()` on Google Fit. + /// + /// Not implemented on iOS as there is no way to programmatically remove access. + Future revokePermissions() async { + try { + if (platformType == PlatformType.IOS) { + throw UnsupportedError( + 'Revoke permissions is not supported on iOS. Please revoke permissions manually in the settings.'); + } + await _channel.invokeMethod('revokePermissions'); + return; + } catch (e) { + debugPrint('$runtimeType - Exception in revokePermissions(): $e'); + } + } + /// Returns the current status of Health Connect availability. /// /// See this for more info: /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#getSdkStatus(android.content.Context,kotlin.String) /// - /// Not implemented on iOS as Health Connect doesn't exist at all there. + /// Android only. Future getHealthConnectSdkStatus() async { try { if (platformType == PlatformType.IOS) { @@ -154,21 +158,19 @@ class Health { } } - /// Revokes permissions of all types. - /// - /// Uses `disableFit()` on Google Fit. + /// Prompt the user to install the Health Connect app via the installed store + /// (most likely Play Store). /// - /// Not implemented on iOS as there is no way to programmatically remove access. - Future revokePermissions() async { + /// Android only. + Future installHealthConnect() async { try { - if (platformType == PlatformType.IOS) { + if (platformType != PlatformType.ANDROID) { throw UnsupportedError( - 'Revoke permissions is not supported on iOS. Please revoke permissions manually in the settings.'); + 'installHealthConnect is only available on Android'); } - await _channel.invokeMethod('revokePermissions'); - return; + await _channel.invokeMethod('installHealthConnect'); } catch (e) { - debugPrint('$runtimeType - Exception in revokePermissions(): $e'); + debugPrint('$runtimeType - Exception in installHealthConnect(): $e'); } } @@ -327,28 +329,30 @@ class Health { /// Returns true if successful, false otherwise. /// /// Parameters: - /// * [value] - the health data's value in double - /// * [type] - the value's HealthDataType - /// * [startTime] - the start time when this [value] is measured. - /// + It must be equal to or earlier than [endTime]. - /// * [endTime] - the end time when this [value] is measured. - /// + It must be equal to or later than [startTime]. - /// + Simply set [endTime] equal to [startTime] if the [value] is measured only at a specific point in time. - /// * [unit] - (iOS ONLY) the unit the health data is measured in. + /// * [value] - the health data's value in double + /// * [unit] - **iOS ONLY** the unit the health data is measured in. + /// * [type] - the value's HealthDataType + /// * [startTime] - the start time when this [value] is measured. + /// It must be equal to or earlier than [endTime]. + /// * [endTime] - the end time when this [value] is measured. + /// It must be equal to or later than [startTime]. + /// Simply set [endTime] equal to [startTime] if the [value] is measured + /// only at a specific point in time (default). /// /// Values for Sleep and Headache are ignored and will be automatically assigned /// the default value. - Future writeHealthData( - double value, - HealthDataType type, - DateTime startTime, - DateTime endTime, { + Future writeHealthData({ + required double value, HealthDataUnit? unit, + required HealthDataType type, + required DateTime startTime, + DateTime? endTime, }) async { if (type == HealthDataType.WORKOUT) { throw ArgumentError( "Adding workouts should be done using the writeWorkoutData method."); } + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } @@ -405,11 +409,12 @@ class Health { /// Must be equal to or earlier than [endTime]. /// * [endTime] - the end time when this [value] is measured. /// Must be equal to or later than [startTime]. - Future delete( - HealthDataType type, - DateTime startTime, - DateTime endTime, - ) async { + Future delete({ + required HealthDataType type, + required DateTime startTime, + DateTime? endTime, + }) async { + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } @@ -435,13 +440,14 @@ class Health { /// * [endTime] - the end time when this [value] is measured. /// Must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the blood pressure is measured - /// only at a specific point in time. - Future writeBloodPressure( - int systolic, - int diastolic, - DateTime startTime, - DateTime endTime, - ) async { + /// only at a specific point in time. If omitted, [endTime] is set to [startTime]. + Future writeBloodPressure({ + required int systolic, + required int diastolic, + required DateTime startTime, + DateTime? endTime, + }) async { + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } @@ -463,18 +469,19 @@ class Health { /// * [saturation] - the saturation of the blood oxygen in percentage /// * [flowRate] - optional supplemental oxygen flow rate, only supported on /// Google Fit (default 0.0) - /// * [startTime] - the start time when this [value] is measured. + /// * [startTime] - the start time when this [saturation] is measured. /// Must be equal to or earlier than [endTime]. - /// * [endTime] - the end time when this [value] is measured. + /// * [endTime] - the end time when this [saturation] is measured. /// Must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the blood oxygen saturation - /// is measured only at a specific point in time. - Future writeBloodOxygen( - double saturation, - DateTime startTime, - DateTime endTime, { + /// is measured only at a specific point in time (default). + Future writeBloodOxygen({ + required double saturation, double flowRate = 0.0, + required DateTime startTime, + DateTime? endTime, }) async { + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } @@ -482,7 +489,10 @@ class Health { if (platformType == PlatformType.IOS) { success = await writeHealthData( - saturation, HealthDataType.BLOOD_OXYGEN, startTime, endTime); + value: saturation, + type: HealthDataType.BLOOD_OXYGEN, + startTime: startTime, + endTime: endTime); } else if (platformType == PlatformType.ANDROID) { Map args = { 'value': saturation, @@ -496,30 +506,32 @@ class Health { return success ?? false; } - /// Saves meal record into Apple Health or Google Fit. + /// Saves meal record into Apple Health or Google Fit / Health Connect. /// /// Returns true if successful, false otherwise. /// /// Parameters: - /// * [startTime] - the start time when the meal was consumed. - /// + It must be equal to or earlier than [endTime]. - /// * [endTime] - the end time when the meal was consumed. - /// + It must be equal to or later than [startTime]. - /// * [caloriesConsumed] - total calories consumed with this meal. - /// * [carbohydrates] - optional carbohydrates information. - /// * [protein] - optional protein information. - /// * [fatTotal] - optional total fat information. - /// * [name] - optional name information about this meal. - Future writeMeal( - DateTime startTime, - DateTime endTime, - double? caloriesConsumed, - double? carbohydrates, - double? protein, - double? fatTotal, - String? name, - double? caffeine, - MealType mealType) async { + /// * [mealType] - the type of meal. + /// * [startTime] - the start time when the meal was consumed. + /// It must be equal to or earlier than [endTime]. + /// * [endTime] - the end time when the meal was consumed. + /// It must be equal to or later than [startTime]. + /// * [caloriesConsumed] - total calories consumed with this meal. + /// * [carbohydrates] - optional carbohydrates information. + /// * [protein] - optional protein information. + /// * [fatTotal] - optional total fat information. + /// * [name] - optional name information about this meal. + Future writeMeal({ + required MealType mealType, + required DateTime startTime, + required DateTime endTime, + double? caloriesConsumed, + double? carbohydrates, + double? protein, + double? fatTotal, + String? name, + double? caffeine, + }) async { if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } @@ -544,22 +556,25 @@ class Health { /// Returns true if successful, false otherwise. /// /// Parameters: - /// * [frequencies] - array of frequencies of the test - /// * [leftEarSensitivities] threshold in decibel for the left ear - /// * [rightEarSensitivities] threshold in decibel for the left ear - /// * [startTime] - the start time when the audiogram is measured. - /// It must be equal to or earlier than [endTime]. - /// * [endTime] - the end time when the audiogram is measured. - /// It must be equal to or later than [startTime]. - /// Simply set [endTime] equal to [startTime] if the audiogram is measured only at a specific point in time. - /// * [metadata] - optional map of keys, both HKMetadataKeyExternalUUID and HKMetadataKeyDeviceName are required - Future writeAudiogram( - List frequencies, - List leftEarSensitivities, - List rightEarSensitivities, - DateTime startTime, - DateTime endTime, - {Map? metadata}) async { + /// * [frequencies] - array of frequencies of the test + /// * [leftEarSensitivities] threshold in decibel for the left ear + /// * [rightEarSensitivities] threshold in decibel for the left ear + /// * [startTime] - the start time when the audiogram is measured. + /// It must be equal to or earlier than [endTime]. + /// * [endTime] - the end time when the audiogram is measured. + /// It must be equal to or later than [startTime]. + /// Simply set [endTime] equal to [startTime] if the audiogram is measured + /// only at a specific point in time (default). + /// * [metadata] - optional map of keys, both HKMetadataKeyExternalUUID + /// and HKMetadataKeyDeviceName are required + Future writeAudiogram({ + required List frequencies, + required List leftEarSensitivities, + required List rightEarSensitivities, + required DateTime startTime, + DateTime? endTime, + Map? metadata, + }) async { if (frequencies.isEmpty || leftEarSensitivities.isEmpty || rightEarSensitivities.isEmpty) { @@ -571,6 +586,7 @@ class Health { throw ArgumentError( "frequencies, leftEarSensitivities and rightEarSensitivities need to be of the same length"); } + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } @@ -591,10 +607,10 @@ class Health { } /// Fetch a list of health data points based on [types]. - Future> getHealthDataFromTypes( - DateTime startTime, - DateTime endTime, - List types, { + Future> getHealthDataFromTypes({ + required List types, + required DateTime startTime, + required DateTime endTime, bool includeManualEntry = true, }) async { List dataPoints = []; @@ -615,11 +631,11 @@ class Health { /// Fetch a list of health data points based on [types]. Future> getHealthIntervalDataFromTypes( - DateTime startDate, - DateTime endDate, - List types, - int interval, - {bool includeManualEntry = true}) async { + {required DateTime startDate, + required DateTime endDate, + required List types, + required int interval, + bool includeManualEntry = true}) async { List dataPoints = []; for (var type in types) { @@ -632,10 +648,10 @@ class Health { } /// Fetch a list of health data points based on [types]. - Future> getHealthAggregateDataFromTypes( - DateTime startDate, - DateTime endDate, - List types, { + Future> getHealthAggregateDataFromTypes({ + required List types, + required DateTime startDate, + required DateTime endDate, int activitySegmentDuration = 1, bool includeManualEntry = true, }) async { @@ -856,23 +872,26 @@ class Health { "HealthDataType was not aligned correctly - please report bug at https://github.com/cph-cachet/flutter-plugins/issues"), }; - /// Write workout data to Apple Health + /// Write workout data to Apple Health or Google Fit or Google Health Connect. /// - /// Returns true if successfully added workout data. + /// Returns true if the workout data was successfully added. /// /// Parameters: - /// - [activityType] The type of activity performed - /// - [start] The start time of the workout - /// - [end] The end time of the workout - /// - [totalEnergyBurned] The total energy burned during the workout - /// - [totalEnergyBurnedUnit] The UNIT used to measure [totalEnergyBurned] *ONLY FOR IOS* Default value is KILOCALORIE. - /// - [totalDistance] The total distance traveled during the workout - /// - [totalDistanceUnit] The UNIT used to measure [totalDistance] *ONLY FOR IOS* Default value is METER. - /// - [title] The title of the workout. *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING" - Future writeWorkoutData( - HealthWorkoutActivityType activityType, - DateTime start, - DateTime end, { + /// - [activityType] The type of activity performed. + /// - [start] The start time of the workout. + /// - [end] The end time of the workout. + /// - [totalEnergyBurned] The total energy burned during the workout. + /// - [totalEnergyBurnedUnit] The UNIT used to measure [totalEnergyBurned] + /// *ONLY FOR IOS* Default value is KILOCALORIE. + /// - [totalDistance] The total distance traveled during the workout. + /// - [totalDistanceUnit] The UNIT used to measure [totalDistance] + /// *ONLY FOR IOS* Default value is METER. + /// - [title] The title of the workout. + /// *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING". + Future writeWorkoutData({ + required HealthWorkoutActivityType activityType, + required DateTime start, + required DateTime end, int? totalEnergyBurned, HealthDataUnit totalEnergyBurnedUnit = HealthDataUnit.KILOCALORIE, int? totalDistance, diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index 9d89e5104..cc118c503 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -1,6 +1,6 @@ name: health description: Wrapper for HealthKit on iOS and Google Fit and Health Connect on Android. -version: 10.1.1 +version: 10.2.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: From 122b3aec2ba73f9d279d887bde43cd04b27f7d62 Mon Sep 17 00:00:00 2001 From: bardram Date: Wed, 3 Apr 2024 13:48:03 +0200 Subject: [PATCH 11/11] PR #919 & support for HealthPlatformType --- packages/health/CHANGELOG.md | 4 +- packages/health/README.md | 28 ++++++++-- packages/health/example/lib/main.dart | 34 +++++------ packages/health/lib/health.g.dart | 16 +++--- packages/health/lib/src/functions.dart | 3 - .../health/lib/src/health_data_point.dart | 29 +++++----- packages/health/lib/src/health_plugin.dart | 56 +++++++++---------- packages/health/lib/src/heath_data_types.dart | 8 +-- 8 files changed, 98 insertions(+), 80 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index da6965245..0d464dcb2 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,10 +1,12 @@ ## 10.2.0 * Using named parameters in most methods for consistency. -* Improved support for Google Health Connect +* Added a `HealthPlatformType` to save which health platform the data originates from (Apple Health, Google Fit, or Google Health Connect). +* Android: Improved support for Google Health Connect * getHealthConnectSdkStatus, PR [#941](https://github.com/cph-cachet/flutter-plugins/pull/941) * installHealthConnect, PR [#943](https://github.com/cph-cachet/flutter-plugins/pull/943) * workout title, PR [#938](https://github.com/cph-cachet/flutter-plugins/pull/938) +* iOS: Add support for saving blood pressure as a correlation, PR [#919](https://github.com/cph-cachet/flutter-plugins/pull/919) ## 10.1.1 diff --git a/packages/health/README.md b/packages/health/README.md index 9a6ebc06c..30f87c6ca 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -234,17 +234,35 @@ HealthDataType type; HealthDataUnit unit; DateTime dateFrom; DateTime dateTo; -PlatformType platform; -String uuid, deviceId; +HealthPlatformType sourcePlatform; +String sourceDeviceId; String sourceId; String sourceName; bool isManualEntry; WorkoutSummary? workoutSummary; ``` -where a [HealthValue](https://pub.dev/documentation/health/latest/health/HealthValue-class.html) can be any type of `AudiogramHealthValue`, `ElectrocardiogramHealthValue`, `ElectrocardiogramVoltageValue`, `NumericHealthValue`, `NutritionHealthValue`, or `WorkoutHealthValue`. - -A `HealthDataPoint` object can be serialized to and from JSON using the `toJson()` and `fromJson()` methods. JSON serialization is using camel_case notation. +where a [`HealthValue`](https://pub.dev/documentation/health/latest/health/HealthValue-class.html) can be any type of `AudiogramHealthValue`, `ElectrocardiogramHealthValue`, `ElectrocardiogramVoltageValue`, `NumericHealthValue`, `NutritionHealthValue`, or `WorkoutHealthValue`. + +A `HealthDataPoint` object can be serialized to and from JSON using the `toJson()` and `fromJson()` methods. JSON serialization is using camel_case notation. Null values are not serialized. For example; + +```json +{ + "value": { + "__type": "NumericHealthValue", + "numeric_value": 141.0 + }, + "type": "STEPS", + "unit": "COUNT", + "date_from": "2024-04-03T10:06:57.736", + "date_to": "2024-04-03T10:12:51.724", + "source_platform": "appleHealth", + "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", + "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", + "source_name": "iPhone12.bardram.net", + "is_manual_entry": false +} +``` ### Fetch health data diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 1dd7d8133..84e93cf07 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -7,10 +7,6 @@ import 'package:health_example/util.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:carp_serializable/carp_serializable.dart'; -// /// A connivent function to convert a Dart object into a formatted JSON string. -// String toJsonString(Object? object) => -// const JsonEncoder.withIndent(' ').convert(object); - void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { @@ -135,23 +131,23 @@ class _HealthAppState extends State { // Clear old data points _healthDataList.clear(); - try { - // fetch health data - List healthData = await Health().getHealthDataFromTypes( - types: types, - startTime: yesterday, - endTime: now, - ); + // try { + // fetch health data + List healthData = await Health().getHealthDataFromTypes( + types: types, + startTime: yesterday, + endTime: now, + ); - debugPrint('Total number of data points: ${healthData.length}. ' - '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); + debugPrint('Total number of data points: ${healthData.length}. ' + '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); - // save all the new data points (only the first 100) - _healthDataList.addAll( - (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); - } catch (error) { - debugPrint("Exception in getHealthDataFromTypes: $error"); - } + // save all the new data points (only the first 100) + _healthDataList.addAll( + (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); + // } catch (error) { + // debugPrint("Exception in getHealthDataFromTypes: $error"); + // } // filter out duplicates _healthDataList = Health().removeDuplicates(_healthDataList); diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 1e52a3762..8b23ccc0e 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -13,8 +13,9 @@ HealthDataPoint _$HealthDataPointFromJson(Map json) => unit: $enumDecode(_$HealthDataUnitEnumMap, json['unit']), dateFrom: DateTime.parse(json['date_from'] as String), dateTo: DateTime.parse(json['date_to'] as String), - platform: $enumDecode(_$PlatformTypeEnumMap, json['platform']), - deviceId: json['device_id'] as String, + sourcePlatform: + $enumDecode(_$HealthPlatformTypeEnumMap, json['source_platform']), + sourceDeviceId: json['source_device_id'] as String, sourceId: json['source_id'] as String, sourceName: json['source_name'] as String, isManualEntry: json['is_manual_entry'] as bool? ?? false, @@ -31,8 +32,8 @@ Map _$HealthDataPointToJson(HealthDataPoint instance) { 'unit': _$HealthDataUnitEnumMap[instance.unit]!, 'date_from': instance.dateFrom.toIso8601String(), 'date_to': instance.dateTo.toIso8601String(), - 'platform': _$PlatformTypeEnumMap[instance.platform]!, - 'device_id': instance.deviceId, + 'source_platform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, + 'source_device_id': instance.sourceDeviceId, 'source_id': instance.sourceId, 'source_name': instance.sourceName, 'is_manual_entry': instance.isManualEntry, @@ -163,9 +164,10 @@ const _$HealthDataUnitEnumMap = { HealthDataUnit.NO_UNIT: 'NO_UNIT', }; -const _$PlatformTypeEnumMap = { - PlatformType.IOS: 'IOS', - PlatformType.ANDROID: 'ANDROID', +const _$HealthPlatformTypeEnumMap = { + HealthPlatformType.appleHealth: 'appleHealth', + HealthPlatformType.googleFit: 'googleFit', + HealthPlatformType.googleHealthConnect: 'googleHealthConnect', }; HealthValue _$HealthValueFromJson(Map json) => diff --git a/packages/health/lib/src/functions.dart b/packages/health/lib/src/functions.dart index 4cb6a2b1c..cc17b988c 100644 --- a/packages/health/lib/src/functions.dart +++ b/packages/health/lib/src/functions.dart @@ -16,9 +16,6 @@ class HealthException implements Exception { "Error requesting health data type '$dataType' - cause: $cause"; } -/// A list of supported platforms. -enum PlatformType { IOS, ANDROID } - /// The status of Google Health Connect. /// /// **NOTE** - The enum order is arbitrary. If you need the native value, diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index a734aa690..3a6f65f95 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -1,5 +1,8 @@ part of '../health.dart'; +/// Types of health platforms. +enum HealthPlatformType { appleHealth, googleFit, googleHealthConnect } + /// A [HealthDataPoint] object corresponds to a data point capture from /// Apple HealthKit or Google Fit or Google Health Connect with a [HealthValue] /// as value. @@ -26,11 +29,11 @@ class HealthDataPoint { /// The end of the time interval. DateTime dateTo; - /// The software platform of the data point. - PlatformType platform; + /// The health platform that this data point was fetched. + HealthPlatformType sourcePlatform; /// The id of the device from which the data point was fetched. - String deviceId; + String sourceDeviceId; /// The id of the source from which the data point was fetched. String sourceId; @@ -50,8 +53,8 @@ class HealthDataPoint { required this.unit, required this.dateFrom, required this.dateTo, - required this.platform, - required this.deviceId, + required this.sourcePlatform, + required this.sourceDeviceId, required this.sourceId, required this.sourceName, this.isManualEntry = false, @@ -131,8 +134,8 @@ class HealthDataPoint { unit: unit, dateFrom: from, dateTo: to, - platform: Health().platformType, - deviceId: Health().deviceId, + sourcePlatform: Health().platformType, + sourceDeviceId: Health().deviceId, sourceId: sourceId, sourceName: sourceName, isManualEntry: isManualEntry, @@ -147,8 +150,8 @@ class HealthDataPoint { dateFrom: $dateFrom, dateTo: $dateTo, dataType: ${type.name}, - platform: $platform, - deviceId: $deviceId, + platform: $sourcePlatform, + deviceId: $sourceDeviceId, sourceId: $sourceId, sourceName: $sourceName isManualEntry: $isManualEntry @@ -162,13 +165,13 @@ class HealthDataPoint { dateFrom == other.dateFrom && dateTo == other.dateTo && type == other.type && - platform == other.platform && - deviceId == other.deviceId && + sourcePlatform == other.sourcePlatform && + sourceDeviceId == other.sourceDeviceId && sourceId == other.sourceId && sourceName == other.sourceName && isManualEntry == other.isManualEntry; @override - int get hashCode => Object.hash(value, unit, dateFrom, dateTo, type, platform, - deviceId, sourceId, sourceName); + int get hashCode => Object.hash(value, unit, dateFrom, dateTo, type, + sourcePlatform, sourceDeviceId, sourceId, sourceName); } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 317e9db4e..cb755553c 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -37,8 +37,11 @@ class Health { factory Health() => _instance; /// The type of platform of this device. - PlatformType get platformType => - Platform.isAndroid ? PlatformType.ANDROID : PlatformType.IOS; + HealthPlatformType get platformType => Platform.isIOS + ? HealthPlatformType.appleHealth + : useHealthConnectIfAvailable + ? HealthPlatformType.googleHealthConnect + : HealthPlatformType.googleFit; /// The id of this device. /// @@ -51,7 +54,7 @@ class Health { /// If [useHealthConnectIfAvailable] is true, Google Health Connect on /// Android will be used. Has no effect on iOS. Future configure({bool useHealthConnectIfAvailable = false}) async { - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -67,10 +70,9 @@ class Health { bool get useHealthConnectIfAvailable => _useHealthConnectIfAvailable; /// Check if a given data type is available on the platform - bool isDataTypeAvailable(HealthDataType dataType) => - platformType == PlatformType.ANDROID - ? dataTypeKeysAndroid.contains(dataType) - : dataTypeKeysIOS.contains(dataType); + bool isDataTypeAvailable(HealthDataType dataType) => Platform.isAndroid + ? dataTypeKeysAndroid.contains(dataType) + : dataTypeKeysIOS.contains(dataType); /// Determines if the health data [types] have been granted with the specified /// access rights [permissions]. @@ -112,7 +114,7 @@ class Health { : permissions.map((permission) => permission.index).toList(); /// On Android, if BMI is requested, then also ask for weight and height - if (platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions); + if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); return await _channel.invokeMethod('hasPermissions', { "types": mTypes.map((type) => type.name).toList(), @@ -127,7 +129,7 @@ class Health { /// Not implemented on iOS as there is no way to programmatically remove access. Future revokePermissions() async { try { - if (platformType == PlatformType.IOS) { + if (Platform.isIOS) { throw UnsupportedError( 'Revoke permissions is not supported on iOS. Please revoke permissions manually in the settings.'); } @@ -146,7 +148,7 @@ class Health { /// Android only. Future getHealthConnectSdkStatus() async { try { - if (platformType == PlatformType.IOS) { + if (Platform.isIOS) { throw UnsupportedError('Health Connect is not available on iOS.'); } final int status = @@ -164,7 +166,7 @@ class Health { /// Android only. Future installHealthConnect() async { try { - if (platformType != PlatformType.ANDROID) { + if (!Platform.isAndroid) { throw UnsupportedError( 'installHealthConnect is only available on Android'); } @@ -193,7 +195,7 @@ class Health { : permissions.map((permission) => permission.index).toList(); // on Android, if BMI is requested, then also ask for weight and height - if (platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions); + if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); List keys = mTypes.map((dataType) => dataType.name).toList(); @@ -253,7 +255,7 @@ class Health { : permissions.map((permission) => permission.index).toList(); // on Android, if BMI is requested, then also ask for weight and height - if (platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions); + if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); List keys = mTypes.map((e) => e.name).toList(); final bool? isAuthorized = await _channel.invokeMethod( @@ -265,7 +267,7 @@ class Health { void _handleBMI(List mTypes, List mPermissions) { final index = mTypes.indexOf(HealthDataType.BODY_MASS_INDEX); - if (index != -1 && platformType == PlatformType.ANDROID) { + if (index != -1 && Platform.isAndroid) { if (!mTypes.contains(HealthDataType.WEIGHT)) { mTypes.add(HealthDataType.WEIGHT); mPermissions.add(mPermissions[index]); @@ -312,8 +314,8 @@ class Health { unit: unit, dateFrom: weights[i].dateFrom, dateTo: weights[i].dateTo, - platform: platformType, - deviceId: _deviceId!, + sourcePlatform: platformType, + sourceDeviceId: _deviceId!, sourceId: '', sourceName: '', isManualEntry: !includeManualEntry, @@ -362,7 +364,7 @@ class Health { HealthDataType.IRREGULAR_HEART_RATE_EVENT, HealthDataType.ELECTROCARDIOGRAM, }.contains(type) && - platformType == PlatformType.IOS) { + Platform.isIOS) { throw ArgumentError( "$type - iOS does not support writing this data type in HealthKit"); } @@ -487,13 +489,13 @@ class Health { } bool? success; - if (platformType == PlatformType.IOS) { + if (Platform.isIOS) { success = await writeHealthData( value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, endTime: endTime); - } else if (platformType == PlatformType.ANDROID) { + } else if (Platform.isAndroid) { Map args = { 'value': saturation, 'flowRate': flowRate, @@ -590,7 +592,7 @@ class Health { if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } - if (platformType == PlatformType.ANDROID) { + if (Platform.isAndroid) { throw UnsupportedError("writeAudiogram is not supported on Android"); } @@ -672,7 +674,7 @@ class Health { bool includeManualEntry, ) async { // Ask for device ID only once - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -683,8 +685,7 @@ class Health { } // If BodyMassIndex is requested on Android, calculate this manually - if (dataType == HealthDataType.BODY_MASS_INDEX && - platformType == PlatformType.ANDROID) { + if (dataType == HealthDataType.BODY_MASS_INDEX && Platform.isAndroid) { return _computeAndroidBMI(startTime, endTime, includeManualEntry); } return await _dataQuery(startTime, endTime, dataType, includeManualEntry); @@ -698,7 +699,7 @@ class Health { int interval, bool includeManualEntry) async { // Ask for device ID only once - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -720,7 +721,7 @@ class Health { int activitySegmentDuration, bool includeManualEntry) async { // Ask for device ID only once - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -899,11 +900,10 @@ class Health { String? title, }) async { // Check that value is on the current Platform - if (platformType == PlatformType.IOS && !_isOnIOS(activityType)) { + if (Platform.isIOS && !_isOnIOS(activityType)) { throw HealthException(activityType, "Workout activity type $activityType is not supported on iOS"); - } else if (platformType == PlatformType.ANDROID && - !_isOnAndroid(activityType)) { + } else if (Platform.isAndroid && !_isOnAndroid(activityType)) { throw HealthException(activityType, "Workout activity type $activityType is not supported on Android"); } diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index 37ca9b5cd..dfd35efce 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -239,10 +239,10 @@ const Map dataTypeToUnit = { HealthDataType.TOTAL_CALORIES_BURNED: HealthDataUnit.KILOCALORIE, }; -const PlatformTypeJsonValue = { - PlatformType.IOS: 'ios', - PlatformType.ANDROID: 'android', -}; +// const PlatformTypeJsonValue = { +// PlatformType.IOS: 'ios', +// PlatformType.ANDROID: 'android', +// }; /// List of all [HealthDataUnit]s. enum HealthDataUnit {