From 310d13d8f91b6481b82389a8f558c1010678b5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bijok?= <71793107+PawelBijok@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:39:37 +0200 Subject: [PATCH 1/5] [HEALTH]: Add default activity type when there is no match (#1016) --- packages/health/lib/src/health_value_types.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index d3d364141..6c161ac1e 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -153,7 +153,9 @@ class WorkoutHealthValue extends HealthValue { factory WorkoutHealthValue.fromHealthDataPoint(dynamic dataPoint) => WorkoutHealthValue( workoutActivityType: HealthWorkoutActivityType.values.firstWhere( - (element) => element.name == dataPoint['workoutActivityType']), + (element) => element.name == dataPoint['workoutActivityType'], + orElse: () => HealthWorkoutActivityType.OTHER, + ), totalEnergyBurned: dataPoint['totalEnergyBurned'] != null ? (dataPoint['totalEnergyBurned'] as num).toInt() : null, From e076c8eb93bd855478fda611c56bb69935ed8509 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 22 Aug 2024 11:13:12 +0200 Subject: [PATCH 2/5] [Health]: Remove Google Fit support (#1014) * Remove Google Fit and imports from Android code * Formatting * Remove Google Fit column from readme * Remove support for Google Fit types not supported by Health Connect * Remove more Google Fit workout types * Remove references to Google Fit, remove `useHealthConnectIfAvailable` * Remove `disconect` method channel * Remove `flowRate` from `writeBloodOxygen` as it is not supported in Health Connect * Remove more unsupported workout types * Add missing import * Remove Google Fit as dependency * Add notice in README * Improve logging for HC permission callback * Update some documentation * Android: Fix `requestAuthorization` not returning a result on success * Remove additional workout types that are not supported * Remove another workout type * Add missing unimplemented method * small updates to the README * Fix an issue in generated file * When writing data, check if the type is available on the requested platform --------- Co-authored-by: bardram --- packages/health/README.md | 446 +- packages/health/android/build.gradle | 3 - .../cachet/plugins/health/HealthPlugin.kt | 6338 ++++++----------- .../health/example/android/app/build.gradle | 4 - packages/health/example/lib/main.dart | 12 +- packages/health/example/lib/util.dart | 3 +- .../ios/Classes/SwiftHealthPlugin.swift | 12 - packages/health/ios/health.podspec | 4 +- packages/health/lib/health.g.dart | 110 +- packages/health/lib/health.json.dart | 1 - .../health/lib/src/health_data_point.dart | 4 +- packages/health/lib/src/health_plugin.dart | 217 +- .../health/lib/src/health_value_types.dart | 2 +- packages/health/lib/src/heath_data_types.dart | 107 +- packages/health/pubspec.yaml | 2 +- 15 files changed, 2495 insertions(+), 4770 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index f5d4a6437..ccd2d913c 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -1,8 +1,8 @@ # Health -Enables reading and writing health data from/to Apple Health, Google Fit and Health Connect. +Enables reading and writing health data from/to Apple Health and Health Connect. -> Google Fitness API is deprecated and will be turned down in 2024, thus this package will also transition to only support Health Connect. +> **NOTE:** Google has deprecated the Google Fit API. According to the [documentation](https://developers.google.com/fit/android), as of **May 1st 2014** developers cannot sign up for using the API. As such, this package has removed support for Google Fit as of version 11.0.0 and users are urged to upgrade as soon as possible. The plugin supports: @@ -17,7 +17,7 @@ The plugin supports: - cleaning up duplicate data points via the `removeDuplicates` method. - removing data of a given type in a selected period of time using the `delete` method. -Note that for Android, the target phone **needs** to have [Google Fit](https://www.google.com/fit/) or [Health Connect](https://health.google/health-connect-android/) (which is currently in beta) installed and have access to the internet, otherwise this plugin will not work. +Note that for Android, the target phone **needs** to have [Health Connect](https://health.google/health-connect-android/) (which is currently in beta) installed and have access to the internet, otherwise this plugin will not work. See the tables below for supported health and workout data types. @@ -25,7 +25,7 @@ See the tables below for supported health and workout data types. ### Apple Health (iOS) -Step 1: Append the `Info.plist` with the following 2 entries +First, add the following 2 entries to the `Info.plist`: ```xml NSHealthShareUsageDescription @@ -34,59 +34,9 @@ Step 1: Append the `Info.plist` with the following 2 entries We will sync your data with the Apple Health app to give you better insights ``` -Step 2: Open your Flutter project in Xcode by right clicking on the "ios" folder and selecting "Open in Xcode". Next, enable "HealthKit" by adding a capability inside the "Signing & Capabilities" tab of the Runner target's settings. +Then, open your Flutter project in Xcode by right clicking on the "ios" folder and selecting "Open in Xcode". Next, enable "HealthKit" by adding a capability inside the "Signing & Capabilities" tab of the Runner target's settings. -### Android - -Starting from API level 28 (Android 9.0) accessing some fitness data (e.g. Steps) requires a special permission. To set it add the following line to your `AndroidManifest.xml` file. - -```xml - -``` - -Additionally, for workouts, if the distance of a workout is requested then the location permissions below are needed. - -```xml - - -``` - -#### Google Fit (Android option 1) - -Follow the guide at . Below is an example of following the guide. - -Change directory to your key-store directory (MacOS): - -`cd ~/.android/` - -Get your keystore SHA1 fingerprint: - -`keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android` - -Example output: - -```bash -Alias name: androiddebugkey -Creation date: Jan 01, 2013 -Entry type: PrivateKeyEntry -Certificate chain length: 1 -Certificate[1]: -Owner: CN=Android Debug, O=Android, C=US -Issuer: CN=Android Debug, O=Android, C=US -Serial number: 4aa9b300 -Valid from: Mon Jan 01 08:04:04 UTC 2013 until: Mon Jan 01 18:04:04 PST 2033 -Certificate fingerprints: - MD5: AE:9F:95:D0:A6:86:89:BC:A8:70:BA:34:FF:6A:AC:F9 - SHA1: BB:0D:AC:74:D3:21:E1:43:07:71:9B:62:90:AF:A1:66:6E:44:5D:75 - Signature algorithm name: SHA1withRSA - Version: 3 -``` - -Follow the instructions at for setting up an OAuth2 Client ID for a Google project, and adding the SHA1 fingerprint to that OAuth2 credential. - -The client id will look something like `YOUR_CLIENT_ID.apps.googleusercontent.com`. - -#### Health Connect (Android option 2) +### Google Health Connect (Android) Health Connect requires the following lines in the `AndroidManifest.xml` file (see also the example app): @@ -115,17 +65,38 @@ In the Health Connect permissions activity there is a link to your privacy polic ``` -If using Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file. The permissions can be found here: +For each data type you want to access, the READ and WRITE permissions need to be added to the `AndroidManifest.xml` file. The list of [permissions](https://developer.android.com/health-and-fitness/guides/health-connect/plan/data-types#permissions) can be found here on the [data types](https://developer.android.com/health-and-fitness/guides/health-connect/plan/data-types) page. -Example shown here (can also be found in the example app): +An example of asking for permission to read and write heart rate data is shown below and more examples can also be found in the example app. ```xml -... ``` -Furthermore, an `intent-filter` needs to be added to the `.MainActivity` activity. +Accessing fitness data (e.g. Steps) requires permission to access the "Activity Recognition" API. To set it add the following line to your `AndroidManifest.xml` file. + +```xml + +``` + +Additionally, for workouts, if the distance of a workout is requested then the location permissions below are needed. + +```xml + + +``` + +Because this is labeled as a `dangerous` protection level, the permission system will not grant it automatically and it requires the user's action. +You can prompt the user for it using the [permission_handler](https://pub.dev/packages/permission_handler) plugin. +Follow the plugin setup instructions and add the following line before requesting the data: + +```dart +await Permission.activityRecognition.request(); +await Permission.location.request(); +``` + +Finally, an `intent-filter` needs to be added to the `.MainActivity` activity. ```xml >? = - null - private lateinit var healthConnectClient: HealthConnectClient - private lateinit var scope: CoroutineScope - - private var BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" - private var HEIGHT = "HEIGHT" - private var WEIGHT = "WEIGHT" - private var STEPS = "STEPS" - private var AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" - 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" - private var BLOOD_GLUCOSE = "BLOOD_GLUCOSE" - private var HEART_RATE_VARIABILITY_RMSSD = "HEART_RATE_VARIABILITY_RMSSD" - private var MOVE_MINUTES = "MOVE_MINUTES" - private var DISTANCE_DELTA = "DISTANCE_DELTA" - private var WATER = "WATER" - private var RESTING_HEART_RATE = "RESTING_HEART_RATE" - private var BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" - private var FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" - private var RESPIRATORY_RATE = "RESPIRATORY_RATE" - private var MENSTRUATION_FLOW = "MENSTRUATION_FLOW" - - // TODO support unknown? - private var SLEEP_ASLEEP = "SLEEP_ASLEEP" - private var SLEEP_AWAKE = "SLEEP_AWAKE" - private var SLEEP_IN_BED = "SLEEP_IN_BED" - private var SLEEP_SESSION = "SLEEP_SESSION" - private var SLEEP_LIGHT = "SLEEP_LIGHT" - private var SLEEP_DEEP = "SLEEP_DEEP" - private var SLEEP_REM = "SLEEP_REM" - private var SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" - private var WORKOUT = "WORKOUT" - private var NUTRITION = "NUTRITION" - private var BREAKFAST = "BREAKFAST" - private var LUNCH = "LUNCH" - private var DINNER = "DINNER" - private var SNACK = "SNACK" - private var MEAL_UNKNOWN = "UNKNOWN" - - private var TOTAL_CALORIES_BURNED = "TOTAL_CALORIES_BURNED" - - val workoutTypeMap = - mapOf( - "AEROBICS" to FitnessActivities.AEROBICS, - "AMERICAN_FOOTBALL" to FitnessActivities.FOOTBALL_AMERICAN, - "ARCHERY" to FitnessActivities.ARCHERY, - "AUSTRALIAN_FOOTBALL" to - FitnessActivities.FOOTBALL_AUSTRALIAN, - "BADMINTON" to FitnessActivities.BADMINTON, - "BASEBALL" to FitnessActivities.BASEBALL, - "BASKETBALL" to FitnessActivities.BASKETBALL, - "BIATHLON" to FitnessActivities.BIATHLON, - "BIKING" to FitnessActivities.BIKING, - "BIKING_HAND" to FitnessActivities.BIKING_HAND, - "BIKING_MOUNTAIN" to FitnessActivities.BIKING_MOUNTAIN, - "BIKING_ROAD" to FitnessActivities.BIKING_ROAD, - "BIKING_SPINNING" to FitnessActivities.BIKING_SPINNING, - "BIKING_STATIONARY" to FitnessActivities.BIKING_STATIONARY, - "BIKING_UTILITY" to FitnessActivities.BIKING_UTILITY, - "BOXING" to FitnessActivities.BOXING, - "CALISTHENICS" to FitnessActivities.CALISTHENICS, - "CIRCUIT_TRAINING" to FitnessActivities.CIRCUIT_TRAINING, - "CRICKET" to FitnessActivities.CRICKET, - "CROSS_COUNTRY_SKIING" to - FitnessActivities.SKIING_CROSS_COUNTRY, - "CROSS_FIT" to FitnessActivities.CROSSFIT, - "CURLING" to FitnessActivities.CURLING, - "DANCING" to FitnessActivities.DANCING, - "DIVING" to FitnessActivities.DIVING, - "DOWNHILL_SKIING" to FitnessActivities.SKIING_DOWNHILL, - "ELEVATOR" to FitnessActivities.ELEVATOR, - "ELLIPTICAL" to FitnessActivities.ELLIPTICAL, - "ERGOMETER" to FitnessActivities.ERGOMETER, - "ESCALATOR" to FitnessActivities.ESCALATOR, - "FENCING" to FitnessActivities.FENCING, - "FRISBEE_DISC" to FitnessActivities.FRISBEE_DISC, - "GARDENING" to FitnessActivities.GARDENING, - "GOLF" to FitnessActivities.GOLF, - "GUIDED_BREATHING" to FitnessActivities.GUIDED_BREATHING, - "GYMNASTICS" to FitnessActivities.GYMNASTICS, - "HANDBALL" to FitnessActivities.HANDBALL, - "HIGH_INTENSITY_INTERVAL_TRAINING" to - FitnessActivities - .HIGH_INTENSITY_INTERVAL_TRAINING, - "HIKING" to FitnessActivities.HIKING, - "HOCKEY" to FitnessActivities.HOCKEY, - "HORSEBACK_RIDING" to FitnessActivities.HORSEBACK_RIDING, - "HOUSEWORK" to FitnessActivities.HOUSEWORK, - "IN_VEHICLE" to FitnessActivities.IN_VEHICLE, - "ICE_SKATING" to FitnessActivities.ICE_SKATING, - "INTERVAL_TRAINING" to FitnessActivities.INTERVAL_TRAINING, - "JUMP_ROPE" to FitnessActivities.JUMP_ROPE, - "KAYAKING" to FitnessActivities.KAYAKING, - "KETTLEBELL_TRAINING" to - FitnessActivities.KETTLEBELL_TRAINING, - "KICK_SCOOTER" to FitnessActivities.KICK_SCOOTER, - "KICKBOXING" to FitnessActivities.KICKBOXING, - "KITE_SURFING" to FitnessActivities.KITESURFING, - "MARTIAL_ARTS" to FitnessActivities.MARTIAL_ARTS, - "MEDITATION" to FitnessActivities.MEDITATION, - "MIXED_MARTIAL_ARTS" to - FitnessActivities.MIXED_MARTIAL_ARTS, - "P90X" to FitnessActivities.P90X, - "PARAGLIDING" to FitnessActivities.PARAGLIDING, - "PILATES" to FitnessActivities.PILATES, - "POLO" to FitnessActivities.POLO, - "RACQUETBALL" to FitnessActivities.RACQUETBALL, - "ROCK_CLIMBING" to FitnessActivities.ROCK_CLIMBING, - "ROWING" to FitnessActivities.ROWING, - "ROWING_MACHINE" to FitnessActivities.ROWING_MACHINE, - "RUGBY" to FitnessActivities.RUGBY, - "RUNNING_JOGGING" to FitnessActivities.RUNNING_JOGGING, - "RUNNING_SAND" to FitnessActivities.RUNNING_SAND, - "RUNNING_TREADMILL" to FitnessActivities.RUNNING_TREADMILL, - "RUNNING" to FitnessActivities.RUNNING, - "SAILING" to FitnessActivities.SAILING, - "SCUBA_DIVING" to FitnessActivities.SCUBA_DIVING, - "SKATING_CROSS" to FitnessActivities.SKATING_CROSS, - "SKATING_INDOOR" to FitnessActivities.SKATING_INDOOR, - "SKATING_INLINE" to FitnessActivities.SKATING_INLINE, - "SKATING" to FitnessActivities.SKATING, - "SKIING" to FitnessActivities.SKIING, - "SKIING_BACK_COUNTRY" to - FitnessActivities.SKIING_BACK_COUNTRY, - "SKIING_KITE" to FitnessActivities.SKIING_KITE, - "SKIING_ROLLER" to FitnessActivities.SKIING_ROLLER, - "SLEDDING" to FitnessActivities.SLEDDING, - "SNOWBOARDING" to FitnessActivities.SNOWBOARDING, - "SNOWMOBILE" to FitnessActivities.SNOWMOBILE, - "SNOWSHOEING" to FitnessActivities.SNOWSHOEING, - "SOCCER" to FitnessActivities.FOOTBALL_SOCCER, - "SOFTBALL" to FitnessActivities.SOFTBALL, - "SQUASH" to FitnessActivities.SQUASH, - "STAIR_CLIMBING_MACHINE" to - FitnessActivities.STAIR_CLIMBING_MACHINE, - "STAIR_CLIMBING" to FitnessActivities.STAIR_CLIMBING, - "STANDUP_PADDLEBOARDING" to - FitnessActivities.STANDUP_PADDLEBOARDING, - "STILL" to FitnessActivities.STILL, - "STRENGTH_TRAINING" to FitnessActivities.STRENGTH_TRAINING, - "SURFING" to FitnessActivities.SURFING, - "SWIMMING_OPEN_WATER" to - FitnessActivities.SWIMMING_OPEN_WATER, - "SWIMMING_POOL" to FitnessActivities.SWIMMING_POOL, - "SWIMMING" to FitnessActivities.SWIMMING, - "TABLE_TENNIS" to FitnessActivities.TABLE_TENNIS, - "TEAM_SPORTS" to FitnessActivities.TEAM_SPORTS, - "TENNIS" to FitnessActivities.TENNIS, - "TILTING" to FitnessActivities.TILTING, - "VOLLEYBALL_BEACH" to FitnessActivities.VOLLEYBALL_BEACH, - "VOLLEYBALL_INDOOR" to FitnessActivities.VOLLEYBALL_INDOOR, - "VOLLEYBALL" to FitnessActivities.VOLLEYBALL, - "WAKEBOARDING" to FitnessActivities.WAKEBOARDING, - "WALKING_FITNESS" to FitnessActivities.WALKING_FITNESS, - "WALKING_PACED" to FitnessActivities.WALKING_PACED, - "WALKING_NORDIC" to FitnessActivities.WALKING_NORDIC, - "WALKING_STROLLER" to FitnessActivities.WALKING_STROLLER, - "WALKING_TREADMILL" to FitnessActivities.WALKING_TREADMILL, - "WALKING" to FitnessActivities.WALKING, - "WATER_POLO" to FitnessActivities.WATER_POLO, - "WEIGHTLIFTING" to FitnessActivities.WEIGHTLIFTING, - "WHEELCHAIR" to FitnessActivities.WHEELCHAIR, - "WINDSURFING" to FitnessActivities.WINDSURFING, - "YOGA" to FitnessActivities.YOGA, - "ZUMBA" to FitnessActivities.ZUMBA, - "OTHER" to FitnessActivities.OTHER, - ) - - // TODO: Update with new workout types when Health Connect becomes the standard. - val workoutTypeMapHealthConnect = - mapOf( - // "AEROBICS" to - // ExerciseSessionRecord.EXERCISE_TYPE_AEROBICS, - "AMERICAN_FOOTBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_FOOTBALL_AMERICAN, - // "ARCHERY" to ExerciseSessionRecord.EXERCISE_TYPE_ARCHERY, - "AUSTRALIAN_FOOTBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_FOOTBALL_AUSTRALIAN, - "BADMINTON" to - ExerciseSessionRecord - .EXERCISE_TYPE_BADMINTON, - "BASEBALL" to ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL, - "BASKETBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_BASKETBALL, - // "BIATHLON" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIATHLON, - "BIKING" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING, - // "BIKING_HAND" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_HAND, - // "BIKING_MOUNTAIN" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_MOUNTAIN, - // "BIKING_ROAD" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_ROAD, - // "BIKING_SPINNING" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_SPINNING, - // "BIKING_STATIONARY" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY, - // "BIKING_UTILITY" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_UTILITY, - "BOXING" to ExerciseSessionRecord.EXERCISE_TYPE_BOXING, - "CALISTHENICS" to - ExerciseSessionRecord - .EXERCISE_TYPE_CALISTHENICS, - // "CIRCUIT_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_CIRCUIT_TRAINING, - "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, - // "CROSS_COUNTRY_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, - // "CROSS_FIT" to - // ExerciseSessionRecord.EXERCISE_TYPE_CROSSFIT, - // "CURLING" to ExerciseSessionRecord.EXERCISE_TYPE_CURLING, - "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, - // "DIVING" to ExerciseSessionRecord.EXERCISE_TYPE_DIVING, - // "DOWNHILL_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, - // "ELEVATOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_ELEVATOR, - "ELLIPTICAL" to - ExerciseSessionRecord - .EXERCISE_TYPE_ELLIPTICAL, - // "ERGOMETER" to - // ExerciseSessionRecord.EXERCISE_TYPE_ERGOMETER, - // "ESCALATOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_ESCALATOR, - "FENCING" to ExerciseSessionRecord.EXERCISE_TYPE_FENCING, - "FRISBEE_DISC" to - ExerciseSessionRecord - .EXERCISE_TYPE_FRISBEE_DISC, - // "GARDENING" to - // ExerciseSessionRecord.EXERCISE_TYPE_GARDENING, - "GOLF" to ExerciseSessionRecord.EXERCISE_TYPE_GOLF, - "GUIDED_BREATHING" to - ExerciseSessionRecord - .EXERCISE_TYPE_GUIDED_BREATHING, - "GYMNASTICS" to - ExerciseSessionRecord - .EXERCISE_TYPE_GYMNASTICS, - "HANDBALL" to ExerciseSessionRecord.EXERCISE_TYPE_HANDBALL, - "HIGH_INTENSITY_INTERVAL_TRAINING" to - ExerciseSessionRecord - .EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING, - "HIKING" to ExerciseSessionRecord.EXERCISE_TYPE_HIKING, - // "HOCKEY" to ExerciseSessionRecord.EXERCISE_TYPE_HOCKEY, - // "HORSEBACK_RIDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_HORSEBACK_RIDING, - // "HOUSEWORK" to - // ExerciseSessionRecord.EXERCISE_TYPE_HOUSEWORK, - // "IN_VEHICLE" to - // ExerciseSessionRecord.EXERCISE_TYPE_IN_VEHICLE, - "ICE_SKATING" to - ExerciseSessionRecord - .EXERCISE_TYPE_ICE_SKATING, - // "INTERVAL_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_INTERVAL_TRAINING, - // "JUMP_ROPE" to - // ExerciseSessionRecord.EXERCISE_TYPE_JUMP_ROPE, - // "KAYAKING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KAYAKING, - // "KETTLEBELL_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KETTLEBELL_TRAINING, - // "KICK_SCOOTER" to - // ExerciseSessionRecord.EXERCISE_TYPE_KICK_SCOOTER, - // "KICKBOXING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KICKBOXING, - // "KITE_SURFING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KITESURFING, - "MARTIAL_ARTS" to - ExerciseSessionRecord - .EXERCISE_TYPE_MARTIAL_ARTS, - // "MEDITATION" to - // ExerciseSessionRecord.EXERCISE_TYPE_MEDITATION, - // "MIXED_MARTIAL_ARTS" to - // ExerciseSessionRecord.EXERCISE_TYPE_MIXED_MARTIAL_ARTS, - // "P90X" to ExerciseSessionRecord.EXERCISE_TYPE_P90X, - "PARAGLIDING" to - ExerciseSessionRecord - .EXERCISE_TYPE_PARAGLIDING, - "PILATES" to ExerciseSessionRecord.EXERCISE_TYPE_PILATES, - // "POLO" to ExerciseSessionRecord.EXERCISE_TYPE_POLO, - "RACQUETBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_RACQUETBALL, - "ROCK_CLIMBING" to - ExerciseSessionRecord - .EXERCISE_TYPE_ROCK_CLIMBING, - "ROWING" to ExerciseSessionRecord.EXERCISE_TYPE_ROWING, - "ROWING_MACHINE" to - ExerciseSessionRecord - .EXERCISE_TYPE_ROWING_MACHINE, - "RUGBY" to ExerciseSessionRecord.EXERCISE_TYPE_RUGBY, - // "RUNNING_JOGGING" to - // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_JOGGING, - // "RUNNING_SAND" to - // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_SAND, - "RUNNING_TREADMILL" to - ExerciseSessionRecord - .EXERCISE_TYPE_RUNNING_TREADMILL, - "RUNNING" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, - "SAILING" to ExerciseSessionRecord.EXERCISE_TYPE_SAILING, - "SCUBA_DIVING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SCUBA_DIVING, - // "SKATING_CROSS" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_CROSS, - // "SKATING_INDOOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INDOOR, - // "SKATING_INLINE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INLINE, - "SKATING" to ExerciseSessionRecord.EXERCISE_TYPE_SKATING, - "SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, - // "SKIING_BACK_COUNTRY" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_BACK_COUNTRY, - // "SKIING_KITE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_KITE, - // "SKIING_ROLLER" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_ROLLER, - // "SLEDDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SLEDDING, - "SNOWBOARDING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SNOWBOARDING, - // "SNOWMOBILE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SNOWMOBILE, - "SNOWSHOEING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SNOWSHOEING, - // "SOCCER" to - // ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, - "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, - "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, - "STAIR_CLIMBING_MACHINE" to - ExerciseSessionRecord - .EXERCISE_TYPE_STAIR_CLIMBING_MACHINE, - "STAIR_CLIMBING" to - ExerciseSessionRecord - .EXERCISE_TYPE_STAIR_CLIMBING, - // "STANDUP_PADDLEBOARDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_STANDUP_PADDLEBOARDING, - // "STILL" to ExerciseSessionRecord.EXERCISE_TYPE_STILL, - "STRENGTH_TRAINING" to - ExerciseSessionRecord - .EXERCISE_TYPE_STRENGTH_TRAINING, - "SURFING" to ExerciseSessionRecord.EXERCISE_TYPE_SURFING, - "SWIMMING_OPEN_WATER" to - ExerciseSessionRecord - .EXERCISE_TYPE_SWIMMING_OPEN_WATER, - "SWIMMING_POOL" to - ExerciseSessionRecord - .EXERCISE_TYPE_SWIMMING_POOL, - // "SWIMMING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING, - "TABLE_TENNIS" to - ExerciseSessionRecord - .EXERCISE_TYPE_TABLE_TENNIS, - // "TEAM_SPORTS" to - // ExerciseSessionRecord.EXERCISE_TYPE_TEAM_SPORTS, - "TENNIS" to ExerciseSessionRecord.EXERCISE_TYPE_TENNIS, - // "TILTING" to ExerciseSessionRecord.EXERCISE_TYPE_TILTING, - // "VOLLEYBALL_BEACH" to - // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_BEACH, - // "VOLLEYBALL_INDOOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_INDOOR, - "VOLLEYBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_VOLLEYBALL, - // "WAKEBOARDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_WAKEBOARDING, - // "WALKING_FITNESS" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_FITNESS, - // "WALKING_PACED" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_PACED, - // "WALKING_NORDIC" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_NORDIC, - // "WALKING_STROLLER" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_STROLLER, - // "WALKING_TREADMILL" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_TREADMILL, - "WALKING" to ExerciseSessionRecord.EXERCISE_TYPE_WALKING, - "WATER_POLO" to - ExerciseSessionRecord - .EXERCISE_TYPE_WATER_POLO, - "WEIGHTLIFTING" to - ExerciseSessionRecord - .EXERCISE_TYPE_WEIGHTLIFTING, - "WHEELCHAIR" to - ExerciseSessionRecord - .EXERCISE_TYPE_WHEELCHAIR, - // "WINDSURFING" to - // ExerciseSessionRecord.EXERCISE_TYPE_WINDSURFING, - "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, - // "ZUMBA" to ExerciseSessionRecord.EXERCISE_TYPE_ZUMBA, - // "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER, - ) - - override fun onAttachedToEngine( - @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding - ) { - scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) - channel?.setMethodCallHandler(this) - context = flutterPluginBinding.applicationContext - threadPoolExecutor = Executors.newFixedThreadPool(4) - checkAvailability() - if (healthConnectAvailable) { - healthConnectClient = - HealthConnectClient.getOrCreate( - flutterPluginBinding.applicationContext - ) - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel = null - activity = null - threadPoolExecutor!!.shutdown() - threadPoolExecutor = null - } - - // This static function is optional and equivalent to onAttachedToEngine. It supports the - // old - // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting - // plugin registration via this function while apps migrate to use the new Android APIs - // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. - // - // It is encouraged to share logic between onAttachedToEngine and registerWith to keep - // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be - // called - // depending on the user's project. onAttachedToEngine or registerWith must both be defined - // in the same class. - companion object { - @Suppress("unused") - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), CHANNEL_NAME) - val plugin = HealthPlugin(channel) - registrar.addActivityResultListener(plugin) - channel.setMethodCallHandler(plugin) - } - } - - override fun success(p0: Any?) { - handler?.post { mResult?.success(p0) } - } - - override fun notImplemented() { - handler?.post { mResult?.notImplemented() } - } - - override fun error( - errorCode: String, - errorMessage: String?, - errorDetails: Any?, - ) { - handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == GOOGLE_FIT_PERMISSIONS_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK) { - Log.i("FLUTTER_HEALTH", "Access Granted!") - mResult?.success(true) - } else if (resultCode == Activity.RESULT_CANCELED) { - Log.i("FLUTTER_HEALTH", "Access Denied!") - mResult?.success(false) - } - } - return false - } - - private fun onHealthConnectPermissionCallback(permissionGranted: Set) { - if (permissionGranted.isEmpty()) { - mResult?.success(false) - Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect)!") - } else { - mResult?.success(true) - Log.i("FLUTTER_HEALTH", "Access Granted (to Health Connect)!") - } - } - - private fun keyToHealthDataType(type: String): DataType { - return when (type) { - BODY_FAT_PERCENTAGE -> DataType.TYPE_BODY_FAT_PERCENTAGE - HEIGHT -> DataType.TYPE_HEIGHT - WEIGHT -> DataType.TYPE_WEIGHT - STEPS -> DataType.TYPE_STEP_COUNT_DELTA - AGGREGATE_STEP_COUNT -> DataType.AGGREGATE_STEP_COUNT_DELTA - ACTIVE_ENERGY_BURNED -> DataType.TYPE_CALORIES_EXPENDED - HEART_RATE -> DataType.TYPE_HEART_RATE_BPM - BODY_TEMPERATURE -> HealthDataTypes.TYPE_BODY_TEMPERATURE - BLOOD_PRESSURE_SYSTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE - BLOOD_PRESSURE_DIASTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE - BLOOD_OXYGEN -> HealthDataTypes.TYPE_OXYGEN_SATURATION - BLOOD_GLUCOSE -> HealthDataTypes.TYPE_BLOOD_GLUCOSE - MOVE_MINUTES -> DataType.TYPE_MOVE_MINUTES - DISTANCE_DELTA -> DataType.TYPE_DISTANCE_DELTA - WATER -> DataType.TYPE_HYDRATION - SLEEP_ASLEEP -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_AWAKE -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_IN_BED -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_LIGHT -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_REM -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_DEEP -> DataType.TYPE_SLEEP_SEGMENT - WORKOUT -> DataType.TYPE_ACTIVITY_SEGMENT - NUTRITION -> DataType.TYPE_NUTRITION - else -> throw IllegalArgumentException("Unsupported dataType: $type") - } - } - - private fun getField(type: String): Field { - return when (type) { - BODY_FAT_PERCENTAGE -> Field.FIELD_PERCENTAGE - HEIGHT -> Field.FIELD_HEIGHT - WEIGHT -> Field.FIELD_WEIGHT - STEPS -> Field.FIELD_STEPS - ACTIVE_ENERGY_BURNED -> Field.FIELD_CALORIES - HEART_RATE -> Field.FIELD_BPM - BODY_TEMPERATURE -> HealthFields.FIELD_BODY_TEMPERATURE - BLOOD_PRESSURE_SYSTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC - BLOOD_PRESSURE_DIASTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC - BLOOD_OXYGEN -> HealthFields.FIELD_OXYGEN_SATURATION - BLOOD_GLUCOSE -> HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - MOVE_MINUTES -> Field.FIELD_DURATION - DISTANCE_DELTA -> Field.FIELD_DISTANCE - WATER -> Field.FIELD_VOLUME - SLEEP_ASLEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_AWAKE -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_IN_BED -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_LIGHT -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_REM -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_DEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE - WORKOUT -> Field.FIELD_ACTIVITY - NUTRITION -> Field.FIELD_NUTRIENTS - else -> throw IllegalArgumentException("Unsupported dataType: $type") - } - } - - private fun isIntField(dataSource: DataSource, unit: Field): Boolean { - val dataPoint = DataPoint.builder(dataSource).build() - val value = dataPoint.getValue(unit) - return value.format == Field.FORMAT_INT32 - } - - // / Extracts the (numeric) value from a Health Data Point - private fun getHealthDataValue(dataPoint: DataPoint, field: Field): Any { - val value = dataPoint.getValue(field) - // Conversion is needed because glucose is stored as mmoll in Google Fit; - // while mgdl is used for glucose in this plugin. - val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - return when (value.format) { - Field.FORMAT_FLOAT -> - if (!isGlucose) value.asFloat() - else value.asFloat() * MMOLL_2_MGDL - Field.FORMAT_INT32 -> value.asInt() - Field.FORMAT_STRING -> value.asString() - else -> Log.e("Unsupported format:", value.format.toString()) - } - } - - /** Delete records of the given type in the time range */ - private fun delete(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - deleteHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataDeleteRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .addDataType(dataType) - .deleteAllSessions() - .build() - - val fitnessOptions = typesBuilder.build() - - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .deleteData(dataSource) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Dataset deleted successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error deleting the dataset" - ) - ) - } catch (e3: Exception) { - result.success(false) - } - } - - /** Save a Blood Pressure measurement with systolic and diastolic values */ - private fun writeBloodPressure(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeBloodPressureHC(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val dataType = HealthDataTypes.TYPE_BLOOD_PRESSURE - val systolic = call.argument("systolic")!! - val diastolic = call.argument("diastolic")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC, - systolic - ) - .setField( - HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC, - diastolic - ) - .build() - - val dataPoint = builder - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Blood Pressure added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the blood pressure data!", - ), - ) - } catch (e3: Exception) { - result.success(false) - } - } - - private fun writeMealHC(call: MethodCall, result: Result) { - val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) - val endTime = Instant.ofEpochMilli(call.argument("end_time")!!) - val calories = call.argument("calories") - val protein = call.argument("protein") as Double? - val carbs = call.argument("carbs") as Double? - val fat = call.argument("fat") as Double? - val caffeine = call.argument("caffeine") as Double? - val vitaminA = call.argument("vitamin_a") as Double? - val b1Thiamine = call.argument("b1_thiamine") as Double? - val b2Riboflavin = call.argument("b2_riboflavin") as Double? - val b3Niacin = call.argument("b3_niacin") as Double? - val b5PantothenicAcid = call.argument("b5_pantothenic_acid") as Double? - val b6Pyridoxine = call.argument("b6_pyridoxine") as Double? - val b7Biotin = call.argument("b7_biotin") as Double? - val b9Folate = call.argument("b9_folate") as Double? - val b12Cobalamin = call.argument("b12_cobalamin") as Double? - val vitaminC = call.argument("vitamin_c") as Double? - val vitaminD = call.argument("vitamin_d") as Double? - val vitaminE = call.argument("vitamin_e") as Double? - val vitaminK = call.argument("vitamin_k") as Double? - val calcium = call.argument("calcium") as Double? - val chloride = call.argument("chloride") as Double? - val cholesterol = call.argument("cholesterol") as Double? - // Choline is not yet supported by Health Connect - // val choline = call.argument("choline") as Double? - val chromium = call.argument("chromium") as Double? - val copper = call.argument("copper") as Double? - val fatUnsaturated = call.argument("fat_unsaturated") as Double? - val fatMonounsaturated = call.argument("fat_monounsaturated") as Double? - val fatPolyunsaturated = call.argument("fat_polyunsaturated") as Double? - val fatSaturated = call.argument("fat_saturated") as Double? - val fatTransMonoenoic = call.argument("fat_trans_monoenoic") as Double? - val fiber = call.argument("fiber") as Double? - val iodine = call.argument("iodine") as Double? - val iron = call.argument("iron") as Double? - val magnesium = call.argument("magnesium") as Double? - val manganese = call.argument("manganese") as Double? - val molybdenum = call.argument("molybdenum") as Double? - val phosphorus = call.argument("phosphorus") as Double? - val potassium = call.argument("potassium") as Double? - val selenium = call.argument("selenium") as Double? - val sodium = call.argument("sodium") as Double? - val sugar = call.argument("sugar") as Double? - // Water is not support on a food in Health Connect - // val water = call.argument("water") as Double? - val zinc = call.argument("zinc") as Double? - - val name = call.argument("name") - val mealType = call.argument("meal_type")!! - - scope.launch { - try { - val list = mutableListOf() - list.add( - NutritionRecord( - name = name, - energy = calories?.kilocalories, - totalCarbohydrate = carbs?.grams, - protein = protein?.grams, - totalFat = fat?.grams, - caffeine = caffeine?.grams, - vitaminA = vitaminA?.grams, - thiamin = b1Thiamine?.grams, - riboflavin = b2Riboflavin?.grams, - niacin = b3Niacin?.grams, - pantothenicAcid = b5PantothenicAcid?.grams, - vitaminB6 = b6Pyridoxine?.grams, - biotin = b7Biotin?.grams, - folate = b9Folate?.grams, - vitaminB12 = b12Cobalamin?.grams, - vitaminC = vitaminC?.grams, - vitaminD = vitaminD?.grams, - vitaminE = vitaminE?.grams, - vitaminK = vitaminK?.grams, - calcium = calcium?.grams, - chloride = chloride?.grams, - cholesterol = cholesterol?.grams, - chromium = chromium?.grams, - copper = copper?.grams, - unsaturatedFat = fatUnsaturated?.grams, - monounsaturatedFat = fatMonounsaturated?.grams, - polyunsaturatedFat = fatPolyunsaturated?.grams, - saturatedFat = fatSaturated?.grams, - transFat = fatTransMonoenoic?.grams, - dietaryFiber = fiber?.grams, - iodine = iodine?.grams, - iron = iron?.grams, - magnesium = magnesium?.grams, - manganese = manganese?.grams, - molybdenum = molybdenum?.grams, - phosphorus = phosphorus?.grams, - potassium = potassium?.grams, - selenium = selenium?.grams, - sodium = sodium?.grams, - sugar = sugar?.grams, - zinc = zinc?.grams, - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - mealType = - MapMealTypeToTypeHC[ - mealType] - ?: MEAL_TYPE_UNKNOWN, - ), - ) - healthConnectClient.insertRecords( - list, - ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Meal was successfully added!" - ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the meal", - ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } - } - - /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ - private fun writeMeal(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeMealHC(call, result) - return - } - - if (context == null) { - result.success(false) - return - } - - val startTime = call.argument("start_time")!! - val endTime = call.argument("end_time")!! - val calories = call.argument("calories") - val carbs = call.argument("carbs") as Double? - val protein = call.argument("protein") as Double? - val fat = call.argument("fat") as Double? - - - val name = call.argument("name") - val mealType = call.argument("meal_type")!! - - val dataType = DataType.TYPE_NUTRITION - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val nutrients = mutableMapOf(Field.NUTRIENT_CALORIES to calories?.toFloat()) - - if (carbs != null) { - nutrients[Field.NUTRIENT_TOTAL_CARBS] = carbs.toFloat() - } - - if (protein != null) { - nutrients[Field.NUTRIENT_PROTEIN] = protein.toFloat() - } - - if (fat != null) { - nutrients[Field.NUTRIENT_TOTAL_FAT] = fat.toFloat() - } - - val dataBuilder = - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_NUTRIENTS, - // Remove null values - nutrients.filterValues { it != null }.toMutableMap(), - ) - - if (name != null) { - dataBuilder.setField(Field.FIELD_FOOD_ITEM, name as String) - } - - dataBuilder.setField( - Field.FIELD_MEAL_TYPE, - MapMealTypeToType[mealType] ?: Field.MEAL_TYPE_UNKNOWN - ) - - val dataPoint = dataBuilder.build() - - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Meal added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the meal data!" - ) - ) - } catch (e3: Exception) { - result.success(false) - } - } - - /** Save a data type in Google Fit */ - private fun writeData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val value = call.argument("value")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - if (startTime == endTime) { - DataPoint.builder(dataSource) - .setTimestamp( - startTime, - TimeUnit.MILLISECONDS - ) - } else { - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - } - - // Conversion is needed because glucose is stored as mmoll in Google Fit; - // while mgdl is used for glucose in this plugin. - val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - val dataPoint = - if (!isIntField(dataSource, field)) { - builder.setField( - field, - (if (!isGlucose) value - else - (value / - MMOLL_2_MGDL) - .toFloat()) - ) - .build() - } else { - builder.setField(field, value.toInt()).build() - } - - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Dataset added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the dataset" - ) - ) - } catch (e3: Exception) { - result.success(false) - } - } - - /** - * Save menstrual flow data - */ - private fun writeMenstruationFlow(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } - } - - /** - * Save the blood oxygen saturation, in Google Fit with the supplemental flow rate, in - * HealthConnect without - */ - private fun writeBloodOxygen(call: MethodCall, result: Result) { - // Health Connect does not support supplemental flow rate, thus it is ignored - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } - - if (context == null) { - result.success(false) - return - } - - val dataType = HealthDataTypes.TYPE_OXYGEN_SATURATION - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val saturation = call.argument("value")!! - val flowRate = call.argument("flowRate")!! - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - if (startTime == endTime) { - DataPoint.builder(dataSource) - .setTimestamp( - startTime, - TimeUnit.MILLISECONDS - ) - } else { - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - } - - builder.setField(HealthFields.FIELD_SUPPLEMENTAL_OXYGEN_FLOW_RATE, flowRate) - builder.setField(HealthFields.FIELD_OXYGEN_SATURATION, saturation) - - val dataPoint = builder.build() - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Blood Oxygen added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the blood oxygen data!", - ), - ) - } catch (e3: Exception) { - result.success(false) - } - } - - /** Save a Workout session with options for distance and calories expended */ - private fun writeWorkoutData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeWorkoutHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val type = call.argument("activityType")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val totalEnergyBurned = call.argument("totalEnergyBurned") - val totalDistance = call.argument("totalDistance") - - val activityType = getActivityType(type) - // Create the Activity Segment DataSource - val activitySegmentDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType(DataType.TYPE_ACTIVITY_SEGMENT) - .setStreamName("FLUTTER_HEALTH - Activity") - .setType(DataSource.TYPE_RAW) - .build() - // Create the Activity Segment - val activityDataPoint = - DataPoint.builder(activitySegmentDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setActivityField( - Field.FIELD_ACTIVITY, - activityType - ) - .build() - // Add DataPoint to DataSet - val activitySegments = - DataSet.builder(activitySegmentDataSource) - .add(activityDataPoint) - .build() - - // If distance is provided - var distanceDataSet: DataSet? = null - if (totalDistance != null) { - // Create a data source - val distanceDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType(DataType.TYPE_DISTANCE_DELTA) - .setStreamName("FLUTTER_HEALTH - Distance") - .setType(DataSource.TYPE_RAW) - .build() - - val distanceDataPoint = - DataPoint.builder(distanceDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_DISTANCE, - totalDistance.toFloat() - ) - .build() - // Create a data set - distanceDataSet = - DataSet.builder(distanceDataSource) - .add(distanceDataPoint) - .build() - } - // If energyBurned is provided - var energyDataSet: DataSet? = null - if (totalEnergyBurned != null) { - // Create a data source - val energyDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType( - DataType.TYPE_CALORIES_EXPENDED - ) - .setStreamName("FLUTTER_HEALTH - Calories") - .setType(DataSource.TYPE_RAW) - .build() - - val energyDataPoint = - DataPoint.builder(energyDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_CALORIES, - totalEnergyBurned.toFloat() - ) - .build() - // Create a data set - energyDataSet = - DataSet.builder(energyDataSource) - .add(energyDataPoint) - .build() - } - - // Finish session setup - val session = - Session.Builder() - .setName( - activityType - ) // TODO: Make a sensible name / allow user to set - // name - .setDescription("") - .setIdentifier(UUID.randomUUID().toString()) - .setActivity(activityType) - .setStartTime(startTime, TimeUnit.MILLISECONDS) - .setEndTime(endTime, TimeUnit.MILLISECONDS) - .build() - // Build a session and add the values provided - val sessionInsertRequestBuilder = - SessionInsertRequest.Builder() - .setSession(session) - .addDataSet(activitySegments) - if (totalDistance != null) { - sessionInsertRequestBuilder.addDataSet(distanceDataSet!!) - } - if (totalEnergyBurned != null) { - sessionInsertRequestBuilder.addDataSet(energyDataSet!!) - } - val insertRequest = sessionInsertRequestBuilder.build() - - val fitnessOptionsBuilder = - FitnessOptions.builder() - .addDataType( - DataType.TYPE_ACTIVITY_SEGMENT, - FitnessOptions.ACCESS_WRITE - ) - if (totalDistance != null) { - fitnessOptionsBuilder.addDataType( - DataType.TYPE_DISTANCE_DELTA, - FitnessOptions.ACCESS_WRITE, - ) - } - if (totalEnergyBurned != null) { - fitnessOptionsBuilder.addDataType( - DataType.TYPE_CALORIES_EXPENDED, - FitnessOptions.ACCESS_WRITE, - ) - } - val fitnessOptions = fitnessOptionsBuilder.build() - - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount, - ) - .insertSession(insertRequest) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Workout was successfully added!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the workout" - ) - ) - } catch (e: Exception) { - result.success(false) - } - } - - /** Get all datapoints of the DataType within the given time range */ - private fun getData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getHCData(call, result) - return - } - - if (context == null) { - result.success(null) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val includeManualEntry = call.argument("includeManualEntry")!! - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType) - - // Add special cases for accessing workouts or sleep data. - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } else if (dataType == DataType.TYPE_ACTIVITY_SEGMENT) { - typesBuilder.accessActivitySessions(FitnessOptions.ACCESS_READ) - .addDataType( - DataType.TYPE_CALORIES_EXPENDED, - FitnessOptions.ACCESS_READ - ) - .addDataType( - DataType.TYPE_DISTANCE_DELTA, - FitnessOptions.ACCESS_READ - ) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - // Handle data types - when (dataType) { - DataType.TYPE_SLEEP_SEGMENT -> { - // request to the sessions for sleep data - val request = - SessionReadRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .enableServerQueries() - .readSessionsFromAllApps() - .includeSleepSessions() - .build() - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount - ) - .readSession(request) - .addOnSuccessListener( - threadPoolExecutor!!, - sleepDataHandler(type, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the sleeping data!", - ), - ) - } - DataType.TYPE_ACTIVITY_SEGMENT -> { - val readRequest: SessionReadRequest - val readRequestBuilder = - SessionReadRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .enableServerQueries() - .readSessionsFromAllApps() - .includeActivitySessions() - .read(dataType) - .read( - DataType.TYPE_CALORIES_EXPENDED - ) - - // If fine location is enabled, read distance data - if (ContextCompat.checkSelfPermission( - context!!.applicationContext, - android.Manifest.permission - .ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - ) { - readRequestBuilder.read(DataType.TYPE_DISTANCE_DELTA) - } - readRequest = readRequestBuilder.build() - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount - ) - .readSession(readRequest) - .addOnSuccessListener( - threadPoolExecutor!!, - workoutDataHandler(type, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the workout data!", - ), - ) - } - else -> { - Fitness.getHistoryClient( - context!!.applicationContext, - googleSignInAccount - ) - .readData( - DataReadRequest.Builder() - .read(dataType) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .build(), - ) - .addOnSuccessListener( - threadPoolExecutor!!, - dataHandler( - dataType, - field, - includeManualEntry, - result - ), - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the data!", - ), - ) - } - } - } - - private fun getIntervalData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getAggregateHCData(call, result) - return - } - - if (context == null) { - result.success(null) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val interval = call.argument("interval")!! - val includeManualEntry = call.argument("includeManualEntry")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType) - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .readData( - DataReadRequest.Builder() - .aggregate(dataType) - .bucketByTime( - interval, - TimeUnit.SECONDS - ) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .build() - ) - .addOnSuccessListener( - threadPoolExecutor!!, - intervalDataHandler( - dataType, - field, - includeManualEntry, - result - ) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the interval data!" - ) - ) - } - - private fun getAggregateData(call: MethodCall, result: Result) { - if (context == null) { - result.success(null) - return - } - - val types = call.argument>("dataTypeKeys")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val activitySegmentDuration = call.argument("activitySegmentDuration")!! - val includeManualEntry = call.argument("includeManualEntry")!! - - val typesBuilder = FitnessOptions.builder() - for (type in types) { - val dataType = keyToHealthDataType(type) - typesBuilder.addDataType(dataType) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - - val readWorkoutsRequest = - DataReadRequest.Builder() - .bucketByActivitySegment( - activitySegmentDuration, - TimeUnit.SECONDS - ) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - - for (type in types) { - val dataType = keyToHealthDataType(type) - readWorkoutsRequest.aggregate(dataType) - } - - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .readData(readWorkoutsRequest.build()) - .addOnSuccessListener( - threadPoolExecutor!!, - aggregateDataHandler(includeManualEntry, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the aggregate data!" - ) - ) - } - - private fun dataHandler( - dataType: DataType, - field: Field, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - // / Fetch all data points for the specified DataType - val dataSet = response.getDataSet(dataType) - /// For each data point, extract the contents and send them to Flutter, along with - // date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource.streamName.contains( - "user_input" - ) - } - } - // For each data point, extract the contents and send them to Flutter, along with - // date and unit. - val healthData = - dataPoints.mapIndexed { _, dataPoint -> - return@mapIndexed hashMapOf( - "value" to - getHealthDataValue( - dataPoint, - field - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ) - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun errHandler(result: Result, addMessage: String) = - OnFailureListener { exception -> - Handler(context!!.mainLooper).run { result.success(null) } - Log.w("FLUTTER_HEALTH::ERROR", addMessage) - Log.w("FLUTTER_HEALTH::ERROR", exception.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", exception.stackTrace.toString()) - } - - private fun sleepDataHandler(type: String, result: Result) = - OnSuccessListener { response: SessionReadResponse -> - val healthData: MutableList> = mutableListOf() - for (session in response.sessions) { - // Return sleep time in Minutes if requested ASLEEP data - if (type == SLEEP_ASLEEP) { - healthData.add( - hashMapOf( - "value" to - session.getEndTime( - TimeUnit.MINUTES - ) - - session.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - - if (type == SLEEP_IN_BED) { - val dataSets = response.getDataSet(session) - - // If the sleep session has finer granularity - // sub-components, extract them: - if (dataSets.isNotEmpty()) { - for (dataSet in dataSets) { - for (dataPoint in - dataSet.dataPoints) { - // searching OUT OF BED data - if (dataPoint.getValue( - Field.FIELD_SLEEP_SEGMENT_TYPE - ) - .asInt() != - 3 - ) { - healthData.add( - hashMapOf( - "value" to - dataPoint.getEndTime( - TimeUnit.MINUTES - ) - - dataPoint.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "unknown")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ), - ) - } - } - } - } else { - healthData.add( - hashMapOf( - "value" to - session.getEndTime( - TimeUnit.MINUTES - ) - - session.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - } - - if (type == SLEEP_AWAKE) { - val dataSets = response.getDataSet(session) - for (dataSet in dataSets) { - for (dataPoint in dataSet.dataPoints) { - // searching SLEEP AWAKE data - if (dataPoint.getValue( - Field.FIELD_SLEEP_SEGMENT_TYPE - ) - .asInt() == - 1 - ) { - healthData.add( - hashMapOf( - "value" to - dataPoint.getEndTime( - TimeUnit.MINUTES - ) - - dataPoint.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "unknown")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ), - ) - } - } - } - } - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun intervalDataHandler( - dataType: DataType, - field: Field, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - val healthData = mutableListOf>() - for (bucket in response.buckets) { - /// Fetch all data points for the specified DataType - // val dataSet = response.getDataSet(dataType) - for (dataSet in bucket.dataSets) { - /// For each data point, extract the contents and send them to - // Flutter, along with - // date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dataPoint in dataPoints) { - for (field in dataPoint.dataType.fields) { - val healthDataItems = - dataPoints.mapIndexed { _, dataPoint - -> - return@mapIndexed hashMapOf( - "value" to - getHealthDataValue( - dataPoint, - field - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - "is_manual_entry" to - dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - ) - } - healthData.addAll(healthDataItems) - } - } - } - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun aggregateDataHandler(includeManualEntry: Boolean, result: Result) = - OnSuccessListener { response: DataReadResponse -> - val healthData = mutableListOf>() - for (bucket in response.buckets) { - var sourceName: Any = "" - var sourceId: Any = "" - var isManualEntry: Any = false - var totalSteps: Any = 0 - var totalDistance: Any = 0 - var totalEnergyBurned: Any = 0 - /// Fetch all data points for the specified DataType - for (dataSet in bucket.dataSets) { - /// For each data point, extract the contents and - // send them to Flutter, - // along with date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { - _, - dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dataPoint in dataPoints) { - sourceName = - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")) - sourceId = - dataPoint.originalDataSource - .streamIdentifier - isManualEntry = - dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - for (field in dataPoint.dataType.fields) { - when (field) { - getField(STEPS) -> { - totalSteps = - getHealthDataValue( - dataPoint, - field - ) - } - getField( - DISTANCE_DELTA - ) -> { - totalDistance = - getHealthDataValue( - dataPoint, - field - ) - } - getField( - ACTIVE_ENERGY_BURNED - ) -> { - totalEnergyBurned = - getHealthDataValue( - dataPoint, - field - ) - } - } - } - } - } - val healthDataItems = - hashMapOf( - "value" to - bucket.getEndTime( - TimeUnit.MINUTES - ) - - bucket.getStartTime( - TimeUnit.MINUTES - ), - "date_from" to - bucket.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - bucket.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to sourceName, - "source_id" to sourceId, - "is_manual_entry" to - isManualEntry, - "workout_type" to - bucket.activity - .toLowerCase(), - "total_steps" to totalSteps, - "total_distance" to - totalDistance, - "total_energy_burned" to - totalEnergyBurned - ) - healthData.add(healthDataItems) - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun workoutDataHandler(type: String, result: Result) = - OnSuccessListener { response: SessionReadResponse -> - val healthData: MutableList> = mutableListOf() - for (session in response.sessions) { - // Look for calories and distance if they - var totalEnergyBurned = 0.0 - var totalDistance = 0.0 - for (dataSet in response.getDataSet(session)) { - if (dataSet.dataType == - DataType.TYPE_CALORIES_EXPENDED - ) { - for (dataPoint in dataSet.dataPoints) { - totalEnergyBurned += - dataPoint.getValue( - Field.FIELD_CALORIES - ) - .toString() - .toDouble() - } - } - if (dataSet.dataType == DataType.TYPE_DISTANCE_DELTA - ) { - for (dataPoint in dataSet.dataPoints) { - totalDistance += - dataPoint.getValue( - Field.FIELD_DISTANCE - ) - .toString() - .toDouble() - } - } - } - healthData.add( - hashMapOf( - "workoutActivityType" to - (workoutTypeMap - .filterValues { - it == - session.activity - } - .keys - .firstOrNull() - ?: "OTHER"), - "totalEnergyBurned" to - if (totalEnergyBurned == - 0.0 - ) - null - else - totalEnergyBurned, - "totalEnergyBurnedUnit" to - "KILOCALORIE", - "totalDistance" to - if (totalDistance == - 0.0 - ) - null - else - totalDistance, - "totalDistanceUnit" to - "METER", - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun callToHealthTypes(call: MethodCall): FitnessOptions { - val typesBuilder = FitnessOptions.builder() - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance() - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance() - - assert(types != null) - assert(permissions != null) - assert(types!!.count() == permissions!!.count()) - - for ((i, typeKey) in types.withIndex()) { - val access = permissions[i] - val dataType = keyToHealthDataType(typeKey) - when (access) { - 0 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_READ) - 1 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - 2 -> { - typesBuilder.addDataType( - dataType, - FitnessOptions.ACCESS_READ - ) - typesBuilder.addDataType( - dataType, - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } - if (typeKey == SLEEP_ASLEEP || - typeKey == SLEEP_AWAKE || - typeKey == SLEEP_IN_BED - ) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - when (access) { - 0 -> - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_READ - ) - 1 -> - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_WRITE - ) - 2 -> { - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_READ - ) - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } - } - if (typeKey == WORKOUT) { - when (access) { - 0 -> - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_READ - ) - 1 -> - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_WRITE - ) - 2 -> { - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_READ - ) - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } - } - } - return typesBuilder.build() - } - - private fun hasPermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - hasPermissionsHC(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val optionsToRegister = callToHealthTypes(call) - val isGranted = - GoogleSignIn.hasPermissions( - GoogleSignIn.getLastSignedInAccount(context!!), - optionsToRegister, - ) - - result?.success(isGranted) +class HealthPlugin(private var channel: MethodChannel? = null) : + MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin { + private var mResult: Result? = null + private var handler: Handler? = null + private var activity: Activity? = null + private var context: Context? = null + private var threadPoolExecutor: ExecutorService? = null + private var healthConnectRequestPermissionsLauncher: ActivityResultLauncher>? = + null + private lateinit var healthConnectClient: HealthConnectClient + private lateinit var scope: CoroutineScope + + + override fun onAttachedToEngine( + @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding + ) { + scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel?.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + threadPoolExecutor = Executors.newFixedThreadPool(4) + checkAvailability() + if (healthConnectAvailable) { + healthConnectClient = + HealthConnectClient.getOrCreate( + flutterPluginBinding.applicationContext + ) } - - /** - * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission - * type. - */ - private fun requestAuthorization(call: MethodCall, result: Result) { - if (context == null) { - result.success(false) - return - } - mResult = result - - if (useHealthConnectIfAvailable && healthConnectAvailable) { - requestAuthorizationHC(call, result) - return - } - - val optionsToRegister = callToHealthTypes(call) - - // Set to false due to bug described in - // https://github.com/cph-cachet/flutter-plugins/issues/640#issuecomment-1366830132 - val isGranted = false - - // If not granted then ask for permission - if (!isGranted && activity != null) { - GoogleSignIn.requestPermissions( - activity!!, - GOOGLE_FIT_PERMISSIONS_REQUEST_CODE, - GoogleSignIn.getLastSignedInAccount(context!!), - optionsToRegister, - ) - } else { // / Permission already granted - result?.success(true) - } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel = null + activity = null + threadPoolExecutor!!.shutdown() + threadPoolExecutor = null + } + + // This static function is optional and equivalent to onAttachedToEngine. It supports the + // old + // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting + // plugin registration via this function while apps migrate to use the new Android APIs + // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. + // + // It is encouraged to share logic between onAttachedToEngine and registerWith to keep + // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be + // called + // depending on the user's project. onAttachedToEngine or registerWith must both be defined + // in the same class. + companion object { + @Suppress("unused") + @JvmStatic + fun registerWith(registrar: Registrar) { + val channel = MethodChannel(registrar.messenger(), CHANNEL_NAME) + val plugin = HealthPlugin(channel) + registrar.addActivityResultListener(plugin) + channel.setMethodCallHandler(plugin) } - - /** - * Revokes access to Health Connect using `revokeAllPermissions` and Google Fit using the `disableFit`-method. - * - * Note: Using the `revokeAccess` creates a bug on android when trying to reapply for - * permissions afterwards, hence `disableFit` was used. - * Note: When using `revokePermissions` with Health Connect, the app must be completely killed - * for it to take effect. - */ - private fun revokePermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - scope.launch { - Log.i("Health", "Disabling Health Connect") - healthConnectClient.permissionController.revokeAllPermissions() - } - result.success(true) - } - if (context == null) { - result.success(false) - return - } - Fitness.getConfigClient( - activity!!, - GoogleSignIn.getLastSignedInAccount(context!!)!! - ) - .disableFit() - .addOnSuccessListener { - Log.i("Health", "Disabled Google Fit") - result.success(true) - } - .addOnFailureListener { e -> - Log.w( - "Health", - "There was an error disabling Google Fit", - e - ) - result.success(false) - } + } + + override fun success(p0: Any?) { + handler?.post { mResult?.success(p0) } + } + + override fun notImplemented() { + handler?.post { mResult?.notImplemented() } + } + + override fun error( + errorCode: String, + errorMessage: String?, + errorDetails: Any?, + ) { + handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + return false + } + + /** Handle calls from the MethodChannel */ + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "installHealthConnect" -> installHealthConnect(call, result) + "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) + "hasPermissions" -> hasPermissions(call, result) + "requestAuthorization" -> requestAuthorization(call, result) + "revokePermissions" -> revokePermissions(call, result) + "getData" -> getData(call, result) + "getIntervalData" -> getIntervalData(call, result) + "writeData" -> writeData(call, result) + "delete" -> deleteData(call, result) + "getAggregateData" -> getAggregateData(call, result) + "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) + "writeWorkoutData" -> writeWorkoutData(call, result) + "writeBloodPressure" -> writeBloodPressure(call, result) + "writeBloodOxygen" -> writeBloodOxygen(call, result) + "writeMenstruationFlow" -> writeMenstruationFlow(call, result) + "writeMeal" -> writeMeal(call, result) + else -> result.notImplemented() } + } - private fun getTotalStepsInInterval(call: MethodCall, result: Result) { - val start = call.argument("startTime")!! - val end = call.argument("endTime")!! - val includeManualEntry = call.argument("includeManualEntry")!! - - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getStepsHealthConnect(start, end, result) - return - } - - val context = context ?: return - - val stepsDataType = keyToHealthDataType(STEPS) - val aggregatedDataType = keyToHealthDataType(AGGREGATE_STEP_COUNT) - - val fitnessOptions = - FitnessOptions.builder() - .addDataType(stepsDataType) - .addDataType(aggregatedDataType) - .build() - val gsa = GoogleSignIn.getAccountForExtension(context, fitnessOptions) - - val ds = - DataSource.Builder() - .setAppPackageName("com.google.android.gms") - .setDataType(stepsDataType) - .setType(DataSource.TYPE_DERIVED) - .setStreamName("estimated_steps") - .build() - - val duration = (end - start).toInt() - - val request = - DataReadRequest.Builder() - .read(ds) - .bucketByTime(duration, TimeUnit.MILLISECONDS) - .setTimeRange(start, end, TimeUnit.MILLISECONDS) - .build() - - Fitness.getHistoryClient(context, gsa) - .readData(request) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the total steps in the interval!", - ), - ) - .addOnSuccessListener( - threadPoolExecutor!!, - getStepsInRange( - start, - end, - includeManualEntry, - result - ), - ) + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + if (channel == null) { + return } + binding.addActivityResultListener(this) + activity = binding.activity - private fun getStepsHealthConnect(start: Long, end: Long, result: Result) = - scope.launch { - try { - val startInstant = Instant.ofEpochMilli(start) - val endInstant = Instant.ofEpochMilli(end) - val response = - healthConnectClient.aggregate( - AggregateRequest( - metrics = - setOf( - StepsRecord.COUNT_TOTAL - ), - timeRangeFilter = - TimeRangeFilter.between( - startInstant, - endInstant - ), - ), - ) - // The result may be null if no data is available in the - // time range. - val stepsInInterval = - response[StepsRecord.COUNT_TOTAL] ?: 0L - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "returning $stepsInInterval steps" - ) - result.success(stepsInInterval) - } catch (e: Exception) { - Log.e("FLUTTER_HEALTH::ERROR", "Unable to return steps due to the following exception:") - Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) - } - } - - private fun getStepsInRange( - start: Long, - end: Long, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - var totalSteps = 0 // Variable to accumulate the total steps. - - for (bucket in response.buckets) { - for (dataSet in bucket.dataSets) { - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dp in dataPoints) { - val streamName = dp.originalDataSource.streamName - if (!includeManualEntry && streamName.contains("user_input")) { - // Skip this data point if manual entry is not included - Log.i("FLUTTER_HEALTH::SKIPPED", "Skipping manual entry data point with stream name $streamName") - continue - } - - val count = dp.getValue(Field.FIELD_STEPS) - totalSteps += count.asInt() + val requestPermissionActivityContract = + PermissionController.createRequestPermissionResultContract() - val startTime = dp.getStartTime(TimeUnit.MILLISECONDS) - val startDate = Date(startTime) - val endDate = Date(dp.getEndTime(TimeUnit.MILLISECONDS)) - Log.i( - "FLUTTER_HEALTH::INFO", - "adding $count steps for $startDate - $endDate. Total so far: $totalSteps", - ) - } - } - } + healthConnectRequestPermissionsLauncher = + (activity as ComponentActivity).registerForActivityResult( + requestPermissionActivityContract + ) { granted -> onHealthConnectPermissionCallback(granted) } + } - if (totalSteps == 0) { - val startDay = Date(start) - val endDay = Date(end) - Log.i("FLUTTER_HEALTH::ERROR", "no steps for $startDay - $endDay") - } + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } - Log.i("FLUTTER_HEALTH::SUCCESS", "Final total steps in interval: $totalSteps") + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } - Handler(context!!.mainLooper).run { result.success(totalSteps) } + override fun onDetachedFromActivity() { + if (channel == null) { + return } - - /// Disconnect Google fit - private fun disconnect(call: MethodCall, result: Result) { - if (activity == null) { - result.success(false) - return - } - val context = activity!!.applicationContext - - val fitnessOptions = callToHealthTypes(call) - val googleAccount = GoogleSignIn.getAccountForExtension(context, fitnessOptions) - Fitness.getConfigClient(context, googleAccount).disableFit().continueWith { - val signinOption = - GoogleSignInOptions.Builder( - GoogleSignInOptions - .DEFAULT_SIGN_IN - ) - .requestId() - .requestEmail() - .build() - val googleSignInClient = GoogleSignIn.getClient(context, signinOption) - googleSignInClient.signOut() - result.success(true) - } + activity = null + healthConnectRequestPermissionsLauncher = null + } + + private var healthConnectAvailable = false + private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + + private fun checkAvailability() { + healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) + 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) + } + + private fun onHealthConnectPermissionCallback(permissionGranted: Set) { + if (permissionGranted.isEmpty()) { + mResult?.success(false) + Log.i("FLUTTER_HEALTH", "Health Connect permissions were not granted! Make sure to declare the required permissions in the AndroidManifest.xml file.") + } else { + mResult?.success(true) + Log.i("FLUTTER_HEALTH", "${permissionGranted.size} Health Connect permissions were granted!") + + // log the permissions granted for debugging + Log.i("FLUTTER_HEALTH", "Permissions granted: $permissionGranted") } - - private fun getActivityType(type: String): String { - return workoutTypeMap[type] ?: FitnessActivities.UNKNOWN + } + + /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ + private fun writeMeal(call: MethodCall, result: Result) { + val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) + val endTime = Instant.ofEpochMilli(call.argument("end_time")!!) + val calories = call.argument("calories") + val protein = call.argument("protein") as Double? + val carbs = call.argument("carbs") as Double? + val fat = call.argument("fat") as Double? + val caffeine = call.argument("caffeine") as Double? + val vitaminA = call.argument("vitamin_a") as Double? + val b1Thiamine = call.argument("b1_thiamine") as Double? + val b2Riboflavin = call.argument("b2_riboflavin") as Double? + val b3Niacin = call.argument("b3_niacin") as Double? + val b5PantothenicAcid = call.argument("b5_pantothenic_acid") as Double? + val b6Pyridoxine = call.argument("b6_pyridoxine") as Double? + val b7Biotin = call.argument("b7_biotin") as Double? + val b9Folate = call.argument("b9_folate") as Double? + val b12Cobalamin = call.argument("b12_cobalamin") as Double? + val vitaminC = call.argument("vitamin_c") as Double? + val vitaminD = call.argument("vitamin_d") as Double? + val vitaminE = call.argument("vitamin_e") as Double? + val vitaminK = call.argument("vitamin_k") as Double? + val calcium = call.argument("calcium") as Double? + val chloride = call.argument("chloride") as Double? + val cholesterol = call.argument("cholesterol") as Double? + // Choline is not yet supported by Health Connect + // val choline = call.argument("choline") as Double? + val chromium = call.argument("chromium") as Double? + val copper = call.argument("copper") as Double? + val fatUnsaturated = call.argument("fat_unsaturated") as Double? + val fatMonounsaturated = call.argument("fat_monounsaturated") as Double? + val fatPolyunsaturated = call.argument("fat_polyunsaturated") as Double? + val fatSaturated = call.argument("fat_saturated") as Double? + val fatTransMonoenoic = call.argument("fat_trans_monoenoic") as Double? + val fiber = call.argument("fiber") as Double? + val iodine = call.argument("iodine") as Double? + val iron = call.argument("iron") as Double? + val magnesium = call.argument("magnesium") as Double? + val manganese = call.argument("manganese") as Double? + val molybdenum = call.argument("molybdenum") as Double? + val phosphorus = call.argument("phosphorus") as Double? + val potassium = call.argument("potassium") as Double? + val selenium = call.argument("selenium") as Double? + val sodium = call.argument("sodium") as Double? + val sugar = call.argument("sugar") as Double? + // Water is not support on a food in Health Connect + // val water = call.argument("water") as Double? + val zinc = call.argument("zinc") as Double? + + val name = call.argument("name") + val mealType = call.argument("meal_type")!! + + scope.launch { + try { + val list = mutableListOf() + list.add( + NutritionRecord( + name = name, + energy = calories?.kilocalories, + totalCarbohydrate = carbs?.grams, + protein = protein?.grams, + totalFat = fat?.grams, + caffeine = caffeine?.grams, + vitaminA = vitaminA?.grams, + thiamin = b1Thiamine?.grams, + riboflavin = b2Riboflavin?.grams, + niacin = b3Niacin?.grams, + pantothenicAcid = b5PantothenicAcid?.grams, + vitaminB6 = b6Pyridoxine?.grams, + biotin = b7Biotin?.grams, + folate = b9Folate?.grams, + vitaminB12 = b12Cobalamin?.grams, + vitaminC = vitaminC?.grams, + vitaminD = vitaminD?.grams, + vitaminE = vitaminE?.grams, + vitaminK = vitaminK?.grams, + calcium = calcium?.grams, + chloride = chloride?.grams, + cholesterol = cholesterol?.grams, + chromium = chromium?.grams, + copper = copper?.grams, + unsaturatedFat = fatUnsaturated?.grams, + monounsaturatedFat = fatMonounsaturated?.grams, + polyunsaturatedFat = fatPolyunsaturated?.grams, + saturatedFat = fatSaturated?.grams, + transFat = fatTransMonoenoic?.grams, + dietaryFiber = fiber?.grams, + iodine = iodine?.grams, + iron = iron?.grams, + magnesium = magnesium?.grams, + manganese = manganese?.grams, + molybdenum = molybdenum?.grams, + phosphorus = phosphorus?.grams, + potassium = potassium?.grams, + selenium = selenium?.grams, + sodium = sodium?.grams, + sugar = sugar?.grams, + zinc = zinc?.grams, + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + mealType = + mapMealTypeToType[ + mealType] + ?: MEAL_TYPE_UNKNOWN, + ), + ) + healthConnectClient.insertRecords( + list, + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Meal was successfully added!" + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the meal", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } } - - /** Handle calls from the MethodChannel */ - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "installHealthConnect" -> installHealthConnect(call, result) - "useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result) - "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) - "hasPermissions" -> hasPermissions(call, result) - "requestAuthorization" -> requestAuthorization(call, result) - "revokePermissions" -> revokePermissions(call, result) - "getData" -> getData(call, result) - "getIntervalData" -> getIntervalData(call, result) - "writeData" -> writeData(call, result) - "delete" -> delete(call, result) - "getAggregateData" -> getAggregateData(call, result) - "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) - "writeWorkoutData" -> writeWorkoutData(call, result) - "writeBloodPressure" -> writeBloodPressure(call, result) - "writeBloodOxygen" -> writeBloodOxygen(call, result) - "writeMenstruationFlow" -> writeMenstruationFlow(call, result) - "writeMeal" -> writeMeal(call, result) - "disconnect" -> disconnect(call, result) - else -> result.notImplemented() - } + } + + /** + * Save menstrual flow data + */ + private fun writeMenstruationFlow(call: MethodCall, result: Result) { + writeData(call, result) + } + + /** + * Save the blood oxygen saturation + */ + private fun writeBloodOxygen(call: MethodCall, result: Result) { + writeData(call, result) + } + + private fun getIntervalData(call: MethodCall, result: Result) { + getAggregateData(call, result) + } + + /** + * Revokes access to Health Connect using `revokeAllPermissions`. + * + * Note: When using `revokePermissions` with Health Connect, the app must be completely killed + * for it to take effect. + */ + private fun revokePermissions(call: MethodCall, result: Result) { + scope.launch { + Log.i("Health", "Disabling Health Connect") + healthConnectClient.permissionController.revokeAllPermissions() } + result.success(true) + } + + private fun getTotalStepsInInterval(call: MethodCall, result: Result) { + val start = call.argument("startTime")!! + val end = call.argument("endTime")!! + + scope.launch { + try { + val startInstant = Instant.ofEpochMilli(start) + val endInstant = Instant.ofEpochMilli(end) + val response = + healthConnectClient.aggregate( + AggregateRequest( + metrics = + setOf( + StepsRecord.COUNT_TOTAL + ), + timeRangeFilter = + TimeRangeFilter.between( + startInstant, + endInstant + ), + ), + ) + // The result may be null if no data is available in the + // time range. + val stepsInInterval = + response[StepsRecord.COUNT_TOTAL] ?: 0L + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "returning $stepsInInterval steps" + ) + result.success(stepsInInterval) + } catch (e: Exception) { + Log.e( + "FLUTTER_HEALTH::ERROR", + "Unable to return steps due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } + } + } - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - if (channel == null) { - return - } - binding.addActivityResultListener(this) - activity = binding.activity - - val requestPermissionActivityContract = - PermissionController.createRequestPermissionResultContract() - healthConnectRequestPermissionsLauncher = - (activity as ComponentActivity).registerForActivityResult( - requestPermissionActivityContract - ) { granted -> onHealthConnectPermissionCallback(granted) } + private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { + checkAvailability() + if (healthConnectAvailable) { + healthConnectClient = + HealthConnectClient.getOrCreate( + context!! + ) } - - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() + result.success(healthConnectStatus) + } + + private fun hasPermissions(call: MethodCall, result: Result) { + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + + val permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!mapToType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype $typeKey not found in HC" + ) + result.success(false) + return + } + val access = permissions[i] + val dataType = mapToType[typeKey]!! + if (access == 0) { + permList.add( + HealthPermission.getReadPermission(dataType), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + dataType + ), + HealthPermission.getWritePermission( + dataType + ), + ), + ) + } + // 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 + ), + ), + ) + } + } } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) + scope.launch { + result.success( + healthConnectClient + .permissionController + .getGrantedPermissions() + .containsAll(permList), + ) } - - override fun onDetachedFromActivity() { - if (channel == null) { - return - } - activity = null - healthConnectRequestPermissionsLauncher = null + } + + /** + * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission + * type. + */ + private fun requestAuthorization(call: MethodCall, result: Result) { + if (context == null) { + result.success(false) + return } - /** HEALTH CONNECT BELOW */ - var healthConnectAvailable = false - var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! - fun checkAvailability() { - healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) - 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) - } + val permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!mapToType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype $typeKey not found in HC" ) - result.success(null) + result.success(false) + return + } + val access = permissions[i]!! + val dataType = mapToType[typeKey]!! + if (access == 0) { + permList.add( + HealthPermission.getReadPermission(dataType), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + dataType + ), + HealthPermission.getWritePermission( + dataType + ), + ), + ) + } + // 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 + ), + ), + ) + } + } } - - fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { - useHealthConnectIfAvailable = true - result.success(null) + if (healthConnectRequestPermissionsLauncher == null) { + result.success(false) + Log.i("FLUTTER_HEALTH", "Permission launcher not found") + return } - private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { - checkAvailability() - if (healthConnectAvailable) { - healthConnectClient = - HealthConnectClient.getOrCreate( - context!! + // Store the result to be called in [onHealthConnectPermissionCallback] + mResult = result + healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) + } + + /** Get all datapoints of the DataType within the given time range */ + private fun getData(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val healthConnectData = mutableListOf>() + scope.launch { + try { + mapToType[dataType]?.let { classType -> + val records = mutableListOf() + + // Set up the initial request to read health records with specified + // parameters + var request = + ReadRecordsRequest( + recordType = classType, + // Define the maximum amount of data + // that HealthConnect can return + // in a single request + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), ) - } - result.success(healthConnectStatus) - } - private fun hasPermissionsHC(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + var response = healthConnectClient.readRecords(request) + var pageToken = response.pageToken + + // Add the records from the initial response to the records list + records.addAll(response.records) + + // Continue making requests and fetching records while there is a + // page token + while (!pageToken.isNullOrEmpty()) { + request = + ReadRecordsRequest( + recordType = classType, + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + pageToken = pageToken + ) + response = healthConnectClient.readRecords(request) + + pageToken = response.pageToken + records.addAll(response.records) + } - var permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" + // Workout needs distance and total calories burned too + if (dataType == WORKOUT) { + for (rec in records) { + val record = rec as ExerciseSessionRecord + val distanceRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + DistanceRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), ) - result.success(false) - return - } - val access = permissions[i] - val dataType = MapToHCType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), + var totalDistance = 0.0 + for (distanceRec in distanceRequest.records) { + totalDistance += + distanceRec.distance + .inMeters + } + + val energyBurnedRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + TotalCaloriesBurnedRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), + var totalEnergyBurned = 0.0 + for (energyBurnedRec in + energyBurnedRequest.records) { + totalEnergyBurned += + energyBurnedRec.energy + .inKilocalories + } + + val stepRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + StepsRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime + ), + ), ) + var totalSteps = 0.0 + for (stepRec in stepRequest.records) { + totalSteps += stepRec.count + } + + // val metadata = (rec as Record).metadata + // Add final datapoint + healthConnectData.add( + // mapOf( + mapOf( + "workoutActivityType" to + (workoutTypeMap + .filterValues { + it == + record.exerciseType + } + .keys + .firstOrNull() + ?: "OTHER"), + "totalDistance" to + if (totalDistance == + 0.0 + ) + null + else + totalDistance, + "totalDistanceUnit" to + "METER", + "totalEnergyBurned" to + if (totalEnergyBurned == + 0.0 + ) + null + else + totalEnergyBurned, + "totalEnergyBurnedUnit" to + "KILOCALORIE", + "totalSteps" to + if (totalSteps == + 0.0 + ) + null + else + totalSteps, + "totalStepsUnit" to + "COUNT", + "unit" to "MINUTES", + "date_from" to + rec.startTime + .toEpochMilli(), + "date_to" to + rec.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to + record.metadata + .dataOrigin + .packageName, + ), + ) } - // 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 - ), - ), + // Filter sleep stages for requested stage + } else if (classType == SleepSessionRecord::class) { + for (rec in response.records) { + if (rec is SleepSessionRecord) { + if (dataType == SLEEP_SESSION) { + healthConnectData.addAll( + convertRecord( + rec, + dataType ) + ) } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) + for (recStage in rec.stages) { + if (dataType == + mapSleepStageToType[ + recStage.stage] + ) { + healthConnectData + .addAll( + convertRecordStage( + recStage, + dataType, + rec.metadata.dataOrigin + .packageName + ) + ) + } + } } + } } + } else { + for (rec in records) { + healthConnectData.addAll( + convertRecord(rec, dataType) + ) + } + } } - scope.launch { - result.success( - healthConnectClient - .permissionController - .getGrantedPermissions() - .containsAll(permList), - ) - } + Handler(context!!.mainLooper).run { result.success(healthConnectData) } + } catch (e: Exception) { + Log.i( + "FLUTTER_HEALTH::ERROR", + "Unable to return $dataType due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } + } + + private fun convertRecordStage( + stage: SleepSessionRecord.Stage, + dataType: String, + sourceName: String + ): List> { + return listOf( + mapOf( + "stage" to stage.stage, + "value" to + ChronoUnit.MINUTES.between( + stage.startTime, + stage.endTime + ), + "date_from" to stage.startTime.toEpochMilli(), + "date_to" to stage.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to sourceName, + ), + ) + } + + private fun getAggregateData(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val interval = call.argument("interval")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val healthConnectData = mutableListOf>() + scope.launch { + try { + mapToAggregateMetric[dataType]?.let { metricClassType -> + val request = + AggregateGroupByDurationRequest( + metrics = setOf(metricClassType), + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + timeRangeSlicer = + Duration.ofSeconds( + interval + ) + ) + val response = healthConnectClient.aggregateGroupByDuration(request) + + for (durationResult in response) { + // The result may be null if no data is available in the + // time range + var totalValue = durationResult.result[metricClassType] + if (totalValue is Length) { + totalValue = totalValue.inMeters + } else if (totalValue is Energy) { + totalValue = totalValue.inKilocalories + } - private fun requestAuthorizationHC(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + val packageNames = + durationResult.result.dataOrigins + .joinToString { origin -> + origin.packageName + } - var permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" - ) - result.success(false) - return - } - val access = permissions[i]!! - val dataType = MapToHCType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), - ) - } - // 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 - ), - ), + val data = + mapOf( + "value" to + (totalValue + ?: 0), + "date_from" to + durationResult.startTime + .toEpochMilli(), + "date_to" to + durationResult.endTime + .toEpochMilli(), + "source_name" to + packageNames, + "source_id" to "", + "is_manual_entry" to + packageNames.contains( + "user_input" ) - } - } - } - if (healthConnectRequestPermissionsLauncher == null) { - result.success(false) - Log.i("FLUTTER_HEALTH", "Permission launcher not found") - return + ) + healthConnectData.add(data) + } } - - healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) + Handler(context!!.mainLooper).run { result.success(healthConnectData) } + } catch (e: Exception) { + Log.i( + "FLUTTER_HEALTH::ERROR", + "Unable to return $dataType due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } + } - fun getHCData(call: MethodCall, result: Result) { - val dataType = call.argument("dataTypeKey")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val healthConnectData = mutableListOf>() - scope.launch { - try { - MapToHCType[dataType]?.let { classType -> - val records = mutableListOf() + // TODO: Find alternative to SOURCE_ID or make it nullable? + private fun convertRecord(record: Any, dataType: String): List> { + val metadata = (record as Record).metadata + when (record) { + is WeightRecord -> + return listOf( + mapOf( + "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, + ), + ) - // Set up the initial request to read health records with specified - // parameters - var request = - ReadRecordsRequest( - recordType = classType, - // Define the maximum amount of data - // that HealthConnect can return - // in a single request - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - ) + is HeightRecord -> + return listOf( + mapOf( + "value" to + record.height + .inMeters, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - var response = healthConnectClient.readRecords(request) - var pageToken = response.pageToken + is BodyFatRecord -> + return listOf( + mapOf( + "value" to + record.percentage + .value, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - // Add the records from the initial response to the records list - records.addAll(response.records) + is StepsRecord -> + return listOf( + mapOf( + "value" to record.count, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - // Continue making requests and fetching records while there is a - // page token - while (!pageToken.isNullOrEmpty()) { - request = - ReadRecordsRequest( - recordType = classType, - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - pageToken = pageToken - ) - response = healthConnectClient.readRecords(request) + is ActiveCaloriesBurnedRecord -> + return listOf( + mapOf( + "value" to + record.energy + .inKilocalories, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - pageToken = response.pageToken - records.addAll(response.records) - } + is HeartRateRecord -> + return record.samples.map { + mapOf( + "value" to it.beatsPerMinute, + "date_from" to + it.time.toEpochMilli(), + "date_to" to it.time.toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + } + + is HeartRateVariabilityRmssdRecord -> + return listOf( + mapOf( + "value" to + record.heartRateVariabilityMillis, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - // Workout needs distance and total calories burned too - if (dataType == WORKOUT) { - for (rec in records) { - val record = rec as ExerciseSessionRecord - val distanceRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - DistanceRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime, - ), - ), - ) - var totalDistance = 0.0 - for (distanceRec in distanceRequest.records) { - totalDistance += - distanceRec.distance - .inMeters - } + is BodyTemperatureRecord -> + return listOf( + mapOf( + "value" to + record.temperature + .inCelsius, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - val energyBurnedRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - TotalCaloriesBurnedRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime, - ), - ), - ) - var totalEnergyBurned = 0.0 - for (energyBurnedRec in - energyBurnedRequest.records) { - totalEnergyBurned += - energyBurnedRec.energy - .inKilocalories - } + 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, + ), + ) - val stepRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - StepsRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime - ), - ), - ) - var totalSteps = 0.0 - for (stepRec in stepRequest.records) { - totalSteps += stepRec.count - } + is BloodPressureRecord -> + return listOf( + mapOf( + "value" to + if (dataType == + BLOOD_PRESSURE_DIASTOLIC + ) + record.diastolic + .inMillimetersOfMercury + else + record.systolic + .inMillimetersOfMercury, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - // val metadata = (rec as Record).metadata - // Add final datapoint - healthConnectData.add( - // mapOf( - mapOf( - "workoutActivityType" to - (workoutTypeMapHealthConnect - .filterValues { - it == - record.exerciseType - } - .keys - .firstOrNull() - ?: "OTHER"), - "totalDistance" to - if (totalDistance == - 0.0 - ) - null - else - totalDistance, - "totalDistanceUnit" to - "METER", - "totalEnergyBurned" to - if (totalEnergyBurned == - 0.0 - ) - null - else - totalEnergyBurned, - "totalEnergyBurnedUnit" to - "KILOCALORIE", - "totalSteps" to - if (totalSteps == - 0.0 - ) - null - else - totalSteps, - "totalStepsUnit" to - "COUNT", - "unit" to "MINUTES", - "date_from" to - rec.startTime - .toEpochMilli(), - "date_to" to - rec.endTime.toEpochMilli(), - "source_id" to "", - "source_name" to - record.metadata - .dataOrigin - .packageName, - ), - ) - } - // Filter sleep stages for requested stage - } else if (classType == SleepSessionRecord::class) { - for (rec in response.records) { - if (rec is SleepSessionRecord) { - if (dataType == SLEEP_SESSION) { - healthConnectData.addAll( - convertRecord( - rec, - dataType - ) - ) - } else { - for (recStage in rec.stages) { - if (dataType == - MapSleepStageToType[ - recStage.stage] - ) { - healthConnectData - .addAll( - convertRecordStage( - recStage, - dataType, - rec.metadata.dataOrigin - .packageName - ) - ) - } - } - } - } - } - } else { - for (rec in records) { - healthConnectData.addAll( - convertRecord(rec, dataType) - ) - } - } - } - Handler(context!!.mainLooper).run { result.success(healthConnectData) } - } catch (e: Exception) { - Log.i("FLUTTER_HEALTH::ERROR", "Unable to return $dataType due to the following exception:") - Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) - } - } - } + is OxygenSaturationRecord -> + return listOf( + mapOf( + "value" to + record.percentage + .value, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - fun convertRecordStage( - stage: SleepSessionRecord.Stage, - dataType: String, - sourceName: String - ): List> { + is BloodGlucoseRecord -> return listOf( - mapOf( - "stage" to stage.stage, - "value" to - ChronoUnit.MINUTES.between( - stage.startTime, - stage.endTime - ), - "date_from" to stage.startTime.toEpochMilli(), - "date_to" to stage.endTime.toEpochMilli(), - "source_id" to "", - "source_name" to sourceName, - ), + mapOf( + "value" to + record.level + .inMilligramsPerDeciliter, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), ) - } - fun getAggregateHCData(call: MethodCall, result: Result) { - val dataType = call.argument("dataTypeKey")!! - val interval = call.argument("interval")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val healthConnectData = mutableListOf>() - scope.launch { - try { - MapToHCAggregateMetric[dataType]?.let { metricClassType -> - val request = - AggregateGroupByDurationRequest( - metrics = setOf(metricClassType), - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - timeRangeSlicer = - Duration.ofSeconds( - interval - ) - ) - val response = healthConnectClient.aggregateGroupByDuration(request) + is DistanceRecord -> + return listOf( + mapOf( + "value" to + record.distance + .inMeters, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - for (durationResult in response) { - // The result may be null if no data is available in the - // time range - var totalValue = durationResult.result[metricClassType] - if (totalValue is Length) { - totalValue = totalValue.inMeters - } else if (totalValue is Energy) { - totalValue = totalValue.inKilocalories - } + is HydrationRecord -> + return listOf( + mapOf( + "value" to + record.volume + .inLiters, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - val packageNames = - durationResult.result.dataOrigins - .joinToString { origin -> - "${origin.packageName}" - } + is TotalCaloriesBurnedRecord -> + return listOf( + mapOf( + "value" to + record.energy + .inKilocalories, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - val data = - mapOf( - "value" to - (totalValue - ?: 0), - "date_from" to - durationResult.startTime - .toEpochMilli(), - "date_to" to - durationResult.endTime - .toEpochMilli(), - "source_name" to - packageNames, - "source_id" to "", - "is_manual_entry" to - packageNames.contains( - "user_input" - ) - ) - healthConnectData.add(data) - } - } - Handler(context!!.mainLooper).run { result.success(healthConnectData) } - } catch (e: Exception) { - Log.i("FLUTTER_HEALTH::ERROR", "Unable to return $dataType due to the following exception:") - Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) - } - } - } + is BasalMetabolicRateRecord -> + return listOf( + mapOf( + "value" to + record.basalMetabolicRate + .inKilocaloriesPerDay, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - // TODO: Find alternative to SOURCE_ID or make it nullable? - fun convertRecord(record: Any, dataType: String): List> { - val metadata = (record as Record).metadata - when (record) { - is WeightRecord -> - return listOf( - mapOf( - "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, - ), - ) - is HeightRecord -> - return listOf( - mapOf( - "value" to - record.height - .inMeters, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BodyFatRecord -> - return listOf( - mapOf( - "value" to - record.percentage - .value, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is StepsRecord -> - return listOf( - mapOf( - "value" to record.count, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is ActiveCaloriesBurnedRecord -> - return listOf( - mapOf( - "value" to - record.energy - .inKilocalories, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HeartRateRecord -> - return record.samples.map { - mapOf( - "value" to it.beatsPerMinute, - "date_from" to - it.time.toEpochMilli(), - "date_to" to it.time.toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - } - is HeartRateVariabilityRmssdRecord -> - return listOf( - mapOf( - "value" to - record.heartRateVariabilityMillis, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BodyTemperatureRecord -> - return listOf( - mapOf( - "value" to - record.temperature - .inCelsius, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - 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, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is OxygenSaturationRecord -> - return listOf( - mapOf( - "value" to - record.percentage - .value, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BloodGlucoseRecord -> - return listOf( - mapOf( - "value" to - record.level - .inMilligramsPerDeciliter, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is DistanceRecord -> - return listOf( - mapOf( - "value" to - record.distance - .inMeters, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HydrationRecord -> - return listOf( - mapOf( - "value" to - record.volume - .inLiters, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is TotalCaloriesBurnedRecord -> - return listOf( - mapOf( - "value" to - record.energy - .inKilocalories, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BasalMetabolicRateRecord -> - return listOf( - mapOf( - "value" to - record.basalMetabolicRate - .inKilocaloriesPerDay, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is SleepSessionRecord -> - return listOf( - mapOf( - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "value" to - ChronoUnit.MINUTES - .between( - record.startTime, - record.endTime - ), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is RestingHeartRateRecord -> - return listOf( - mapOf( - "value" to - record.beatsPerMinute, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is BasalMetabolicRateRecord -> - return listOf( - mapOf( - "value" to - record.basalMetabolicRate - .inKilocaloriesPerDay, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is FloorsClimbedRecord -> - return listOf( - mapOf( - "value" to record.floors, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is RespiratoryRateRecord -> - return listOf( - mapOf( - "value" to record.rate, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is NutritionRecord -> - return listOf( - mapOf( - "calories" to record.energy?.inKilocalories, - "protein" to record.protein?.inGrams, - "carbs" to record.totalCarbohydrate?.inGrams, - "fat" to record.totalFat?.inGrams, - "caffeine" to record.caffeine?.inGrams, - "vitamin_a" to record.vitaminA?.inGrams, - "b1_thiamine" to record.thiamin?.inGrams, - "b2_riboflavin" to record.riboflavin?.inGrams, - "b3_niacin" to record.niacin?.inGrams, - "b5_pantothenic_acid" to record.pantothenicAcid?.inGrams, - "b6_pyridoxine" to record.vitaminB6?.inGrams, - "b7_biotin" to record.biotin?.inGrams, - "b9_folate" to record.folate?.inGrams, - "b12_cobalamin" to record.vitaminB12?.inGrams, - "vitamin_c" to record.vitaminC?.inGrams, - "vitamin_d" to record.vitaminD?.inGrams, - "vitamin_e" to record.vitaminE?.inGrams, - "vitamin_k" to record.vitaminK?.inGrams, - "calcium" to record.calcium?.inGrams, - "chloride" to record.chloride?.inGrams, - "cholesterol" to record.cholesterol?.inGrams, - "choline" to null, - "chromium" to record.chromium?.inGrams, - "copper" to record.copper?.inGrams, - "fat_unsaturated" to record.unsaturatedFat?.inGrams, - "fat_monounsaturated" to record.monounsaturatedFat?.inGrams, - "fat_polyunsaturated" to record.polyunsaturatedFat?.inGrams, - "fat_saturated" to record.saturatedFat?.inGrams, - "fat_trans_monoenoic" to record.transFat?.inGrams, - "fiber" to record.dietaryFiber?.inGrams, - "iodine" to record.iodine?.inGrams, - "iron" to record.iron?.inGrams, - "magnesium" to record.magnesium?.inGrams, - "manganese" to record.manganese?.inGrams, - "molybdenum" to record.molybdenum?.inGrams, - "phosphorus" to record.phosphorus?.inGrams, - "potassium" to record.potassium?.inGrams, - "selenium" to record.selenium?.inGrams, - "sodium" to record.sodium?.inGrams, - "sugar" to record.sugar?.inGrams, - "water" to null, - "zinc" to record.zinc?.inGrams, - "name" to record.name!!, - "meal_type" to - (MapTypeToMealTypeHC[ - record.mealType] - ?: MEAL_TYPE_UNKNOWN), - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is MenstruationFlowRecord -> - return listOf( - mapOf( - "value" to record.flow, - "date_from" to record.time.toEpochMilli(), - "date_to" to record.time.toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - // is ExerciseSessionRecord -> return listOf(mapOf("value" to , - // "date_from" to , - // "date_to" to , - // "source_id" to "", - // "source_name" to - // metadata.dataOrigin.packageName)) - else -> - throw IllegalArgumentException( - "Health data type not supported" - ) // TODO: Exception or error? - } - } + is SleepSessionRecord -> + return listOf( + mapOf( + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "value" to + ChronoUnit.MINUTES + .between( + record.startTime, + record.endTime + ), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - // TODO rewrite sleep to fit new update better --> compare with Apple and see if we should - // not - // adopt a single type with attached stages approach - fun writeHCData(call: MethodCall, result: Result) { - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val value = call.argument("value")!! - val record = - when (type) { - BODY_FAT_PERCENTAGE -> - BodyFatRecord( - time = - Instant.ofEpochMilli( - startTime - ), - percentage = - Percentage( - value - ), - zoneOffset = null, - ) - HEIGHT -> - HeightRecord( - time = - Instant.ofEpochMilli( - startTime - ), - height = - Length.meters( - value - ), - zoneOffset = null, - ) - WEIGHT -> - WeightRecord( - time = - Instant.ofEpochMilli( - startTime - ), - weight = - Mass.kilograms( - value - ), - zoneOffset = null, - ) - STEPS -> - StepsRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - count = value.toLong(), - startZoneOffset = null, - endZoneOffset = null, - ) - ACTIVE_ENERGY_BURNED -> - ActiveCaloriesBurnedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - energy = - Energy.kilocalories( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - HEART_RATE -> - HeartRateRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - samples = - listOf< - HeartRateRecord.Sample>( - HeartRateRecord.Sample( - time = - Instant.ofEpochMilli( - startTime - ), - beatsPerMinute = - value.toLong(), - ), - ), - startZoneOffset = null, - endZoneOffset = null, - ) - BODY_TEMPERATURE -> - BodyTemperatureRecord( - time = - Instant.ofEpochMilli( - startTime - ), - temperature = - Temperature.celsius( - value - ), - 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 - ), - zoneOffset = null, - ) - BLOOD_GLUCOSE -> - BloodGlucoseRecord( - time = - Instant.ofEpochMilli( - startTime - ), - level = - BloodGlucose.milligramsPerDeciliter( - value - ), - zoneOffset = null, - ) - HEART_RATE_VARIABILITY_RMSSD -> - HeartRateVariabilityRmssdRecord( - time = - Instant.ofEpochMilli( - startTime - ), - heartRateVariabilityMillis = - value, - - zoneOffset = null, - ) - DISTANCE_DELTA -> - DistanceRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - distance = - Length.meters( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - WATER -> - HydrationRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - volume = - Volume.liters( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - SLEEP_ASLEEP -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_SLEEPING - ) - ), - ) - SLEEP_LIGHT -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_LIGHT - ) - ), - ) - SLEEP_DEEP -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_DEEP - ) - ), - ) - SLEEP_REM -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_REM - ) - ), - ) - SLEEP_OUT_OF_BED -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_OUT_OF_BED - ) - ), - ) - SLEEP_AWAKE -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_AWAKE - ) - ), - ) - SLEEP_SESSION -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - ) - RESTING_HEART_RATE -> - RestingHeartRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - beatsPerMinute = - value.toLong(), - zoneOffset = null, - ) - BASAL_ENERGY_BURNED -> - BasalMetabolicRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - basalMetabolicRate = - Power.kilocaloriesPerDay( - value - ), - zoneOffset = null, - ) - FLIGHTS_CLIMBED -> - FloorsClimbedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - floors = value, - startZoneOffset = null, - endZoneOffset = null, - ) - RESPIRATORY_RATE -> - RespiratoryRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - rate = value, - zoneOffset = null, - ) - // AGGREGATE_STEP_COUNT -> StepsRecord() - TOTAL_CALORIES_BURNED -> - TotalCaloriesBurnedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - energy = - Energy.kilocalories( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - MENSTRUATION_FLOW -> MenstruationFlowRecord( - time = Instant.ofEpochMilli(startTime), - flow = value.toInt(), - zoneOffset = null, - ) - BLOOD_PRESSURE_SYSTOLIC -> - throw IllegalArgumentException( - "You must use the [writeBloodPressure] API " - ) - BLOOD_PRESSURE_DIASTOLIC -> - throw IllegalArgumentException( - "You must use the [writeBloodPressure] API " - ) - WORKOUT -> - throw IllegalArgumentException( - "You must use the [writeWorkoutData] API " - ) - NUTRITION -> - throw IllegalArgumentException( - "You must use the [writeMeal] API " - ) - else -> - throw IllegalArgumentException( - "The type $type was not supported by the Health plugin or you must use another API " - ) - } - scope.launch { - try { - healthConnectClient.insertRecords(listOf(record)) - result.success(true) - } catch (e: Exception) { - result.success(false) - } - } - } + is RestingHeartRateRecord -> + return listOf( + mapOf( + "value" to + record.beatsPerMinute, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) - fun writeWorkoutHCData(call: MethodCall, result: Result) { - val type = call.argument("activityType")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val totalEnergyBurned = call.argument("totalEnergyBurned") - val totalDistance = call.argument("totalDistance") - if (workoutTypeMapHealthConnect.containsKey(type) == false) { - result.success(false) - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] Workout type not supported" - ) - return - } - val workoutType = workoutTypeMapHealthConnect[type]!! - val title = call.argument("title") ?: type + is FloorsClimbedRecord -> + return listOf( + mapOf( + "value" to record.floors, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) - scope.launch { - try { - val list = mutableListOf() - list.add( - ExerciseSessionRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - exerciseType = workoutType, - title = title, - ), - ) - if (totalDistance != null) { - list.add( - DistanceRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - distance = - Length.meters( - totalDistance.toDouble() - ), - ), - ) - } - if (totalEnergyBurned != null) { - list.add( - TotalCaloriesBurnedRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - energy = - Energy.kilocalories( - totalEnergyBurned - .toDouble() - ), - ), - ) - } - healthConnectClient.insertRecords( - list, + is RespiratoryRateRecord -> + return listOf( + mapOf( + "value" to record.rate, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) + + is NutritionRecord -> + return listOf( + mapOf( + "calories" to record.energy?.inKilocalories, + "protein" to record.protein?.inGrams, + "carbs" to record.totalCarbohydrate?.inGrams, + "fat" to record.totalFat?.inGrams, + "caffeine" to record.caffeine?.inGrams, + "vitamin_a" to record.vitaminA?.inGrams, + "b1_thiamine" to record.thiamin?.inGrams, + "b2_riboflavin" to record.riboflavin?.inGrams, + "b3_niacin" to record.niacin?.inGrams, + "b5_pantothenic_acid" to record.pantothenicAcid?.inGrams, + "b6_pyridoxine" to record.vitaminB6?.inGrams, + "b7_biotin" to record.biotin?.inGrams, + "b9_folate" to record.folate?.inGrams, + "b12_cobalamin" to record.vitaminB12?.inGrams, + "vitamin_c" to record.vitaminC?.inGrams, + "vitamin_d" to record.vitaminD?.inGrams, + "vitamin_e" to record.vitaminE?.inGrams, + "vitamin_k" to record.vitaminK?.inGrams, + "calcium" to record.calcium?.inGrams, + "chloride" to record.chloride?.inGrams, + "cholesterol" to record.cholesterol?.inGrams, + "choline" to null, + "chromium" to record.chromium?.inGrams, + "copper" to record.copper?.inGrams, + "fat_unsaturated" to record.unsaturatedFat?.inGrams, + "fat_monounsaturated" to record.monounsaturatedFat?.inGrams, + "fat_polyunsaturated" to record.polyunsaturatedFat?.inGrams, + "fat_saturated" to record.saturatedFat?.inGrams, + "fat_trans_monoenoic" to record.transFat?.inGrams, + "fiber" to record.dietaryFiber?.inGrams, + "iodine" to record.iodine?.inGrams, + "iron" to record.iron?.inGrams, + "magnesium" to record.magnesium?.inGrams, + "manganese" to record.manganese?.inGrams, + "molybdenum" to record.molybdenum?.inGrams, + "phosphorus" to record.phosphorus?.inGrams, + "potassium" to record.potassium?.inGrams, + "selenium" to record.selenium?.inGrams, + "sodium" to record.sodium?.inGrams, + "sugar" to record.sugar?.inGrams, + "water" to null, + "zinc" to record.zinc?.inGrams, + "name" to record.name!!, + "meal_type" to + (mapTypeToMealType[ + record.mealType] + ?: MEAL_TYPE_UNKNOWN), + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) + + is MenstruationFlowRecord -> + return listOf( + mapOf( + "value" to record.flow, + "date_from" to record.time.toEpochMilli(), + "date_to" to record.time.toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) + // is ExerciseSessionRecord -> return listOf(mapOf("value" to , + // "date_from" to , + // "date_to" to , + // "source_id" to "", + // "source_name" to + // metadata.dataOrigin.packageName)) + else -> + throw IllegalArgumentException( + "Health data type not supported" + ) // TODO: Exception or error? + } + } + + // TODO rewrite sleep to fit new update better --> compare with Apple and see if we should + // not adopt a single type with attached stages approach + private fun writeData(call: MethodCall, result: Result) { + val type = call.argument("dataTypeKey")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val value = call.argument("value")!! + val record = + when (type) { + BODY_FAT_PERCENTAGE -> + BodyFatRecord( + time = + Instant.ofEpochMilli( + startTime + ), + percentage = + Percentage( + value + ), + zoneOffset = null, + ) + + HEIGHT -> + HeightRecord( + time = + Instant.ofEpochMilli( + startTime + ), + height = + Length.meters( + value + ), + zoneOffset = null, + ) + + WEIGHT -> + WeightRecord( + time = + Instant.ofEpochMilli( + startTime + ), + weight = + Mass.kilograms( + value + ), + zoneOffset = null, + ) + + STEPS -> + StepsRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + count = value.toLong(), + startZoneOffset = null, + endZoneOffset = null, + ) + + ACTIVE_ENERGY_BURNED -> + ActiveCaloriesBurnedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + energy = + Energy.kilocalories( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + HEART_RATE -> + HeartRateRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + samples = + listOf( + HeartRateRecord.Sample( + time = + Instant.ofEpochMilli( + startTime + ), + beatsPerMinute = + value.toLong(), + ), + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + BODY_TEMPERATURE -> + BodyTemperatureRecord( + time = + Instant.ofEpochMilli( + startTime + ), + temperature = + Temperature.celsius( + value + ), + 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 + ), + zoneOffset = null, + ) + + BLOOD_GLUCOSE -> + BloodGlucoseRecord( + time = + Instant.ofEpochMilli( + startTime + ), + level = + BloodGlucose.milligramsPerDeciliter( + value + ), + zoneOffset = null, + ) + + HEART_RATE_VARIABILITY_RMSSD -> + HeartRateVariabilityRmssdRecord( + time = + Instant.ofEpochMilli( + startTime + ), + heartRateVariabilityMillis = + value, + + zoneOffset = null, + ) + + DISTANCE_DELTA -> + DistanceRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + distance = + Length.meters( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + WATER -> + HydrationRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + volume = + Volume.liters( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + SLEEP_ASLEEP -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_SLEEPING ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Workout was successfully added!" + ), + ) + + SLEEP_LIGHT -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_LIGHT ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the workout", + ), + ) + + SLEEP_DEEP -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_DEEP ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } - } - - fun writeBloodPressureHC(call: MethodCall, result: Result) { - val systolic = call.argument("systolic")!! - val diastolic = call.argument("diastolic")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - - scope.launch { - try { - healthConnectClient.insertRecords( - listOf( - BloodPressureRecord( - time = startTime, - systolic = - Pressure.millimetersOfMercury( - systolic - ), - diastolic = - Pressure.millimetersOfMercury( - diastolic - ), - zoneOffset = null, - ), - ), + ), + ) + + SLEEP_REM -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_REM ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Blood pressure was successfully added!", + ), + ) + + SLEEP_OUT_OF_BED -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_OUT_OF_BED ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the blood pressure", + ), + ) + + SLEEP_AWAKE -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_AWAKE ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } - } - - fun deleteHCData(call: MethodCall, result: Result) { - val type = call.argument("dataTypeKey")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - if (!MapToHCType.containsKey(type)) { - Log.w("FLUTTER_HEALTH::ERROR", "Datatype " + type + " not found in HC") - result.success(false) - return - } - val classType = MapToHCType[type]!! + ), + ) + + SLEEP_SESSION -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + RESTING_HEART_RATE -> + RestingHeartRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + beatsPerMinute = + value.toLong(), + zoneOffset = null, + ) + + BASAL_ENERGY_BURNED -> + BasalMetabolicRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + basalMetabolicRate = + Power.kilocaloriesPerDay( + value + ), + zoneOffset = null, + ) + + FLIGHTS_CLIMBED -> + FloorsClimbedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + floors = value, + startZoneOffset = null, + endZoneOffset = null, + ) + + RESPIRATORY_RATE -> + RespiratoryRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + rate = value, + zoneOffset = null, + ) + // AGGREGATE_STEP_COUNT -> StepsRecord() + TOTAL_CALORIES_BURNED -> + TotalCaloriesBurnedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + energy = + Energy.kilocalories( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + MENSTRUATION_FLOW -> MenstruationFlowRecord( + time = Instant.ofEpochMilli(startTime), + flow = value.toInt(), + zoneOffset = null, + ) - scope.launch { - try { - healthConnectClient.deleteRecords( - recordType = classType, - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - ) - result.success(true) - } catch (e: Exception) { - result.success(false) - } + BLOOD_PRESSURE_SYSTOLIC -> + throw IllegalArgumentException( + "You must use the [writeBloodPressure] API " + ) + + BLOOD_PRESSURE_DIASTOLIC -> + throw IllegalArgumentException( + "You must use the [writeBloodPressure] API " + ) + + WORKOUT -> + throw IllegalArgumentException( + "You must use the [writeWorkoutData] API " + ) + + NUTRITION -> + throw IllegalArgumentException( + "You must use the [writeMeal] API " + ) + + else -> + throw IllegalArgumentException( + "The type $type was not supported by the Health plugin or you must use another API " + ) + } + scope.launch { + try { + healthConnectClient.insertRecords(listOf(record)) + result.success(true) + } catch (e: Exception) { + result.success(false) + } + } + } + + /** Save a Workout session with options for distance and calories expended */ + private fun writeWorkoutData(call: MethodCall, result: Result) { + val type = call.argument("activityType")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val totalEnergyBurned = call.argument("totalEnergyBurned") + val totalDistance = call.argument("totalDistance") + if (!workoutTypeMap.containsKey(type)) { + result.success(false) + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] Workout type not supported" + ) + return + } + val workoutType = workoutTypeMap[type]!! + val title = call.argument("title") ?: type + + scope.launch { + try { + val list = mutableListOf() + list.add( + ExerciseSessionRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + exerciseType = workoutType, + title = title, + ), + ) + if (totalDistance != null) { + list.add( + DistanceRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + distance = + Length.meters( + totalDistance.toDouble() + ), + ), + ) } + if (totalEnergyBurned != null) { + list.add( + TotalCaloriesBurnedRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + energy = + Energy.kilocalories( + totalEnergyBurned + .toDouble() + ), + ), + ) + } + healthConnectClient.insertRecords( + list, + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Workout was successfully added!" + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the workout", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } } - - val MapSleepStageToType = - hashMapOf( - 1 to SLEEP_AWAKE, - 2 to SLEEP_ASLEEP, - 3 to SLEEP_OUT_OF_BED, - 4 to SLEEP_LIGHT, - 5 to SLEEP_DEEP, - 6 to SLEEP_REM, - ) - - private val MapMealTypeToTypeHC = - hashMapOf( - BREAKFAST to MEAL_TYPE_BREAKFAST, - LUNCH to MEAL_TYPE_LUNCH, - DINNER to MEAL_TYPE_DINNER, - SNACK to MEAL_TYPE_SNACK, - MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, - ) - - private val MapTypeToMealTypeHC = - hashMapOf( - MEAL_TYPE_BREAKFAST to BREAKFAST, - MEAL_TYPE_LUNCH to LUNCH, - MEAL_TYPE_DINNER to DINNER, - MEAL_TYPE_SNACK to SNACK, - MEAL_TYPE_UNKNOWN to MEAL_UNKNOWN, - ) - - private val MapMealTypeToType = - hashMapOf( - BREAKFAST to Field.MEAL_TYPE_BREAKFAST, - LUNCH to Field.MEAL_TYPE_LUNCH, - DINNER to Field.MEAL_TYPE_DINNER, - SNACK to Field.MEAL_TYPE_SNACK, - MEAL_UNKNOWN to Field.MEAL_TYPE_UNKNOWN, - ) - - val MapToHCType = - hashMapOf( - BODY_FAT_PERCENTAGE to BodyFatRecord::class, - HEIGHT to HeightRecord::class, - WEIGHT to WeightRecord::class, - STEPS to StepsRecord::class, - AGGREGATE_STEP_COUNT to StepsRecord::class, - 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, - BLOOD_GLUCOSE to BloodGlucoseRecord::class, - HEART_RATE_VARIABILITY_RMSSD to HeartRateVariabilityRmssdRecord::class, - DISTANCE_DELTA to DistanceRecord::class, - WATER to HydrationRecord::class, - SLEEP_ASLEEP to SleepSessionRecord::class, - SLEEP_AWAKE to SleepSessionRecord::class, - SLEEP_LIGHT to SleepSessionRecord::class, - SLEEP_DEEP to SleepSessionRecord::class, - SLEEP_REM to SleepSessionRecord::class, - SLEEP_OUT_OF_BED to SleepSessionRecord::class, - SLEEP_SESSION to SleepSessionRecord::class, - WORKOUT to ExerciseSessionRecord::class, - NUTRITION to NutritionRecord::class, - RESTING_HEART_RATE to RestingHeartRateRecord::class, - BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class, - FLIGHTS_CLIMBED to FloorsClimbedRecord::class, - RESPIRATORY_RATE to RespiratoryRateRecord::class, - TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord::class, - MENSTRUATION_FLOW to MenstruationFlowRecord::class, - // MOVE_MINUTES to TODO: Find alternative? - // TODO: Implement remaining types - // "ActiveCaloriesBurned" to - // ActiveCaloriesBurnedRecord::class, - // "BasalBodyTemperature" to - // BasalBodyTemperatureRecord::class, - // "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, - // "CyclingPedalingCadence" to - // CyclingPedalingCadenceRecord::class, - // "Distance" to DistanceRecord::class, - // "ElevationGained" to ElevationGainedRecord::class, - // "ExerciseSession" to ExerciseSessionRecord::class, - // "FloorsClimbed" to FloorsClimbedRecord::class, - // "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, - // "OxygenSaturation" to OxygenSaturationRecord::class, - // "Power" to PowerRecord::class, - // "RespiratoryRate" to RespiratoryRateRecord::class, - // "RestingHeartRate" to RestingHeartRateRecord::class, - // "SexualActivity" to SexualActivityRecord::class, - // "SleepSession" to SleepSessionRecord::class, - // "SleepStage" to SleepStageRecord::class, - // "Speed" to SpeedRecord::class, - // "StepsCadence" to StepsCadenceRecord::class, - // "Steps" to StepsRecord::class, - // "TotalCaloriesBurned" to - // TotalCaloriesBurnedRecord::class, - // "Vo2Max" to Vo2MaxRecord::class, - // "Weight" to WeightRecord::class, - // "WheelchairPushes" to WheelchairPushesRecord::class, - ) - - val MapToHCAggregateMetric = - hashMapOf( - HEIGHT to HeightRecord.HEIGHT_AVG, - WEIGHT to WeightRecord.WEIGHT_AVG, - STEPS to StepsRecord.COUNT_TOTAL, - AGGREGATE_STEP_COUNT to StepsRecord.COUNT_TOTAL, - ACTIVE_ENERGY_BURNED to - ActiveCaloriesBurnedRecord - .ACTIVE_CALORIES_TOTAL, - HEART_RATE to HeartRateRecord.MEASUREMENTS_COUNT, - DISTANCE_DELTA to DistanceRecord.DISTANCE_TOTAL, - WATER to HydrationRecord.VOLUME_TOTAL, - SLEEP_ASLEEP to SleepSessionRecord.SLEEP_DURATION_TOTAL, - SLEEP_AWAKE to SleepSessionRecord.SLEEP_DURATION_TOTAL, - SLEEP_IN_BED to SleepSessionRecord.SLEEP_DURATION_TOTAL, - TOTAL_CALORIES_BURNED to - TotalCaloriesBurnedRecord.ENERGY_TOTAL - ) + } + + /** Save a Blood Pressure measurement with systolic and diastolic values */ + private fun writeBloodPressure(call: MethodCall, result: Result) { + val systolic = call.argument("systolic")!! + val diastolic = call.argument("diastolic")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + + scope.launch { + try { + healthConnectClient.insertRecords( + listOf( + BloodPressureRecord( + time = startTime, + systolic = + Pressure.millimetersOfMercury( + systolic + ), + diastolic = + Pressure.millimetersOfMercury( + diastolic + ), + zoneOffset = null, + ), + ), + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Blood pressure was successfully added!", + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the blood pressure", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } + } + } + + /** Delete records of the given type in the time range */ + private fun deleteData(call: MethodCall, result: Result) { + val type = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + if (!mapToType.containsKey(type)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $type not found in HC") + result.success(false) + return + } + val classType = mapToType[type]!! + + scope.launch { + try { + healthConnectClient.deleteRecords( + recordType = classType, + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + ) + result.success(true) + } catch (e: Exception) { + result.success(false) + } + } + } + + private val mapSleepStageToType = + hashMapOf( + 1 to SLEEP_AWAKE, + 2 to SLEEP_ASLEEP, + 3 to SLEEP_OUT_OF_BED, + 4 to SLEEP_LIGHT, + 5 to SLEEP_DEEP, + 6 to SLEEP_REM, + ) + + private val mapMealTypeToType = + hashMapOf( + BREAKFAST to MEAL_TYPE_BREAKFAST, + LUNCH to MEAL_TYPE_LUNCH, + DINNER to MEAL_TYPE_DINNER, + SNACK to MEAL_TYPE_SNACK, + MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, + ) + + private val mapTypeToMealType = + hashMapOf( + MEAL_TYPE_BREAKFAST to BREAKFAST, + MEAL_TYPE_LUNCH to LUNCH, + MEAL_TYPE_DINNER to DINNER, + MEAL_TYPE_SNACK to SNACK, + MEAL_TYPE_UNKNOWN to MEAL_UNKNOWN, + ) + + + private val mapToType = + hashMapOf( + BODY_FAT_PERCENTAGE to BodyFatRecord::class, + HEIGHT to HeightRecord::class, + WEIGHT to WeightRecord::class, + STEPS to StepsRecord::class, + AGGREGATE_STEP_COUNT to StepsRecord::class, + 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, + BLOOD_GLUCOSE to BloodGlucoseRecord::class, + HEART_RATE_VARIABILITY_RMSSD to HeartRateVariabilityRmssdRecord::class, + DISTANCE_DELTA to DistanceRecord::class, + WATER to HydrationRecord::class, + SLEEP_ASLEEP to SleepSessionRecord::class, + SLEEP_AWAKE to SleepSessionRecord::class, + SLEEP_LIGHT to SleepSessionRecord::class, + SLEEP_DEEP to SleepSessionRecord::class, + SLEEP_REM to SleepSessionRecord::class, + SLEEP_OUT_OF_BED to SleepSessionRecord::class, + SLEEP_SESSION to SleepSessionRecord::class, + WORKOUT to ExerciseSessionRecord::class, + NUTRITION to NutritionRecord::class, + RESTING_HEART_RATE to RestingHeartRateRecord::class, + BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class, + FLIGHTS_CLIMBED to FloorsClimbedRecord::class, + RESPIRATORY_RATE to RespiratoryRateRecord::class, + TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord::class, + MENSTRUATION_FLOW to MenstruationFlowRecord::class, + // TODO: Implement remaining types + // "ActiveCaloriesBurned" to + // ActiveCaloriesBurnedRecord::class, + // "BasalBodyTemperature" to + // BasalBodyTemperatureRecord::class, + // "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, + // "CyclingPedalingCadence" to + // CyclingPedalingCadenceRecord::class, + // "Distance" to DistanceRecord::class, + // "ElevationGained" to ElevationGainedRecord::class, + // "ExerciseSession" to ExerciseSessionRecord::class, + // "FloorsClimbed" to FloorsClimbedRecord::class, + // "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, + // "OxygenSaturation" to OxygenSaturationRecord::class, + // "Power" to PowerRecord::class, + // "RespiratoryRate" to RespiratoryRateRecord::class, + // "RestingHeartRate" to RestingHeartRateRecord::class, + // "SexualActivity" to SexualActivityRecord::class, + // "SleepSession" to SleepSessionRecord::class, + // "SleepStage" to SleepStageRecord::class, + // "Speed" to SpeedRecord::class, + // "StepsCadence" to StepsCadenceRecord::class, + // "Steps" to StepsRecord::class, + // "TotalCaloriesBurned" to + // TotalCaloriesBurnedRecord::class, + // "Vo2Max" to Vo2MaxRecord::class, + // "Weight" to WeightRecord::class, + // "WheelchairPushes" to WheelchairPushesRecord::class, + ) + + private val mapToAggregateMetric = + hashMapOf( + HEIGHT to HeightRecord.HEIGHT_AVG, + WEIGHT to WeightRecord.WEIGHT_AVG, + STEPS to StepsRecord.COUNT_TOTAL, + AGGREGATE_STEP_COUNT to StepsRecord.COUNT_TOTAL, + ACTIVE_ENERGY_BURNED to + ActiveCaloriesBurnedRecord + .ACTIVE_CALORIES_TOTAL, + HEART_RATE to HeartRateRecord.MEASUREMENTS_COUNT, + DISTANCE_DELTA to DistanceRecord.DISTANCE_TOTAL, + WATER to HydrationRecord.VOLUME_TOTAL, + SLEEP_ASLEEP to SleepSessionRecord.SLEEP_DURATION_TOTAL, + SLEEP_AWAKE to SleepSessionRecord.SLEEP_DURATION_TOTAL, + SLEEP_IN_BED to SleepSessionRecord.SLEEP_DURATION_TOTAL, + TOTAL_CALORIES_BURNED to + TotalCaloriesBurnedRecord.ENERGY_TOTAL + ) + + private val workoutTypeMap = + mapOf( + // TODO: add skiing + // TODO: add skating + // TODO: add soccer + // TOOD: look into paddling + // TODO: add runnning + // TODO: look into hockey + "AMERICAN_FOOTBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_FOOTBALL_AMERICAN, + "AUSTRALIAN_FOOTBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_FOOTBALL_AUSTRALIAN, + "BADMINTON" to + ExerciseSessionRecord + .EXERCISE_TYPE_BADMINTON, + "BASEBALL" to ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL, + "BASKETBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_BASKETBALL, + "BIKING" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING, + // "BIKING_STATIONARY" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY, + "BOXING" to ExerciseSessionRecord.EXERCISE_TYPE_BOXING, + "CALISTHENICS" to + ExerciseSessionRecord + .EXERCISE_TYPE_CALISTHENICS, + "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, + // "CROSS_COUNTRY_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, + "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, + // "DOWNHILL_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, + "ELLIPTICAL" to + ExerciseSessionRecord + .EXERCISE_TYPE_ELLIPTICAL, + "FENCING" to ExerciseSessionRecord.EXERCISE_TYPE_FENCING, + "FRISBEE_DISC" to + ExerciseSessionRecord + .EXERCISE_TYPE_FRISBEE_DISC, + "GOLF" to ExerciseSessionRecord.EXERCISE_TYPE_GOLF, + "GUIDED_BREATHING" to + ExerciseSessionRecord + .EXERCISE_TYPE_GUIDED_BREATHING, + "GYMNASTICS" to + ExerciseSessionRecord + .EXERCISE_TYPE_GYMNASTICS, + "HANDBALL" to ExerciseSessionRecord.EXERCISE_TYPE_HANDBALL, + "HIGH_INTENSITY_INTERVAL_TRAINING" to + ExerciseSessionRecord + .EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING, + "HIKING" to ExerciseSessionRecord.EXERCISE_TYPE_HIKING, + // "HOCKEY" to ExerciseSessionRecord.EXERCISE_TYPE_HOCKEY, + "ICE_SKATING" to + ExerciseSessionRecord + .EXERCISE_TYPE_ICE_SKATING, + "MARTIAL_ARTS" to + ExerciseSessionRecord + .EXERCISE_TYPE_MARTIAL_ARTS, + "PARAGLIDING" to + ExerciseSessionRecord + .EXERCISE_TYPE_PARAGLIDING, + "PILATES" to ExerciseSessionRecord.EXERCISE_TYPE_PILATES, + "RACQUETBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_RACQUETBALL, + "ROCK_CLIMBING" to + ExerciseSessionRecord + .EXERCISE_TYPE_ROCK_CLIMBING, + "ROWING" to ExerciseSessionRecord.EXERCISE_TYPE_ROWING, + "ROWING_MACHINE" to + ExerciseSessionRecord + .EXERCISE_TYPE_ROWING_MACHINE, + "RUGBY" to ExerciseSessionRecord.EXERCISE_TYPE_RUGBY, + "RUNNING_TREADMILL" to + ExerciseSessionRecord + .EXERCISE_TYPE_RUNNING_TREADMILL, + "RUNNING" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, + "SAILING" to ExerciseSessionRecord.EXERCISE_TYPE_SAILING, + "SCUBA_DIVING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SCUBA_DIVING, + "SKATING" to ExerciseSessionRecord.EXERCISE_TYPE_SKATING, + "SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, + "SNOWBOARDING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SNOWBOARDING, + "SNOWSHOEING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SNOWSHOEING, + // "SOCCER" to ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, + "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, + "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, + "STAIR_CLIMBING_MACHINE" to + ExerciseSessionRecord + .EXERCISE_TYPE_STAIR_CLIMBING_MACHINE, + "STAIR_CLIMBING" to + ExerciseSessionRecord + .EXERCISE_TYPE_STAIR_CLIMBING, + "STRENGTH_TRAINING" to + ExerciseSessionRecord + .EXERCISE_TYPE_STRENGTH_TRAINING, + "SURFING" to ExerciseSessionRecord.EXERCISE_TYPE_SURFING, + "SWIMMING_OPEN_WATER" to + ExerciseSessionRecord + .EXERCISE_TYPE_SWIMMING_OPEN_WATER, + "SWIMMING_POOL" to + ExerciseSessionRecord + .EXERCISE_TYPE_SWIMMING_POOL, + "TABLE_TENNIS" to + ExerciseSessionRecord + .EXERCISE_TYPE_TABLE_TENNIS, + "TENNIS" to ExerciseSessionRecord.EXERCISE_TYPE_TENNIS, + "VOLLEYBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_VOLLEYBALL, + "WALKING" to ExerciseSessionRecord.EXERCISE_TYPE_WALKING, + "WATER_POLO" to + ExerciseSessionRecord + .EXERCISE_TYPE_WATER_POLO, + "WEIGHTLIFTING" to + ExerciseSessionRecord + .EXERCISE_TYPE_WEIGHTLIFTING, + "WHEELCHAIR" to + ExerciseSessionRecord + .EXERCISE_TYPE_WHEELCHAIR, + "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, + "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT, + ) } diff --git a/packages/health/example/android/app/build.gradle b/packages/health/example/android/app/build.gradle index 95636d301..63e07ba82 100644 --- a/packages/health/example/android/app/build.gradle +++ b/packages/health/example/android/app/build.gradle @@ -64,10 +64,6 @@ dependencies { implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' - implementation("com.google.android.gms:play-services-fitness:21.1.0") - implementation("com.google.android.gms:play-services-auth:20.2.0") androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - // The new health connect api - // implementation("androidx.health.connect:connect-client:1.0.0-alpha11") } diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 59eb830c9..3912bda41 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -81,7 +81,7 @@ class _HealthAppState extends State { void initState() { // configure the health plugin before use. - Health().configure(useHealthConnectIfAvailable: true); + Health().configure(); super.initState(); } @@ -185,7 +185,7 @@ class _HealthAppState extends State { // Add data for supported types // NOTE: These are only the ones supported on Androids new API Health Connect. - // Both Android's Google Fit and iOS' HealthKit have more types that we support in the enum list [HealthDataType] + // Both Android's Health Connect and iOS' HealthKit have more types that we support in the enum list [HealthDataType] // Add more - like AUDIOGRAM, HEADACHE_SEVERE etc. to try them. bool success = true; @@ -217,11 +217,6 @@ class _HealthAppState extends State { type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, - startTime: earlier, - endTime: now); success &= await Health().writeHealthData( value: 37, type: HealthDataType.BODY_TEMPERATURE, @@ -265,7 +260,6 @@ class _HealthAppState extends State { saturation: 98, startTime: earlier, endTime: now, - flowRate: 1.0, ); success &= await Health().writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, @@ -591,8 +585,6 @@ class _HealthAppState extends State { Widget _authorizationNotGranted = const Column( children: [ const Text('Authorization not given.'), - const Text( - 'For Google Fit please check your OAUTH2 client ID is correct in Google Developer Console.'), const 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.'), diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index f1bfcb44f..e135a30f6 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -62,7 +62,7 @@ const List dataTypesIOS = [ /// List of data types available on Android. /// /// Note that these are only the ones supported on Android's Health Connect API. -/// Android's Google Fit have more types that we support in the [HealthDataType] +/// Android's Health Connect has more types that we support in the [HealthDataType] /// enumeration. const List dataTypesAndroid = [ HealthDataType.ACTIVE_ENERGY_BURNED, @@ -79,7 +79,6 @@ const List dataTypesAndroid = [ HealthDataType.HEART_RATE, HealthDataType.HEART_RATE_VARIABILITY_RMSSD, HealthDataType.STEPS, - // HealthDataType.MOVE_MINUTES, // TODO: Find alternative for Health Connect HealthDataType.DISTANCE_DELTA, HealthDataType.RESPIRATORY_RATE, HealthDataType.SLEEP_AWAKE, diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 996c8dc32..9b00a3c2b 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -291,13 +291,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else if call.method.elementsEqual("delete") { try! delete(call: call, result: result) } - - /// Disconnect - else if (call.method.elementsEqual("disconnect")){ - // Do nothing. - result(true) - } - } func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -1303,8 +1296,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["FLEXIBILITY"] = .flexibility workoutActivityTypeMap["WALKING"] = .walking workoutActivityTypeMap["RUNNING"] = .running - workoutActivityTypeMap["RUNNING_JOGGING"] = .running // Supported due to combining with Android naming - workoutActivityTypeMap["RUNNING_SAND"] = .running // Supported due to combining with Android naming workoutActivityTypeMap["RUNNING_TREADMILL"] = .running // Supported due to combining with Android naming workoutActivityTypeMap["WHEELCHAIR_WALK_PACE"] = .wheelchairWalkPace workoutActivityTypeMap["WHEELCHAIR_RUN_PACE"] = .wheelchairRunPace @@ -1345,9 +1336,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["SNOW_SPORTS"] = .snowSports workoutActivityTypeMap["SNOWBOARDING"] = .snowboarding workoutActivityTypeMap["SKATING"] = .skatingSports - workoutActivityTypeMap["SKATING_CROSS,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INDOOR,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INLINE,"] = .skatingSports // Supported due to combining with Android naming workoutActivityTypeMap["PADDLE_SPORTS"] = .paddleSports workoutActivityTypeMap["ROWING"] = .rowing workoutActivityTypeMap["SAILING"] = .sailing diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec index 443b44c7c..3a8d89370 100644 --- a/packages/health/ios/health.podspec +++ b/packages/health/ios/health.podspec @@ -4,9 +4,9 @@ Pod::Spec.new do |s| s.name = 'health' s.version = '1.0.4' - s.summary = 'Wrapper for the iOS HealthKit and Android GoogleFit services.' + s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.' s.description = <<-DESC -Wrapper for the iOS HealthKit and Android GoogleFit services. +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' } diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index fbbfcc13c..104be599d 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -118,7 +118,6 @@ const _$HealthDataTypeEnumMap = { HealthDataType.DISTANCE_SWIMMING: 'DISTANCE_SWIMMING', HealthDataType.DISTANCE_CYCLING: 'DISTANCE_CYCLING', HealthDataType.FLIGHTS_CLIMBED: 'FLIGHTS_CLIMBED', - HealthDataType.MOVE_MINUTES: 'MOVE_MINUTES', HealthDataType.DISTANCE_DELTA: 'DISTANCE_DELTA', HealthDataType.MINDFULNESS: 'MINDFULNESS', HealthDataType.WATER: 'WATER', @@ -207,7 +206,6 @@ const _$HealthDataUnitEnumMap = { const _$HealthPlatformTypeEnumMap = { HealthPlatformType.appleHealth: 'appleHealth', - HealthPlatformType.googleFit: 'googleFit', HealthPlatformType.googleHealthConnect: 'googleHealthConnect', }; @@ -316,19 +314,20 @@ Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) { } const _$HealthWorkoutActivityTypeEnumMap = { + HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', HealthWorkoutActivityType.ARCHERY: 'ARCHERY', + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', HealthWorkoutActivityType.BADMINTON: 'BADMINTON', HealthWorkoutActivityType.BASEBALL: 'BASEBALL', HealthWorkoutActivityType.BASKETBALL: 'BASKETBALL', HealthWorkoutActivityType.BIKING: 'BIKING', HealthWorkoutActivityType.BOXING: 'BOXING', HealthWorkoutActivityType.CRICKET: 'CRICKET', + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', HealthWorkoutActivityType.CURLING: 'CURLING', + HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', HealthWorkoutActivityType.ELLIPTICAL: 'ELLIPTICAL', HealthWorkoutActivityType.FENCING: 'FENCING', - HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', - HealthWorkoutActivityType.SOCCER: 'SOCCER', HealthWorkoutActivityType.GOLF: 'GOLF', HealthWorkoutActivityType.GYMNASTICS: 'GYMNASTICS', HealthWorkoutActivityType.HANDBALL: 'HANDBALL', @@ -336,7 +335,6 @@ const _$HealthWorkoutActivityTypeEnumMap = { 'HIGH_INTENSITY_INTERVAL_TRAINING', HealthWorkoutActivityType.HIKING: 'HIKING', HealthWorkoutActivityType.HOCKEY: 'HOCKEY', - HealthWorkoutActivityType.SKATING: 'SKATING', HealthWorkoutActivityType.JUMP_ROPE: 'JUMP_ROPE', HealthWorkoutActivityType.KICKBOXING: 'KICKBOXING', HealthWorkoutActivityType.MARTIAL_ARTS: 'MARTIAL_ARTS', @@ -346,9 +344,9 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.RUGBY: 'RUGBY', HealthWorkoutActivityType.RUNNING: 'RUNNING', HealthWorkoutActivityType.SAILING: 'SAILING', - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', - HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', + HealthWorkoutActivityType.SKATING: 'SKATING', HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', + HealthWorkoutActivityType.SOCCER: 'SOCCER', HealthWorkoutActivityType.SOFTBALL: 'SOFTBALL', HealthWorkoutActivityType.SQUASH: 'SQUASH', HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', @@ -359,113 +357,65 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.WALKING: 'WALKING', HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', HealthWorkoutActivityType.YOGA: 'YOGA', + HealthWorkoutActivityType.BARRE: 'BARRE', HealthWorkoutActivityType.BOWLING: 'BOWLING', + HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', + HealthWorkoutActivityType.CLIMBING: 'CLIMBING', + HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', + HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', - HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', - HealthWorkoutActivityType.LACROSSE: 'LACROSSE', - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: - 'PREPARATION_AND_RECOVERY', + HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', + HealthWorkoutActivityType.FISHING: 'FISHING', + HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', - HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', - HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', - HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', - HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', - HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: 'FUNCTIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: - 'TRADITIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', - HealthWorkoutActivityType.STAIRS: 'STAIRS', - HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', - HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', - HealthWorkoutActivityType.BARRE: 'BARRE', - HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', - HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', + HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', + HealthWorkoutActivityType.HUNTING: 'HUNTING', + HealthWorkoutActivityType.LACROSSE: 'LACROSSE', HealthWorkoutActivityType.MIND_AND_BODY: 'MIND_AND_BODY', + HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', + HealthWorkoutActivityType.PADDLE_SPORTS: 'PADDLE_SPORTS', HealthWorkoutActivityType.PICKLEBALL: 'PICKLEBALL', - HealthWorkoutActivityType.CLIMBING: 'CLIMBING', - HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', - HealthWorkoutActivityType.FISHING: 'FISHING', - HealthWorkoutActivityType.HUNTING: 'HUNTING', HealthWorkoutActivityType.PLAY: 'PLAY', + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: + 'PREPARATION_AND_RECOVERY', HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', - HealthWorkoutActivityType.PADDLE_SPORTS: 'PADDLE_SPORTS', + HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', + HealthWorkoutActivityType.STAIRS: 'STAIRS', + HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', HealthWorkoutActivityType.SURFING_SPORTS: 'SURFING_SPORTS', + HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', + HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', + HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: + 'TRADITIONAL_STRENGTH_TRAINING', HealthWorkoutActivityType.WATER_FITNESS: 'WATER_FITNESS', HealthWorkoutActivityType.WATER_SPORTS: 'WATER_SPORTS', - HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', HealthWorkoutActivityType.WRESTLING: 'WRESTLING', - HealthWorkoutActivityType.AEROBICS: 'AEROBICS', - HealthWorkoutActivityType.BIATHLON: 'BIATHLON', - HealthWorkoutActivityType.BIKING_HAND: 'BIKING_HAND', - HealthWorkoutActivityType.BIKING_MOUNTAIN: 'BIKING_MOUNTAIN', - HealthWorkoutActivityType.BIKING_ROAD: 'BIKING_ROAD', - HealthWorkoutActivityType.BIKING_SPINNING: 'BIKING_SPINNING', HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', - HealthWorkoutActivityType.BIKING_UTILITY: 'BIKING_UTILITY', HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', - HealthWorkoutActivityType.CIRCUIT_TRAINING: 'CIRCUIT_TRAINING', - HealthWorkoutActivityType.CROSS_FIT: 'CROSS_FIT', HealthWorkoutActivityType.DANCING: 'DANCING', - HealthWorkoutActivityType.DIVING: 'DIVING', - HealthWorkoutActivityType.ELEVATOR: 'ELEVATOR', - HealthWorkoutActivityType.ERGOMETER: 'ERGOMETER', - HealthWorkoutActivityType.ESCALATOR: 'ESCALATOR', HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', - HealthWorkoutActivityType.GARDENING: 'GARDENING', HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', - HealthWorkoutActivityType.HORSEBACK_RIDING: 'HORSEBACK_RIDING', - HealthWorkoutActivityType.HOUSEWORK: 'HOUSEWORK', - HealthWorkoutActivityType.INTERVAL_TRAINING: 'INTERVAL_TRAINING', - HealthWorkoutActivityType.IN_VEHICLE: 'IN_VEHICLE', HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', - HealthWorkoutActivityType.KAYAKING: 'KAYAKING', - HealthWorkoutActivityType.KETTLEBELL_TRAINING: 'KETTLEBELL_TRAINING', - HealthWorkoutActivityType.KICK_SCOOTER: 'KICK_SCOOTER', - HealthWorkoutActivityType.KITE_SURFING: 'KITE_SURFING', - HealthWorkoutActivityType.MEDITATION: 'MEDITATION', - HealthWorkoutActivityType.MIXED_MARTIAL_ARTS: 'MIXED_MARTIAL_ARTS', - HealthWorkoutActivityType.P90X: 'P90X', HealthWorkoutActivityType.PARAGLIDING: 'PARAGLIDING', - HealthWorkoutActivityType.POLO: 'POLO', HealthWorkoutActivityType.ROCK_CLIMBING: 'ROCK_CLIMBING', HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', - HealthWorkoutActivityType.RUNNING_JOGGING: 'RUNNING_JOGGING', - HealthWorkoutActivityType.RUNNING_SAND: 'RUNNING_SAND', HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', - HealthWorkoutActivityType.SKATING_CROSS: 'SKATING_CROSS', - HealthWorkoutActivityType.SKATING_INDOOR: 'SKATING_INDOOR', - HealthWorkoutActivityType.SKATING_INLINE: 'SKATING_INLINE', HealthWorkoutActivityType.SKIING: 'SKIING', - HealthWorkoutActivityType.SKIING_BACK_COUNTRY: 'SKIING_BACK_COUNTRY', - HealthWorkoutActivityType.SKIING_KITE: 'SKIING_KITE', - HealthWorkoutActivityType.SKIING_ROLLER: 'SKIING_ROLLER', - HealthWorkoutActivityType.SLEDDING: 'SLEDDING', - HealthWorkoutActivityType.SNOWMOBILE: 'SNOWMOBILE', HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', - HealthWorkoutActivityType.STANDUP_PADDLEBOARDING: 'STANDUP_PADDLEBOARDING', - HealthWorkoutActivityType.STILL: 'STILL', HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', HealthWorkoutActivityType.SURFING: 'SURFING', HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', - HealthWorkoutActivityType.TEAM_SPORTS: 'TEAM_SPORTS', - HealthWorkoutActivityType.TILTING: 'TILTING', - HealthWorkoutActivityType.VOLLEYBALL_BEACH: 'VOLLEYBALL_BEACH', - HealthWorkoutActivityType.VOLLEYBALL_INDOOR: 'VOLLEYBALL_INDOOR', - HealthWorkoutActivityType.WAKEBOARDING: 'WAKEBOARDING', - HealthWorkoutActivityType.WALKING_FITNESS: 'WALKING_FITNESS', - HealthWorkoutActivityType.WALKING_NORDIC: 'WALKING_NORDIC', - HealthWorkoutActivityType.WALKING_STROLLER: 'WALKING_STROLLER', HealthWorkoutActivityType.WALKING_TREADMILL: 'WALKING_TREADMILL', HealthWorkoutActivityType.WEIGHTLIFTING: 'WEIGHTLIFTING', HealthWorkoutActivityType.WHEELCHAIR: 'WHEELCHAIR', - HealthWorkoutActivityType.WINDSURFING: 'WINDSURFING', - HealthWorkoutActivityType.ZUMBA: 'ZUMBA', HealthWorkoutActivityType.OTHER: 'OTHER', }; diff --git a/packages/health/lib/health.json.dart b/packages/health/lib/health.json.dart index 72a43924b..351098a28 100644 --- a/packages/health/lib/health.json.dart +++ b/packages/health/lib/health.json.dart @@ -15,7 +15,6 @@ void _registerFromJsonFunctions() { leftEarSensitivities: [], rightEarSensitivities: [], ), - WorkoutHealthValue(workoutActivityType: HealthWorkoutActivityType.AEROBICS), ElectrocardiogramHealthValue(voltageValues: []), ElectrocardiogramVoltageValue(voltage: 12, timeSinceSampleStart: 0), NutritionHealthValue(), diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 417991fb2..b98734c5a 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -1,10 +1,10 @@ part of '../health.dart'; /// Types of health platforms. -enum HealthPlatformType { appleHealth, googleFit, googleHealthConnect } +enum HealthPlatformType { appleHealth, googleHealthConnect } /// A [HealthDataPoint] object corresponds to a data point capture from -/// Apple HealthKit or Google Fit or Google Health Connect with a [HealthValue] +/// Apple HealthKit or Google Health Connect with a [HealthValue] /// as value. @JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) class HealthDataPoint { diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 6fe05ef05..a55bc49e4 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -27,7 +27,6 @@ class Health { String? _deviceId; final _deviceInfo = DeviceInfoPlugin(); - bool _useHealthConnectIfAvailable = false; Health._() { _registerFromJsonFunctions(); @@ -39,9 +38,7 @@ class Health { /// The type of platform of this device. HealthPlatformType get platformType => Platform.isIOS ? HealthPlatformType.appleHealth - : useHealthConnectIfAvailable - ? HealthPlatformType.googleHealthConnect - : HealthPlatformType.googleFit; + : HealthPlatformType.googleHealthConnect; /// The id of this device. /// @@ -50,24 +47,12 @@ class Health { String get deviceId => _deviceId ?? 'unknown'; /// Configure the health plugin. Must be called before using the plugin. - /// - /// If [useHealthConnectIfAvailable] is true, Google Health Connect on - /// Android will be used. Has no effect on iOS. - Future configure({bool useHealthConnectIfAvailable = false}) async { - if (Platform.isAndroid) { - _deviceId = (await _deviceInfo.androidInfo).id; - _useHealthConnectIfAvailable = useHealthConnectIfAvailable; - await _channel.invokeMethod('useHealthConnectIfAvailable'); - } else { - _deviceId = (await _deviceInfo.iosInfo).identifierForVendor; - } + Future configure() async { + _deviceId = Platform.isAndroid + ? (await _deviceInfo.androidInfo).id + : (await _deviceInfo.iosInfo).identifierForVendor; } - /// Is this plugin using Health Connect (true) or Google Fit (false)? - /// - /// This is set in the [configure] method. - bool get useHealthConnectIfAvailable => _useHealthConnectIfAvailable; - /// Check if a given data type is available on the platform bool isDataTypeAvailable(HealthDataType dataType) => Platform.isAndroid ? dataTypeKeysAndroid.contains(dataType) @@ -121,10 +106,9 @@ class Health { }); } - /// Revokes permissions of all types. - /// - /// Uses `disableFit()` on Google Fit. + /// Revokes Google Health Connect permissions on Android of all types. /// + /// NOTE: The app must be completely killed and restarted for the changes to take effect. /// Not implemented on iOS as there is no way to programmatically remove access. Future revokePermissions() async { try { @@ -175,33 +159,6 @@ class Health { } } - /// Disconnect from Google fit. - /// - /// Not supported on iOS and Google Health Connect, and the method does nothing. - Future disconnect( - List types, { - List? permissions, - }) async { - if (permissions != null && permissions.length != types.length) { - throw ArgumentError( - 'The length of [types] must be same as that of [permissions].'); - } - - final mTypes = List.from(types, growable: true); - final mPermissions = permissions == null - ? List.filled(types.length, HealthDataAccess.READ.index, - growable: true) - : permissions.map((permission) => permission.index).toList(); - - // on Android, if BMI is requested, then also ask for weight and height - if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); - - List keys = mTypes.map((dataType) => dataType.name).toList(); - - return await _channel.invokeMethod( - 'disconnect', {'types': keys, "permissions": mPermissions}); - } - /// Requests permissions to access health data [types]. /// /// Returns true if successful, false otherwise. @@ -353,6 +310,10 @@ class Health { throw ArgumentError( "Adding workouts should be done using the writeWorkoutData method."); } + // If not implemented on platform, throw an exception + if (!isDataTypeAvailable(type)) { + throw HealthException(type, 'Not available on platform $platformType'); + } endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -468,8 +429,6 @@ class Health { /// /// Parameters: /// * [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 [saturation] is measured. /// Must be equal to or earlier than [endTime]. /// * [endTime] - the end time when this [saturation] is measured. @@ -478,7 +437,6 @@ class Health { /// 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 { @@ -497,7 +455,6 @@ class Health { } else if (Platform.isAndroid) { Map args = { 'value': saturation, - 'flowRate': flowRate, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, @@ -507,7 +464,7 @@ class Health { return success ?? false; } - /// Saves meal record into Apple Health or Google Fit / Health Connect. + /// Saves meal record into Apple Health or Health Connect. /// /// Returns true if successful, false otherwise. /// @@ -1020,8 +977,6 @@ class Health { /// Get the total number of steps within a specific time period. /// Returns null if not successful. - /// - /// Is a fix according to https://stackoverflow.com/questions/29414386/step-count-retrieved-through-google-fit-api-does-not-match-step-count-displayed/29415091#29415091 Future getTotalStepsInInterval(DateTime startTime, DateTime endTime, {bool includeManualEntry = true}) async { final args = { @@ -1055,7 +1010,7 @@ 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 or Google Fit or Google Health Connect. + /// Write workout data to Apple Health or Google Health Connect. /// /// Returns true if the workout data was successfully added. /// @@ -1106,84 +1061,84 @@ class Health { bool _isOnIOS(HealthWorkoutActivityType type) { // Returns true if the type is part of the iOS set return { + HealthWorkoutActivityType.AMERICAN_FOOTBALL, HealthWorkoutActivityType.ARCHERY, + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, HealthWorkoutActivityType.BADMINTON, + HealthWorkoutActivityType.BARRE, HealthWorkoutActivityType.BASEBALL, HealthWorkoutActivityType.BASKETBALL, HealthWorkoutActivityType.BIKING, + HealthWorkoutActivityType.BOWLING, HealthWorkoutActivityType.BOXING, + HealthWorkoutActivityType.CARDIO_DANCE, + HealthWorkoutActivityType.CLIMBING, + HealthWorkoutActivityType.COOLDOWN, + HealthWorkoutActivityType.CORE_TRAINING, HealthWorkoutActivityType.CRICKET, + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, + HealthWorkoutActivityType.CROSS_TRAINING, HealthWorkoutActivityType.CURLING, + HealthWorkoutActivityType.DISC_SPORTS, + HealthWorkoutActivityType.DOWNHILL_SKIING, HealthWorkoutActivityType.ELLIPTICAL, + HealthWorkoutActivityType.EQUESTRIAN_SPORTS, HealthWorkoutActivityType.FENCING, - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, - HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.FISHING, + HealthWorkoutActivityType.FITNESS_GAMING, + HealthWorkoutActivityType.FLEXIBILITY, + HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING, HealthWorkoutActivityType.GOLF, HealthWorkoutActivityType.GYMNASTICS, + HealthWorkoutActivityType.HAND_CYCLING, HealthWorkoutActivityType.HANDBALL, HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING, HealthWorkoutActivityType.HIKING, HealthWorkoutActivityType.HOCKEY, - HealthWorkoutActivityType.SKATING, + HealthWorkoutActivityType.HUNTING, HealthWorkoutActivityType.JUMP_ROPE, HealthWorkoutActivityType.KICKBOXING, + HealthWorkoutActivityType.LACROSSE, HealthWorkoutActivityType.MARTIAL_ARTS, + HealthWorkoutActivityType.MIND_AND_BODY, + HealthWorkoutActivityType.MIXED_CARDIO, + HealthWorkoutActivityType.OTHER, + HealthWorkoutActivityType.PADDLE_SPORTS, + HealthWorkoutActivityType.PICKLEBALL, HealthWorkoutActivityType.PILATES, + HealthWorkoutActivityType.PLAY, + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY, HealthWorkoutActivityType.RACQUETBALL, HealthWorkoutActivityType.ROWING, HealthWorkoutActivityType.RUGBY, HealthWorkoutActivityType.RUNNING, HealthWorkoutActivityType.SAILING, - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, - HealthWorkoutActivityType.DOWNHILL_SKIING, + HealthWorkoutActivityType.SKATING, + HealthWorkoutActivityType.SNOW_SPORTS, HealthWorkoutActivityType.SNOWBOARDING, + HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.SOCIAL_DANCE, HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, + HealthWorkoutActivityType.STAIRS, + HealthWorkoutActivityType.STEP_TRAINING, + HealthWorkoutActivityType.SURFING_SPORTS, HealthWorkoutActivityType.SWIMMING, HealthWorkoutActivityType.TABLE_TENNIS, + HealthWorkoutActivityType.TAI_CHI, HealthWorkoutActivityType.TENNIS, - HealthWorkoutActivityType.VOLLEYBALL, - HealthWorkoutActivityType.WALKING, - HealthWorkoutActivityType.WATER_POLO, - HealthWorkoutActivityType.YOGA, - HealthWorkoutActivityType.BOWLING, - HealthWorkoutActivityType.CROSS_TRAINING, HealthWorkoutActivityType.TRACK_AND_FIELD, - HealthWorkoutActivityType.DISC_SPORTS, - HealthWorkoutActivityType.LACROSSE, - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY, - HealthWorkoutActivityType.FLEXIBILITY, - HealthWorkoutActivityType.COOLDOWN, - HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, - HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, - HealthWorkoutActivityType.HAND_CYCLING, - HealthWorkoutActivityType.CORE_TRAINING, - HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING, HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING, - HealthWorkoutActivityType.MIXED_CARDIO, - HealthWorkoutActivityType.STAIRS, - HealthWorkoutActivityType.STEP_TRAINING, - HealthWorkoutActivityType.FITNESS_GAMING, - HealthWorkoutActivityType.BARRE, - HealthWorkoutActivityType.CARDIO_DANCE, - HealthWorkoutActivityType.SOCIAL_DANCE, - HealthWorkoutActivityType.MIND_AND_BODY, - HealthWorkoutActivityType.PICKLEBALL, - HealthWorkoutActivityType.CLIMBING, - HealthWorkoutActivityType.EQUESTRIAN_SPORTS, - HealthWorkoutActivityType.FISHING, - HealthWorkoutActivityType.HUNTING, - HealthWorkoutActivityType.PLAY, - HealthWorkoutActivityType.SNOW_SPORTS, - HealthWorkoutActivityType.PADDLE_SPORTS, - HealthWorkoutActivityType.SURFING_SPORTS, + HealthWorkoutActivityType.VOLLEYBALL, + HealthWorkoutActivityType.WALKING, HealthWorkoutActivityType.WATER_FITNESS, + HealthWorkoutActivityType.WATER_POLO, HealthWorkoutActivityType.WATER_SPORTS, - HealthWorkoutActivityType.TAI_CHI, + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, HealthWorkoutActivityType.WRESTLING, - HealthWorkoutActivityType.OTHER, + HealthWorkoutActivityType.YOGA, }.contains(type); } @@ -1192,28 +1147,26 @@ class Health { // Returns true if the type is part of the Android set return { // Both + HealthWorkoutActivityType.AMERICAN_FOOTBALL, HealthWorkoutActivityType.ARCHERY, + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, HealthWorkoutActivityType.BADMINTON, HealthWorkoutActivityType.BASEBALL, HealthWorkoutActivityType.BASKETBALL, HealthWorkoutActivityType.BIKING, HealthWorkoutActivityType.BOXING, HealthWorkoutActivityType.CRICKET, + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, HealthWorkoutActivityType.CURLING, + HealthWorkoutActivityType.DOWNHILL_SKIING, HealthWorkoutActivityType.ELLIPTICAL, HealthWorkoutActivityType.FENCING, - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, - HealthWorkoutActivityType.SOCCER, HealthWorkoutActivityType.GOLF, HealthWorkoutActivityType.GYMNASTICS, HealthWorkoutActivityType.HANDBALL, HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING, HealthWorkoutActivityType.HIKING, HealthWorkoutActivityType.HOCKEY, - HealthWorkoutActivityType.SKATING, - HealthWorkoutActivityType.JUMP_ROPE, - HealthWorkoutActivityType.KICKBOXING, HealthWorkoutActivityType.MARTIAL_ARTS, HealthWorkoutActivityType.PILATES, HealthWorkoutActivityType.RACQUETBALL, @@ -1221,13 +1174,12 @@ class Health { HealthWorkoutActivityType.RUGBY, HealthWorkoutActivityType.RUNNING, HealthWorkoutActivityType.SAILING, - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, - HealthWorkoutActivityType.DOWNHILL_SKIING, + HealthWorkoutActivityType.SKATING, HealthWorkoutActivityType.SNOWBOARDING, + HealthWorkoutActivityType.SOCCER, HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, - HealthWorkoutActivityType.SWIMMING, HealthWorkoutActivityType.TABLE_TENNIS, HealthWorkoutActivityType.TENNIS, HealthWorkoutActivityType.VOLLEYBALL, @@ -1236,76 +1188,27 @@ class Health { HealthWorkoutActivityType.YOGA, // Android only - // Once Google Fit is removed, this list needs to be changed - HealthWorkoutActivityType.AEROBICS, - HealthWorkoutActivityType.BIATHLON, - HealthWorkoutActivityType.BIKING_HAND, - HealthWorkoutActivityType.BIKING_MOUNTAIN, - HealthWorkoutActivityType.BIKING_ROAD, - HealthWorkoutActivityType.BIKING_SPINNING, HealthWorkoutActivityType.BIKING_STATIONARY, - HealthWorkoutActivityType.BIKING_UTILITY, HealthWorkoutActivityType.CALISTHENICS, - HealthWorkoutActivityType.CIRCUIT_TRAINING, - HealthWorkoutActivityType.CROSS_FIT, HealthWorkoutActivityType.DANCING, - HealthWorkoutActivityType.DIVING, - HealthWorkoutActivityType.ELEVATOR, - HealthWorkoutActivityType.ERGOMETER, - HealthWorkoutActivityType.ESCALATOR, HealthWorkoutActivityType.FRISBEE_DISC, - HealthWorkoutActivityType.GARDENING, HealthWorkoutActivityType.GUIDED_BREATHING, - HealthWorkoutActivityType.HORSEBACK_RIDING, - HealthWorkoutActivityType.HOUSEWORK, - HealthWorkoutActivityType.INTERVAL_TRAINING, - HealthWorkoutActivityType.IN_VEHICLE, HealthWorkoutActivityType.ICE_SKATING, - HealthWorkoutActivityType.KAYAKING, - HealthWorkoutActivityType.KETTLEBELL_TRAINING, - HealthWorkoutActivityType.KICK_SCOOTER, - HealthWorkoutActivityType.KITE_SURFING, - HealthWorkoutActivityType.MEDITATION, - HealthWorkoutActivityType.MIXED_MARTIAL_ARTS, - HealthWorkoutActivityType.P90X, HealthWorkoutActivityType.PARAGLIDING, - HealthWorkoutActivityType.POLO, HealthWorkoutActivityType.ROCK_CLIMBING, HealthWorkoutActivityType.ROWING_MACHINE, - HealthWorkoutActivityType.RUNNING_JOGGING, - HealthWorkoutActivityType.RUNNING_SAND, HealthWorkoutActivityType.RUNNING_TREADMILL, HealthWorkoutActivityType.SCUBA_DIVING, - HealthWorkoutActivityType.SKATING_CROSS, - HealthWorkoutActivityType.SKATING_INDOOR, - HealthWorkoutActivityType.SKATING_INLINE, HealthWorkoutActivityType.SKIING, - HealthWorkoutActivityType.SKIING_BACK_COUNTRY, - HealthWorkoutActivityType.SKIING_KITE, - HealthWorkoutActivityType.SKIING_ROLLER, - HealthWorkoutActivityType.SLEDDING, - HealthWorkoutActivityType.SNOWMOBILE, HealthWorkoutActivityType.SNOWSHOEING, HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE, - HealthWorkoutActivityType.STANDUP_PADDLEBOARDING, - HealthWorkoutActivityType.STILL, HealthWorkoutActivityType.STRENGTH_TRAINING, HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, - HealthWorkoutActivityType.TEAM_SPORTS, - HealthWorkoutActivityType.TILTING, - HealthWorkoutActivityType.VOLLEYBALL_BEACH, - HealthWorkoutActivityType.VOLLEYBALL_INDOOR, - HealthWorkoutActivityType.WAKEBOARDING, - HealthWorkoutActivityType.WALKING_FITNESS, - HealthWorkoutActivityType.WALKING_NORDIC, - HealthWorkoutActivityType.WALKING_STROLLER, HealthWorkoutActivityType.WALKING_TREADMILL, HealthWorkoutActivityType.WEIGHTLIFTING, HealthWorkoutActivityType.WHEELCHAIR, - HealthWorkoutActivityType.WINDSURFING, - HealthWorkoutActivityType.ZUMBA, HealthWorkoutActivityType.OTHER, }.contains(type); } diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index 6c161ac1e..502328b51 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -13,7 +13,7 @@ class HealthValue extends Serializable { Map toJson() => _$HealthValueToJson(this); } -/// A numerical value from Apple HealthKit or Google Fit +/// A numerical value from Apple HealthKit or Google Health Connect /// such as integer or double. E.g. 1, 2.9, -3 /// /// Parameters: diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index ba04a21e2..014cfe1da 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -68,7 +68,6 @@ enum HealthDataType { DISTANCE_SWIMMING, DISTANCE_CYCLING, FLIGHTS_CLIMBED, - MOVE_MINUTES, DISTANCE_DELTA, MINDFULNESS, WATER, @@ -226,7 +225,6 @@ const List dataTypeKeysAndroid = [ HealthDataType.HEIGHT, HealthDataType.STEPS, HealthDataType.WEIGHT, - HealthDataType.MOVE_MINUTES, HealthDataType.DISTANCE_DELTA, HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, @@ -315,7 +313,6 @@ const Map dataTypeToUnit = { HealthDataType.DISTANCE_SWIMMING: HealthDataUnit.METER, HealthDataType.DISTANCE_CYCLING: HealthDataUnit.METER, HealthDataType.FLIGHTS_CLIMBED: HealthDataUnit.COUNT, - HealthDataType.MOVE_MINUTES: HealthDataUnit.MINUTE, HealthDataType.DISTANCE_DELTA: HealthDataUnit.METER, HealthDataType.WATER: HealthDataUnit.LITER, @@ -451,26 +448,26 @@ enum HealthWorkoutActivityType { // Commented for which platform the type are supported // Both + AMERICAN_FOOTBALL, ARCHERY, + AUSTRALIAN_FOOTBALL, BADMINTON, BASEBALL, BASKETBALL, BIKING, // This also entails the iOS version where it is called CYCLING BOXING, CRICKET, + CROSS_COUNTRY_SKIING, CURLING, + DOWNHILL_SKIING, ELLIPTICAL, FENCING, - AMERICAN_FOOTBALL, - AUSTRALIAN_FOOTBALL, - SOCCER, GOLF, GYMNASTICS, HANDBALL, HIGH_INTENSITY_INTERVAL_TRAINING, HIKING, HOCKEY, - SKATING, JUMP_ROPE, KICKBOXING, MARTIAL_ARTS, @@ -480,9 +477,9 @@ enum HealthWorkoutActivityType { RUGBY, RUNNING, SAILING, - CROSS_COUNTRY_SKIING, - DOWNHILL_SKIING, + SKATING, SNOWBOARDING, + SOCCER, SOFTBALL, SQUASH, STAIR_CLIMBING, @@ -495,112 +492,64 @@ enum HealthWorkoutActivityType { YOGA, // iOS only + BARRE, BOWLING, + CARDIO_DANCE, + CLIMBING, + COOLDOWN, + CORE_TRAINING, CROSS_TRAINING, - TRACK_AND_FIELD, DISC_SPORTS, - LACROSSE, - PREPARATION_AND_RECOVERY, + EQUESTRIAN_SPORTS, + FISHING, + FITNESS_GAMING, FLEXIBILITY, - COOLDOWN, - WHEELCHAIR_WALK_PACE, - WHEELCHAIR_RUN_PACE, - HAND_CYCLING, - CORE_TRAINING, FUNCTIONAL_STRENGTH_TRAINING, - TRADITIONAL_STRENGTH_TRAINING, - MIXED_CARDIO, - STAIRS, - STEP_TRAINING, - FITNESS_GAMING, - BARRE, - CARDIO_DANCE, - SOCIAL_DANCE, + HAND_CYCLING, + HUNTING, + LACROSSE, MIND_AND_BODY, + MIXED_CARDIO, + PADDLE_SPORTS, PICKLEBALL, - CLIMBING, - EQUESTRIAN_SPORTS, - FISHING, - HUNTING, PLAY, + PREPARATION_AND_RECOVERY, SNOW_SPORTS, - PADDLE_SPORTS, + SOCIAL_DANCE, + STAIRS, + STEP_TRAINING, SURFING_SPORTS, + TAI_CHI, + TRACK_AND_FIELD, + TRADITIONAL_STRENGTH_TRAINING, WATER_FITNESS, WATER_SPORTS, - TAI_CHI, + WHEELCHAIR_RUN_PACE, + WHEELCHAIR_WALK_PACE, WRESTLING, // Android only - AEROBICS, - BIATHLON, - BIKING_HAND, - BIKING_MOUNTAIN, - BIKING_ROAD, - BIKING_SPINNING, BIKING_STATIONARY, - BIKING_UTILITY, CALISTHENICS, - CIRCUIT_TRAINING, - CROSS_FIT, DANCING, - DIVING, - ELEVATOR, - ERGOMETER, - ESCALATOR, FRISBEE_DISC, - GARDENING, GUIDED_BREATHING, - HORSEBACK_RIDING, - HOUSEWORK, - INTERVAL_TRAINING, - IN_VEHICLE, ICE_SKATING, - KAYAKING, - KETTLEBELL_TRAINING, - KICK_SCOOTER, - KITE_SURFING, - MEDITATION, - MIXED_MARTIAL_ARTS, - P90X, PARAGLIDING, - POLO, ROCK_CLIMBING, // on iOS this is the same as CLIMBING ROWING_MACHINE, - RUNNING_JOGGING, // on iOS this is the same as RUNNING - RUNNING_SAND, // on iOS this is the same as RUNNING RUNNING_TREADMILL, // on iOS this is the same as RUNNING SCUBA_DIVING, - SKATING_CROSS, // on iOS this is the same as SKATING - SKATING_INDOOR, // on iOS this is the same as SKATING - SKATING_INLINE, // on iOS this is the same as SKATING SKIING, - SKIING_BACK_COUNTRY, - SKIING_KITE, - SKIING_ROLLER, - SLEDDING, - SNOWMOBILE, SNOWSHOEING, STAIR_CLIMBING_MACHINE, - STANDUP_PADDLEBOARDING, - STILL, STRENGTH_TRAINING, SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, - TEAM_SPORTS, - TILTING, - VOLLEYBALL_BEACH, - VOLLEYBALL_INDOOR, - WAKEBOARDING, - WALKING_FITNESS, - WALKING_NORDIC, - WALKING_STROLLER, WALKING_TREADMILL, WEIGHTLIFTING, WHEELCHAIR, - WINDSURFING, - ZUMBA, // OTHER, diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index cc118c503..dbfe4d751 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -1,5 +1,5 @@ name: health -description: Wrapper for HealthKit on iOS and Google Fit and Health Connect on Android. +description: Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. version: 10.2.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health From d1a89123ff5d2e1443ea0a527ce050e21fb8737f Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 22 Aug 2024 13:49:47 +0200 Subject: [PATCH 3/5] [Health] Combine certain workout types for consistency across iOS and Android (#1020) * Remove Google Fit and imports from Android code * Formatting * Remove Google Fit column from readme * Remove support for Google Fit types not supported by Health Connect * Remove more Google Fit workout types * Remove references to Google Fit, remove `useHealthConnectIfAvailable` * Remove `disconect` method channel * Remove `flowRate` from `writeBloodOxygen` as it is not supported in Health Connect * Remove more unsupported workout types * Add missing import * Remove Google Fit as dependency * Add notice in README * Improve logging for HC permission callback * Update some documentation * Android: Fix `requestAuthorization` not returning a result on success * Combine some types for consistency across iOS and Android --- packages/health/README.md | 190 +++++++++--------- .../cachet/plugins/health/HealthPlugin.kt | 16 +- .../ios/Classes/SwiftHealthPlugin.swift | 4 +- packages/health/lib/health.g.dart | 3 +- packages/health/lib/src/health_plugin.dart | 8 +- packages/health/lib/src/heath_data_types.dart | 3 +- 6 files changed, 121 insertions(+), 103 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index ccd2d913c..c18e7e729 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -329,98 +329,98 @@ The plugin supports the following [`HealthDataType`](https://pub.dev/documentati The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/documentation/health/latest/health/HealthWorkoutActivityType.html). -| **Workout Type** | **Apple Health** | **Google Health Connect** | **Comments** | -| -------------------------------- | ---------------- | ------------------------- | ----------------------------------------------------------------- | -| AMERICAN_FOOTBALL | yes | yes | | -| ARCHERY | yes | | | -| AUSTRALIAN_FOOTBALL | yes | yes | | -| BADMINTON | yes | yes | | -| BARRE | yes | | | -| BASEBALL | yes | yes | | -| BASKETBALL | yes | yes | | -| BIKING | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | -| BOWLING | yes | | | -| BOXING | yes | yes | | -| CALISTHENICS | | yes | | -| CARDIO_DANCE | yes | | | -| CLIMBING | yes | | | -| COOLDOWN | yes | | | -| CORE_TRAINING | yes | | | -| CRICKET | yes | yes | | -| CROSS_COUNTRY_SKIING | yes | | | -| CROSS_TRAINING | yes | | | -| CURLING | yes | | | -| DANCING | yes | yes | on iOS this is DANCE, but name changed here to fit with Android | -| DISC_SPORTS | yes | | | -| DOWNHILL_SKIING | yes | | | -| ELLIPTICAL | yes | yes | | -| EQUESTRIAN_SPORTS | yes | | | -| FENCING | yes | yes | | -| FISHING | yes | | | -| FITNESS_GAMING | yes | | | -| FLEXIBILITY | yes | | | -| FRISBEE_DISC | | yes | | -| FUNCTIONAL_STRENGTH_TRAINING | yes | | | -| GOLF | yes | yes | | -| GUIDED_BREATHING | | yes | | -| GYMNASTICS | yes | yes | | -| HAND_CYCLING | yes | | | -| HANDBALL | yes | yes | | -| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | | -| HIKING | yes | yes | | -| HOCKEY | yes | | | -| HUNTING | yes | | | -| JUMP_ROPE | yes | | | -| KICKBOXING | yes | | | -| LACROSSE | yes | | | -| MARTIAL_ARTS | yes | yes | | -| MIND_AND_BODY | yes | | | -| MIXED_CARDIO | yes | | | -| PADDLE_SPORTS | yes | | | -| PARAGLIDING | | yes | | -| PICKLEBALL | yes | | | -| PILATES | yes | yes | | -| PLAY | yes | | | -| PREPARATION_AND_RECOVERY | yes | | | -| RACQUETBALL | yes | yes | | -| ROCK_CLIMBING | (yes) | yes | on iOS this will be stored as CLIMBING | -| ROWING | yes | yes | | -| RUGBY | yes | yes | | -| RUNNING | yes | yes | | -| RUNNING_TREADMILL | (yes) | yes | on iOS this will be stored as RUNNING | -| SAILING | yes | yes | | -| SCUBA_DIVING | | yes | | -| SKATING | yes | yes | On iOS this is skating_sports | -| SNOW_SPORTS | yes | | | -| SNOWBOARDING | yes | yes | | -| SOCCER | yes | | | -| SOCIAL_DANCE | yes | | | -| SOFTBALL | yes | yes | | -| SQUASH | yes | yes | | -| STAIR_CLIMBING | yes | yes | | -| STAIR_CLIMBING_MACHINE | | yes | | -| STAIRS | yes | | | -| STEP_TRAINING | yes | | | -| STRENGTH_TRAINING | | yes | | -| SURFING | | yes | | -| SURFING_SPORTS | yes | | | -| SWIMMING | yes | | | -| SWIMMING_OPEN_WATER | | yes | | -| SWIMMING_POOL | | yes | | -| TABLE_TENNIS | yes | yes | | -| TAI_CHI | yes | | | -| TENNIS | yes | yes | | -| TRACK_AND_FIELD | yes | | | -| TRADITIONAL_STRENGTH_TRAINING | yes | | | -| VOLLEYBALL | yes | yes | | -| WALKING | yes | yes | | -| WATER_FITNESS | yes | | | -| WATER_POLO | yes | yes | | -| WATER_SPORTS | yes | | | -| WEIGHTLIFTING | | yes | | -| WHEELCHAIR | | yes | | -| WHEELCHAIR_RUN_PACE | yes | | | -| WHEELCHAIR_WALK_PACE | yes | | | -| WRESTLING | yes | | | -| YOGA | yes | yes | | -| OTHER | yes | yes | | +| **Workout Type** | **Apple Health** | **Google Health Connect** | **Comments** | +| -------------------------------- | ---------------- | ------------------------- | ----------------------------------------------------------------------------------------------- | +| AMERICAN_FOOTBALL | yes | yes | | +| ARCHERY | yes | | | +| AUSTRALIAN_FOOTBALL | yes | yes | | +| BADMINTON | yes | yes | | +| BARRE | yes | | | +| BASEBALL | yes | yes | | +| BASKETBALL | yes | yes | | +| BIKING | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | +| BOWLING | yes | | | +| BOXING | yes | yes | | +| CALISTHENICS | | yes | | +| CARDIO_DANCE | yes | (yes) | on Android this will be stored as DANCING | +| CLIMBING | yes | | | +| COOLDOWN | yes | | | +| CORE_TRAINING | yes | | | +| CRICKET | yes | yes | | +| CROSS_COUNTRY_SKIING | yes | (yes) | on Android this will be stored as SKIING | +| CROSS_TRAINING | yes | | | +| CURLING | yes | | | +| DANCING | yes | yes | on iOS this is DANCE, but name changed here to fit with Android | +| DISC_SPORTS | yes | | | +| DOWNHILL_SKIING | yes | (yes) | on Android this will be stored as SKIING | +| ELLIPTICAL | yes | yes | | +| EQUESTRIAN_SPORTS | yes | | | +| FENCING | yes | yes | | +| FISHING | yes | | | +| FITNESS_GAMING | yes | | | +| FLEXIBILITY | yes | | | +| FRISBEE_DISC | | yes | | +| FUNCTIONAL_STRENGTH_TRAINING | yes | (yes) | on Android this will be stored as STRENGTH_TRAINING | +| GOLF | yes | yes | | +| GUIDED_BREATHING | | yes | | +| GYMNASTICS | yes | yes | | +| HAND_CYCLING | yes | | | +| HANDBALL | yes | yes | | +| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | | +| HIKING | yes | yes | | +| HOCKEY | yes | | | +| HUNTING | yes | | | +| JUMP_ROPE | yes | | | +| KICKBOXING | yes | | | +| LACROSSE | yes | | | +| MARTIAL_ARTS | yes | yes | | +| MIND_AND_BODY | yes | | | +| MIXED_CARDIO | yes | | | +| PADDLE_SPORTS | yes | | | +| PARAGLIDING | | yes | | +| PICKLEBALL | yes | | | +| PILATES | yes | yes | | +| PLAY | yes | | | +| PREPARATION_AND_RECOVERY | yes | | | +| RACQUETBALL | yes | yes | | +| ROCK_CLIMBING | (yes) | yes | on iOS this will be stored as CLIMBING | +| ROWING | yes | yes | | +| RUGBY | yes | yes | | +| RUNNING | yes | yes | | +| RUNNING_TREADMILL | (yes) | yes | on iOS this will be stored as RUNNING | +| SAILING | yes | yes | | +| SCUBA_DIVING | | yes | | +| SKATING | yes | yes | On iOS this will be stored as SKATING_SPORTS | +| SKIING | (yes) | yes | on iOS you have to choose between CROSS_COUNTRY_SKIING and DOWNHILL_SKIING | +| SNOW_SPORTS | yes | | | +| SNOWBOARDING | yes | yes | | +| SOCCER | yes | | | +| SOCIAL_DANCE | yes | (yes) | on Android this will be stored as DANCING | +| SOFTBALL | yes | yes | | +| SQUASH | yes | yes | | +| STAIR_CLIMBING | yes | yes | | +| STAIR_CLIMBING_MACHINE | | yes | | +| STAIRS | yes | | | +| STEP_TRAINING | yes | | | +| STRENGTH_TRAINING | (yes) | yes | on iOS you have to choose between FUNCTIONAL_STRENGTH_TRAINING or TRADITIONAL_STRENGTH_TRAINING | +| SURFING | yes | yes | on iOS this is SURFING_SPORTS, but name changed here to fit with Android | +| SWIMMING | yes | (yes) | on Android you have to choose between SWIMMING_OPEN_WATER and SWIMMING_POOL | +| SWIMMING_OPEN_WATER | (yes) | yes | on iOS this will be stored as SWIMMING | +| SWIMMING_POOL | (yes) | yes | on iOS this will be stored as SWIMMING | +| TABLE_TENNIS | yes | yes | | +| TAI_CHI | yes | | | +| TENNIS | yes | yes | | +| TRACK_AND_FIELD | yes | | | +| TRADITIONAL_STRENGTH_TRAINING | yes | (yes) | on Android this will be stored as STRENGTH_TRAINING | +| VOLLEYBALL | yes | yes | | +| WALKING | yes | yes | | +| WATER_FITNESS | yes | | | +| WATER_POLO | yes | yes | | +| WATER_SPORTS | yes | | | +| WEIGHTLIFTING | | yes | | +| WHEELCHAIR | (yes) | yes | on iOS you have to choose between WHEELCHAIR_RUN_PACE or WHEELCHAIR_WALK_PACE | +| WHEELCHAIR_RUN_PACE | yes | (yes) | on Android this will be stored as WHEELCHAIR | +| WHEELCHAIR_WALK_PACE | yes | (yes) | on Android this will be stored as WHEELCHAIR | +| WRESTLING | yes | | | +| YOGA | yes | yes | | +| OTHER | 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 b80b2b53d..a4761f48f 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 @@ -2203,10 +2203,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "CALISTHENICS" to ExerciseSessionRecord .EXERCISE_TYPE_CALISTHENICS, + "CARDIO_DANCE" to + ExerciseSessionRecord + .EXERCISE_TYPE_DANCING, "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, - // "CROSS_COUNTRY_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, + "CROSS_COUNTRY_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, - // "DOWNHILL_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, + "DOWNHILL_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, "ELLIPTICAL" to ExerciseSessionRecord .EXERCISE_TYPE_ELLIPTICAL, @@ -2265,6 +2268,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ExerciseSessionRecord .EXERCISE_TYPE_SNOWSHOEING, // "SOCCER" to ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, + "SOCIAL_DANCE" to + ExerciseSessionRecord + .EXERCISE_TYPE_DANCING, "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, "STAIR_CLIMBING_MACHINE" to @@ -2300,6 +2306,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "WHEELCHAIR" to ExerciseSessionRecord .EXERCISE_TYPE_WHEELCHAIR, + "WHEELCHAIR_RUN_PACE" to + ExerciseSessionRecord + .EXERCISE_TYPE_WHEELCHAIR, + "WHEELCHAIR_WALK_PACE" to + ExerciseSessionRecord + .EXERCISE_TYPE_WHEELCHAIR, "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT, ) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 9b00a3c2b..19d8cc207 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -1339,8 +1339,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["PADDLE_SPORTS"] = .paddleSports workoutActivityTypeMap["ROWING"] = .rowing workoutActivityTypeMap["SAILING"] = .sailing - workoutActivityTypeMap["SURFING_SPORTS"] = .surfingSports + workoutActivityTypeMap["SURFING"] = .surfingSports workoutActivityTypeMap["SWIMMING"] = .swimming + workoutActivityTypeMap["SWIMMING_OPEN_WATER"] = .swimming + workoutActivityTypeMap["SWIMMING_POOL"] = .swimming workoutActivityTypeMap["WATER_FITNESS"] = .waterFitness workoutActivityTypeMap["WATER_POLO"] = .waterPolo workoutActivityTypeMap["WATER_SPORTS"] = .waterSports diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 104be599d..7740670ae 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -385,7 +385,7 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', HealthWorkoutActivityType.STAIRS: 'STAIRS', HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', - HealthWorkoutActivityType.SURFING_SPORTS: 'SURFING_SPORTS', + HealthWorkoutActivityType.SURFING: 'SURFING', HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: @@ -410,7 +410,6 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', - HealthWorkoutActivityType.SURFING: 'SURFING', HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', HealthWorkoutActivityType.WALKING_TREADMILL: 'WALKING_TREADMILL', diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index a55bc49e4..a6232e202 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1123,7 +1123,7 @@ class Health { HealthWorkoutActivityType.STAIR_CLIMBING, HealthWorkoutActivityType.STAIRS, HealthWorkoutActivityType.STEP_TRAINING, - HealthWorkoutActivityType.SURFING_SPORTS, + HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING, HealthWorkoutActivityType.TABLE_TENNIS, HealthWorkoutActivityType.TAI_CHI, @@ -1139,6 +1139,8 @@ class Health { HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, HealthWorkoutActivityType.WRESTLING, HealthWorkoutActivityType.YOGA, + HealthWorkoutActivityType.SWIMMING_OPEN_WATER, + HealthWorkoutActivityType.SWIMMING_POOL, }.contains(type); } @@ -1155,6 +1157,7 @@ class Health { HealthWorkoutActivityType.BASKETBALL, HealthWorkoutActivityType.BIKING, HealthWorkoutActivityType.BOXING, + HealthWorkoutActivityType.CARDIO_DANCE, HealthWorkoutActivityType.CRICKET, HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, HealthWorkoutActivityType.CURLING, @@ -1177,6 +1180,7 @@ class Health { HealthWorkoutActivityType.SKATING, HealthWorkoutActivityType.SNOWBOARDING, HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.SOCIAL_DANCE, HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, @@ -1185,6 +1189,8 @@ class Health { HealthWorkoutActivityType.VOLLEYBALL, HealthWorkoutActivityType.WALKING, HealthWorkoutActivityType.WATER_POLO, + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, HealthWorkoutActivityType.YOGA, // Android only diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index 014cfe1da..c498de3a1 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -518,7 +518,7 @@ enum HealthWorkoutActivityType { SOCIAL_DANCE, STAIRS, STEP_TRAINING, - SURFING_SPORTS, + SURFING, TAI_CHI, TRACK_AND_FIELD, TRADITIONAL_STRENGTH_TRAINING, @@ -544,7 +544,6 @@ enum HealthWorkoutActivityType { SNOWSHOEING, STAIR_CLIMBING_MACHINE, STRENGTH_TRAINING, - SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, WALKING_TREADMILL, From b70d1d7a5accdb019c9c3d8a54f8a53e5628913e Mon Sep 17 00:00:00 2001 From: Panagiotis Giannoutsos <36935711+Panosfunk@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:37:18 +0200 Subject: [PATCH 4/5] Update README.md --- 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 c18e7e729..12f71cbdc 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -2,7 +2,7 @@ Enables reading and writing health data from/to Apple Health and Health Connect. -> **NOTE:** Google has deprecated the Google Fit API. According to the [documentation](https://developers.google.com/fit/android), as of **May 1st 2014** developers cannot sign up for using the API. As such, this package has removed support for Google Fit as of version 11.0.0 and users are urged to upgrade as soon as possible. +> **NOTE:** Google has deprecated the Google Fit API. According to the [documentation](https://developers.google.com/fit/android), as of **May 1st 2024** developers cannot sign up for using the API. As such, this package has removed support for Google Fit as of version 11.0.0 and users are urged to upgrade as soon as possible. The plugin supports: From 4b19432f8717317cb3567bee09c12a2d3d9d2b02 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 29 Aug 2024 16:18:06 +0200 Subject: [PATCH 5/5] [Health] Add UUID field to `HealthDataPoint` (#1019) * Remove Google Fit and imports from Android code * Formatting * Remove Google Fit column from readme * Remove support for Google Fit types not supported by Health Connect * Remove more Google Fit workout types * Remove references to Google Fit, remove `useHealthConnectIfAvailable` * Remove `disconect` method channel * Remove `flowRate` from `writeBloodOxygen` as it is not supported in Health Connect * Remove more unsupported workout types * Add missing import * Remove Google Fit as dependency * Add notice in README * Improve logging for HC permission callback * Update some documentation * Android: Fix `requestAuthorization` not returning a result on success * Remove additional workout types that are not supported * Remove another workout type * Add missing unimplemented method * Add `uuid` field to `HealthDataPoint` and include it in datapoints * Update README.md --------- Co-authored-by: bardram --- packages/health/README.md | 1 + .../cachet/plugins/health/HealthPlugin.kt | 52 +++++++++++++++++-- packages/health/lib/health.g.dart | 2 + .../health/lib/src/health_data_point.dart | 10 +++- packages/health/lib/src/health_plugin.dart | 1 + 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 12f71cbdc..bbf0cd1fe 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -191,6 +191,7 @@ Below is a simplified flow of how to use the plugin. A [`HealthDataPoint`](https://pub.dev/documentation/health/latest/health/HealthDataPoint-class.html) object contains the following data fields: ```dart +String uuid; HealthValue value; HealthDataType type; HealthDataUnit unit; 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 a4761f48f..844d350f6 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 @@ -18,6 +18,7 @@ import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER import androidx.health.connect.client.records.MealType.MEAL_TYPE_LUNCH import androidx.health.connect.client.records.MealType.MEAL_TYPE_SNACK import androidx.health.connect.client.records.MealType.MEAL_TYPE_UNKNOWN +import androidx.health.connect.client.records.metadata.Metadata import androidx.health.connect.client.request.AggregateGroupByDurationRequest import androidx.health.connect.client.request.AggregateRequest import androidx.health.connect.client.request.ReadRecordsRequest @@ -743,6 +744,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : healthConnectData.add( // mapOf( mapOf( + "uuid" to record.metadata.id, "workoutActivityType" to (workoutTypeMap .filterValues { @@ -815,8 +817,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : convertRecordStage( recStage, dataType, - rec.metadata.dataOrigin - .packageName + rec.metadata ) ) } @@ -847,10 +848,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : private fun convertRecordStage( stage: SleepSessionRecord.Stage, dataType: String, - sourceName: String + metadata: Metadata ): List> { + var sourceName = metadata.dataOrigin + .packageName return listOf( mapOf( + "uuid" to metadata.id, "stage" to stage.stage, "value" to ChronoUnit.MINUTES.between( @@ -946,6 +950,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is WeightRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.weight .inKilograms, @@ -965,6 +971,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is HeightRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.height .inMeters, @@ -984,6 +992,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is BodyFatRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.percentage .value, @@ -1003,6 +1013,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is StepsRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.count, "date_from" to record.startTime @@ -1020,6 +1032,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is ActiveCaloriesBurnedRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.energy .inKilocalories, @@ -1039,6 +1053,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is HeartRateRecord -> return record.samples.map { mapOf( + "uuid" to + metadata.id, "value" to it.beatsPerMinute, "date_from" to it.time.toEpochMilli(), @@ -1053,6 +1069,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is HeartRateVariabilityRmssdRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.heartRateVariabilityMillis, "date_from" to @@ -1071,6 +1089,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is BodyTemperatureRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.temperature .inCelsius, @@ -1090,6 +1110,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is BodyWaterMassRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.mass .inKilograms, @@ -1109,6 +1131,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is BloodPressureRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to if (dataType == BLOOD_PRESSURE_DIASTOLIC @@ -1134,6 +1158,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is OxygenSaturationRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.percentage .value, @@ -1153,6 +1179,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is BloodGlucoseRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.level .inMilligramsPerDeciliter, @@ -1172,6 +1200,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is DistanceRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.distance .inMeters, @@ -1191,6 +1221,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is HydrationRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.volume .inLiters, @@ -1210,6 +1242,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is TotalCaloriesBurnedRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.energy .inKilocalories, @@ -1229,6 +1263,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is BasalMetabolicRateRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.basalMetabolicRate .inKilocaloriesPerDay, @@ -1248,6 +1284,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is SleepSessionRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "date_from" to record.startTime .toEpochMilli(), @@ -1270,6 +1308,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is RestingHeartRateRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.beatsPerMinute, "date_from" to @@ -1288,6 +1328,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is FloorsClimbedRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.floors, "date_from" to record.startTime @@ -1305,6 +1347,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is RespiratoryRateRecord -> return listOf( mapOf( + "uuid" to + metadata.id, "value" to record.rate, "date_from" to record.time @@ -1322,6 +1366,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is NutritionRecord -> return listOf( mapOf( + "uuid" to metadata.id, "calories" to record.energy?.inKilocalories, "protein" to record.protein?.inGrams, "carbs" to record.totalCarbohydrate?.inGrams, @@ -1385,6 +1430,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : is MenstruationFlowRecord -> return listOf( mapOf( + "uuid" to metadata.id, "value" to record.flow, "date_from" to record.time.toEpochMilli(), "date_to" to record.time.toEpochMilli(), diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 7740670ae..13f14c5a6 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -8,6 +8,7 @@ part of 'health.dart'; HealthDataPoint _$HealthDataPointFromJson(Map json) => HealthDataPoint( + uuid: json['uuid'] as String, value: HealthValue.fromJson(json['value'] as Map), type: $enumDecode(_$HealthDataTypeEnumMap, json['type']), unit: $enumDecode(_$HealthDataUnitEnumMap, json['unit']), @@ -28,6 +29,7 @@ HealthDataPoint _$HealthDataPointFromJson(Map json) => Map _$HealthDataPointToJson(HealthDataPoint instance) { final val = { + 'uuid': instance.uuid, 'value': instance.value, 'type': _$HealthDataTypeEnumMap[instance.type]!, 'unit': _$HealthDataUnitEnumMap[instance.unit]!, diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index b98734c5a..ec5a98a9c 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -8,6 +8,9 @@ enum HealthPlatformType { appleHealth, googleHealthConnect } /// as value. @JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) class HealthDataPoint { + /// UUID of the data point. + String uuid; + /// The quantity value of the data point HealthValue value; @@ -51,6 +54,7 @@ class HealthDataPoint { Map? metadata; HealthDataPoint({ + required this.uuid, required this.value, required this.type, required this.unit, @@ -129,6 +133,7 @@ class HealthDataPoint { ? null : Map.from(dataPoint['metadata'] as Map); final unit = dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT; + final String? uuid = dataPoint["uuid"] as String?; // Set WorkoutSummary, if available. WorkoutSummary? workoutSummary; @@ -140,6 +145,7 @@ class HealthDataPoint { } return HealthDataPoint( + uuid: uuid ?? "", value: value, type: dataType, unit: unit, @@ -157,6 +163,7 @@ class HealthDataPoint { @override String toString() => """$runtimeType - + uuid: $uuid, value: ${value.toString()}, unit: ${unit.name}, dateFrom: $dateFrom, @@ -173,6 +180,7 @@ class HealthDataPoint { @override bool operator ==(Object other) => other is HealthDataPoint && + uuid == other.uuid && value == other.value && unit == other.unit && dateFrom == other.dateFrom && @@ -186,6 +194,6 @@ class HealthDataPoint { metadata == other.metadata; @override - int get hashCode => Object.hash(value, unit, dateFrom, dateTo, type, + int get hashCode => Object.hash(uuid, value, unit, dateFrom, dateTo, type, sourcePlatform, sourceDeviceId, sourceId, sourceName, metadata); } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index a6232e202..9375e5c81 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -265,6 +265,7 @@ class Health { (weights[i].value as NumericHealthValue).numericValue.toDouble() / (h * h); final x = HealthDataPoint( + uuid: '', value: NumericHealthValue(numericValue: bmiValue), type: dataType, unit: unit,