From 323b454c6f8c34b32d49b584c072a01489b089f0 Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Tue, 5 Nov 2024 14:57:02 +0700 Subject: [PATCH 01/25] Update health.json.dart --- packages/health/lib/health.json.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/health/lib/health.json.dart b/packages/health/lib/health.json.dart index 351098a28..2162869da 100644 --- a/packages/health/lib/health.json.dart +++ b/packages/health/lib/health.json.dart @@ -10,6 +10,7 @@ void _registerFromJsonFunctions() { FromJsonFactory().registerAll([ HealthValue(), NumericHealthValue(numericValue: 12), + WorkoutHealthValue(workoutActivityType: HealthWorkoutActivityType.RUNNING), AudiogramHealthValue( frequencies: [], leftEarSensitivities: [], From 4484e45e7352ed616b69ab2b369001c4f1eb6f1c Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Thu, 28 Nov 2024 14:51:38 +0700 Subject: [PATCH 02/25] Update HealthPlugin.kt --- .../kotlin/cachet/plugins/health/HealthPlugin.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 70690126a..f220d1e3b 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 @@ -32,11 +32,11 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.ActivityResultListener +import kotlinx.coroutines.* import java.time.* import java.time.temporal.ChronoUnit import java.util.* import java.util.concurrent.* -import kotlinx.coroutines.* const val CHANNEL_NAME = "flutter_health" @@ -677,9 +677,21 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val healthConnectData = mutableListOf>() val recordingMethodsToFilter = call.argument>("recordingMethodsToFilter")!! + val zoneId = ZoneId.systemDefault() // Use the system default time zone + val localDate = startTime.atZone(zoneId) + .toLocalDate() + .atStartOfDay(zoneId) + val localDateInstant = localDate + .toInstant() + + + Log.i("ZONE_ID", "$zoneId") + Log.i("DATE_TIME", "$localDate") + Log.i("DATE_TIME_INSTANT", "$localDateInstant") + Log.i( "FLUTTER_HEALTH", - "Getting data for $dataType between $startTime and $endTime, filtering by $recordingMethodsToFilter" + "Getting data $dataType between $startTime - $endTime" ) scope.launch { From f6a73a962c8ef0ac85a1c12830c2dfac67f8a275 Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Mon, 17 Mar 2025 10:18:51 +0700 Subject: [PATCH 03/25] Update example app to align with latest Flutter version --- packages/health/example/.gitignore | 43 ++ packages/health/example/.metadata | 39 +- packages/health/example/README.md | 12 +- packages/health/example/analysis_options.yaml | 29 ++ packages/health/example/android/.gitignore | 13 + .../health/example/android/app/build.gradle | 71 ++-- .../android/app/src/debug/AndroidManifest.xml | 6 +- .../android/app/src/main/AndroidManifest.xml | 46 ++- .../plugins/example_app/MainActivity.kt | 2 - .../res/drawable-v21/launch_background.xml | 12 + .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 2 +- .../app/src/profile/AndroidManifest.xml | 6 +- packages/health/example/android/build.gradle | 17 +- .../health/example/android/gradle.properties | 5 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../health/example/android/settings.gradle | 30 +- packages/health/example/ios/.gitignore | 34 ++ .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../health/example/ios/Flutter/Debug.xcconfig | 2 +- .../example/ios/Flutter/Release.xcconfig | 2 +- packages/health/example/ios/Podfile | 3 + .../ios/Runner.xcodeproj/project.pbxproj | 378 +++++++++++------- .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 11 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 2 +- .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 1418 bytes packages/health/example/ios/Runner/Info.plist | 2 + .../ios/Runner/Runner-Bridging-Header.h | 2 +- .../example/ios/RunnerTests/RunnerTests.swift | 12 + packages/health/example/lib/main.dart | 6 +- packages/health/example/pubspec.yaml | 84 +++- 46 files changed, 648 insertions(+), 261 deletions(-) create mode 100644 packages/health/example/.gitignore create mode 100644 packages/health/example/analysis_options.yaml create mode 100644 packages/health/example/android/.gitignore create mode 100644 packages/health/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/health/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/health/example/ios/.gitignore create mode 100644 packages/health/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/health/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/health/example/ios/RunnerTests/RunnerTests.swift diff --git a/packages/health/example/.gitignore b/packages/health/example/.gitignore new file mode 100644 index 000000000..29a3a5017 --- /dev/null +++ b/packages/health/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/health/example/.metadata b/packages/health/example/.metadata index 959f67f99..391c336b2 100644 --- a/packages/health/example/.metadata +++ b/packages/health/example/.metadata @@ -4,7 +4,42 @@ # This file should be version controlled and should not be manually edited. version: - revision: cc949a8e8b9cf394b9290a8e80f87af3e207dce5 - channel: beta + revision: "5874a72aa4c779a02553007c47dacbefba2374dc" + channel: "stable" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: android + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: ios + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: linux + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: macos + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: web + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + - platform: windows + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/health/example/README.md b/packages/health/example/README.md index 1c4dbcfc1..388bfb9bb 100644 --- a/packages/health/example/README.md +++ b/packages/health/example/README.md @@ -1,6 +1,6 @@ -# health_example +# example_new -Demonstrates how to use the health plugin. +A new Flutter project. ## Getting Started @@ -8,9 +8,9 @@ This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, samples, guidance on mobile development, and a full API reference. diff --git a/packages/health/example/analysis_options.yaml b/packages/health/example/analysis_options.yaml new file mode 100644 index 000000000..a55a06311 --- /dev/null +++ b/packages/health/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + constant_identifier_names: false + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/health/example/android/.gitignore b/packages/health/example/android/.gitignore new file mode 100644 index 000000000..55afd919c --- /dev/null +++ b/packages/health/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/health/example/android/app/build.gradle b/packages/health/example/android/app/build.gradle index 63e07ba82..1d719ae05 100644 --- a/packages/health/example/android/app/build.gradle +++ b/packages/health/example/android/app/build.gradle @@ -1,69 +1,44 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion 34 + namespace = "cachet.plugins.example_app" + compileSdk = 34 + ndkVersion = '25.1.8937393' - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "cachet.plugins.example_app" - minSdkVersion 26 - targetSdkVersion 34 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + applicationId = "cachet.plugins.example_app" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 26 + targetSdk = 34 + versionCode = flutter.versionCode + versionName = flutter.versionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } - lint { - disable 'InvalidPackage' - } } flutter { - source '../..' -} - -dependencies { - def composeBom = platform('androidx.compose:compose-bom:2022.10.00') - implementation(composeBom) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + source = "../.." } diff --git a/packages/health/example/android/app/src/debug/AndroidManifest.xml b/packages/health/example/android/app/src/debug/AndroidManifest.xml index b4e67ed6e..399f6981d 100644 --- a/packages/health/example/android/app/src/debug/AndroidManifest.xml +++ b/packages/health/example/android/app/src/debug/AndroidManifest.xml @@ -1,6 +1,6 @@ - - diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml index 996adaf97..a1a2eebdb 100644 --- a/packages/health/example/android/app/src/main/AndroidManifest.xml +++ b/packages/health/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + @@ -68,18 +67,27 @@ - - - - + + @@ -90,6 +98,7 @@ + - + + + + + + + + + + diff --git a/packages/health/example/android/app/src/main/kotlin/cachet/plugins/example_app/MainActivity.kt b/packages/health/example/android/app/src/main/kotlin/cachet/plugins/example_app/MainActivity.kt index 720eb0f47..3ce87442f 100644 --- a/packages/health/example/android/app/src/main/kotlin/cachet/plugins/example_app/MainActivity.kt +++ b/packages/health/example/android/app/src/main/kotlin/cachet/plugins/example_app/MainActivity.kt @@ -1,7 +1,5 @@ package cachet.plugins.example_app -import android.os.Bundle - import io.flutter.embedding.android.FlutterFragmentActivity class MainActivity: FlutterFragmentActivity() { diff --git a/packages/health/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/health/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/packages/health/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/health/example/android/app/src/main/res/values-night/styles.xml b/packages/health/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..06952be74 --- /dev/null +++ b/packages/health/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/health/example/android/app/src/main/res/values/styles.xml b/packages/health/example/android/app/src/main/res/values/styles.xml index d460d1e92..cb1ef8805 100644 --- a/packages/health/example/android/app/src/main/res/values/styles.xml +++ b/packages/health/example/android/app/src/main/res/values/styles.xml @@ -3,7 +3,7 @@ diff --git a/packages/health/example/android/build.gradle b/packages/health/example/android/build.gradle index e306d8ebf..d2ffbffa4 100644 --- a/packages/health/example/android/build.gradle +++ b/packages/health/example/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.0' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -18,12 +5,12 @@ allprojects { } } -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { - project.evaluationDependsOn(':app') + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { diff --git a/packages/health/example/android/gradle.properties b/packages/health/example/android/gradle.properties index d2032bce8..259717082 100644 --- a/packages/health/example/android/gradle.properties +++ b/packages/health/example/android/gradle.properties @@ -1,4 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableJetifier=true +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableR8=true +android.enableJetifier=true diff --git a/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties index 8049c684f..7bb2df6ba 100644 --- a/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/packages/health/example/android/settings.gradle b/packages/health/example/android/settings.gradle index 5a2f14fb1..ff1ea2190 100644 --- a/packages/health/example/android/settings.gradle +++ b/packages/health/example/android/settings.gradle @@ -1,15 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.2.2" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false } + +include ":app" diff --git a/packages/health/example/ios/.gitignore b/packages/health/example/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/packages/health/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/health/example/ios/Flutter/AppFrameworkInfo.plist b/packages/health/example/ios/Flutter/AppFrameworkInfo.plist index 8c6e56146..7c5696400 100644 --- a/packages/health/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/health/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) + en CFBundleExecutable App CFBundleIdentifier diff --git a/packages/health/example/ios/Flutter/Debug.xcconfig b/packages/health/example/ios/Flutter/Debug.xcconfig index e8efba114..ec97fc6f3 100644 --- a/packages/health/example/ios/Flutter/Debug.xcconfig +++ b/packages/health/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/health/example/ios/Flutter/Release.xcconfig b/packages/health/example/ios/Flutter/Release.xcconfig index 399e9340e..c4855bfe2 100644 --- a/packages/health/example/ios/Flutter/Release.xcconfig +++ b/packages/health/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/health/example/ios/Podfile b/packages/health/example/ios/Podfile index 0b62d79ed..04c36cf46 100644 --- a/packages/health/example/ios/Podfile +++ b/packages/health/example/ios/Podfile @@ -32,6 +32,9 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index 77f2ffb80..04af93dd6 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,15 +8,26 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 20939D465FC7D9B071A7D0EE /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 48CB653842D0CA705AA8BA01 /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 5FC8384EA5BC0E70B579910E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87EA13F4837A2485245BF0FD /* Pods_Runner.framework */; }; + 5FFE590FDD29C4FD5D6394B5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E129ECD4A5F2D7BF0C7B21A7 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 82F0E6512375BDAE0022096E /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82F0E6502375BDAE0022096E /* HealthKit.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -33,16 +44,16 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 1C793EC91E7B6FC11F0E5E44 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 311CD59E63C33ABB13AB7081 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3C038B01E9A5A754A9CA72B6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 4F5A1AFCB405AB82BD233633 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 48CB653842D0CA705AA8BA01 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6ECE2F283BAAC3A051E6C8E7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 82F0E64E2375BDAE0022096E /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - 82F0E6502375BDAE0022096E /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; - 87EA13F4837A2485245BF0FD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 846E37F8BA9FF9D4591F14A9 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -50,27 +61,59 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D6912A28277CD12C0012EA21 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; + C3BB7B959F80595D4B969F00 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + CBFB02E263E2832FB7A41212 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + E129ECD4A5F2D7BF0C7B21A7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F661B1B5FF0034259235461C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 92515CC2FB6927D4DD3921DC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 20939D465FC7D9B071A7D0EE /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 82F0E6512375BDAE0022096E /* HealthKit.framework in Frameworks */, - 5FC8384EA5BC0E70B579910E /* Pods_Runner.framework in Frameworks */, + 5FFE590FDD29C4FD5D6394B5 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 82F0E64F2375BDAE0022096E /* Frameworks */ = { + 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( - 82F0E6502375BDAE0022096E /* HealthKit.framework */, - 87EA13F4837A2485245BF0FD /* Pods_Runner.framework */, + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 5A0C11D934441F0383944A0C /* Pods */ = { + isa = PBXGroup; + children = ( + 311CD59E63C33ABB13AB7081 /* Pods-Runner.debug.xcconfig */, + 6ECE2F283BAAC3A051E6C8E7 /* Pods-Runner.release.xcconfig */, + CBFB02E263E2832FB7A41212 /* Pods-Runner.profile.xcconfig */, + C3BB7B959F80595D4B969F00 /* Pods-RunnerTests.debug.xcconfig */, + F661B1B5FF0034259235461C /* Pods-RunnerTests.release.xcconfig */, + 846E37F8BA9FF9D4591F14A9 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 790E7CE3EB69552F02D2723D /* Frameworks */ = { + isa = PBXGroup; + children = ( + E129ECD4A5F2D7BF0C7B21A7 /* Pods_Runner.framework */, + 48CB653842D0CA705AA8BA01 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -92,8 +135,9 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 82F0E64F2375BDAE0022096E /* Frameworks */, - 9AAFFBA17DFD34F5375C5D92 /* Pods */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 5A0C11D934441F0383944A0C /* Pods */, + 790E7CE3EB69552F02D2723D /* Frameworks */, ); sourceTree = ""; }; @@ -101,6 +145,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -108,13 +153,10 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - D6912A28277CD12C0012EA21 /* RunnerDebug.entitlements */, - 82F0E64E2375BDAE0022096E /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -123,39 +165,41 @@ path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; - 9AAFFBA17DFD34F5375C5D92 /* Pods */ = { - isa = PBXGroup; - children = ( - 1C793EC91E7B6FC11F0E5E44 /* Pods-Runner.debug.xcconfig */, - 3C038B01E9A5A754A9CA72B6 /* Pods-Runner.release.xcconfig */, - 4F5A1AFCB405AB82BD233633 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + CF9A537E347C1C3E9A947F1C /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 92515CC2FB6927D4DD3921DC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - AFF7CCF5217A091E1625CD54 /* [CP] Check Pods Manifest.lock */, + 27344022B9BCC96D033AEDE6 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 8AB8966E9F27B6C816D51EA9 /* [CP] Embed Pods Frameworks */, - 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */, + C5B1177E499C542655C8E54F /* [CP] Embed Pods Frameworks */, + 14A158005174031405FFC08C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -172,24 +216,22 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 59TCTNUBMQ; - LastSwiftMigration = 0910; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.HealthKit = { - enabled = 1; - }; - }; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -202,11 +244,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -221,41 +271,60 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 14A158005174031405FFC08C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 27344022B9BCC96D033AEDE6 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 8AB8966E9F27B6C816D51EA9 /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/health/health.framework", + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "[CP] Embed Pods Frameworks"; + name = "Thin Binary"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/health.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -272,25 +341,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */ = { + C5B1177E499C542655C8E54F /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - AFF7CCF5217A091E1625CD54 /* [CP] Check Pods Manifest.lock */ = { + CF9A537E347C1C3E9A947F1C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -305,34 +373,24 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - FEE6262278064D703A8D8D21 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -344,6 +402,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -368,6 +434,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -397,6 +464,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -405,9 +473,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -419,39 +488,77 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 59TCTNUBMQ; + DEVELOPMENT_TEAM = 4A2HNSB52U; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleNew; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C3BB7B959F80595D4B969F00 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleNew.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F661B1B5FF0034259235461C /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleNew.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 846E37F8BA9FF9D4591F14A9 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleNew.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -481,6 +588,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -495,7 +603,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -507,6 +615,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -536,6 +645,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -544,10 +654,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -559,32 +671,19 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 59TCTNUBMQ; + DEVELOPMENT_TEAM = 4A2HNSB52U; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleNew; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -595,31 +694,18 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 59TCTNUBMQ; + DEVELOPMENT_TEAM = 4A2HNSB52U; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.exampleNew; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -627,6 +713,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/health/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/health/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/packages/health/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d34..8e3ca5dfe 100644 --- a/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,17 @@ + + + + + + + + PreviewsEnabled + + + diff --git a/packages/health/example/ios/Runner/AppDelegate.swift b/packages/health/example/ios/Runner/AppDelegate.swift index b63630348..626664468 100644 --- a/packages/health/example/ios/Runner/AppDelegate.swift +++ b/packages/health/example/ios/Runner/AppDelegate.swift @@ -1,5 +1,5 @@ -import UIKit import Flutter +import UIKit @main @objc class AppDelegate: FlutterAppDelegate { diff --git a/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf03016f6c994b70f38d1b7346e5831b531f..7353c41ecf9ca08017312dc233d9830079b50717 100644 GIT binary patch delta 279 zcmV+y0qFj;1g8R!8Gi!+006pI?LPnj0Blf9R7L;)|5U~J`u_j-{Qm)0oAmqtj@kOz z^8J|I`-|B6ht~R5kG+%I`zf~eztraM`u^bc{`dO)zUlmg)%x%C`E}6wSI77~z4s`y z^XT{f(eM4n?EUff`e@AgO~UxV*5*r_%Uhbj5N)LaQj!wdIe!-b004GLL_t&-)18pX z4udcZ1u-#g(~z+5JN*AY5?>Gw7hsN~k)CYt4dQDFxbs5*_&e@Hj)wtt(&JE<3Eq*D z;_gQLvqXoKv=I*gWqM9C(Tvu0>=?hTbOp9!6k6AF;>f6|S5%jGEE}TA9h)e`Yuiu8 d7)l?o1NFcJg%EAfM$P~L002ovPDHLkV1g^Dnv?(l delta 550 zcmV+>0@?ki0<;8>8Gi-<0051N9Sr~g00DDSM?wIu&K&6g00HhvL_t(I5v`QFOB_)Y z#?QI;j_a;jjf#Z$YJ7mH(xecJU?W)A`9CN~KrBV85C}GDQ=|;GDFPNjtWty!L{u=? zh>8yo%^GE+J9o~_IZFoiamQVQXP7%LzTbT3F@uf+9x&7cvVV%GdjTaC;zf>@mq<=3 z!c<%*UT)@yJ|0BK6~d4Jx-*KV`ZQ(@VyUPupum=XhInNG#Z_k-X|hK{B}~9IfiWx} zLD5QY6Vm)p0NrWymdkrHPN5Vgwd>5>4HI1=@PA+e^rq~CEj|n2X`??)0mUI*D{KBn zjv{V=y5X9|X@3grkpcXC6oou4ML~ezCc2EtnsQTB4tWNg?4bkf;hG7IMfhgNI(FV5 zGs4|*GyMTIY0$B=_*mso9+>eB z?J{?+FLkYu+4_Uk`r_>LHF~flZm0oBf#vr8%vJ>#p~!KNvqGG3)|f1T_)ydeh8$vDceZ>oNbH^|*hJ*t?Yc*1`WB&W>VYVEzu) zq#7;;VjO)t*nbgf(!`OXJBr45rP>>AQr$6c7slJWvbpNW@KTwna6d?PP>hvXCcp=4 zF;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f<+$JJpcdz delta 1274 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 diff --git a/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde12118dda48d71e01fcb589a74d069c5d7cb5..4cd7b0099ca80c806f8fe495613e8d6c69460d76 100644 GIT binary patch delta 266 zcmV+l0rmcY2$}+r8Gi!+003c4mpuRg09{Z_R7L;)|5U~JDYo_jSDX9(|7FYh`2GLd z^Zv2r{H^2sT*&w!Y^SB+`<>qVZqE6)=lqo0`vF#&*75!I`TIh@_d&k*HoEtQyV-iD z%Xz2D9EQRbeYh5Nr~y=#0ZD;^+vz0$004MNL_t(2&&|%+4u6C&2tZM$Wf&dzefR%A z(^3-?6X>hnCz2Ba@RH&`m!pgy?n@#@AuLYB&}Q)FGY`?vcft0!vht0Z@M&ZeNCWXh75gzRTXR8EE3oN&6 Q00000NkvXXt^-0~g5kS`djJ3c delta 1014 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 Zxpx7IC@PekH?;r&002ovPDHLkV1i)CYaajr delta 1916 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 delta 1274 zcmV@pi1MCNO0zH7s z{8#}P0)7Ba8DqYf&QgSne>X__O83t$NZM4&R0{XJq|x}oAU?tcfC@|eNz$04T}34& z8DJf78R&>*Zz`k$q{`#gfGHnx7nlH^G{y`jfER)1<_fNi<9aM%_zrm1C`yPkKma(+ ztQ;y*CR2bbBYz>zG*SVsfpkGU(q>uHZf3iogk_%#9E|5SWeHrmAo>P;ejX7mwq#*} zW25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+X$F_KMdb6sRz!~7K zkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&IDi_4_D!s#MVXp|-XhH;H z#&@_;oApJVd}}5O@b=X_gJboD^-fM@6|#V@sA%X)Rlkd}3MLH0dGXGG&-HX|aD~|M zC)W#H7=H?AbtdaV#dGpubj_O^J-SlWpVNv-5(;wR%mvE9`Qaqo>03b&##eNNf=m#B z9@^lsd8tJ;BvI86kNV zc~0CY(7V{s+h%cWG|y=gt|q`z$l<(@qU=i?9q#uz`G?PgDMK!VMGidHZt*N+1L0ZI zFkH=mFtywc6rJ}C_?)=m)18V!ZQ`*-j(D`gCFK|nt#{bk*%%zuQ7o7kvJgA^=(^7b zzkm5GZ;jxRn{Wup8IOUx8D4uh&(=Ox-7$a;U><*5L^!% zxRlw)vAbh;sdlR||&e}8_8%)c2Fwy=F& zH|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}Jb#viX>Oi;kBKp1x_fc0#UIbIeSJ^EkWFox zijdim{ojmn@#7EC*aY;fC0W*WN+DmQtE06pNK3SfZ^#@2K`6RgEuU_KwJTQ>E?Yar zc_9e#I$F8%>kuy-JI6ocSsYvQGbsxUCx04(w1z-pMRz9`kH5smmF@WHEG?dcYkv){ zV?kn3XB$_3zr*h1Uow)(<5)w5;3Wh1jHI)`ZlXp&!yEV{Y_~@;?CLwq;4eeaGOe6( zEsSSbwSGD0-`dUUl014$1_O8Gi!+006nq0-pc?0H{z*R7L;)|5U~JDYo_jSDXF*|5nEMy6F5^ z$M}8I`uzU?*Yf=uXr;5|{0m;6_Wb|A>ik^D_|)+I$?g3CSDK^3+eX0mD!2CP`2NN0 z{dLg!a?km&%iyTt`yiax0acdp`~T(l{$a`ZF1YpsRg(cvjDG_-U$Er-fz#Bw>2W$eUI#iU z)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G!hkE!s;%oku3;IwG3U^2k zw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn_j%}l|2+O?a>_7qq7W zmx(qtA2nV^tZlLpy_#$U%ZNx5;$`0L&dZ!@e7rFXPGAOup%q`|03hpdtXsPP0000< KMNUMnLSTZ1N;Pr- delta 1891 zcmV-p2b}oI1m_Nr8Gi-<0052=@~r>>2QEoOK~#9!?VW3E6jc<*XLh$yKNt;)Mial3 z7z%<>zxaV5DhMs*(b6YIW1=KP6Jj(m21QYbiJ}su&;o5EN=$%gptMj6p|(7#AOTUJ zlt8fsX(iGq?ZQ50=XmbU+~w|cmz~|6$KBbz$-g^IcV>Hk`+q<8%-p?uMi3G-0B~!5 ze-yPCwFPw?HGmpMc~K)7BCq;C528+>zC*o^8h^XKC)IFgkv#xzm!ewK7j|kRa9dFo zC>MoDSR@P2#cWSU{i1oH5K2-Xb3jRz>|h7VOh0K` zhq^--L3H}A0r)nr z;Tr|-kPjB1s=ItpnS`oT%|U=a4oK-ZFIE^YBLH{u2#~@%%D^K)$`9*Tg(~9M-B+Zj z;~H?4LVsEt0eFtN4&>H(DZ@KpI6RhBKLL21CxC`J&m4Gc^9wwMZU#7SR1+KtuhSZM z+yLY}Vekzw6T_ApfEkuB_yU;e&a)L@rX~z70A_N+upOXN!qygmPDmKG0d%7CECcAI zgkd>ArzH$a0XjKsO$X@IgkcH5Y;m3`0G*yNOn(KK4GF_EfL4aB5i1j9o&Z{vFk~k> z&?@K2jQcJO%W!cddG(_DyfSoO55bUMHtbDF8DPkwF^~Ql#Eq4w15k{h%ML5Ar&pzi zl-D7v8kQXQ!&RRgKCW#5DZB$$6?mjWm50rRw*ukK>P-GkA|k69h{NARc>e}uLx+U4 z0DqE>7pa}9Fez+Vc-3jb`%i^uulglFoMzAVR|2%rf= zf#;74FXF^Ku_4+G&-4$KVy%YP>%2rxu2VG_cdm?XRjEhF&wPXJ># z_Q2+jGs=l~Fyx#MmGn+PZ0`@kBfGp|fO;Vov<$;z`(+sSZ7;Y=zXaF(8rb@CuQDV^ zq3i(2LfqO%AS!Ss>V%j7%>{6mtbYQrtQK5V4InPq0NZSaXv+f2U=&2}Z6OvkBfNHi z{LSaVJ!d5dC2K*ft_L^DRk;boQhOoVw!~Kt#0b2vd%!(&DF|~u1F@nG#LA5zR&7Fv z4GKgXooMSKb1g)6Obo-rgpuEP20T;W0Aa>55KC4gtQrKkAq-Hgs@FigV1GG8+rQ=z z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRet3L_uNyQ*c zE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=kyx=~RKa4{iT zm{_>_vSCm?$Ej=i6@=m%@PE9t1zZaoM}@2|h!#1K02~31S_I<0ZV=|K0}n!RRX6Ac zXmMf*5P-dLW}WPVsCKq)-x(0*txpZ2xrv3cxJ%l=7lpoNCyG< zK92ySAcmb-3m&}s@VwXv9(0#p<>B-5$bMxT;rk;OmENa6eM4D&LVo~01soUL39?R{ zyFLt3m|v?rCK7#KNu9E9Q4KV-pEUv^{rrClE&X&9I4-e7%pu_31#zGTOfC=ab%w20R*zBP+uT#l2{a~~~0wuG%6 zco*tVxK&e>%SJj*K!2tq*_h&ES5S9@TKb8WzpK;`&b9dNdxh4S)z%Q)o`aYWUh}9L z(`p!#WO5IxI|nf?yz{90R93Ed6@2qim*}Zjj$H&Esd`?JsFJUnDfiAgF_eYiWR3GC z>M9SHDylEWrA(%mfm~;u7OU9!Wz^!7Z%jZF zi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i0WYBP d*#0Ks^FNSabJA*5${_#%002ovPDHLkV1gB0Vle;! diff --git a/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b8609df07bf62e5100a53a01510388bd2b22..0ec303439225b78712f49115768196d8d76f6790 100644 GIT binary patch delta 850 zcmV-Y1Fih&6y64q8Gi!+000iU#^3+|0OwFlR7L;)|5U~J09TtSw)Xt~|5(QO`~Ck( z!T0|D|3<*~RmJ%E{r+;#`2ba!klFf7!uJMSo%Q?vP{jByxcAZE>;OrUCbaZYjJo^$ z{nGILmD~Da$@upC{`C6(Ey4dPw)Pyc^>5DkHoEo!QcuK-Jwl-l}t(fQKv z{dds$V#@dygS`PvhX6is7Z+@*x-d;$ zb=6f@U3Jw}_s+W3%*+b9H_vS)-R#9?zrXogeLVI2We2RFTTAL}&3C8PS~<5D&v@UI z+`s*$wqQ=yd$laNUY-|ovcS9~n_90tFUdl#qq0tEUXle|k{Op|DHpSrbxEeZ5~$>o%>OSe z^=41qvh3LlC2xXzu+-2eQoqs1^L>7ylB$bCP);(%(xYZL1 cY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f@rA97ytkO literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b8609df07bf62e5100a53a01510388bd2b22..0ec303439225b78712f49115768196d8d76f6790 100644 GIT binary patch delta 850 zcmV-Y1Fih&6y64q8Gi!+000iU#^3+|0OwFlR7L;)|5U~J09TtSw)Xt~|5(QO`~Ck( z!T0|D|3<*~RmJ%E{r+;#`2ba!klFf7!uJMSo%Q?vP{jByxcAZE>;OrUCbaZYjJo^$ z{nGILmD~Da$@upC{`C6(Ey4dPw)Pyc^>5DkHoEo!QcuK-Jwl-l}t(fQKv z{dds$V#@dygS`PvhX6is7Z+@*x-d;$ zb=6f@U3Jw}_s+W3%*+b9H_vS)-R#9?zrXogeLVI2We2RFTTAL}&3C8PS~<5D&v@UI z+`s*$wqQ=yd$laNUY-|ovcS9~n_90tFUdl#qq0tEUXle|k{Op|DHpSrbxEeZ5~$>o%>OSe z^=41qvh3LlC2xXzu+-2eQoqs1^L>7ylB$bCP);(%(xYZL1 cY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f@rA97ytkO literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..e9f5fea27c705180eb716271f41b582e76dcbd90 100644 GIT binary patch delta 1668 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 diff --git a/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/health/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..84ac32ae7d989f82d5e46a60405adcc8279e8001 100644 GIT binary patch delta 749 zcmVg;Ps8|O$@u8^{Z_{KM!@$5TAfS6_e#O{MZfpz`2O`0$7~@NRr(1{THzH08y3x{{PYM{eL;T_A9^tcF_4Sxb`8l z_9V3RD6;a(-0A^Pjsi!1?)d#Ap4Tk3^CP0(07;VpJ7@tgQ}z4)*zx@&yZwC9`DV-b z0ZobH_5IB4{KxD3;p_6%|f=bdFhu+F!zMZ2UFj;GUKX7tI;hv3{q~!*pMj75WP_c}> z6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FMs~w_u?Av_yNBmRxVYrpi(M% zFMP21g+hmocQp3ay*Su=qM6He)*HaaTg$E^sym`(t%s3A)x!M+vfjXUBEpK6X9%iU zU!u9jj3(-$dM~sJ%Liy#?|+!6IY#MTau#O6vVj`yh_7%Ni!?!VS+MPTO(_fG+1<#p zqu;A#i+_(N%CmVnYvb>#nA{>Q%3E`Ds7<~jZMywn@h2t>G-LrYy7?Dj{aZqhQd6tzX%(Trn+ z)HNF}%-F{rr=m*0{=a;s#YDL00000NkvXXu0mjfaGjYE delta 1884 zcmV-i2c!7<1>g>l8Gi-<0076AQ7Zrd2Pa8HK~#9!?VNjT6h$1z_m0EFf5bmb1dTDK zp;kdKV1h(V(8Sc1M<37!RE>znAk{x4#zX@eOeE1j3~!+nB5IL z<xS}u?#DBMB>w^b($1Z)`9G?eP95EKi& z$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD?Uu$P4(=PGA0ShFasNfcIHTL?9WjB9#(2xSLC z`0%$#9DW9F;B4mbU{BlaYx!SjF!QSeF~(msQRxwboh5B_O$BWOQja)GboJz$&!?mgB&3$ytsA zvns&b3Cl5Hx#%p%faR*Q906u&fbXy$maV`n?S>A)vJIH!F-vxCrY+rq5_JA(GcOgu7(Ky4X3ATR9z8*%k&<5qYeV&4Y`~}XYmK(j{)!g8d2UgHXIINM!Rvn zKtEq~Foe0s!U{kux~F6Y7Sp+2f|*Cc${S{@oh8D0=XhB8Ec-w9CflfL+te4ium2cU zoPTCj_m<3d#gjK=<*8R`HP^C$lOPM5d~UhKhRRmvv{LI za^|oavk1$QiEApSrP@~Jjbg`<*dW4TO@DPEEX$Tg$xh?Y>Qd}y@kaH~IT8!lLpS^J zR7(&wZSI6+>Eb)tX>9Z?GX#q$u z4I>7e#b7ojyJ1grOh!^}s8S#ubi^Jkd1?UK)3mp6rI^_zxRY zrx6_QmhoWoDR`fp4R7gu6@OBFGu7IDVR6~nJsB{^f5jHn<{WJ&&f^X?3f8TIk3#U& zu1*Q-e@;snJxNx8-PBnpI|uFTKN!+Lp;fPfZ+eqqU^Y1|#DJY~126?zOx-+d>%4*? z&o`TbrXSNXZW^!P0t2>@$6&aiBtUDh2wLXLD9&a(1J=k_FK|iGbAQ@x4Qmx}Ms+*; zze&q6bH(=wYuXHfz0H6<05!LkE4rl~v^!bj=^9d+vI5fN<;GP>*Pas=q2l9RxDkk` zPRk&EQI+t_0$Y%nKE)Ma)W?jaA@4Z{h zTk*7;;#lG?hvTN_On=Jaxp%bdE;mDq(q#dgdYF|-?wrMeI4h`$idZ6^VyXZVlaCd0 z;i)OYR3npf@9>00Gqn##Zb4HRurgaWFCzL9u6@J@sse>Z1XznxWvSy%Td32I3!#YN zXt9v0)RQtDDZRd?#WY?~KF7A0UcR{jt9 W+;fr}hV%pg0000&=UXv0SHh`R7L;)|5U~JDYo_jSDRDC`1<|-SjPDL z{{Q{{{{H{}09Kk-#rR9Y_viNgVafPO!S|ls`uzR=MZfp^{QU=8od8La1X`Tr_Wmff z_5e$ivgQ1@=KMy$_g9a+`TPAle6cOJ_Fc#L7qIpvwDkd1mw$fK`6IOUD75rX!}mad zv(fMTE4=(Nx%L54lL1hVF1YpqNrC`FddBPg#_Ietx%Lrkq5wX00X1L{S%Cm9QY*av z#_Rh5PKy9KYTWbvz3BX9%J>0Hi1+#X{rLA{m%$Kamk?i!03AC38#Yrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`?TG`AHia671e^vgmp!llK zp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?tc*y?iZ$PR7_ceEIapF3KB14K0Pog?7wtd+^xgUCa_GVmlD z<^nU>AU_Yn-JU?NFdu|wf^bTCNf-wSBYVZltDdvGBln-YrbeGvJ!|s{#`gjN@yAMb zM6cjFz0eFECCsc|_8hTa3*9-JQGehksdoVP^K4m?&wpA~+|b%{EP5D-+7h)6CE; z*{>BP=GRR3Ea}xyV*bqry{l^J=0#DaC4ej;1qs8_by?H6Tr@7hl>UKNZt)^B&yl;)&oqzLg zcfZxpE?3k%_iTOVywh%`XVN-E#COl+($9{v(pqSQcrz=)>G!!3HeNxbXGM@})1|9g zG4*@(OBaMvY0P0_TfMFPh fVHk#CZX3S=^^2mI>Ux-D00000NkvXXu0mjfzK(<8 literal 3294 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 cX)(b1KSk$MOV07*qoM6N<$f&{Qds= z{r_0T`1}6fwc-8!#-TGX}_?g)CZq4{k!uZ_g@DrQdoW0kI zu+W69&uN^)W`CK&06mMNcYMVF00dG=L_t(|+U?wHQxh>12H+Dm+1+fh+IF>G0SjJM zkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJTkdTm&kdTm&kdTm&kdP`e zsgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>VI$fQI%^ugM`#6By?GeadWcu z0gy9!D`m!H>Bd!JW(@avE8`|5XX(0PN}!8K>`dkavs;rHL+wy96QGNT=S@#7%xtlm zIW!++@*2zm-Py#Zr`DzqsLm!b{iskFNULSqE9A>SqHem>o31A%XL>S_5?=;V_i_y+ z(xxXhnt#r-l1Y8_*h`r?8Tr|)(RAiO)4jQR`13X0mx07C&p@KBP_2s``KEhv^|*8c z$$_T(v6^1Ig=#R}sE{vjA?ErGDZGUsyoJuWdJMc7Nb1^KF)-u<7q zPy$=;)0>vuWuK2hQhswLf!9yg`88u&eBbR8uhod?Nw09AXH}-#qOLLxeT2%C;R)QQ$Za#qp~cM&YVmS4i-*Fpd!cC zBXc?(4wcg>sHmXGd^VdE<5QX{Kyz$;$sCPl(_*-P2Iw?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF34$0Z;QO!J zOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUCUoZo%k(yku QW&i*H07*qoM6N<$g47z!?*IS* literal 3612 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 iW8 CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Health Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/packages/health/example/ios/Runner/Runner-Bridging-Header.h b/packages/health/example/ios/Runner/Runner-Bridging-Header.h index 7335fdf90..308a2a560 100644 --- a/packages/health/example/ios/Runner/Runner-Bridging-Header.h +++ b/packages/health/example/ios/Runner/Runner-Bridging-Header.h @@ -1 +1 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file +#import "GeneratedPluginRegistrant.h" diff --git a/packages/health/example/ios/RunnerTests/RunnerTests.swift b/packages/health/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/packages/health/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 59c963dac..5fa178938 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -1,16 +1,16 @@ import 'dart:async'; import 'dart:io'; +import 'package:carp_serializable/carp_serializable.dart'; +import 'package:example_new/util.dart'; import 'package:flutter/material.dart'; import 'package:health/health.dart'; -import 'package:health_example/util.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:carp_serializable/carp_serializable.dart'; // Global Health instance final health = Health(); -void main() => runApp(HealthApp()); +void main() => runApp(const HealthApp()); class HealthApp extends StatefulWidget { const HealthApp({super.key}); diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml index 031bcc6c2..342e4a071 100644 --- a/packages/health/example/pubspec.yaml +++ b/packages/health/example/pubspec.yaml @@ -1,16 +1,40 @@ -name: health_example -description: Demonstrates how to use the health plugin. -publish_to: "none" -version: 4.5.0 +name: example_new +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 environment: - sdk: ">=3.2.0 <4.0.0" - flutter: ">=3.6.0" + sdk: ^3.5.1 +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.2 + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 permission_handler: ^11.3.1 carp_serializable: ^2.0.0 # polymorphic json serialization health: @@ -20,5 +44,51 @@ dev_dependencies: flutter_test: sdk: flutter + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^4.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package From 702b213018ddfec4325096c40a82a07ff0db2f4a Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Fri, 2 May 2025 10:53:23 +0700 Subject: [PATCH 04/25] Add getHealthDataByUUID for iOS - Add getHealthDataByUUID example - Add getHealthDataByUUID documentation on README --- packages/health/README.md | 37 +++ packages/health/example/lib/main.dart | 102 +++++++- .../ios/Classes/SwiftHealthPlugin.swift | 237 ++++++++++++++++++ packages/health/lib/src/health_plugin.dart | 86 ++++++- 4 files changed, 452 insertions(+), 10 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 65db40ed6..0d08d9de2 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -322,6 +322,43 @@ Furthermore, the plugin now exposes three new functions to help you check and re 2. `isHealthDataInBackgroundAuthorized()`: Checks the current status of the Health Data in Background permission 3. `requestHealthDataInBackgroundAuthorization()`: Requests the Health Data in Background permission. +### Fetch single health data by UUID + +In order to retrieve a single record, it is required to provide `String uuid` and `HealthDataType type`. + +Please see example below: +```dart +HealthDataPoint? healthPoint = await health.getHealthDataByUUID( + uuid: 'E9F2EEAD-8FC5-4CE5-9FF5-7C4E535FB8B8', + type: HealthDataType.WORKOUT, +); +``` +``` +data by UUID: HealthDataPoint - + uuid: E9F2EEAD-8FC5-4CE5-9FF5-7C4E535FB8B8, + value: WorkoutHealthValue - workoutActivityType: RUNNING, + totalEnergyBurned: null, + totalEnergyBurnedUnit: KILOCALORIE, + totalDistance: 2400, + totalDistanceUnit: METER + totalSteps: null, + totalStepsUnit: null, + unit: NO_UNIT, + dateFrom: 2025-05-02 07:31:00.000, + dateTo: 2025-05-02 08:25:00.000, + dataType: WORKOUT, + platform: HealthPlatformType.appleHealth, + deviceId: unknown, + sourceId: com.apple.Health, + sourceName: Health + recordingMethod: RecordingMethod.manual + workoutSummary: WorkoutSummary - workoutType: runningtotalDistance: 2400, totalEnergyBurned: 0, totalSteps: 0 + metadata: null + deviceModel: null +``` +> Assuming that the `uuid` and `type` are coming from your database. + + ## Data Types The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html). diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 1ef18e5b5..3df871c35 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -10,7 +10,11 @@ import 'package:carp_serializable/carp_serializable.dart'; // Global Health instance final health = Health(); -void main() => runApp(HealthApp()); +void main() => runApp( + const MaterialApp( + home: HealthApp(), + ), + ); class HealthApp extends StatefulWidget { const HealthApp({super.key}); @@ -122,13 +126,12 @@ class HealthAppState extends State { try { authorized = await health.requestAuthorization(types, permissions: permissions); - + // request access to read historic data await health.requestHealthDataHistoryAuthorization(); // request access in background await health.requestHealthDataInBackgroundAuthorization(); - } catch (error) { debugPrint("Exception in authorize: $error"); } @@ -197,6 +200,26 @@ class HealthAppState extends State { }); } + /// Fetch single data point by UUID and type. + Future fetchDataByUUID({ + required String uuid, + required HealthDataType type, + }) async { + try { + // fetch health data + HealthDataPoint? healthPoint = await health.getHealthDataByUUID( + uuid: uuid, + type: type, + ); + + if (healthPoint != null) { + openDetailBottomSheet(healthPoint); + } + } catch (error) { + debugPrint("Exception in getHealthDataByUUID: $error"); + } + } + /// Add some random health data. /// Note that you should ensure that you have permissions to add the /// following data types. @@ -514,6 +537,22 @@ class HealthAppState extends State { }); } + /// Display bottom sheet dialog of selected HealthDataPoint + void openDetailBottomSheet(HealthDataPoint? healthPoint) { + if (!context.mounted) return; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (BuildContext context) => _detailedBottomSheet( + healthPoint: healthPoint, + ), + ); + } + // UI building below @override @@ -735,6 +774,12 @@ class HealthAppState extends State { trailing: Text((p.value as WorkoutHealthValue).workoutActivityType.name), subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + onTap: () { + fetchDataByUUID( + uuid: p.uuid, + type: p.type, + ); + }, ); } if (p.value is NutritionHealthValue) { @@ -750,6 +795,12 @@ class HealthAppState extends State { title: Text("${p.typeString}: ${p.value}"), trailing: Text(p.unitString), subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + onTap: () { + fetchDataByUUID( + uuid: p.uuid, + type: p.type, + ); + }, ); }); @@ -806,4 +857,49 @@ class HealthAppState extends State { AppState.PERMISSIONS_REVOKED => _permissionsRevoked, AppState.PERMISSIONS_NOT_REVOKED => _permissionsNotRevoked, }; + + Widget _detailedBottomSheet({HealthDataPoint? healthPoint}) { + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.5, + minChildSize: 0.3, + maxChildSize: 0.9, + builder: (BuildContext listContext, scrollController) { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const Text( + "Health Data Details", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + healthPoint == null + ? const Text('UUID Not Found!') + : Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: healthPoint.toJson().entries.length, + itemBuilder: (context, index) { + String key = + healthPoint.toJson().keys.elementAt(index); + var value = healthPoint.toJson()[key]; + + return ListTile( + title: Text( + key.replaceAll('_', ' ').toUpperCase(), + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(value.toString()), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } } diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 3a71a121e..ae00d67f8 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -246,6 +246,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else if call.method.elementsEqual("getData") { getData(call: call, result: result) } + + /// Handle getDataByUUID + else if call.method.elementsEqual("getDataByUUID") { + try! getDataByUUID(call: call, result: result) + } /// Handle getIntervalData else if (call.method.elementsEqual("getIntervalData")){ @@ -1074,6 +1079,238 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { HKHealthStore().execute(query) } + + func getDataByUUID(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + + guard let arguments = call.arguments as? NSDictionary, + let uuidarg = arguments["uuid"] as? String, + let dataTypeKey = arguments["dataTypeKey"] as? String else { + throw PluginError(message: "Invalid Arguments - UUID or DataTypeKey invalid") + } + + let dataUnitKey = arguments["dataUnitKey"] as? String + let recordingMethodsToFilter = (arguments["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) + + var unit: HKUnit? + if let dataUnitKey = dataUnitKey { + unit = unitDict[dataUnitKey] // Ensure unitDict exists and contains the key + } + + let dataTypeToFetch = dataTypeLookUp(key: dataTypeKey) + guard let uuid = UUID(uuidString: uuidarg) else { + result(false) + return + } + + var predicate = HKQuery.predicateForObjects(with: [uuid]) + + let sourceIdForCharacteristic = "com.apple.Health" + let sourceNameForCharacteristic = "Health" + + if (!includeManualEntry) { + let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) + predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) + } + + let query = HKSampleQuery( + sampleType: dataTypeToFetch, + predicate: predicate, + limit: 1, + sortDescriptors: nil + ) { + [self] + x, samplesOrNil, error in + + switch samplesOrNil { + case let (samples as [HKQuantitySample]) as Any: + let dictionaries = samples.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()), + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? RecordingMethod.manual.rawValue + : RecordingMethod.automatic.rawValue, + "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil, + "dataUnitKey": unit?.unitString + ] + } + DispatchQueue.main.async { + result(dictionaries.first) + } + + case var (samplesCategory as [HKCategorySample]) as Any: + + if dataTypeKey == self.SLEEP_IN_BED { + samplesCategory = samplesCategory.filter { $0.value == 0 } + } + if dataTypeKey == self.SLEEP_ASLEEP { + samplesCategory = samplesCategory.filter { $0.value == 1 } + } + if dataTypeKey == self.SLEEP_AWAKE { + samplesCategory = samplesCategory.filter { $0.value == 2 } + } + if dataTypeKey == self.SLEEP_LIGHT { + samplesCategory = samplesCategory.filter { $0.value == 3 } + } + if dataTypeKey == self.SLEEP_DEEP { + samplesCategory = samplesCategory.filter { $0.value == 4 } + } + if dataTypeKey == self.SLEEP_REM { + samplesCategory = samplesCategory.filter { $0.value == 5 } + } + if dataTypeKey == self.HEADACHE_UNSPECIFIED { + samplesCategory = samplesCategory.filter { $0.value == 0 } + } + if dataTypeKey == self.HEADACHE_NOT_PRESENT { + samplesCategory = samplesCategory.filter { $0.value == 1 } + } + if dataTypeKey == self.HEADACHE_MILD { + samplesCategory = samplesCategory.filter { $0.value == 2 } + } + if dataTypeKey == self.HEADACHE_MODERATE { + samplesCategory = samplesCategory.filter { $0.value == 3 } + } + if dataTypeKey == self.HEADACHE_SEVERE { + samplesCategory = samplesCategory.filter { $0.value == 4 } + } + let categories = samplesCategory.map { sample -> NSDictionary in + var metadata: [String: Any] = [:] + + if let sampleMetadata = sample.metadata { + for (key, value) in sampleMetadata { + metadata[key] = value + } + } + + return [ + "uuid": "\(sample.uuid)", + "value": sample.value, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, + "metadata": metadata + ] + } + DispatchQueue.main.async { + result(categories.first) + } + + case let (samplesWorkout as [HKWorkout]) as Any: + + let dictionaries = samplesWorkout.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "workoutActivityType": workoutActivityTypeMap.first(where: { + $0.value == sample.workoutActivityType + })?.key, + "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), + "totalEnergyBurnedUnit": "KILOCALORIE", + "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), + "totalDistanceUnit": "METER", + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, + "workout_type": self.getWorkoutType(type: sample.workoutActivityType), + "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, + "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 + ] + } + + DispatchQueue.main.async { + result(dictionaries.first) + } + + case let (samplesAudiogram as [HKAudiogramSample]) as Any: + let dictionaries = samplesAudiogram.map { sample -> NSDictionary in + var frequencies = [Double]() + var leftEarSensitivities = [Double]() + var rightEarSensitivities = [Double]() + for samplePoint in sample.sensitivityPoints { + frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) + leftEarSensitivities.append( + samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + rightEarSensitivities.append( + samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + } + return [ + "uuid": "\(sample.uuid)", + "frequencies": frequencies, + "leftEarSensitivities": leftEarSensitivities, + "rightEarSensitivities": rightEarSensitivities, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] + } + DispatchQueue.main.async { + result(dictionaries.first) + } + + case let (nutritionSample as [HKCorrelation]) as Any: + var foods: [[String: Any?]] = [] + for food in nutritionSample { + let name = food.metadata?[HKMetadataKeyFoodType] as? String + let mealType = food.metadata?["HKFoodMeal"] + let samples = food.objects + // get first sample if it exists + if let sample = samples.first as? HKQuantitySample { + var sampleDict = [ + "uuid": "\(sample.uuid)", + "name": name, + "meal_type": mealType, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? RecordingMethod.manual.rawValue + : RecordingMethod.automatic.rawValue + ] + for sample in samples { + if let quantitySample = sample as? HKQuantitySample { + for (key, identifier) in NUTRITION_KEYS { + if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: identifier)){ + let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram() + sampleDict[key] = quantitySample.quantity.doubleValue(for: unit) + } + } + } + } + foods.append(sampleDict) + } + } + + DispatchQueue.main.async { + result(foods.first) + } + + default: + if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { + let dictionaries = ecgSamples.map(fetchEcgMeasurements) + DispatchQueue.main.async { + result(dictionaries.first) + } + } else { + DispatchQueue.main.async { + print("Error getting ECG - only available on iOS 14.0 and above!") + result(nil) + } + } + } + } + + HKHealthStore().execute(query) + } private func sanitizeMetadata(_ metadata: [String: Any]?) -> [String: Any] { guard let metadata = metadata else { return [:] } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 536392458..a52dd9725 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -274,8 +274,8 @@ class Health { if (Platform.isIOS) return false; try { - final status = - await _channel.invokeMethod('isHealthDataInBackgroundAvailable'); + final status = await _channel + .invokeMethod('isHealthDataInBackgroundAvailable'); return status ?? false; } catch (e) { debugPrint( @@ -295,8 +295,8 @@ class Health { if (Platform.isIOS) return true; try { - final status = - await _channel.invokeMethod('isHealthDataInBackgroundAuthorized'); + final status = await _channel + .invokeMethod('isHealthDataInBackgroundAuthorized'); return status ?? false; } catch (e) { debugPrint( @@ -318,8 +318,8 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); try { - final bool? isAuthorized = - await _channel.invokeMethod('requestHealthDataInBackgroundAuthorization'); + final bool? isAuthorized = await _channel + .invokeMethod('requestHealthDataInBackgroundAuthorization'); return isAuthorized ?? false; } catch (e) { debugPrint( @@ -592,7 +592,8 @@ class Health { } if (Platform.isIOS && type == null) { - throw ArgumentError("On iOS, both UUID and type are required to delete a record."); + throw ArgumentError( + "On iOS, both UUID and type are required to delete a record."); } Map args = { @@ -1003,6 +1004,53 @@ class Health { return success ?? false; } + /// [iOS only] Fetch a `HealthDataPoint` by `uuid` and `type`. Returns `null` if no matching record. + /// + /// Parameters: + /// * [uuid] - UUID of your saved health data point (e.g. A91A2F10-3D7B-486A-B140-5ADCD3C9C6D0) + /// * [type] - Data type of your saved health data point (e.g. HealthDataType.WORKOUT) + /// + /// Assuming above data are coming from your database. + /// + /// Note: this feature is only for iOS at this moment due to + /// requires refactoring for Android. + Future getHealthDataByUUID({ + required String uuid, + required HealthDataType type, + }) async { + if (Platform.isAndroid) { + throw HealthException( + type, + 'getHealthDataByUUID is not available for Android at this moment.', + ); + } + + if (uuid.isEmpty) { + throw HealthException(type, 'UUID is empty!'); + } + + await _checkIfHealthConnectAvailableOnAndroid(); + + // Ask for device ID only once + _deviceId ??= Platform.isAndroid + ? (await _deviceInfo.androidInfo).id + : (await _deviceInfo.iosInfo).identifierForVendor; + + // If not implemented on platform, throw an exception + if (!isDataTypeAvailable(type)) { + throw HealthException(type, 'Not available on platform $platformType'); + } + + final result = await _dataQueryByUUID( + uuid, + type, + ); + + debugPrint('data by UUID: ${result?.toString()}'); + + return result; + } + /// Fetch a list of health data points based on [types]. /// You can also specify the [recordingMethodsToFilter] to filter the data points. /// If not specified, all data points will be included. @@ -1172,6 +1220,30 @@ class Health { } } + /// Fetches single data point by `uuid` and `type` from Android/iOS native code. + Future _dataQueryByUUID( + String uuid, + HealthDataType dataType, + ) async { + final args = { + 'dataTypeKey': dataType.name, + 'dataUnitKey': dataTypeToUnit[dataType]!.name, + 'uuid': uuid, + }; + final fetchedDataPoint = await _channel.invokeMethod('getDataByUUID', args); + + if (fetchedDataPoint != null) { + final msg = { + "dataType": dataType, + "dataPoints": [fetchedDataPoint], + }; + + return _parse(msg).first; + } else { + return null; + } + } + /// function for fetching statistic health data Future> _dataIntervalQuery( DateTime startDate, From 9f618280034370b77fd222ed61e6be79c3f824c9 Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Fri, 2 May 2025 11:31:10 +0700 Subject: [PATCH 05/25] Add getDataByUUID for Android --- .../cachet/plugins/health/HealthPlugin.kt | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 5fb0a5a79..b8b2a2ebe 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 @@ -161,6 +161,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "requestAuthorization" -> requestAuthorization(call, result) "revokePermissions" -> revokePermissions(call, result) "getData" -> getData(call, result) + "getDataByUUID" -> getDataByUUID(call, result) "getIntervalData" -> getIntervalData(call, result) "writeData" -> writeData(call, result) "delete" -> deleteData(call, result) @@ -958,6 +959,189 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } + private fun getDataByUUID(call: MethodCall, result: Result) { + val arguments = call.arguments as? HashMap<*, *> + val dataType = (arguments?.get("dataTypeKey") as? String)!! + val uuid = (arguments?.get("uuid") as? String)!! + var healthPoint = mapOf() + + if (!mapToType.containsKey(dataType)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataType not found in HC") + result.success(null) + return + } + + val classType = mapToType[dataType]!! + + scope.launch { + try { + + Log.i("FLUTTER_HEALTH", "Getting $uuid with $classType") + + // Execute the request + val response = healthConnectClient.readRecord(classType, uuid) + + // Find the record with the matching UUID + val matchingRecord = response.record + + if (matchingRecord != null) { + // Workout needs distance and total calories burned too + if (dataType == WORKOUT) { + val record = matchingRecord 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 + } + + 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 + } + + 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 + healthPoint = mapOf( + "uuid" to record.metadata.id, + "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 + matchingRecord.startTime + .toEpochMilli(), + "date_to" to + matchingRecord.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to + record.metadata + .dataOrigin + .packageName, + ) + // Filter sleep stages for requested stage + } else if (classType == SleepSessionRecord::class) { + if (matchingRecord is SleepSessionRecord) { + if (dataType == SLEEP_SESSION) { + healthPoint = convertRecord( + matchingRecord, + dataType + )[0] + } else { + for (recStage in matchingRecord.stages) { + if (dataType == + mapSleepStageToType[ + recStage.stage] + ) { + healthPoint = convertRecordStage( + recStage, + dataType, + matchingRecord.metadata + )[0] + } + } + } + } + } else { + healthPoint = convertRecord(matchingRecord, dataType)[0] + } + + Log.i( + "FLUTTER_HEALTH", + "Success: $healthPoint" + ) + + Handler(context!!.mainLooper).run { result.success(healthPoint) } + } else { + Log.e("FLUTTER_HEALTH::ERROR", "Record not found for UUID: $uuid") + result.success(null) + } + } catch (e: Exception) { + Log.e("FLUTTER_HEALTH::ERROR", "Error fetching record with UUID: $uuid") + Log.e("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.e("FLUTTER_HEALTH::ERROR", e.stackTraceToString()) + result.success(null) + } + } + } + private fun convertRecordStage( stage: SleepSessionRecord.Stage, dataType: String, From d91a94b3687b1668911e7147c4d56f3813c82392 Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Fri, 2 May 2025 11:31:37 +0700 Subject: [PATCH 06/25] Fix example Android build sdk version --- packages/health/example/android/app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/health/example/android/app/build.gradle.kts b/packages/health/example/android/app/build.gradle.kts index b5cd45fa6..01b4731b7 100644 --- a/packages/health/example/android/app/build.gradle.kts +++ b/packages/health/example/android/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "cachet.plugins.health.health_example" - compileSdk = flutter.compileSdkVersion + compileSdk = 35 // ndkVersion = flutter.ndkVersion compileOptions { @@ -25,7 +25,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 26 - targetSdk = flutter.targetSdkVersion + targetSdk = 35 versionCode = flutter.versionCode versionName = flutter.versionName } From 827fbde78b5f952e01ebe29a2ef8946f671baf0e Mon Sep 17 00:00:00 2001 From: Connor Date: Wed, 25 Jun 2025 16:47:20 -0700 Subject: [PATCH 07/25] add delete by client id and fetch unit support --- .../plugins/health/HealthDataConverter.kt | 31 +- .../plugins/health/HealthDataOperations.kt | 47 +- .../cachet/plugins/health/HealthDataReader.kt | 5 +- .../cachet/plugins/health/HealthDataWriter.kt | 880 ++++++++++-------- .../cachet/plugins/health/HealthPlugin.kt | 1 + packages/health/lib/src/health_plugin.dart | 39 +- packages/health/lib/src/heath_data_types.dart | 2 + 7 files changed, 596 insertions(+), 409 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt index 7cbd7f951..0fb4b87f5 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt @@ -4,6 +4,11 @@ import java.time.Instant import java.time.temporal.ChronoUnit import androidx.health.connect.client.records.* import androidx.health.connect.client.records.metadata.Metadata +import androidx.health.connect.client.units.BloodGlucose +import androidx.health.connect.client.units.Length +import androidx.health.connect.client.units.Mass +import androidx.health.connect.client.units.Temperature +import androidx.health.connect.client.units.Volume /** * Converts Health Connect records to Flutter-compatible data structures. @@ -21,20 +26,34 @@ class HealthDataConverter { * @return List> List of converted records (some records may split into multiple entries) * @throws IllegalArgumentException If the record type is not supported */ - fun convertRecord(record: Any, dataType: String): List> { + fun convertRecord(record: Any, dataType: String, dataUnit: String? = null): List> { val metadata = (record as Record).metadata return when (record) { // Single-value instant records - is WeightRecord -> listOf(createInstantRecord(metadata, record.time, record.weight.inKilograms)) - is HeightRecord -> listOf(createInstantRecord(metadata, record.time, record.height.inMeters)) + is WeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "POUND" -> record.weight.inPounds + else -> record.weight.inKilograms + })) + is HeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "CENTIMETER" -> (record.height.inMeters * 100) + "INCH" -> record.height.inInches + else -> record.height.inMeters + })) is BodyFatRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value)) is LeanBodyMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms)) is HeartRateVariabilityRmssdRecord -> listOf(createInstantRecord(metadata, record.time, record.heartRateVariabilityMillis)) - is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, record.temperature.inCelsius)) + is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "DEGREE_FAHRENHEIT" -> record.temperature.inFahrenheit + "KELVIN" -> record.temperature.inCelsius + 273.15 + else -> record.temperature.inCelsius + })) is BodyWaterMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms)) is OxygenSaturationRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value)) - is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, record.level.inMilligramsPerDeciliter)) + is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "MILLIMOLES_PER_LITER" -> record.level.inMillimolesPerLiter + else -> record.level.inMilligramsPerDeciliter + })) is BasalMetabolicRateRecord -> listOf(createInstantRecord(metadata, record.time, record.basalMetabolicRate.inKilocaloriesPerDay)) is RestingHeartRateRecord -> listOf(createInstantRecord(metadata, record.time, record.beatsPerMinute)) is RespiratoryRateRecord -> listOf(createInstantRecord(metadata, record.time, record.rate)) @@ -236,7 +255,7 @@ class HealthDataConverter { ) ) } - + companion object { private const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" private const val MEAL_UNKNOWN = "UNKNOWN" diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt index b6b861908..b65ec537b 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt @@ -251,9 +251,48 @@ class HealthDataOperations( } /** - * Internal helper method to prepare Health Connect permission strings. - * Converts data type names and access levels into proper permission format. - * + * Deletes a specific health record by its client record ID and data type. Allows precise + * deletion of individual health records using client-side IDs. + * + * @param call Method call containing 'dataTypeKey', 'recordId', and 'clientRecordId' + * @param result Flutter result callback returning boolean success status + */ + fun deleteByClientRecordId(call: MethodCall, result: Result) { + val arguments = call.arguments as? HashMap<*, *> + val dataTypeKey = (arguments?.get("dataTypeKey") as? String)!! + val recordId = listOfNotNull(arguments?.get("recordId") as? String) + val clientRecordId = listOfNotNull(arguments?.get("clientRecordId") as? String) + if (!HealthConstants.mapToType.containsKey(dataTypeKey)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataTypeKey not found in HC") + result.success(false) + return + } + val classType = HealthConstants.mapToType[dataTypeKey]!! + + scope.launch { + try { + healthConnectClient.deleteRecords( + classType, + recordId, + clientRecordId + ) + result.success(true) + } catch (e: Exception) { + Log.e( + "FLUTTER_HEALTH::ERROR", + "Error deleting record with ClientRecordId: $clientRecordId" + ) + Log.e("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.e("FLUTTER_HEALTH::ERROR", e.stackTraceToString()) + result.success(false) + } + } + } + + /** + * Internal helper method to prepare Health Connect permission strings. Converts data type names + * and access levels into proper permission format. + * * @param types List of health data type strings * @param permissions List of permission level integers (0=read, 1=read+write) * @return List? Formatted permission strings, or null if invalid input @@ -294,4 +333,4 @@ class HealthDataOperations( return permList } -} \ No newline at end of file +} diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt index ba0db624c..71d4ba19f 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt @@ -40,6 +40,7 @@ class HealthDataReader( */ fun getData(call: MethodCall, result: Result) { val dataType = call.argument("dataTypeKey")!! + val dataUnit = call.argument("dataUnitKey")!! val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) val healthConnectData = mutableListOf>() @@ -47,7 +48,7 @@ class HealthDataReader( Log.i( "FLUTTER_HEALTH", - "Getting data for $dataType between $startTime and $endTime, filtering by $recordingMethodsToFilter" + "Getting data for $dataType with unit $dataUnit between $startTime and $endTime, filtering by $recordingMethodsToFilter" ) scope.launch { @@ -92,7 +93,7 @@ class HealthDataReader( ) for (rec in filteredRecords) { healthConnectData.addAll( - dataConverter.convertRecord(rec, dataType) + dataConverter.convertRecord(rec, dataType, dataUnit) ) } } diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt index a3e47beee..e6fddb690 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt @@ -7,25 +7,26 @@ import androidx.health.connect.client.records.metadata.Metadata import androidx.health.connect.client.units.* import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel.Result +import java.time.Instant import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import java.time.Instant /** - * Handles writing health data to Health Connect. - * Manages data insertion for various health metrics, specialized records like workouts and nutrition, - * and proper data type conversion from Flutter to Health Connect format. + * Handles writing health data to Health Connect. Manages data insertion for various health metrics, + * specialized records like workouts and nutrition, and proper data type conversion from Flutter to + * Health Connect format. */ class HealthDataWriter( - private val healthConnectClient: HealthConnectClient, - private val scope: CoroutineScope + private val healthConnectClient: HealthConnectClient, + private val scope: CoroutineScope ) { - + /** - * Writes a single health data record to Health Connect. - * Supports most basic health metrics with automatic type conversion and validation. - * - * @param call Method call containing 'dataTypeKey', 'startTime', 'endTime', 'value', 'recordingMethod' + * Writes a single health data record to Health Connect. Supports most basic health metrics with + * automatic type conversion and validation. + * + * @param call Method call containing 'dataTypeKey', 'startTime', 'endTime', 'value', + * 'recordingMethod' * @param result Flutter result callback returning boolean success status */ fun writeData(call: MethodCall, result: Result) { @@ -33,15 +34,28 @@ class HealthDataWriter( val startTime = call.argument("startTime")!! val endTime = call.argument("endTime")!! val value = call.argument("value")!! + val clientRecordId: String? = call.argument("clientRecordId") + val clientRecordVersion: Long? = call.argument("clientRecordVersion") val recordingMethod = call.argument("recordingMethod")!! Log.i( - "FLUTTER_HEALTH", - "Writing data for $type between $startTime and $endTime, value: $value, recording method: $recordingMethod" + "FLUTTER_HEALTH", + "Writing data for $type between $startTime and $endTime, value: $value, recording method: $recordingMethod" ) - val record = createRecord(type, startTime, endTime, value, recordingMethod) - + val metadata: Metadata = + if ((clientRecordId != null) && (clientRecordVersion != null)) { + Metadata( + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion, + recordingMethod = recordingMethod + ) + } else { + Metadata(recordingMethod = recordingMethod) + } + + val record = createRecord(type, startTime, endTime, value, metadata) + if (record == null) { result.success(false) return @@ -59,13 +73,16 @@ class HealthDataWriter( } /** - * Writes a comprehensive workout session with optional distance and calorie data. - * Creates an ExerciseSessionRecord with associated DistanceRecord and TotalCaloriesBurnedRecord - * if supplementary data is provided. - * - * @param call Method call containing workout details: 'activityType', 'startTime', 'endTime', + * Writes a comprehensive workout session with optional distance and calorie data. Creates an + * ExerciseSessionRecord with associated DistanceRecord and TotalCaloriesBurnedRecord if + * supplementary data is provided. + * + * @param call Method call containing workout details: 'activityType', 'startTime', 'endTime', + * ``` * 'totalEnergyBurned', 'totalDistance', 'recordingMethod', 'title' - * @param result Flutter result callback returning boolean success status + * @param result + * ``` + * Flutter result callback returning boolean success status */ fun writeWorkoutData(call: MethodCall, result: Result) { val type = call.argument("activityType")!! @@ -74,80 +91,77 @@ class HealthDataWriter( val totalEnergyBurned = call.argument("totalEnergyBurned") val totalDistance = call.argument("totalDistance") val recordingMethod = call.argument("recordingMethod")!! - + if (!HealthConstants.workoutTypeMap.containsKey(type)) { result.success(false) - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] Workout type not supported" - ) + Log.w("FLUTTER_HEALTH::ERROR", "[Health Connect] Workout type not supported") return } - + val workoutType = HealthConstants.workoutTypeMap[type]!! val title = call.argument("title") ?: type scope.launch { try { val list = mutableListOf() - + // Add exercise session record list.add( - ExerciseSessionRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - exerciseType = workoutType, - title = title, - metadata = Metadata( - recordingMethod = recordingMethod, + ExerciseSessionRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + exerciseType = workoutType, + title = title, + metadata = + Metadata( + recordingMethod = recordingMethod, + ), ), - ), ) - + // Add distance record if provided if (totalDistance != null) { list.add( - DistanceRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - distance = Length.meters(totalDistance.toDouble()), - metadata = Metadata( - recordingMethod = recordingMethod, + DistanceRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + distance = Length.meters(totalDistance.toDouble()), + metadata = + Metadata( + recordingMethod = recordingMethod, + ), ), - ), ) } - + // Add energy burned record if provided if (totalEnergyBurned != null) { list.add( - TotalCaloriesBurnedRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - energy = Energy.kilocalories(totalEnergyBurned.toDouble()), - metadata = Metadata( - recordingMethod = recordingMethod, + TotalCaloriesBurnedRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + energy = Energy.kilocalories(totalEnergyBurned.toDouble()), + metadata = + Metadata( + recordingMethod = recordingMethod, + ), ), - ), ) } - + healthConnectClient.insertRecords(list) result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Workout was successfully added!" - ) + 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", + "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()) @@ -157,10 +171,9 @@ class HealthDataWriter( } /** - * Writes blood pressure measurement with both systolic and diastolic values. - * Creates a single BloodPressureRecord containing both pressure readings - * taken at the same time point. - * + * Writes blood pressure measurement with both systolic and diastolic values. Creates a single + * BloodPressureRecord containing both pressure readings taken at the same time point. + * * @param call Method call containing 'systolic', 'diastolic', 'startTime', 'recordingMethod' * @param result Flutter result callback returning boolean success status */ @@ -169,31 +182,41 @@ class HealthDataWriter( val diastolic = call.argument("diastolic")!! val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) val recordingMethod = call.argument("recordingMethod")!! + val clientRecordId = call.argument("clientRecordId")!! as String? + val clientRecordVersion = call.argument("clientRecordVersion")!! as Long? scope.launch { try { + val metadata: Metadata = + if ((clientRecordId != null) && (clientRecordVersion != null)) { + Metadata( + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion, + recordingMethod = recordingMethod + ) + } else { + Metadata(recordingMethod = recordingMethod) + } healthConnectClient.insertRecords( - listOf( - BloodPressureRecord( - time = startTime, - systolic = Pressure.millimetersOfMercury(systolic), - diastolic = Pressure.millimetersOfMercury(diastolic), - zoneOffset = null, - metadata = Metadata( - recordingMethod = recordingMethod, - ), + listOf( + BloodPressureRecord( + time = startTime, + systolic = Pressure.millimetersOfMercury(systolic), + diastolic = Pressure.millimetersOfMercury(diastolic), + zoneOffset = null, + metadata = metadata, + ), ), - ), ) result.success(true) Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Blood pressure was successfully added!", + "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", + "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()) @@ -203,9 +226,9 @@ class HealthDataWriter( } /** - * Writes blood oxygen saturation measurement. - * Delegates to standard writeData method for OxygenSaturationRecord handling. - * + * Writes blood oxygen saturation measurement. Delegates to standard writeData method for + * OxygenSaturationRecord handling. + * * @param call Method call with blood oxygen data * @param result Flutter result callback returning success status */ @@ -214,9 +237,9 @@ class HealthDataWriter( } /** - * Writes menstrual flow data. - * Delegates to standard writeData method for MenstruationFlowRecord handling. - * + * Writes menstrual flow data. Delegates to standard writeData method for MenstruationFlowRecord + * handling. + * * @param call Method call with menstruation flow data * @param result Flutter result callback returning success status */ @@ -225,13 +248,16 @@ class HealthDataWriter( } /** - * Writes comprehensive nutrition/meal data with detailed nutrient breakdown. - * Creates NutritionRecord with extensive nutrient information including vitamins, - * minerals, macronutrients, and meal classification. - * - * @param call Method call containing nutrition data: calories, macronutrients, vitamins, + * Writes comprehensive nutrition/meal data with detailed nutrient breakdown. Creates + * NutritionRecord with extensive nutrient information including vitamins, minerals, + * macronutrients, and meal classification. + * + * @param call Method call containing nutrition data: calories, macronutrients, vitamins, + * ``` * minerals, meal details, timing information - * @param result Flutter result callback returning boolean success status + * @param result + * ``` + * Flutter result callback returning boolean success status */ fun writeMeal(call: MethodCall, result: Result) { val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) @@ -279,71 +305,81 @@ class HealthDataWriter( val name = call.argument("name") val mealType = call.argument("meal_type")!! + val clientRecordId = call.argument("clientRecordId") as String? + val clientRecordVersion = call.argument("clientRecordVersion") as Long? scope.launch { try { + val metaData: Metadata = + if ((clientRecordId != null) && (clientRecordVersion != null)) { + Metadata( + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion + ) + } else { + Metadata() + } 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 = HealthConstants.mapMealTypeToType[mealType] - ?: MealType.MEAL_TYPE_UNKNOWN, - ), + NutritionRecord( + name = name, + metadata = metaData, + 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 = HealthConstants.mapMealTypeToType[mealType] + ?: MealType.MEAL_TYPE_UNKNOWN + ), ) healthConnectClient.insertRecords(list) result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Meal was successfully added!" - ) + 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", + "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()) @@ -353,16 +389,19 @@ class HealthDataWriter( } /** - * Writes speed/velocity data with multiple samples to Health Connect. - * Creates a SpeedRecord containing time-series speed measurements captured during - * activities like running, cycling, or walking. Each sample represents the user's - * instantaneous speed at a specific moment within the recording period. + * Writes speed/velocity data with multiple samples to Health Connect. Creates a SpeedRecord + * containing time-series speed measurements captured during activities like running, cycling, + * or walking. Each sample represents the user's instantaneous speed at a specific moment within + * the recording period. * * @param call Method call containing startTime, endTime, recordingMethod, + * ``` * samples: List> List of speed measurements, each * containing: time, speed (m/s) * - * @param result Flutter result callback returning boolean success status + * @param result + * ``` + * Flutter result callback returning boolean success status */ fun writeMultipleSpeedData(call: MethodCall, result: Result) { val startTime = call.argument("startTime")!! @@ -372,27 +411,29 @@ class HealthDataWriter( scope.launch { try { - val speedSamples = samples.map { sample -> - SpeedRecord.Sample( - time = Instant.ofEpochMilli(sample["time"] as Long), - speed = Velocity.metersPerSecond(sample["speed"] as Double) - ) - } + val speedSamples = + samples.map { sample -> + SpeedRecord.Sample( + time = Instant.ofEpochMilli(sample["time"] as Long), + speed = Velocity.metersPerSecond(sample["speed"] as Double) + ) + } - val speedRecord = SpeedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - samples = speedSamples, - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) + val speedRecord = + SpeedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + samples = speedSamples, + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata(recordingMethod = recordingMethod), + ) healthConnectClient.insertRecords(listOf(speedRecord)) result.success(true) Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Successfully wrote ${speedSamples.size} speed samples" + "FLUTTER_HEALTH::SUCCESS", + "Successfully wrote ${speedSamples.size} speed samples" ) } catch (e: Exception) { Log.e("FLUTTER_HEALTH::ERROR", "Error writing speed data: ${e.message}") @@ -401,13 +442,12 @@ class HealthDataWriter( } } - // ---------- Private Methods ---------- + // ---------- Private Methods ---------- /** - * Creates appropriate Health Connect record objects based on data type. - * Factory method that instantiates the correct record type with proper unit conversion - * and metadata assignment. - * + * Creates appropriate Health Connect record objects based on data type. Factory method that + * instantiates the correct record type with proper unit conversion and metadata assignment. + * * @param type Health data type string identifier * @param startTime Record start time in milliseconds * @param endTime Record end time in milliseconds @@ -416,254 +456,308 @@ class HealthDataWriter( * @return Record? Properly configured Health Connect record, or null if type unsupported */ private fun createRecord( - type: String, - startTime: Long, - endTime: Long, - value: Double, - recordingMethod: Int + type: String, + startTime: Long, + endTime: Long, + value: Double, + metadata: Metadata ): Record? { return when (type) { - BODY_FAT_PERCENTAGE -> BodyFatRecord( - time = Instant.ofEpochMilli(startTime), - percentage = Percentage(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - LEAN_BODY_MASS -> LeanBodyMassRecord( - time = Instant.ofEpochMilli(startTime), - mass = Mass.kilograms(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - HEIGHT -> HeightRecord( - time = Instant.ofEpochMilli(startTime), - height = Length.meters(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - WEIGHT -> WeightRecord( - time = Instant.ofEpochMilli(startTime), - weight = Mass.kilograms(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - STEPS -> StepsRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - count = value.toLong(), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - ACTIVE_ENERGY_BURNED -> ActiveCaloriesBurnedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - energy = Energy.kilocalories(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - 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, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BODY_TEMPERATURE -> BodyTemperatureRecord( - time = Instant.ofEpochMilli(startTime), - temperature = Temperature.celsius(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BODY_WATER_MASS -> BodyWaterMassRecord( - time = Instant.ofEpochMilli(startTime), - mass = Mass.kilograms(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BLOOD_OXYGEN -> OxygenSaturationRecord( - time = Instant.ofEpochMilli(startTime), - percentage = Percentage(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BLOOD_GLUCOSE -> BloodGlucoseRecord( - time = Instant.ofEpochMilli(startTime), - level = BloodGlucose.milligramsPerDeciliter(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - HEART_RATE_VARIABILITY_RMSSD -> HeartRateVariabilityRmssdRecord( - time = Instant.ofEpochMilli(startTime), - heartRateVariabilityMillis = value, - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - DISTANCE_DELTA -> DistanceRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - distance = Length.meters(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - WATER -> HydrationRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - volume = Volume.liters(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - SLEEP_ASLEEP -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_SLEEPING, recordingMethod) - SLEEP_LIGHT -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_LIGHT, recordingMethod) - SLEEP_DEEP -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_DEEP, recordingMethod) - SLEEP_REM -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_REM, recordingMethod) - SLEEP_OUT_OF_BED -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_OUT_OF_BED, recordingMethod) - SLEEP_AWAKE -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_AWAKE, recordingMethod) - SLEEP_AWAKE_IN_BED -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_AWAKE_IN_BED, recordingMethod) - SLEEP_UNKNOWN -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_UNKNOWN, recordingMethod) - - SLEEP_SESSION -> SleepSessionRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - RESTING_HEART_RATE -> RestingHeartRateRecord( - time = Instant.ofEpochMilli(startTime), - beatsPerMinute = value.toLong(), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BASAL_ENERGY_BURNED -> BasalMetabolicRateRecord( - time = Instant.ofEpochMilli(startTime), - basalMetabolicRate = Power.kilocaloriesPerDay(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - FLIGHTS_CLIMBED -> FloorsClimbedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - floors = value, - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - RESPIRATORY_RATE -> RespiratoryRateRecord( - time = Instant.ofEpochMilli(startTime), - rate = value, - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - TOTAL_CALORIES_BURNED -> TotalCaloriesBurnedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - energy = Energy.kilocalories(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - MENSTRUATION_FLOW -> MenstruationFlowRecord( - time = Instant.ofEpochMilli(startTime), - flow = value.toInt(), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - SPEED -> SpeedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - samples = listOf( - SpeedRecord.Sample( - time = Instant.ofEpochMilli(startTime), - speed = Velocity.metersPerSecond(value), + BODY_FAT_PERCENTAGE -> + BodyFatRecord( + time = Instant.ofEpochMilli(startTime), + percentage = Percentage(value), + zoneOffset = null, + metadata = metadata, + ) + LEAN_BODY_MASS -> + LeanBodyMassRecord( + time = Instant.ofEpochMilli(startTime), + mass = Mass.kilograms(value), + zoneOffset = null, + metadata = metadata, + ) + HEIGHT -> + HeightRecord( + time = Instant.ofEpochMilli(startTime), + height = Length.meters(value), + zoneOffset = null, + metadata = metadata, + ) + WEIGHT -> + WeightRecord( + time = Instant.ofEpochMilli(startTime), + weight = Mass.kilograms(value), + zoneOffset = null, + metadata = metadata, + ) + STEPS -> + StepsRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + count = value.toLong(), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + ACTIVE_ENERGY_BURNED -> + ActiveCaloriesBurnedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + energy = Energy.kilocalories(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + 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, + metadata = metadata, + ) + BODY_TEMPERATURE -> + BodyTemperatureRecord( + time = Instant.ofEpochMilli(startTime), + temperature = Temperature.celsius(value), + zoneOffset = null, + metadata = metadata, + ) + BODY_WATER_MASS -> + BodyWaterMassRecord( + time = Instant.ofEpochMilli(startTime), + mass = Mass.kilograms(value), + zoneOffset = null, + metadata = metadata, + ) + BLOOD_OXYGEN -> + OxygenSaturationRecord( + time = Instant.ofEpochMilli(startTime), + percentage = Percentage(value), + zoneOffset = null, + metadata = metadata, + ) + BLOOD_GLUCOSE -> + BloodGlucoseRecord( + time = Instant.ofEpochMilli(startTime), + level = BloodGlucose.milligramsPerDeciliter(value), + zoneOffset = null, + metadata = metadata, + ) + HEART_RATE_VARIABILITY_RMSSD -> + HeartRateVariabilityRmssdRecord( + time = Instant.ofEpochMilli(startTime), + heartRateVariabilityMillis = value, + zoneOffset = null, + metadata = metadata, + ) + DISTANCE_DELTA -> + DistanceRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + distance = Length.meters(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + WATER -> + HydrationRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + volume = Volume.liters(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + SLEEP_ASLEEP -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_SLEEPING, + metadata + ) + SLEEP_LIGHT -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_LIGHT, + metadata + ) + SLEEP_DEEP -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_DEEP, + metadata + ) + SLEEP_REM -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_REM, + metadata + ) + SLEEP_OUT_OF_BED -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_OUT_OF_BED, + metadata + ) + SLEEP_AWAKE -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_AWAKE, + metadata + ) + SLEEP_AWAKE_IN_BED -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_AWAKE_IN_BED, + metadata + ) + SLEEP_UNKNOWN -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_UNKNOWN, + metadata + ) + SLEEP_SESSION -> + SleepSessionRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + RESTING_HEART_RATE -> + RestingHeartRateRecord( + time = Instant.ofEpochMilli(startTime), + beatsPerMinute = value.toLong(), + zoneOffset = null, + metadata = metadata, + ) + BASAL_ENERGY_BURNED -> + BasalMetabolicRateRecord( + time = Instant.ofEpochMilli(startTime), + basalMetabolicRate = Power.kilocaloriesPerDay(value), + zoneOffset = null, + metadata = metadata, + ) + FLIGHTS_CLIMBED -> + FloorsClimbedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + floors = value, + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + RESPIRATORY_RATE -> + RespiratoryRateRecord( + time = Instant.ofEpochMilli(startTime), + rate = value, + zoneOffset = null, + metadata = metadata, + ) + TOTAL_CALORIES_BURNED -> + TotalCaloriesBurnedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + energy = Energy.kilocalories(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + MENSTRUATION_FLOW -> + MenstruationFlowRecord( + time = Instant.ofEpochMilli(startTime), + flow = value.toInt(), + zoneOffset = null, + metadata = metadata, + ) + SPEED -> + SpeedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + samples = + listOf( + SpeedRecord.Sample( + time = Instant.ofEpochMilli(startTime), + speed = Velocity.metersPerSecond(value), + ) + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, ) - ), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - BLOOD_PRESSURE_SYSTOLIC -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeBloodPressure] API") null } - BLOOD_PRESSURE_DIASTOLIC -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeBloodPressure] API") null } - WORKOUT -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeWorkoutData] API") null } - NUTRITION -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeMeal] API") null } - else -> { - Log.e("FLUTTER_HEALTH::ERROR", "The type $type was not supported by the Health plugin or you must use another API") + Log.e( + "FLUTTER_HEALTH::ERROR", + "The type $type was not supported by the Health plugin or you must use another API" + ) null } } } /** - * Creates sleep session records with stage information. - * Builds SleepSessionRecord with appropriate sleep stage data and timing. - * + * Creates sleep session records with stage information. Builds SleepSessionRecord with + * appropriate sleep stage data and timing. + * * @param startTime Sleep period start time in milliseconds * @param endTime Sleep period end time in milliseconds * @param stageType Sleep stage type constant * @param recordingMethod How sleep data was recorded * @return SleepSessionRecord Configured sleep session record */ - private fun createSleepRecord(startTime: Long, endTime: Long, stageType: Int, recordingMethod: Int): SleepSessionRecord { + private fun createSleepRecord( + startTime: Long, + endTime: Long, + stageType: Int, + metadata: Metadata + ): SleepSessionRecord { return SleepSessionRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - startZoneOffset = null, - endZoneOffset = null, - stages = listOf( - SleepSessionRecord.Stage( - Instant.ofEpochMilli(startTime), - Instant.ofEpochMilli(endTime), - stageType - ) - ), - metadata = Metadata(recordingMethod = recordingMethod), + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord.Stage( + Instant.ofEpochMilli(startTime), + Instant.ofEpochMilli(endTime), + stageType + ) + ), + metadata = metadata, ) } @@ -694,7 +788,7 @@ class HealthDataWriter( private const val WORKOUT = "WORKOUT" private const val NUTRITION = "NUTRITION" private const val SPEED = "SPEED" - + // Sleep types private const val SLEEP_ASLEEP = "SLEEP_ASLEEP" private const val SLEEP_LIGHT = "SLEEP_LIGHT" @@ -706,4 +800,4 @@ class HealthDataWriter( private const val SLEEP_UNKNOWN = "SLEEP_UNKNOWN" private const val SLEEP_SESSION = "SLEEP_SESSION" } -} \ No newline at end of file +} 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 2da6af19e..0f39800e0 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 @@ -156,6 +156,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // Deleting data "delete" -> dataOperations.deleteData(call, result) "deleteByUUID" -> dataOperations.deleteByUUID(call, result) + "deleteByClientRecordId" -> dataOperations.deleteByClientRecordId(call, result) else -> result.notImplemented() } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 536392458..e3a489864 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -478,6 +478,8 @@ class Health { HealthDataUnit? unit, required HealthDataType type, required DateTime startTime, + String? clientRecordId, + int? clientRecordVersion, DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { @@ -537,6 +539,8 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), + 'clientRecordId' : clientRecordId, + 'clientRecordVersion' : clientRecordVersion, }; bool? success = await _channel.invokeMethod('writeData', args); return success ?? false; @@ -604,6 +608,22 @@ class Health { return success ?? false; } + Future deleteByClientRecordId({ + required HealthDataType dataTypeKey, + required String clientRecordId, + String? recordId, + }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + + Map args = { + 'dataTypeKey': dataTypeKey.name, + 'recordId': recordId, + 'clientRecordId': clientRecordId + }; + bool? success = await _channel.invokeMethod('deleteByClientRecordId', args); + return success ?? false; + } + /// Saves a blood pressure record. /// /// Returns true if successful, false otherwise. @@ -622,6 +642,8 @@ class Health { required int systolic, required int diastolic, required DateTime startTime, + String? clientRecordId, + int? clientRecordVersion, DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { @@ -643,6 +665,8 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), + 'clientRecordId' : clientRecordId, + 'clientRecordVersion' : clientRecordVersion, }; return await _channel.invokeMethod('writeBloodPressure', args) == true; } @@ -756,6 +780,8 @@ class Health { required MealType mealType, required DateTime startTime, required DateTime endTime, + String? clientRecordId, + int? clientRecordVersion, double? caloriesConsumed, double? carbohydrates, double? protein, @@ -816,6 +842,8 @@ class Health { 'meal_type': mealType.name, 'start_time': startTime.millisecondsSinceEpoch, 'end_time': endTime.millisecondsSinceEpoch, + 'clientRecordId' : clientRecordId, + 'clientRecordVersion' : clientRecordVersion, 'calories': caloriesConsumed, 'carbs': carbohydrates, 'protein': protein, @@ -1008,6 +1036,7 @@ class Health { /// If not specified, all data points will be included. Future> getHealthDataFromTypes({ required List types, + Map? preferredUnits, required DateTime startTime, required DateTime endTime, List recordingMethodsToFilter = const [], @@ -1017,7 +1046,7 @@ class Health { for (var type in types) { final result = await _prepareQuery( - startTime, endTime, type, recordingMethodsToFilter); + startTime, endTime, type, recordingMethodsToFilter, dataUnit: preferredUnits?[type]); dataPoints.addAll(result); } @@ -1074,6 +1103,7 @@ class Health { DateTime endTime, HealthDataType dataType, List recordingMethodsToFilter, + {HealthDataUnit? dataUnit} ) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid @@ -1091,7 +1121,7 @@ class Health { return _computeAndroidBMI(startTime, endTime, recordingMethodsToFilter); } return await _dataQuery( - startTime, endTime, dataType, recordingMethodsToFilter); + startTime, endTime, dataType, recordingMethodsToFilter, dataUnit: dataUnit); } /// Prepares an interval query, i.e. checks if the types are available, etc. @@ -1144,10 +1174,11 @@ class Health { DateTime startTime, DateTime endTime, HealthDataType dataType, - List recordingMethodsToFilter) async { + List recordingMethodsToFilter, + {HealthDataUnit? dataUnit}) async { final args = { 'dataTypeKey': dataType.name, - 'dataUnitKey': dataTypeToUnit[dataType]!.name, + 'dataUnitKey': dataUnit?.name ?? dataTypeToUnit[dataType]!.name, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethodsToFilter': diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index ba605eb33..3d23592e3 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -404,6 +404,7 @@ enum HealthDataUnit { // Length units METER, + CENTIMETER, INCH, FOOT, YARD, @@ -468,6 +469,7 @@ enum HealthDataUnit { BEATS_PER_MINUTE, RESPIRATIONS_PER_MINUTE, MILLIGRAM_PER_DECILITER, + MILLIMOLES_PER_LITER, METER_PER_SECOND, UNKNOWN_UNIT, NO_UNIT, From 42e0b73ae7662db3f1cb2f92f83ac464b194823d Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 26 Jun 2025 09:41:33 -0700 Subject: [PATCH 08/25] add better null safety --- .../kotlin/cachet/plugins/health/HealthDataOperations.kt | 4 ++-- .../main/kotlin/cachet/plugins/health/HealthDataReader.kt | 2 +- .../main/kotlin/cachet/plugins/health/HealthDataWriter.kt | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt index b65ec537b..8ee7e640b 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt @@ -260,8 +260,8 @@ class HealthDataOperations( fun deleteByClientRecordId(call: MethodCall, result: Result) { val arguments = call.arguments as? HashMap<*, *> val dataTypeKey = (arguments?.get("dataTypeKey") as? String)!! - val recordId = listOfNotNull(arguments?.get("recordId") as? String) - val clientRecordId = listOfNotNull(arguments?.get("clientRecordId") as? String) + val recordId = listOfNotNull(arguments["recordId"] as? String) + val clientRecordId = listOfNotNull(arguments["clientRecordId"] as? String) if (!HealthConstants.mapToType.containsKey(dataTypeKey)) { Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataTypeKey not found in HC") result.success(false) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt index 71d4ba19f..fb82f0f74 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt @@ -40,7 +40,7 @@ class HealthDataReader( */ fun getData(call: MethodCall, result: Result) { val dataType = call.argument("dataTypeKey")!! - val dataUnit = call.argument("dataUnitKey")!! + val dataUnit: String? = call.argument("dataUnitKey") val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) val healthConnectData = mutableListOf>() diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt index e6fddb690..4c09b8b0d 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt @@ -182,8 +182,8 @@ class HealthDataWriter( val diastolic = call.argument("diastolic")!! val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) val recordingMethod = call.argument("recordingMethod")!! - val clientRecordId = call.argument("clientRecordId")!! as String? - val clientRecordVersion = call.argument("clientRecordVersion")!! as Long? + val clientRecordId: String? = call.argument("clientRecordId") + val clientRecordVersion: Long? = call.argument("clientRecordVersion") scope.launch { try { @@ -305,8 +305,8 @@ class HealthDataWriter( val name = call.argument("name") val mealType = call.argument("meal_type")!! - val clientRecordId = call.argument("clientRecordId") as String? - val clientRecordVersion = call.argument("clientRecordVersion") as Long? + val clientRecordId: String? = call.argument("clientRecordId") + val clientRecordVersion: Long? = call.argument("clientRecordVersion") scope.launch { try { From 141c877da1c7bc9defee14b8a413673a2afd2a4d Mon Sep 17 00:00:00 2001 From: Connor Date: Thu, 26 Jun 2025 10:01:37 -0700 Subject: [PATCH 09/25] remove unused imports --- .../main/kotlin/cachet/plugins/health/HealthDataConverter.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt index 0fb4b87f5..6d288170b 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt @@ -4,11 +4,6 @@ import java.time.Instant import java.time.temporal.ChronoUnit import androidx.health.connect.client.records.* import androidx.health.connect.client.records.metadata.Metadata -import androidx.health.connect.client.units.BloodGlucose -import androidx.health.connect.client.units.Length -import androidx.health.connect.client.units.Mass -import androidx.health.connect.client.units.Temperature -import androidx.health.connect.client.units.Volume /** * Converts Health Connect records to Flutter-compatible data structures. From 9a8972f7eb7b569ca24ee4f6eb992a105447e5c1 Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Thu, 17 Jul 2025 12:13:48 +0700 Subject: [PATCH 10/25] Sync with latest master changes - Fix fetchDataByUUID and return HealthDataPoint - Fix writeHealthData to return HealthDataPoint - Fix writeBloodOxygen to return HealthDataPoint - Revert pubspec.yaml to original --- packages/health/README.md | 16 + packages/health/example/lib/main.dart | 365 ++++++++++-------- packages/health/example/pubspec.yaml | 84 +--- .../health/ios/Classes/HealthDataReader.swift | 244 ++++++++++++ .../ios/Classes/SwiftHealthPlugin.swift | 3 + packages/health/lib/src/health_plugin.dart | 40 +- packages/health/pubspec.yaml | 4 +- 7 files changed, 515 insertions(+), 241 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index b9091c832..f090b744e 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -270,6 +270,22 @@ flutter: Health Plugin Error: flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible})) ``` +### Fetch single health data by UUID + +In order to retrieve a single record, it is required to provide `String uuid` and `HealthDataType type`. + +Please see example below: +```dart +HealthDataPoint? healthPoint = await health.getHealthDataByUUID( + uuid: 'random-uuid-string', + type: HealthDataType.STEPS, +); +``` +``` +I/FLUTTER_HEALTH( 9161): Success: {uuid=random-uuid-string, value=12, date_from=1742259061009, date_to=1742259092888, source_id=, source_name=com.google.android.apps.fitness, recording_method=0} +``` +> Assuming that the `uuid` and `type` are coming from your database. + ### Filtering by recording method Google Health Connect and Apple HealthKit both provide ways to distinguish samples collected "automatically" and manually entered data by the user. diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 974c06ca4..d5d03ed82 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:carp_serializable/carp_serializable.dart'; -import 'package:example_new/util.dart'; +import 'package:health_example/util.dart'; import 'package:flutter/material.dart'; import 'package:health/health.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -200,7 +200,8 @@ class HealthAppState extends State { } /// Fetch single data point by UUID and type. - Future fetchDataByUUID({ + Future fetchDataByUUID( + BuildContext context, { required String uuid, required HealthDataType type, }) async { @@ -212,7 +213,8 @@ class HealthAppState extends State { ); if (healthPoint != null) { - openDetailBottomSheet(healthPoint); + // save all the new data points (only the first 100) + if (context.mounted) openDetailBottomSheet(context, healthPoint); } } catch (error) { debugPrint("Exception in getHealthDataByUUID: $error"); @@ -234,90 +236,105 @@ class HealthAppState extends State { // misc. health data examples using the writeHealthData() method success &= await health.writeHealthData( - value: 1.925, - type: HealthDataType.HEIGHT, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 1.925, + type: HealthDataType.HEIGHT, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; success &= await health.writeHealthData( - value: 90, - type: HealthDataType.WEIGHT, - startTime: now, - recordingMethod: RecordingMethod.manual); + value: 90, + type: HealthDataType.WEIGHT, + startTime: now, + recordingMethod: RecordingMethod.manual) != + null; success &= await health.writeHealthData( - value: 90, - type: HealthDataType.HEART_RATE, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 90, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; success &= await health.writeHealthData( - value: 90, - type: HealthDataType.STEPS, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 90, + type: HealthDataType.STEPS, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; success &= await health.writeHealthData( - value: 200, - type: HealthDataType.ACTIVE_ENERGY_BURNED, - startTime: earlier, - endTime: now, - ); + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + ) != + null; success &= await health.writeHealthData( - value: 70, - type: HealthDataType.HEART_RATE, - startTime: earlier, - endTime: now); + value: 70, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now) != + null; success &= await health.writeHealthData( - value: 37, - type: HealthDataType.BODY_TEMPERATURE, - startTime: earlier, - endTime: now); + value: 37, + type: HealthDataType.BODY_TEMPERATURE, + startTime: earlier, + endTime: now) != + null; success &= await health.writeHealthData( - value: 105, - type: HealthDataType.BLOOD_GLUCOSE, - startTime: earlier, - endTime: now); + value: 105, + type: HealthDataType.BLOOD_GLUCOSE, + startTime: earlier, + endTime: now) != + null; success &= await health.writeInsulinDelivery( 5, InsulinDeliveryReason.BOLUS, earlier, now); success &= await health.writeHealthData( - value: 1.8, - type: HealthDataType.WATER, - startTime: earlier, - endTime: now); + value: 1.8, + type: HealthDataType.WATER, + startTime: earlier, + endTime: now) != + null; // different types of sleep success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_REM, - startTime: earlier, - endTime: now); + value: 0.0, + type: HealthDataType.SLEEP_REM, + startTime: earlier, + endTime: now) != + null; success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_ASLEEP, - startTime: earlier, - endTime: now); + value: 0.0, + type: HealthDataType.SLEEP_ASLEEP, + startTime: earlier, + endTime: now) != + null; success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_AWAKE, - startTime: earlier, - endTime: now); + value: 0.0, + type: HealthDataType.SLEEP_AWAKE, + startTime: earlier, + endTime: now) != + null; success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_DEEP, - startTime: earlier, - endTime: now); + value: 0.0, + type: HealthDataType.SLEEP_DEEP, + startTime: earlier, + endTime: now) != + null; success &= await health.writeHealthData( - value: 22, - type: HealthDataType.LEAN_BODY_MASS, - startTime: earlier, - endTime: now); + value: 22, + type: HealthDataType.LEAN_BODY_MASS, + startTime: earlier, + endTime: now) != + null; // specialized write methods success &= await health.writeBloodOxygen( - saturation: 98, - startTime: earlier, - endTime: now, - ); + saturation: 98, + startTime: earlier, + endTime: now, + ) != + null; success &= await health.writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, title: "Random workout name that shows up in Health Connect", @@ -404,51 +421,58 @@ class HealthAppState extends State { if (Platform.isIOS) { success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, - startTime: earlier, - endTime: now); + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, + startTime: earlier, + endTime: now) != + null; success &= await health.writeHealthData( - value: 1.5, // 1.5 m/s (typical walking speed) - type: HealthDataType.WALKING_SPEED, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 1.5, // 1.5 m/s (typical walking speed) + type: HealthDataType.WALKING_SPEED, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; } else { success &= await health.writeHealthData( - value: 2.0, // 2.0 m/s (typical jogging speed) - type: HealthDataType.SPEED, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 2.0, // 2.0 m/s (typical jogging speed) + type: HealthDataType.SPEED, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, - startTime: earlier, - endTime: now); + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + startTime: earlier, + endTime: now) != + null; } // Available on iOS or iOS 16.0+ only if (Platform.isIOS) { success &= await health.writeHealthData( - value: 22, - type: HealthDataType.WATER_TEMPERATURE, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 22, + type: HealthDataType.WATER_TEMPERATURE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; success &= await health.writeHealthData( - value: 55, - type: HealthDataType.UNDERWATER_DEPTH, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 55, + type: HealthDataType.UNDERWATER_DEPTH, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; success &= await health.writeHealthData( - value: 4.3, - type: HealthDataType.UV_INDEX, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 4.3, + type: HealthDataType.UV_INDEX, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual) != + null; } setState(() { @@ -587,6 +611,23 @@ class HealthAppState extends State { }); } + /// Display bottom sheet dialog of selected HealthDataPoint + void openDetailBottomSheet( + BuildContext context, + HealthDataPoint? healthPoint, + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (BuildContext context) => _detailedBottomSheet( + healthPoint: healthPoint, + ), + ); + } + // UI building below @override @@ -791,58 +832,80 @@ class HealthAppState extends State { ], ); - Widget get _contentDataReady => ListView.builder( - itemCount: _healthDataList.length, - itemBuilder: (_, index) { - // filter out manual entires if not wanted - if (recordingMethodsToFilter - .contains(_healthDataList[index].recordingMethod)) { - return Container(); - } - - HealthDataPoint p = _healthDataList[index]; - if (p.value is AudiogramHealthValue) { - return ListTile( - title: Text("${p.typeString}: ${p.value}"), - trailing: Text(p.unitString), - subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - ); - } - if (p.value is WorkoutHealthValue) { - return ListTile( - title: Text( - "${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"), - trailing: - Text((p.value as WorkoutHealthValue).workoutActivityType.name), - subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - onTap: () { - fetchDataByUUID( - uuid: p.uuid, - type: p.type, + Widget get _contentDataReady => Builder(builder: (context) { + return ListView.builder( + itemCount: _healthDataList.length, + itemBuilder: (_, index) { + // filter out manual entires if not wanted + if (recordingMethodsToFilter + .contains(_healthDataList[index].recordingMethod)) { + return Container(); + } + + HealthDataPoint p = _healthDataList[index]; + if (p.value is AudiogramHealthValue) { + return ListTile( + title: Text("${p.typeString}: ${p.value}"), + trailing: Text(p.unitString), + subtitle: + Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + onTap: () { + fetchDataByUUID( + context, + uuid: p.uuid, + type: p.type, + ); + }, + ); + } + if (p.value is WorkoutHealthValue) { + return ListTile( + title: Text( + "${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"), + trailing: Text( + (p.value as WorkoutHealthValue).workoutActivityType.name), + subtitle: + Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + onTap: () { + fetchDataByUUID( + context, + uuid: p.uuid, + type: p.type, + ); + }, + ); + } + if (p.value is NutritionHealthValue) { + return ListTile( + title: Text( + "${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"), + trailing: Text( + '${(p.value as NutritionHealthValue).calories} kcal'), + subtitle: + Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + onTap: () { + fetchDataByUUID( + context, + uuid: p.uuid, + type: p.type, + ); + }, + ); + } + return ListTile( + title: Text("${p.typeString}: ${p.value}"), + trailing: Text(p.unitString), + subtitle: + Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + onTap: () { + fetchDataByUUID( + context, + uuid: p.uuid, + type: p.type, + ); + }, ); - }, - ); - } - if (p.value is NutritionHealthValue) { - return ListTile( - title: Text( - "${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"), - trailing: - Text('${(p.value as NutritionHealthValue).calories} kcal'), - subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - ); - } - return ListTile( - title: Text("${p.typeString}: ${p.value}"), - trailing: Text(p.unitString), - subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - onTap: () { - fetchDataByUUID( - uuid: p.uuid, - type: p.type, - ); - }, - ); + }); }); final Widget _contentNoData = const Text('No Data to show'); diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml index 1b419f1d7..19f8b9007 100644 --- a/packages/health/example/pubspec.yaml +++ b/packages/health/example/pubspec.yaml @@ -1,40 +1,16 @@ -name: example_new -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +name: health_example +description: Demonstrates how to use the health plugin. +publish_to: "none" +version: 4.5.0 environment: - sdk: ^3.5.1 + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.6.0" -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 + cupertino_icons: ^1.0.2 permission_handler: ^11.3.1 carp_serializable: ^2.0.0 # polymorphic json serialization health: @@ -45,51 +21,5 @@ dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^4.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package diff --git a/packages/health/ios/Classes/HealthDataReader.swift b/packages/health/ios/Classes/HealthDataReader.swift index 49d6ef7ad..beae05338 100644 --- a/packages/health/ios/Classes/HealthDataReader.swift +++ b/packages/health/ios/Classes/HealthDataReader.swift @@ -318,6 +318,250 @@ class HealthDataReader { healthStore.execute(query) } + + func getDataByUUID(call: FlutterMethodCall, result: @escaping FlutterResult) { + + guard let arguments = call.arguments as? NSDictionary, + let uuidarg = arguments["uuid"] as? String, + let dataTypeKey = arguments["dataTypeKey"] as? String else { + DispatchQueue.main.async { + result(FlutterError(code: "HEALTH_ERROR", + message: "Invalid Arguments - UUID or DataTypeKey invalid", + details: nil)) + } + return + } + + let dataUnitKey = arguments["dataUnitKey"] as? String + let recordingMethodsToFilter = (arguments["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue) + + var unit: HKUnit? + if let dataUnitKey = dataUnitKey { + unit = unitDict[dataUnitKey] // Ensure unitDict exists and contains the key + } + + guard let dataType = dataTypesDict[dataTypeKey] else { + DispatchQueue.main.async { + result(FlutterError(code: "INVALID_TYPE", + message: "Invalid dataTypeKey: \(dataTypeKey)", + details: nil)) + } + return + } + + guard let uuid = UUID(uuidString: uuidarg) else { + result(false) + return + } + + var predicate = HKQuery.predicateForObjects(with: [uuid]) + + let sourceIdForCharacteristic = "com.apple.Health" + let sourceNameForCharacteristic = "Health" + + if (!includeManualEntry) { + let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) + predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) + } + + let query = HKSampleQuery( + sampleType: dataType, + predicate: predicate, + limit: 1, + sortDescriptors: nil + ) { + [self] + x, samplesOrNil, error in + + + guard error == nil else { + DispatchQueue.main.async { + result(FlutterError(code: "HEALTH_ERROR", + message: "Error getting health data by UUID: \(error!.localizedDescription)", + details: nil)) + } + return + } + + guard let samples = samplesOrNil else { + DispatchQueue.main.async { + result([]) + } + return + } + + if let quantitySamples = samples as? [HKQuantitySample] { + let dictionaries = quantitySamples.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()), + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, + "dataUnitKey": unit?.unitString, + "metadata": HealthUtilities.sanitizeMetadata(sample.metadata) + ] + } + DispatchQueue.main.async { + result(dictionaries) + } + } else if var categorySamples = samples as? [HKCategorySample] { + // filter category samples based on dataTypeKey + switch dataTypeKey { + case HealthConstants.SLEEP_IN_BED: + categorySamples = categorySamples.filter { $0.value == 0 } + case HealthConstants.SLEEP_ASLEEP: + categorySamples = categorySamples.filter { $0.value == 1 } + case HealthConstants.SLEEP_AWAKE: + categorySamples = categorySamples.filter { $0.value == 2 } + case HealthConstants.SLEEP_LIGHT: + categorySamples = categorySamples.filter { $0.value == 3 } + case HealthConstants.SLEEP_DEEP: + categorySamples = categorySamples.filter { $0.value == 4 } + case HealthConstants.SLEEP_REM: + categorySamples = categorySamples.filter { $0.value == 5 } + case HealthConstants.HEADACHE_UNSPECIFIED: + categorySamples = categorySamples.filter { $0.value == 0 } + case HealthConstants.HEADACHE_NOT_PRESENT: + categorySamples = categorySamples.filter { $0.value == 1 } + case HealthConstants.HEADACHE_MILD: + categorySamples = categorySamples.filter { $0.value == 2 } + case HealthConstants.HEADACHE_MODERATE: + categorySamples = categorySamples.filter { $0.value == 3 } + case HealthConstants.HEADACHE_SEVERE: + categorySamples = categorySamples.filter { $0.value == 4 } + default: + break + } + + let categories = categorySamples.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "value": sample.value, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, + "metadata": HealthUtilities.sanitizeMetadata(sample.metadata) + ] + } + DispatchQueue.main.async { + result(categories) + } + } else if let workoutSamples = samples as? [HKWorkout] { + let dictionaries = workoutSamples.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "workoutActivityType": self.workoutActivityTypeMap.first(where: { + $0.value == sample.workoutActivityType + })?.key, + "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), + "totalEnergyBurnedUnit": "KILOCALORIE", + "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), + "totalDistanceUnit": "METER", + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, + "workout_type": HKWorkoutActivityType.toString(sample.workoutActivityType), + "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, + "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 + ] + } + + DispatchQueue.main.async { + result(dictionaries) + } + } else if let audiogramSamples = samples as? [HKAudiogramSample] { + let dictionaries = audiogramSamples.map { sample -> NSDictionary in + var frequencies = [Double]() + var leftEarSensitivities = [Double]() + var rightEarSensitivities = [Double]() + for samplePoint in sample.sensitivityPoints { + frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) + leftEarSensitivities.append( + samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + rightEarSensitivities.append( + samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + } + return [ + "uuid": "\(sample.uuid)", + "frequencies": frequencies, + "leftEarSensitivities": leftEarSensitivities, + "rightEarSensitivities": rightEarSensitivities, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] + } + DispatchQueue.main.async { + result(dictionaries) + } + } else if let nutritionSamples = samples as? [HKCorrelation] { + var foods: [[String: Any?]] = [] + for food in nutritionSamples { + let name = food.metadata?[HKMetadataKeyFoodType] as? String + let mealType = food.metadata?["HKFoodMeal"] + let samples = food.objects + if let sample = samples.first as? HKQuantitySample { + var sampleDict = [ + "uuid": "\(sample.uuid)", + "name": name, + "meal_type": mealType, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue + ] + for sample in samples { + if let quantitySample = sample as? HKQuantitySample { + for (key, identifier) in HealthConstants.NUTRITION_KEYS { + if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: identifier)){ + let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram() + sampleDict[key] = quantitySample.quantity.doubleValue(for: unit) + } + } + } + } + foods.append(sampleDict as! [String : Any?]) + } + } + + DispatchQueue.main.async { + result(foods) + } + } else { + if #available(iOS 14.0, *), let ecgSamples = samples as? [HKElectrocardiogram] { + let dictionaries = ecgSamples.map(self.fetchEcgMeasurements) + DispatchQueue.main.async { + result(dictionaries) + } + } else { + DispatchQueue.main.async { + print("Error getting ECG - only available on iOS 14.0 and above!") + result(nil) + } + } + } + } + + healthStore.execute(query) + } /// Gets interval health data /// - Parameters: diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 52e68af12..d87e3cc09 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -77,6 +77,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { case "getData": healthDataReader.getData(call: call, result: result) + case "getDataByUUID": + healthDataReader.getDataByUUID(call: call, result: result) + case "getIntervalData": healthDataReader.getIntervalData(call: call, result: result) diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index a52dd9725..1dc775de8 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -473,7 +473,7 @@ class Health { /// /// Values for Sleep and Headache are ignored and will be automatically assigned /// the default value. - Future writeHealthData({ + Future writeHealthData({ required double value, HealthDataUnit? unit, required HealthDataType type, @@ -538,8 +538,22 @@ class Health { 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), }; - bool? success = await _channel.invokeMethod('writeData', args); - return success ?? false; + + String uuid = '${await _channel.invokeMethod('writeData', args)}'; + if (uuid.isEmpty) { + return null; + } + + try { + final healthPoint = await getHealthDataByUUID( + uuid: uuid, + type: type, + ); + + return healthPoint; + } catch (e) { + return null; + } } /// Deletes all records of the given [type] for a given period of time. @@ -661,7 +675,7 @@ class Health { /// Simply set [endTime] equal to [startTime] if the blood oxygen saturation /// is measured only at a specific point in time (default). /// * [recordingMethod] - the recording method of the data point. - Future writeBloodOxygen({ + Future writeBloodOxygen({ required double saturation, required DateTime startTime, DateTime? endTime, @@ -678,10 +692,11 @@ class Health { if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } - bool? success; + + HealthDataPoint? healthDataPoint; if (Platform.isIOS) { - success = await writeHealthData( + healthDataPoint = await writeHealthData( value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, @@ -695,9 +710,9 @@ class Health { 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, 'recordingMethod': recordingMethod.toInt(), }; - success = await _channel.invokeMethod('writeBloodOxygen', args); + healthDataPoint = await _channel.invokeMethod('writeBloodOxygen', args); } - return success ?? false; + return healthDataPoint; } /// Saves meal record into Apple Health or Health Connect. @@ -1230,14 +1245,17 @@ class Health { 'dataUnitKey': dataTypeToUnit[dataType]!.name, 'uuid': uuid, }; - final fetchedDataPoint = await _channel.invokeMethod('getDataByUUID', args); - if (fetchedDataPoint != null) { + final fetchedDataPoints = + await _channel.invokeMethod('getDataByUUID', args); + + if (fetchedDataPoints != null) { final msg = { "dataType": dataType, - "dataPoints": [fetchedDataPoint], + "dataPoints": fetchedDataPoints, }; + // get single record of parsed fetchedDataPoints return _parse(msg).first; } else { return null; diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index c86b2a5e8..cbcd717e4 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -10,8 +10,8 @@ environment: dependencies: flutter: sdk: flutter - intl: '>=0.18.0 <0.21.0' - device_info_plus: '>=9.0.0 <12.0.0' + intl: ">=0.18.0 <0.21.0" + device_info_plus: ">=9.0.0 <12.0.0" json_annotation: ^4.8.0 carp_serializable: ^2.0.0 # polymorphic json serialization From 88a0505b5862406600045baba4d8abc16f3ca700 Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Fri, 18 Jul 2025 11:10:56 +0700 Subject: [PATCH 11/25] Revert android build.gradle to original and bump targetSdk --- packages/health/example/android/app/build.gradle | 8 ++++---- packages/health/example/android/settings.gradle | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/health/example/android/app/build.gradle b/packages/health/example/android/app/build.gradle index 1d719ae05..bbaca37bc 100644 --- a/packages/health/example/android/app/build.gradle +++ b/packages/health/example/android/app/build.gradle @@ -6,8 +6,8 @@ plugins { } android { - namespace = "cachet.plugins.example_app" - compileSdk = 34 + namespace = "cachet.plugins.health.health_example" + compileSdk = 35 ndkVersion = '25.1.8937393' compileOptions { @@ -21,11 +21,11 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "cachet.plugins.example_app" + applicationId = "cachet.plugins.health.health_example" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 26 - targetSdk = 34 + targetSdk = 35 versionCode = flutter.versionCode versionName = flutter.versionName } diff --git a/packages/health/example/android/settings.gradle b/packages/health/example/android/settings.gradle index ff1ea2190..6ac9fc18a 100644 --- a/packages/health/example/android/settings.gradle +++ b/packages/health/example/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.2" apply false + id "com.android.application" version "8.3.0" apply false id "org.jetbrains.kotlin.android" version "1.9.10" apply false } From 1cea6b05d97ad549c1fcf2953dc854e94a6cc65b Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Fri, 18 Jul 2025 11:11:29 +0700 Subject: [PATCH 12/25] Sync getDataByUUID with latest master changes --- .../cachet/plugins/health/HealthDataReader.kt | 102 +++++++++++++++--- .../cachet/plugins/health/HealthPlugin.kt | 1 + 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt index ba0db624c..8a82d3cf2 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt @@ -110,6 +110,76 @@ class HealthDataReader( } } + /** + * Retrieves single health data point by given UUID and type. + * + * @param call Method call containing 'UUID' and 'dataTypeKey' + * @param result Flutter result callback returning list of health data maps + */ + fun getDataByUUID(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val uuid = call.argument("uuid")!! + var healthPoint = mapOf() + + if (!HealthConstants.mapToType.containsKey(dataType)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataType not found in HC") + result.success(null) + return + } + + val classType = HealthConstants.mapToType[dataType]!! + + scope.launch { + try { + + Log.i("FLUTTER_HEALTH", "Getting $uuid with $classType") + + // Execute the request + val response = healthConnectClient.readRecord(classType, uuid) + + // Find the record with the matching UUID + val matchingRecord = response.record + + if (matchingRecord != null) { + // Handle special cases using shared logic + when (dataType) { + WORKOUT -> { + val tempData = mutableListOf>() + handleWorkoutData(listOf(matchingRecord), emptyList(), tempData) + healthPoint = if (tempData.isNotEmpty()) tempData[0] else mapOf() + } + SLEEP_SESSION, SLEEP_ASLEEP, SLEEP_AWAKE, SLEEP_AWAKE_IN_BED, + SLEEP_LIGHT, SLEEP_DEEP, SLEEP_REM, SLEEP_OUT_OF_BED, SLEEP_UNKNOWN -> { + if (matchingRecord is SleepSessionRecord) { + val tempData = mutableListOf>() + handleSleepData(listOf(matchingRecord), emptyList(), dataType, tempData) + healthPoint = if (tempData.isNotEmpty()) tempData[0] else mapOf() + } + } + else -> { + healthPoint = dataConverter.convertRecord(matchingRecord, dataType)[0] + } + } + + Log.i( + "FLUTTER_HEALTH", + "Success: $healthPoint" + ) + + Handler(context.mainLooper).run { result.success(healthPoint) } + } else { + Log.e("FLUTTER_HEALTH::ERROR", "Record not found for UUID: $uuid") + result.success(null) + } + } catch (e: Exception) { + Log.e("FLUTTER_HEALTH::ERROR", "Error fetching record with UUID: $uuid") + Log.e("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.e("FLUTTER_HEALTH::ERROR", e.stackTraceToString()) + result.success(null) + } + } + } + /** * Retrieves aggregated health data grouped by time intervals. * Calculates totals, averages, or counts over specified time periods. @@ -289,18 +359,22 @@ class HealthDataReader( * by querying related records within the workout time period. * * @param records List of ExerciseSessionRecord objects - * @param recordingMethodsToFilter Recording methods to exclude + * @param recordingMethodsToFilter Recording methods to exclude (empty list means no filtering) * @param healthConnectData Mutable list to append processed workout data */ private suspend fun handleWorkoutData( records: List, - recordingMethodsToFilter: List, + recordingMethodsToFilter: List = emptyList(), healthConnectData: MutableList> ) { - val filteredRecords = recordingFilter.filterRecordsByRecordingMethods( - recordingMethodsToFilter, + val filteredRecords = if (recordingMethodsToFilter.isEmpty()) { records - ) + } else { + recordingFilter.filterRecordsByRecordingMethods( + recordingMethodsToFilter, + records + ) + } for (rec in filteredRecords) { val record = rec as ExerciseSessionRecord @@ -366,8 +440,8 @@ class HealthDataReader( "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(), + "date_from" to record.startTime.toEpochMilli(), + "date_to" to record.endTime.toEpochMilli(), "source_id" to "", "source_name" to record.metadata.dataOrigin.packageName, ), @@ -381,20 +455,24 @@ class HealthDataReader( * Converts sleep stage enumerations to meaningful duration and type information. * * @param records List of SleepSessionRecord objects - * @param recordingMethodsToFilter Recording methods to exclude + * @param recordingMethodsToFilter Recording methods to exclude (empty list means no filtering) * @param dataType Specific sleep data type being requested * @param healthConnectData Mutable list to append processed sleep data */ private fun handleSleepData( records: List, - recordingMethodsToFilter: List, + recordingMethodsToFilter: List = emptyList(), dataType: String, healthConnectData: MutableList> ) { - val filteredRecords = recordingFilter.filterRecordsByRecordingMethods( - recordingMethodsToFilter, + val filteredRecords = if (recordingMethodsToFilter.isEmpty()) { records - ) + } else { + recordingFilter.filterRecordsByRecordingMethods( + recordingMethodsToFilter, + records + ) + } for (rec in filteredRecords) { if (rec is SleepSessionRecord) { 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 2da6af19e..7a03aed48 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 @@ -139,6 +139,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // Reading data "getData" -> dataReader.getData(call, result) + "getDataByUUID" -> dataReader.getDataByUUID(call, result) "getIntervalData" -> dataReader.getIntervalData(call, result) "getAggregateData" -> dataReader.getAggregateData(call, result) "getTotalStepsInInterval" -> dataReader.getTotalStepsInInterval(call, result) From 75f496140ecdbe80e285502a37c3160a8ea775e9 Mon Sep 17 00:00:00 2001 From: Agil Setiawan Date: Fri, 18 Jul 2025 11:25:27 +0700 Subject: [PATCH 13/25] Fix getDataByUUID for iOS --- .../health/ios/Classes/HealthDataReader.swift | 29 ++++++++----------- packages/health/lib/src/health_plugin.dart | 17 ++++------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/packages/health/ios/Classes/HealthDataReader.swift b/packages/health/ios/Classes/HealthDataReader.swift index beae05338..9db7578f1 100644 --- a/packages/health/ios/Classes/HealthDataReader.swift +++ b/packages/health/ios/Classes/HealthDataReader.swift @@ -319,6 +319,10 @@ class HealthDataReader { healthStore.execute(query) } + /// Gets single health data by UUID + /// - Parameters: + /// - call: Flutter method call + /// - result: Flutter result callback func getDataByUUID(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? NSDictionary, @@ -333,9 +337,6 @@ class HealthDataReader { } let dataUnitKey = arguments["dataUnitKey"] as? String - let recordingMethodsToFilter = (arguments["recordingMethodsToFilter"] as? [Int]) ?? [] - let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue) - var unit: HKUnit? if let dataUnitKey = dataUnitKey { unit = unitDict[dataUnitKey] // Ensure unitDict exists and contains the key @@ -351,7 +352,7 @@ class HealthDataReader { } guard let uuid = UUID(uuidString: uuidarg) else { - result(false) + result(nil) return } @@ -360,11 +361,6 @@ class HealthDataReader { let sourceIdForCharacteristic = "com.apple.Health" let sourceNameForCharacteristic = "Health" - if (!includeManualEntry) { - let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) - predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) - } - let query = HKSampleQuery( sampleType: dataType, predicate: predicate, @@ -374,7 +370,6 @@ class HealthDataReader { [self] x, samplesOrNil, error in - guard error == nil else { DispatchQueue.main.async { result(FlutterError(code: "HEALTH_ERROR", @@ -386,7 +381,7 @@ class HealthDataReader { guard let samples = samplesOrNil else { DispatchQueue.main.async { - result([]) + result(nil) } return } @@ -408,7 +403,7 @@ class HealthDataReader { ] } DispatchQueue.main.async { - result(dictionaries) + result(dictionaries.first) } } else if var categorySamples = samples as? [HKCategorySample] { // filter category samples based on dataTypeKey @@ -454,7 +449,7 @@ class HealthDataReader { ] } DispatchQueue.main.async { - result(categories) + result(categories.first) } } else if let workoutSamples = samples as? [HKWorkout] { let dictionaries = workoutSamples.map { sample -> NSDictionary in @@ -481,7 +476,7 @@ class HealthDataReader { } DispatchQueue.main.async { - result(dictionaries) + result(dictionaries.first) } } else if let audiogramSamples = samples as? [HKAudiogramSample] { let dictionaries = audiogramSamples.map { sample -> NSDictionary in @@ -507,7 +502,7 @@ class HealthDataReader { ] } DispatchQueue.main.async { - result(dictionaries) + result(dictionaries.first) } } else if let nutritionSamples = samples as? [HKCorrelation] { var foods: [[String: Any?]] = [] @@ -543,13 +538,13 @@ class HealthDataReader { } DispatchQueue.main.async { - result(foods) + result(foods.first) } } else { if #available(iOS 14.0, *), let ecgSamples = samples as? [HKElectrocardiogram] { let dictionaries = ecgSamples.map(self.fetchEcgMeasurements) DispatchQueue.main.async { - result(dictionaries) + result(dictionaries.first) } } else { DispatchQueue.main.async { diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 1dc775de8..c71c6cba7 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1033,13 +1033,6 @@ class Health { required String uuid, required HealthDataType type, }) async { - if (Platform.isAndroid) { - throw HealthException( - type, - 'getHealthDataByUUID is not available for Android at this moment.', - ); - } - if (uuid.isEmpty) { throw HealthException(type, 'UUID is empty!'); } @@ -1246,13 +1239,15 @@ class Health { 'uuid': uuid, }; - final fetchedDataPoints = - await _channel.invokeMethod('getDataByUUID', args); + final fetchedDataPoint = await _channel.invokeMethod('getDataByUUID', args); - if (fetchedDataPoints != null) { + // fetchedDataPoint is Map. // Must be converted to List first + // so no need to recreate _parse() to handle single HealthDataPoint. + + if (fetchedDataPoint != null) { final msg = { "dataType": dataType, - "dataPoints": fetchedDataPoints, + "dataPoints": [fetchedDataPoint], }; // get single record of parsed fetchedDataPoints From aa9eec815a116f44779a4593131f3c2d0eb65706 Mon Sep 17 00:00:00 2001 From: Agil Setiawan <153470171+agilst@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:06:08 +0700 Subject: [PATCH 14/25] Revert write data to original --- packages/health/example/lib/main.dart | 216 +++++++++------------ packages/health/lib/src/health_plugin.dart | 31 +-- 2 files changed, 105 insertions(+), 142 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index d5d03ed82..1e2090668 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -236,105 +236,90 @@ class HealthAppState extends State { // misc. health data examples using the writeHealthData() method success &= await health.writeHealthData( - value: 1.925, - type: HealthDataType.HEIGHT, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 1.925, + type: HealthDataType.HEIGHT, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 90, - type: HealthDataType.WEIGHT, - startTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 90, + type: HealthDataType.WEIGHT, + startTime: now, + recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 90, - type: HealthDataType.HEART_RATE, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 90, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 90, - type: HealthDataType.STEPS, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 90, + type: HealthDataType.STEPS, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 200, - type: HealthDataType.ACTIVE_ENERGY_BURNED, - startTime: earlier, - endTime: now, - ) != - null; + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + ); success &= await health.writeHealthData( - value: 70, - type: HealthDataType.HEART_RATE, - startTime: earlier, - endTime: now) != - null; + value: 70, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now); success &= await health.writeHealthData( - value: 37, - type: HealthDataType.BODY_TEMPERATURE, - startTime: earlier, - endTime: now) != - null; + value: 37, + type: HealthDataType.BODY_TEMPERATURE, + startTime: earlier, + endTime: now); success &= await health.writeHealthData( - value: 105, - type: HealthDataType.BLOOD_GLUCOSE, - startTime: earlier, - endTime: now) != - null; + value: 105, + type: HealthDataType.BLOOD_GLUCOSE, + startTime: earlier, + endTime: now); success &= await health.writeInsulinDelivery( 5, InsulinDeliveryReason.BOLUS, earlier, now); success &= await health.writeHealthData( - value: 1.8, - type: HealthDataType.WATER, - startTime: earlier, - endTime: now) != - null; + value: 1.8, + type: HealthDataType.WATER, + startTime: earlier, + endTime: now); // different types of sleep success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_REM, - startTime: earlier, - endTime: now) != - null; + value: 0.0, + type: HealthDataType.SLEEP_REM, + startTime: earlier, + endTime: now); success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_ASLEEP, - startTime: earlier, - endTime: now) != - null; + value: 0.0, + type: HealthDataType.SLEEP_ASLEEP, + startTime: earlier, + endTime: now); success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_AWAKE, - startTime: earlier, - endTime: now) != - null; + value: 0.0, + type: HealthDataType.SLEEP_AWAKE, + startTime: earlier, + endTime: now); success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_DEEP, - startTime: earlier, - endTime: now) != - null; + value: 0.0, + type: HealthDataType.SLEEP_DEEP, + startTime: earlier, + endTime: now); success &= await health.writeHealthData( - value: 22, - type: HealthDataType.LEAN_BODY_MASS, - startTime: earlier, - endTime: now) != - null; + value: 22, + type: HealthDataType.LEAN_BODY_MASS, + startTime: earlier, + endTime: now); // specialized write methods success &= await health.writeBloodOxygen( - saturation: 98, - startTime: earlier, - endTime: now, - ) != - null; + saturation: 98, + startTime: earlier, + endTime: now, + ); success &= await health.writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, title: "Random workout name that shows up in Health Connect", @@ -421,58 +406,51 @@ class HealthAppState extends State { if (Platform.isIOS) { success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, - startTime: earlier, - endTime: now) != - null; + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, + startTime: earlier, + endTime: now); success &= await health.writeHealthData( - value: 1.5, // 1.5 m/s (typical walking speed) - type: HealthDataType.WALKING_SPEED, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 1.5, // 1.5 m/s (typical walking speed) + type: HealthDataType.WALKING_SPEED, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); } else { success &= await health.writeHealthData( - value: 2.0, // 2.0 m/s (typical jogging speed) - type: HealthDataType.SPEED, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 2.0, // 2.0 m/s (typical jogging speed) + type: HealthDataType.SPEED, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, - startTime: earlier, - endTime: now) != - null; + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + startTime: earlier, + endTime: now); } // Available on iOS or iOS 16.0+ only if (Platform.isIOS) { success &= await health.writeHealthData( - value: 22, - type: HealthDataType.WATER_TEMPERATURE, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 22, + type: HealthDataType.WATER_TEMPERATURE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 55, - type: HealthDataType.UNDERWATER_DEPTH, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 55, + type: HealthDataType.UNDERWATER_DEPTH, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 4.3, - type: HealthDataType.UV_INDEX, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual) != - null; + value: 4.3, + type: HealthDataType.UV_INDEX, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); } setState(() { diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index c71c6cba7..d92671d25 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -473,7 +473,7 @@ class Health { /// /// Values for Sleep and Headache are ignored and will be automatically assigned /// the default value. - Future writeHealthData({ + Future writeHealthData({ required double value, HealthDataUnit? unit, required HealthDataType type, @@ -538,22 +538,8 @@ class Health { 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), }; - - String uuid = '${await _channel.invokeMethod('writeData', args)}'; - if (uuid.isEmpty) { - return null; - } - - try { - final healthPoint = await getHealthDataByUUID( - uuid: uuid, - type: type, - ); - - return healthPoint; - } catch (e) { - return null; - } + bool? success = await _channel.invokeMethod('writeData', args); + return success ?? false; } /// Deletes all records of the given [type] for a given period of time. @@ -675,7 +661,7 @@ class Health { /// Simply set [endTime] equal to [startTime] if the blood oxygen saturation /// is measured only at a specific point in time (default). /// * [recordingMethod] - the recording method of the data point. - Future writeBloodOxygen({ + Future writeBloodOxygen({ required double saturation, required DateTime startTime, DateTime? endTime, @@ -692,11 +678,10 @@ class Health { if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } - - HealthDataPoint? healthDataPoint; + bool? success; if (Platform.isIOS) { - healthDataPoint = await writeHealthData( + success = await writeHealthData( value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, @@ -710,9 +695,9 @@ class Health { 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, 'recordingMethod': recordingMethod.toInt(), }; - healthDataPoint = await _channel.invokeMethod('writeBloodOxygen', args); + success = await _channel.invokeMethod('writeBloodOxygen', args); } - return healthDataPoint; + return success ?? false; } /// Saves meal record into Apple Health or Health Connect. From 9369bb3271eee94d0c872d720e95d509be0aac36 Mon Sep 17 00:00:00 2001 From: stuart Date: Tue, 29 Jul 2025 13:24:24 -0700 Subject: [PATCH 15/25] allowing the units to be used when parsing the HealthDataPoint --- .../health/lib/src/health_data_point.dart | 23 ++++++-- packages/health/lib/src/health_plugin.dart | 58 ++++++++++--------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 70dc1d882..1255e7048 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -112,9 +112,7 @@ class HealthDataPoint { /// Create a [HealthDataPoint] based on a health data point from native data format. factory HealthDataPoint.fromHealthDataPoint( - HealthDataType dataType, - dynamic dataPoint, - ) { + HealthDataType dataType, dynamic dataPoint, String? unitName) { // Handling different [HealthValue] types HealthValue value = switch (dataType) { HealthDataType.AUDIOGRAM => @@ -141,7 +139,9 @@ class HealthDataPoint { final Map? metadata = dataPoint["metadata"] == null ? null : Map.from(dataPoint['metadata'] as Map); - final unit = dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT; + final HealthDataUnit unit = HealthDataUnit.values.firstWhere( + (value) => value.name == unitName, + orElse: () => dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT); final String? uuid = dataPoint["uuid"] as String?; final String? deviceModel = dataPoint["device_model"] as String?; @@ -209,6 +209,17 @@ class HealthDataPoint { deviceModel == other.deviceModel; @override - int get hashCode => Object.hash(uuid, value, unit, dateFrom, dateTo, type, - sourcePlatform, sourceDeviceId, sourceId, sourceName, metadata, deviceModel); + int get hashCode => Object.hash( + uuid, + value, + unit, + dateFrom, + dateTo, + type, + sourcePlatform, + sourceDeviceId, + sourceId, + sourceName, + metadata, + deviceModel); } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index e3a489864..942c93143 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -274,8 +274,8 @@ class Health { if (Platform.isIOS) return false; try { - final status = - await _channel.invokeMethod('isHealthDataInBackgroundAvailable'); + final status = await _channel + .invokeMethod('isHealthDataInBackgroundAvailable'); return status ?? false; } catch (e) { debugPrint( @@ -295,8 +295,8 @@ class Health { if (Platform.isIOS) return true; try { - final status = - await _channel.invokeMethod('isHealthDataInBackgroundAuthorized'); + final status = await _channel + .invokeMethod('isHealthDataInBackgroundAuthorized'); return status ?? false; } catch (e) { debugPrint( @@ -318,8 +318,8 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); try { - final bool? isAuthorized = - await _channel.invokeMethod('requestHealthDataInBackgroundAuthorization'); + final bool? isAuthorized = await _channel + .invokeMethod('requestHealthDataInBackgroundAuthorization'); return isAuthorized ?? false; } catch (e) { debugPrint( @@ -539,8 +539,8 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), - 'clientRecordId' : clientRecordId, - 'clientRecordVersion' : clientRecordVersion, + 'clientRecordId': clientRecordId, + 'clientRecordVersion': clientRecordVersion, }; bool? success = await _channel.invokeMethod('writeData', args); return success ?? false; @@ -596,7 +596,8 @@ class Health { } if (Platform.isIOS && type == null) { - throw ArgumentError("On iOS, both UUID and type are required to delete a record."); + throw ArgumentError( + "On iOS, both UUID and type are required to delete a record."); } Map args = { @@ -665,8 +666,8 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), - 'clientRecordId' : clientRecordId, - 'clientRecordVersion' : clientRecordVersion, + 'clientRecordId': clientRecordId, + 'clientRecordVersion': clientRecordVersion, }; return await _channel.invokeMethod('writeBloodPressure', args) == true; } @@ -842,8 +843,8 @@ class Health { 'meal_type': mealType.name, 'start_time': startTime.millisecondsSinceEpoch, 'end_time': endTime.millisecondsSinceEpoch, - 'clientRecordId' : clientRecordId, - 'clientRecordVersion' : clientRecordVersion, + 'clientRecordId': clientRecordId, + 'clientRecordVersion': clientRecordVersion, 'calories': caloriesConsumed, 'carbs': carbohydrates, 'protein': protein, @@ -1046,7 +1047,8 @@ class Health { for (var type in types) { final result = await _prepareQuery( - startTime, endTime, type, recordingMethodsToFilter, dataUnit: preferredUnits?[type]); + startTime, endTime, type, recordingMethodsToFilter, + dataUnit: preferredUnits?[type]); dataPoints.addAll(result); } @@ -1099,12 +1101,11 @@ class Health { /// Prepares an interval query, i.e. checks if the types are available, etc. Future> _prepareQuery( - DateTime startTime, - DateTime endTime, - HealthDataType dataType, - List recordingMethodsToFilter, - {HealthDataUnit? dataUnit} - ) async { + DateTime startTime, + DateTime endTime, + HealthDataType dataType, + List recordingMethodsToFilter, + {HealthDataUnit? dataUnit}) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -1121,7 +1122,8 @@ class Health { return _computeAndroidBMI(startTime, endTime, recordingMethodsToFilter); } return await _dataQuery( - startTime, endTime, dataType, recordingMethodsToFilter, dataUnit: dataUnit); + startTime, endTime, dataType, recordingMethodsToFilter, + dataUnit: dataUnit); } /// Prepares an interval query, i.e. checks if the types are available, etc. @@ -1170,15 +1172,13 @@ class Health { } /// Fetches data points from Android/iOS native code. - Future> _dataQuery( - DateTime startTime, - DateTime endTime, - HealthDataType dataType, - List recordingMethodsToFilter, + Future> _dataQuery(DateTime startTime, DateTime endTime, + HealthDataType dataType, List recordingMethodsToFilter, {HealthDataUnit? dataUnit}) async { + String? unit = dataUnit?.name ?? dataTypeToUnit[dataType]!.name; final args = { 'dataTypeKey': dataType.name, - 'dataUnitKey': dataUnit?.name ?? dataTypeToUnit[dataType]!.name, + 'dataUnitKey': unit, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethodsToFilter': @@ -1190,6 +1190,7 @@ class Health { final msg = { "dataType": dataType, "dataPoints": fetchedDataPoints, + "unit": unit }; const thresHold = 100; // If the no. of data points are larger than the threshold, @@ -1263,10 +1264,11 @@ class Health { List _parse(Map message) { final dataType = message["dataType"] as HealthDataType; final dataPoints = message["dataPoints"] as List; + String? unit = message["unit"] as String?; return dataPoints .map((dataPoint) => - HealthDataPoint.fromHealthDataPoint(dataType, dataPoint)) + HealthDataPoint.fromHealthDataPoint(dataType, dataPoint, unit)) .toList(); } From f47daa8d1d022570350d0b600916b845dc22cc29 Mon Sep 17 00:00:00 2001 From: stuart Date: Tue, 29 Jul 2025 15:50:03 -0700 Subject: [PATCH 16/25] making null safe --- packages/health/lib/src/health_plugin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 942c93143..e88ec6a2c 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1175,7 +1175,7 @@ class Health { Future> _dataQuery(DateTime startTime, DateTime endTime, HealthDataType dataType, List recordingMethodsToFilter, {HealthDataUnit? dataUnit}) async { - String? unit = dataUnit?.name ?? dataTypeToUnit[dataType]!.name; + String? unit = dataUnit?.name ?? dataTypeToUnit[dataType]?.name; final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': unit, From be9f0a2d80855a172e1fca1eeb15da0c14aed098 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:58:39 +0300 Subject: [PATCH 17/25] Clean up --- packages/health/example/.metadata | 4 +- packages/health/example/README.md | 12 +- packages/health/example/analysis_options.yaml | 1 + .../health/example/android/app/build.gradle | 44 -- .../android/app/src/main/AndroidManifest.xml | 1 - .../health/health_example/MainActivity.kt | 2 + packages/health/example/android/build.gradle | 18 - .../health/example/android/settings.gradle | 25 - packages/health/example/lib/main.dart | 14 +- packages/health/lib/health.g.dart | 630 ------------------ packages/health/lib/src/health_plugin.dart | 15 +- packages/health/pubspec.yaml | 4 +- 12 files changed, 26 insertions(+), 744 deletions(-) delete mode 100644 packages/health/example/android/app/build.gradle delete mode 100644 packages/health/example/android/build.gradle delete mode 100644 packages/health/example/android/settings.gradle delete mode 100644 packages/health/lib/health.g.dart diff --git a/packages/health/example/.metadata b/packages/health/example/.metadata index facec9677..9a674c613 100644 --- a/packages/health/example/.metadata +++ b/packages/health/example/.metadata @@ -41,5 +41,5 @@ migration: # # Files that are not part of the templates will be ignored by default. unmanaged_files: - - "lib/main.dart" - - "ios/Runner.xcodeproj/project.pbxproj" + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/health/example/README.md b/packages/health/example/README.md index 388bfb9bb..1c4dbcfc1 100644 --- a/packages/health/example/README.md +++ b/packages/health/example/README.md @@ -1,6 +1,6 @@ -# example_new +# health_example -A new Flutter project. +Demonstrates how to use the health plugin. ## Getting Started @@ -8,9 +8,9 @@ This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. diff --git a/packages/health/example/analysis_options.yaml b/packages/health/example/analysis_options.yaml index 043984832..0d2902135 100644 --- a/packages/health/example/analysis_options.yaml +++ b/packages/health/example/analysis_options.yaml @@ -23,5 +23,6 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/packages/health/example/android/app/build.gradle b/packages/health/example/android/app/build.gradle deleted file mode 100644 index 1d719ae05..000000000 --- a/packages/health/example/android/app/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id "dev.flutter.flutter-gradle-plugin" -} - -android { - namespace = "cachet.plugins.example_app" - compileSdk = 34 - ndkVersion = '25.1.8937393' - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "cachet.plugins.example_app" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 26 - targetSdk = 34 - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.debug - } - } -} - -flutter { - source = "../.." -} diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml index 2d17294ac..70e81aa1e 100644 --- a/packages/health/example/android/app/src/main/AndroidManifest.xml +++ b/packages/health/example/android/app/src/main/AndroidManifest.xml @@ -88,7 +88,6 @@ - runApp(const HealthApp()); +void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { const HealthApp({super.key}); @@ -576,9 +576,8 @@ class HealthAppState extends State { healthDataResponse.sort((a, b) => b.dateTo.compareTo(a.dateTo)); _healthDataList.clear(); - _healthDataList.addAll((healthDataResponse.length < 100) - ? healthDataResponse - : healthDataResponse.sublist(0, 100)); + _healthDataList.addAll( + (healthDataResponse.length < 100) ? healthDataResponse : healthDataResponse.sublist(0, 100)); for (var data in _healthDataList) { debugPrint(toJsonString(data)); @@ -825,8 +824,7 @@ class HealthAppState extends State { return ListTile( title: Text("${p.typeString}: ${p.value}"), trailing: Text(p.unitString), - subtitle: - Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), onTap: () { fetchDataByUUID( context, diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart deleted file mode 100644 index 12f2d3ed8..000000000 --- a/packages/health/lib/health.g.dart +++ /dev/null @@ -1,630 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'health.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -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']), - dateFrom: DateTime.parse(json['dateFrom'] as String), - dateTo: DateTime.parse(json['dateTo'] as String), - sourcePlatform: - $enumDecode(_$HealthPlatformTypeEnumMap, json['sourcePlatform']), - sourceDeviceId: json['sourceDeviceId'] as String, - sourceId: json['sourceId'] as String, - sourceName: json['sourceName'] as String, - recordingMethod: $enumDecodeNullable( - _$RecordingMethodEnumMap, json['recordingMethod']) ?? - RecordingMethod.unknown, - workoutSummary: json['workoutSummary'] == null - ? null - : WorkoutSummary.fromJson( - json['workoutSummary'] as Map), - metadata: json['metadata'] as Map?, - deviceModel: json['deviceModel'] as String?, - ); - -Map _$HealthDataPointToJson(HealthDataPoint instance) => - { - 'uuid': instance.uuid, - 'value': instance.value.toJson(), - 'type': _$HealthDataTypeEnumMap[instance.type]!, - 'unit': _$HealthDataUnitEnumMap[instance.unit]!, - 'dateFrom': instance.dateFrom.toIso8601String(), - 'dateTo': instance.dateTo.toIso8601String(), - 'sourcePlatform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, - 'sourceDeviceId': instance.sourceDeviceId, - 'sourceId': instance.sourceId, - 'sourceName': instance.sourceName, - 'recordingMethod': _$RecordingMethodEnumMap[instance.recordingMethod]!, - if (instance.workoutSummary?.toJson() case final value?) - 'workoutSummary': value, - if (instance.metadata case final value?) 'metadata': value, - if (instance.deviceModel case final value?) 'deviceModel': value, - }; - -const _$HealthDataTypeEnumMap = { - HealthDataType.ACTIVE_ENERGY_BURNED: 'ACTIVE_ENERGY_BURNED', - HealthDataType.ATRIAL_FIBRILLATION_BURDEN: 'ATRIAL_FIBRILLATION_BURDEN', - HealthDataType.APPLE_STAND_HOUR: 'APPLE_STAND_HOUR', - HealthDataType.APPLE_MOVE_TIME: 'APPLE_MOVE_TIME', - HealthDataType.APPLE_STAND_TIME: 'APPLE_STAND_TIME', - HealthDataType.AUDIOGRAM: 'AUDIOGRAM', - HealthDataType.BASAL_ENERGY_BURNED: 'BASAL_ENERGY_BURNED', - HealthDataType.BLOOD_GLUCOSE: 'BLOOD_GLUCOSE', - HealthDataType.BLOOD_OXYGEN: 'BLOOD_OXYGEN', - HealthDataType.BLOOD_PRESSURE_DIASTOLIC: 'BLOOD_PRESSURE_DIASTOLIC', - HealthDataType.BLOOD_PRESSURE_SYSTOLIC: 'BLOOD_PRESSURE_SYSTOLIC', - HealthDataType.BODY_FAT_PERCENTAGE: 'BODY_FAT_PERCENTAGE', - HealthDataType.LEAN_BODY_MASS: 'LEAN_BODY_MASS', - HealthDataType.BODY_MASS_INDEX: 'BODY_MASS_INDEX', - HealthDataType.BODY_TEMPERATURE: 'BODY_TEMPERATURE', - HealthDataType.BODY_WATER_MASS: 'BODY_WATER_MASS', - HealthDataType.DIETARY_CARBS_CONSUMED: 'DIETARY_CARBS_CONSUMED', - HealthDataType.DIETARY_CAFFEINE: 'DIETARY_CAFFEINE', - HealthDataType.DIETARY_ENERGY_CONSUMED: 'DIETARY_ENERGY_CONSUMED', - HealthDataType.DIETARY_FATS_CONSUMED: 'DIETARY_FATS_CONSUMED', - HealthDataType.DIETARY_PROTEIN_CONSUMED: 'DIETARY_PROTEIN_CONSUMED', - HealthDataType.DIETARY_FIBER: 'DIETARY_FIBER', - HealthDataType.DIETARY_SUGAR: 'DIETARY_SUGAR', - HealthDataType.DIETARY_FAT_MONOUNSATURATED: 'DIETARY_FAT_MONOUNSATURATED', - HealthDataType.DIETARY_FAT_POLYUNSATURATED: 'DIETARY_FAT_POLYUNSATURATED', - HealthDataType.DIETARY_FAT_SATURATED: 'DIETARY_FAT_SATURATED', - HealthDataType.DIETARY_CHOLESTEROL: 'DIETARY_CHOLESTEROL', - HealthDataType.DIETARY_VITAMIN_A: 'DIETARY_VITAMIN_A', - HealthDataType.DIETARY_THIAMIN: 'DIETARY_THIAMIN', - HealthDataType.DIETARY_RIBOFLAVIN: 'DIETARY_RIBOFLAVIN', - HealthDataType.DIETARY_NIACIN: 'DIETARY_NIACIN', - HealthDataType.DIETARY_PANTOTHENIC_ACID: 'DIETARY_PANTOTHENIC_ACID', - HealthDataType.DIETARY_VITAMIN_B6: 'DIETARY_VITAMIN_B6', - HealthDataType.DIETARY_BIOTIN: 'DIETARY_BIOTIN', - HealthDataType.DIETARY_VITAMIN_B12: 'DIETARY_VITAMIN_B12', - HealthDataType.DIETARY_VITAMIN_C: 'DIETARY_VITAMIN_C', - HealthDataType.DIETARY_VITAMIN_D: 'DIETARY_VITAMIN_D', - HealthDataType.DIETARY_VITAMIN_E: 'DIETARY_VITAMIN_E', - HealthDataType.DIETARY_VITAMIN_K: 'DIETARY_VITAMIN_K', - HealthDataType.DIETARY_FOLATE: 'DIETARY_FOLATE', - HealthDataType.DIETARY_CALCIUM: 'DIETARY_CALCIUM', - HealthDataType.DIETARY_CHLORIDE: 'DIETARY_CHLORIDE', - HealthDataType.DIETARY_IRON: 'DIETARY_IRON', - HealthDataType.DIETARY_MAGNESIUM: 'DIETARY_MAGNESIUM', - HealthDataType.DIETARY_PHOSPHORUS: 'DIETARY_PHOSPHORUS', - HealthDataType.DIETARY_POTASSIUM: 'DIETARY_POTASSIUM', - HealthDataType.DIETARY_SODIUM: 'DIETARY_SODIUM', - HealthDataType.DIETARY_ZINC: 'DIETARY_ZINC', - HealthDataType.DIETARY_CHROMIUM: 'DIETARY_CHROMIUM', - HealthDataType.DIETARY_COPPER: 'DIETARY_COPPER', - HealthDataType.DIETARY_IODINE: 'DIETARY_IODINE', - HealthDataType.DIETARY_MANGANESE: 'DIETARY_MANGANESE', - HealthDataType.DIETARY_MOLYBDENUM: 'DIETARY_MOLYBDENUM', - HealthDataType.DIETARY_SELENIUM: 'DIETARY_SELENIUM', - HealthDataType.FORCED_EXPIRATORY_VOLUME: 'FORCED_EXPIRATORY_VOLUME', - HealthDataType.HEART_RATE: 'HEART_RATE', - HealthDataType.HEART_RATE_VARIABILITY_SDNN: 'HEART_RATE_VARIABILITY_SDNN', - HealthDataType.HEART_RATE_VARIABILITY_RMSSD: 'HEART_RATE_VARIABILITY_RMSSD', - HealthDataType.HEIGHT: 'HEIGHT', - HealthDataType.INSULIN_DELIVERY: 'INSULIN_DELIVERY', - HealthDataType.RESTING_HEART_RATE: 'RESTING_HEART_RATE', - HealthDataType.RESPIRATORY_RATE: 'RESPIRATORY_RATE', - HealthDataType.PERIPHERAL_PERFUSION_INDEX: 'PERIPHERAL_PERFUSION_INDEX', - HealthDataType.STEPS: 'STEPS', - HealthDataType.WAIST_CIRCUMFERENCE: 'WAIST_CIRCUMFERENCE', - HealthDataType.WALKING_HEART_RATE: 'WALKING_HEART_RATE', - HealthDataType.WEIGHT: 'WEIGHT', - HealthDataType.DISTANCE_WALKING_RUNNING: 'DISTANCE_WALKING_RUNNING', - HealthDataType.DISTANCE_SWIMMING: 'DISTANCE_SWIMMING', - HealthDataType.DISTANCE_CYCLING: 'DISTANCE_CYCLING', - HealthDataType.FLIGHTS_CLIMBED: 'FLIGHTS_CLIMBED', - HealthDataType.DISTANCE_DELTA: 'DISTANCE_DELTA', - HealthDataType.WALKING_SPEED: 'WALKING_SPEED', - HealthDataType.SPEED: 'SPEED', - HealthDataType.MINDFULNESS: 'MINDFULNESS', - HealthDataType.WATER: 'WATER', - HealthDataType.SLEEP_ASLEEP: 'SLEEP_ASLEEP', - HealthDataType.SLEEP_AWAKE_IN_BED: 'SLEEP_AWAKE_IN_BED', - HealthDataType.SLEEP_AWAKE: 'SLEEP_AWAKE', - HealthDataType.SLEEP_DEEP: 'SLEEP_DEEP', - HealthDataType.SLEEP_IN_BED: 'SLEEP_IN_BED', - HealthDataType.SLEEP_LIGHT: 'SLEEP_LIGHT', - HealthDataType.SLEEP_OUT_OF_BED: 'SLEEP_OUT_OF_BED', - HealthDataType.SLEEP_REM: 'SLEEP_REM', - HealthDataType.SLEEP_SESSION: 'SLEEP_SESSION', - HealthDataType.SLEEP_UNKNOWN: 'SLEEP_UNKNOWN', - HealthDataType.EXERCISE_TIME: 'EXERCISE_TIME', - HealthDataType.WORKOUT: 'WORKOUT', - HealthDataType.HEADACHE_NOT_PRESENT: 'HEADACHE_NOT_PRESENT', - HealthDataType.HEADACHE_MILD: 'HEADACHE_MILD', - HealthDataType.HEADACHE_MODERATE: 'HEADACHE_MODERATE', - HealthDataType.HEADACHE_SEVERE: 'HEADACHE_SEVERE', - HealthDataType.HEADACHE_UNSPECIFIED: 'HEADACHE_UNSPECIFIED', - HealthDataType.NUTRITION: 'NUTRITION', - HealthDataType.UV_INDEX: 'UV_INDEX', - HealthDataType.GENDER: 'GENDER', - HealthDataType.BIRTH_DATE: 'BIRTH_DATE', - HealthDataType.BLOOD_TYPE: 'BLOOD_TYPE', - HealthDataType.MENSTRUATION_FLOW: 'MENSTRUATION_FLOW', - HealthDataType.WATER_TEMPERATURE: 'WATER_TEMPERATURE', - HealthDataType.UNDERWATER_DEPTH: 'UNDERWATER_DEPTH', - HealthDataType.HIGH_HEART_RATE_EVENT: 'HIGH_HEART_RATE_EVENT', - HealthDataType.LOW_HEART_RATE_EVENT: 'LOW_HEART_RATE_EVENT', - HealthDataType.IRREGULAR_HEART_RATE_EVENT: 'IRREGULAR_HEART_RATE_EVENT', - HealthDataType.ELECTRODERMAL_ACTIVITY: 'ELECTRODERMAL_ACTIVITY', - HealthDataType.ELECTROCARDIOGRAM: 'ELECTROCARDIOGRAM', - HealthDataType.TOTAL_CALORIES_BURNED: 'TOTAL_CALORIES_BURNED', -}; - -const _$HealthDataUnitEnumMap = { - HealthDataUnit.GRAM: 'GRAM', - HealthDataUnit.KILOGRAM: 'KILOGRAM', - HealthDataUnit.OUNCE: 'OUNCE', - HealthDataUnit.POUND: 'POUND', - HealthDataUnit.STONE: 'STONE', - HealthDataUnit.METER: 'METER', - HealthDataUnit.INCH: 'INCH', - HealthDataUnit.FOOT: 'FOOT', - HealthDataUnit.YARD: 'YARD', - HealthDataUnit.MILE: 'MILE', - HealthDataUnit.LITER: 'LITER', - HealthDataUnit.MILLILITER: 'MILLILITER', - HealthDataUnit.FLUID_OUNCE_US: 'FLUID_OUNCE_US', - HealthDataUnit.FLUID_OUNCE_IMPERIAL: 'FLUID_OUNCE_IMPERIAL', - HealthDataUnit.CUP_US: 'CUP_US', - HealthDataUnit.CUP_IMPERIAL: 'CUP_IMPERIAL', - HealthDataUnit.PINT_US: 'PINT_US', - HealthDataUnit.PINT_IMPERIAL: 'PINT_IMPERIAL', - HealthDataUnit.PASCAL: 'PASCAL', - HealthDataUnit.MILLIMETER_OF_MERCURY: 'MILLIMETER_OF_MERCURY', - HealthDataUnit.INCHES_OF_MERCURY: 'INCHES_OF_MERCURY', - HealthDataUnit.CENTIMETER_OF_WATER: 'CENTIMETER_OF_WATER', - HealthDataUnit.ATMOSPHERE: 'ATMOSPHERE', - HealthDataUnit.DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL: - 'DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL', - HealthDataUnit.SECOND: 'SECOND', - HealthDataUnit.MILLISECOND: 'MILLISECOND', - HealthDataUnit.MINUTE: 'MINUTE', - HealthDataUnit.HOUR: 'HOUR', - HealthDataUnit.DAY: 'DAY', - HealthDataUnit.JOULE: 'JOULE', - HealthDataUnit.KILOCALORIE: 'KILOCALORIE', - HealthDataUnit.LARGE_CALORIE: 'LARGE_CALORIE', - HealthDataUnit.SMALL_CALORIE: 'SMALL_CALORIE', - HealthDataUnit.DEGREE_CELSIUS: 'DEGREE_CELSIUS', - HealthDataUnit.DEGREE_FAHRENHEIT: 'DEGREE_FAHRENHEIT', - HealthDataUnit.KELVIN: 'KELVIN', - HealthDataUnit.DECIBEL_HEARING_LEVEL: 'DECIBEL_HEARING_LEVEL', - HealthDataUnit.HERTZ: 'HERTZ', - HealthDataUnit.SIEMEN: 'SIEMEN', - HealthDataUnit.VOLT: 'VOLT', - HealthDataUnit.INTERNATIONAL_UNIT: 'INTERNATIONAL_UNIT', - HealthDataUnit.COUNT: 'COUNT', - HealthDataUnit.PERCENT: 'PERCENT', - HealthDataUnit.BEATS_PER_MINUTE: 'BEATS_PER_MINUTE', - HealthDataUnit.RESPIRATIONS_PER_MINUTE: 'RESPIRATIONS_PER_MINUTE', - HealthDataUnit.MILLIGRAM_PER_DECILITER: 'MILLIGRAM_PER_DECILITER', - HealthDataUnit.METER_PER_SECOND: 'METER_PER_SECOND', - HealthDataUnit.UNKNOWN_UNIT: 'UNKNOWN_UNIT', - HealthDataUnit.NO_UNIT: 'NO_UNIT', -}; - -const _$HealthPlatformTypeEnumMap = { - HealthPlatformType.appleHealth: 'appleHealth', - HealthPlatformType.googleHealthConnect: 'googleHealthConnect', -}; - -const _$RecordingMethodEnumMap = { - RecordingMethod.unknown: 'unknown', - RecordingMethod.active: 'active', - RecordingMethod.automatic: 'automatic', - RecordingMethod.manual: 'manual', -}; - -HealthValue _$HealthValueFromJson(Map json) => - HealthValue()..$type = json['__type'] as String?; - -Map _$HealthValueToJson(HealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - }; - -NumericHealthValue _$NumericHealthValueFromJson(Map json) => - NumericHealthValue( - numericValue: json['numericValue'] as num, - )..$type = json['__type'] as String?; - -Map _$NumericHealthValueToJson(NumericHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'numericValue': instance.numericValue, - }; - -AudiogramHealthValue _$AudiogramHealthValueFromJson( - Map json) => - AudiogramHealthValue( - frequencies: - (json['frequencies'] as List).map((e) => e as num).toList(), - leftEarSensitivities: (json['leftEarSensitivities'] as List) - .map((e) => e as num) - .toList(), - rightEarSensitivities: (json['rightEarSensitivities'] as List) - .map((e) => e as num) - .toList(), - )..$type = json['__type'] as String?; - -Map _$AudiogramHealthValueToJson( - AudiogramHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'frequencies': instance.frequencies, - 'leftEarSensitivities': instance.leftEarSensitivities, - 'rightEarSensitivities': instance.rightEarSensitivities, - }; - -WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) => - WorkoutHealthValue( - workoutActivityType: $enumDecode( - _$HealthWorkoutActivityTypeEnumMap, json['workoutActivityType']), - totalEnergyBurned: (json['totalEnergyBurned'] as num?)?.toInt(), - totalEnergyBurnedUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['totalEnergyBurnedUnit']), - totalDistance: (json['totalDistance'] as num?)?.toInt(), - totalDistanceUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['totalDistanceUnit']), - totalSteps: (json['totalSteps'] as num?)?.toInt(), - totalStepsUnit: - $enumDecodeNullable(_$HealthDataUnitEnumMap, json['totalStepsUnit']), - )..$type = json['__type'] as String?; - -Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'workoutActivityType': - _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!, - if (instance.totalEnergyBurned case final value?) - 'totalEnergyBurned': value, - if (_$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit] - case final value?) - 'totalEnergyBurnedUnit': value, - if (instance.totalDistance case final value?) 'totalDistance': value, - if (_$HealthDataUnitEnumMap[instance.totalDistanceUnit] case final value?) - 'totalDistanceUnit': value, - if (instance.totalSteps case final value?) 'totalSteps': value, - if (_$HealthDataUnitEnumMap[instance.totalStepsUnit] case final value?) - 'totalStepsUnit': value, - }; - -const _$HealthWorkoutActivityTypeEnumMap = { - HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', - HealthWorkoutActivityType.ARCHERY: 'ARCHERY', - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', - HealthWorkoutActivityType.BADMINTON: 'BADMINTON', - HealthWorkoutActivityType.BASEBALL: 'BASEBALL', - HealthWorkoutActivityType.BASKETBALL: 'BASKETBALL', - HealthWorkoutActivityType.BIKING: 'BIKING', - HealthWorkoutActivityType.BOXING: 'BOXING', - HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', - 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.GOLF: 'GOLF', - HealthWorkoutActivityType.GYMNASTICS: 'GYMNASTICS', - HealthWorkoutActivityType.HANDBALL: 'HANDBALL', - HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING: - 'HIGH_INTENSITY_INTERVAL_TRAINING', - HealthWorkoutActivityType.HIKING: 'HIKING', - HealthWorkoutActivityType.HOCKEY: 'HOCKEY', - HealthWorkoutActivityType.JUMP_ROPE: 'JUMP_ROPE', - HealthWorkoutActivityType.KICKBOXING: 'KICKBOXING', - HealthWorkoutActivityType.MARTIAL_ARTS: 'MARTIAL_ARTS', - HealthWorkoutActivityType.PILATES: 'PILATES', - HealthWorkoutActivityType.RACQUETBALL: 'RACQUETBALL', - HealthWorkoutActivityType.ROWING: 'ROWING', - HealthWorkoutActivityType.RUGBY: 'RUGBY', - HealthWorkoutActivityType.RUNNING: 'RUNNING', - HealthWorkoutActivityType.SAILING: 'SAILING', - HealthWorkoutActivityType.SKATING: 'SKATING', - HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', - HealthWorkoutActivityType.SOCCER: 'SOCCER', - HealthWorkoutActivityType.SOFTBALL: 'SOFTBALL', - HealthWorkoutActivityType.SQUASH: 'SQUASH', - HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', - HealthWorkoutActivityType.SWIMMING: 'SWIMMING', - HealthWorkoutActivityType.TABLE_TENNIS: 'TABLE_TENNIS', - HealthWorkoutActivityType.TENNIS: 'TENNIS', - HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', - HealthWorkoutActivityType.WALKING: 'WALKING', - HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', - HealthWorkoutActivityType.YOGA: 'YOGA', - HealthWorkoutActivityType.BARRE: 'BARRE', - HealthWorkoutActivityType.BOWLING: 'BOWLING', - HealthWorkoutActivityType.CLIMBING: 'CLIMBING', - HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', - HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', - HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', - HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', - HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', - HealthWorkoutActivityType.FISHING: 'FISHING', - HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', - HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', - HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: - 'FUNCTIONAL_STRENGTH_TRAINING', - 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.PLAY: 'PLAY', - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: - 'PREPARATION_AND_RECOVERY', - HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', - HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', - HealthWorkoutActivityType.STAIRS: 'STAIRS', - HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', - HealthWorkoutActivityType.SURFING: 'SURFING', - 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.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', - HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', - HealthWorkoutActivityType.WRESTLING: 'WRESTLING', - HealthWorkoutActivityType.UNDERWATER_DIVING: 'UNDERWATER_DIVING', - HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', - HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', - HealthWorkoutActivityType.DANCING: 'DANCING', - HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', - HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', - HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', - HealthWorkoutActivityType.PARAGLIDING: 'PARAGLIDING', - HealthWorkoutActivityType.ROCK_CLIMBING: 'ROCK_CLIMBING', - HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', - HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', - HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', - HealthWorkoutActivityType.SKIING: 'SKIING', - HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', - HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', - HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', - HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', - HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', - HealthWorkoutActivityType.WALKING_TREADMILL: 'WALKING_TREADMILL', - HealthWorkoutActivityType.WEIGHTLIFTING: 'WEIGHTLIFTING', - HealthWorkoutActivityType.WHEELCHAIR: 'WHEELCHAIR', - HealthWorkoutActivityType.OTHER: 'OTHER', -}; - -ElectrocardiogramHealthValue _$ElectrocardiogramHealthValueFromJson( - Map json) => - ElectrocardiogramHealthValue( - voltageValues: (json['voltageValues'] as List) - .map((e) => - ElectrocardiogramVoltageValue.fromJson(e as Map)) - .toList(), - averageHeartRate: json['averageHeartRate'] as num?, - samplingFrequency: (json['samplingFrequency'] as num?)?.toDouble(), - classification: $enumDecodeNullable( - _$ElectrocardiogramClassificationEnumMap, json['classification']), - )..$type = json['__type'] as String?; - -Map _$ElectrocardiogramHealthValueToJson( - ElectrocardiogramHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'voltageValues': instance.voltageValues.map((e) => e.toJson()).toList(), - if (instance.averageHeartRate case final value?) - 'averageHeartRate': value, - if (instance.samplingFrequency case final value?) - 'samplingFrequency': value, - if (_$ElectrocardiogramClassificationEnumMap[instance.classification] - case final value?) - 'classification': value, - }; - -const _$ElectrocardiogramClassificationEnumMap = { - ElectrocardiogramClassification.NOT_SET: 'NOT_SET', - ElectrocardiogramClassification.SINUS_RHYTHM: 'SINUS_RHYTHM', - ElectrocardiogramClassification.ATRIAL_FIBRILLATION: 'ATRIAL_FIBRILLATION', - ElectrocardiogramClassification.INCONCLUSIVE_LOW_HEART_RATE: - 'INCONCLUSIVE_LOW_HEART_RATE', - ElectrocardiogramClassification.INCONCLUSIVE_HIGH_HEART_RATE: - 'INCONCLUSIVE_HIGH_HEART_RATE', - ElectrocardiogramClassification.INCONCLUSIVE_POOR_READING: - 'INCONCLUSIVE_POOR_READING', - ElectrocardiogramClassification.INCONCLUSIVE_OTHER: 'INCONCLUSIVE_OTHER', - ElectrocardiogramClassification.UNRECOGNIZED: 'UNRECOGNIZED', -}; - -ElectrocardiogramVoltageValue _$ElectrocardiogramVoltageValueFromJson( - Map json) => - ElectrocardiogramVoltageValue( - voltage: json['voltage'] as num, - timeSinceSampleStart: json['timeSinceSampleStart'] as num, - )..$type = json['__type'] as String?; - -Map _$ElectrocardiogramVoltageValueToJson( - ElectrocardiogramVoltageValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'voltage': instance.voltage, - 'timeSinceSampleStart': instance.timeSinceSampleStart, - }; - -InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson( - Map json) => - InsulinDeliveryHealthValue( - units: (json['units'] as num).toDouble(), - reason: $enumDecode(_$InsulinDeliveryReasonEnumMap, json['reason']), - )..$type = json['__type'] as String?; - -Map _$InsulinDeliveryHealthValueToJson( - InsulinDeliveryHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'units': instance.units, - 'reason': _$InsulinDeliveryReasonEnumMap[instance.reason]!, - }; - -const _$InsulinDeliveryReasonEnumMap = { - InsulinDeliveryReason.NOT_SET: 'NOT_SET', - InsulinDeliveryReason.BASAL: 'BASAL', - InsulinDeliveryReason.BOLUS: 'BOLUS', -}; - -NutritionHealthValue _$NutritionHealthValueFromJson( - Map json) => - NutritionHealthValue( - name: json['name'] as String?, - mealType: json['meal_type'] as String?, - calories: (json['calories'] as num?)?.toDouble(), - protein: (json['protein'] as num?)?.toDouble(), - fat: (json['fat'] as num?)?.toDouble(), - carbs: (json['carbs'] as num?)?.toDouble(), - caffeine: (json['caffeine'] as num?)?.toDouble(), - vitaminA: (json['vitamin_a'] as num?)?.toDouble(), - b1Thiamine: (json['b1_thiamine'] as num?)?.toDouble(), - b2Riboflavin: (json['b2_riboflavin'] as num?)?.toDouble(), - b3Niacin: (json['b3_niacin'] as num?)?.toDouble(), - b5PantothenicAcid: (json['b5_pantothenic_acid'] as num?)?.toDouble(), - b6Pyridoxine: (json['b6_pyridoxine'] as num?)?.toDouble(), - b7Biotin: (json['b7_biotin'] as num?)?.toDouble(), - b9Folate: (json['b9_folate'] as num?)?.toDouble(), - b12Cobalamin: (json['b12_cobalamin'] as num?)?.toDouble(), - vitaminC: (json['vitamin_c'] as num?)?.toDouble(), - vitaminD: (json['vitamin_d'] as num?)?.toDouble(), - vitaminE: (json['vitamin_e'] as num?)?.toDouble(), - vitaminK: (json['vitamin_k'] as num?)?.toDouble(), - calcium: (json['calcium'] as num?)?.toDouble(), - chloride: (json['chloride'] as num?)?.toDouble(), - cholesterol: (json['cholesterol'] as num?)?.toDouble(), - choline: (json['choline'] as num?)?.toDouble(), - chromium: (json['chromium'] as num?)?.toDouble(), - copper: (json['copper'] as num?)?.toDouble(), - fatUnsaturated: (json['fat_unsaturated'] as num?)?.toDouble(), - fatMonounsaturated: (json['fat_monounsaturated'] as num?)?.toDouble(), - fatPolyunsaturated: (json['fat_polyunsaturated'] as num?)?.toDouble(), - fatSaturated: (json['fat_saturated'] as num?)?.toDouble(), - fatTransMonoenoic: (json['fat_trans_monoenoic'] as num?)?.toDouble(), - fiber: (json['fiber'] as num?)?.toDouble(), - iodine: (json['iodine'] as num?)?.toDouble(), - iron: (json['iron'] as num?)?.toDouble(), - magnesium: (json['magnesium'] as num?)?.toDouble(), - manganese: (json['manganese'] as num?)?.toDouble(), - molybdenum: (json['molybdenum'] as num?)?.toDouble(), - phosphorus: (json['phosphorus'] as num?)?.toDouble(), - potassium: (json['potassium'] as num?)?.toDouble(), - selenium: (json['selenium'] as num?)?.toDouble(), - sodium: (json['sodium'] as num?)?.toDouble(), - sugar: (json['sugar'] as num?)?.toDouble(), - water: (json['water'] as num?)?.toDouble(), - zinc: (json['zinc'] as num?)?.toDouble(), - )..$type = json['__type'] as String?; - -Map _$NutritionHealthValueToJson( - NutritionHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - if (instance.name case final value?) 'name': value, - if (instance.mealType case final value?) 'meal_type': value, - if (instance.calories case final value?) 'calories': value, - if (instance.protein case final value?) 'protein': value, - if (instance.fat case final value?) 'fat': value, - if (instance.carbs case final value?) 'carbs': value, - if (instance.caffeine case final value?) 'caffeine': value, - if (instance.vitaminA case final value?) 'vitamin_a': value, - if (instance.b1Thiamine case final value?) 'b1_thiamine': value, - if (instance.b2Riboflavin case final value?) 'b2_riboflavin': value, - if (instance.b3Niacin case final value?) 'b3_niacin': value, - if (instance.b5PantothenicAcid case final value?) - 'b5_pantothenic_acid': value, - if (instance.b6Pyridoxine case final value?) 'b6_pyridoxine': value, - if (instance.b7Biotin case final value?) 'b7_biotin': value, - if (instance.b9Folate case final value?) 'b9_folate': value, - if (instance.b12Cobalamin case final value?) 'b12_cobalamin': value, - if (instance.vitaminC case final value?) 'vitamin_c': value, - if (instance.vitaminD case final value?) 'vitamin_d': value, - if (instance.vitaminE case final value?) 'vitamin_e': value, - if (instance.vitaminK case final value?) 'vitamin_k': value, - if (instance.calcium case final value?) 'calcium': value, - if (instance.chloride case final value?) 'chloride': value, - if (instance.cholesterol case final value?) 'cholesterol': value, - if (instance.choline case final value?) 'choline': value, - if (instance.chromium case final value?) 'chromium': value, - if (instance.copper case final value?) 'copper': value, - if (instance.fatUnsaturated case final value?) 'fat_unsaturated': value, - if (instance.fatMonounsaturated case final value?) - 'fat_monounsaturated': value, - if (instance.fatPolyunsaturated case final value?) - 'fat_polyunsaturated': value, - if (instance.fatSaturated case final value?) 'fat_saturated': value, - if (instance.fatTransMonoenoic case final value?) - 'fat_trans_monoenoic': value, - if (instance.fiber case final value?) 'fiber': value, - if (instance.iodine case final value?) 'iodine': value, - if (instance.iron case final value?) 'iron': value, - if (instance.magnesium case final value?) 'magnesium': value, - if (instance.manganese case final value?) 'manganese': value, - if (instance.molybdenum case final value?) 'molybdenum': value, - if (instance.phosphorus case final value?) 'phosphorus': value, - if (instance.potassium case final value?) 'potassium': value, - if (instance.selenium case final value?) 'selenium': value, - if (instance.sodium case final value?) 'sodium': value, - if (instance.sugar case final value?) 'sugar': value, - if (instance.water case final value?) 'water': value, - if (instance.zinc case final value?) 'zinc': value, - }; - -MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson( - Map json) => - MenstruationFlowHealthValue( - flow: $enumDecodeNullable(_$MenstrualFlowEnumMap, json['flow']), - dateTime: DateTime.parse(json['dateTime'] as String), - isStartOfCycle: json['isStartOfCycle'] as bool?, - wasUserEntered: json['wasUserEntered'] as bool?, - )..$type = json['__type'] as String?; - -Map _$MenstruationFlowHealthValueToJson( - MenstruationFlowHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - if (_$MenstrualFlowEnumMap[instance.flow] case final value?) - 'flow': value, - if (instance.isStartOfCycle case final value?) 'isStartOfCycle': value, - if (instance.wasUserEntered case final value?) 'wasUserEntered': value, - 'dateTime': instance.dateTime.toIso8601String(), - }; - -const _$MenstrualFlowEnumMap = { - MenstrualFlow.unspecified: 'unspecified', - MenstrualFlow.none: 'none', - MenstrualFlow.light: 'light', - MenstrualFlow.medium: 'medium', - MenstrualFlow.heavy: 'heavy', - MenstrualFlow.spotting: 'spotting', -}; - -WorkoutSummary _$WorkoutSummaryFromJson(Map json) => - WorkoutSummary( - workoutType: json['workoutType'] as String, - totalDistance: json['totalDistance'] as num, - totalEnergyBurned: json['totalEnergyBurned'] as num, - totalSteps: json['totalSteps'] as num, - ); - -Map _$WorkoutSummaryToJson(WorkoutSummary instance) => - { - 'workoutType': instance.workoutType, - 'totalDistance': instance.totalDistance, - 'totalEnergyBurned': instance.totalEnergyBurned, - 'totalSteps': instance.totalSteps, - }; diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index d92671d25..36672eb4e 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -274,8 +274,8 @@ class Health { if (Platform.isIOS) return false; try { - final status = await _channel - .invokeMethod('isHealthDataInBackgroundAvailable'); + final status = + await _channel.invokeMethod('isHealthDataInBackgroundAvailable'); return status ?? false; } catch (e) { debugPrint( @@ -295,8 +295,8 @@ class Health { if (Platform.isIOS) return true; try { - final status = await _channel - .invokeMethod('isHealthDataInBackgroundAuthorized'); + final status = + await _channel.invokeMethod('isHealthDataInBackgroundAuthorized'); return status ?? false; } catch (e) { debugPrint( @@ -318,8 +318,8 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); try { - final bool? isAuthorized = await _channel - .invokeMethod('requestHealthDataInBackgroundAuthorization'); + final bool? isAuthorized = + await _channel.invokeMethod('requestHealthDataInBackgroundAuthorization'); return isAuthorized ?? false; } catch (e) { debugPrint( @@ -592,8 +592,7 @@ class Health { } if (Platform.isIOS && type == null) { - throw ArgumentError( - "On iOS, both UUID and type are required to delete a record."); + throw ArgumentError("On iOS, both UUID and type are required to delete a record."); } Map args = { diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index cbcd717e4..c86b2a5e8 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -10,8 +10,8 @@ environment: dependencies: flutter: sdk: flutter - intl: ">=0.18.0 <0.21.0" - device_info_plus: ">=9.0.0 <12.0.0" + intl: '>=0.18.0 <0.21.0' + device_info_plus: '>=9.0.0 <12.0.0' json_annotation: ^4.8.0 carp_serializable: ^2.0.0 # polymorphic json serialization From 4f05ae900179d89ac5b81e5f3fdbef774349e547 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 30 Aug 2025 16:01:56 +0300 Subject: [PATCH 18/25] Update to SDK 3.8.0 for health.g.dart --- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 69 +- packages/health/lib/health.g.dart | 629 ++++++++++++++++++ packages/health/pubspec.yaml | 2 +- 4 files changed, 642 insertions(+), 60 deletions(-) create mode 100644 packages/health/lib/health.g.dart diff --git a/packages/health/example/ios/Flutter/AppFrameworkInfo.plist b/packages/health/example/ios/Flutter/AppFrameworkInfo.plist index 7c5696400..1dc6cf765 100644 --- a/packages/health/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/health/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index 42b4de456..da93a7ca4 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ 66EFA595EFC367CCF62B5486 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7CF05C63DD5841073CB4E39B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -112,6 +113,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -249,6 +251,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ABB05D852D6BB16700FA4740 /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; @@ -368,61 +371,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - C5B1177E499C542655C8E54F /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - CF9A537E347C1C3E9A947F1C /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; }; F39DD7443B254A13543A9E9D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -539,7 +488,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -670,7 +619,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -721,7 +670,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -815,6 +764,10 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; ABB05D852D6BB16700FA4740 /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart new file mode 100644 index 000000000..d12054286 --- /dev/null +++ b/packages/health/lib/health.g.dart @@ -0,0 +1,629 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'health.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +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']), + dateFrom: DateTime.parse(json['dateFrom'] as String), + dateTo: DateTime.parse(json['dateTo'] as String), + sourcePlatform: $enumDecode( + _$HealthPlatformTypeEnumMap, + json['sourcePlatform'], + ), + sourceDeviceId: json['sourceDeviceId'] as String, + sourceId: json['sourceId'] as String, + sourceName: json['sourceName'] as String, + recordingMethod: + $enumDecodeNullable(_$RecordingMethodEnumMap, json['recordingMethod']) ?? + RecordingMethod.unknown, + workoutSummary: json['workoutSummary'] == null + ? null + : WorkoutSummary.fromJson(json['workoutSummary'] as Map), + metadata: json['metadata'] as Map?, + deviceModel: json['deviceModel'] as String?, +); + +Map _$HealthDataPointToJson(HealthDataPoint instance) => + { + 'uuid': instance.uuid, + 'value': instance.value.toJson(), + 'type': _$HealthDataTypeEnumMap[instance.type]!, + 'unit': _$HealthDataUnitEnumMap[instance.unit]!, + 'dateFrom': instance.dateFrom.toIso8601String(), + 'dateTo': instance.dateTo.toIso8601String(), + 'sourcePlatform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, + 'sourceDeviceId': instance.sourceDeviceId, + 'sourceId': instance.sourceId, + 'sourceName': instance.sourceName, + 'recordingMethod': _$RecordingMethodEnumMap[instance.recordingMethod]!, + 'workoutSummary': ?instance.workoutSummary?.toJson(), + 'metadata': ?instance.metadata, + 'deviceModel': ?instance.deviceModel, + }; + +const _$HealthDataTypeEnumMap = { + HealthDataType.ACTIVE_ENERGY_BURNED: 'ACTIVE_ENERGY_BURNED', + HealthDataType.ATRIAL_FIBRILLATION_BURDEN: 'ATRIAL_FIBRILLATION_BURDEN', + HealthDataType.APPLE_STAND_HOUR: 'APPLE_STAND_HOUR', + HealthDataType.APPLE_MOVE_TIME: 'APPLE_MOVE_TIME', + HealthDataType.APPLE_STAND_TIME: 'APPLE_STAND_TIME', + HealthDataType.AUDIOGRAM: 'AUDIOGRAM', + HealthDataType.BASAL_ENERGY_BURNED: 'BASAL_ENERGY_BURNED', + HealthDataType.BLOOD_GLUCOSE: 'BLOOD_GLUCOSE', + HealthDataType.BLOOD_OXYGEN: 'BLOOD_OXYGEN', + HealthDataType.BLOOD_PRESSURE_DIASTOLIC: 'BLOOD_PRESSURE_DIASTOLIC', + HealthDataType.BLOOD_PRESSURE_SYSTOLIC: 'BLOOD_PRESSURE_SYSTOLIC', + HealthDataType.BODY_FAT_PERCENTAGE: 'BODY_FAT_PERCENTAGE', + HealthDataType.LEAN_BODY_MASS: 'LEAN_BODY_MASS', + HealthDataType.BODY_MASS_INDEX: 'BODY_MASS_INDEX', + HealthDataType.BODY_TEMPERATURE: 'BODY_TEMPERATURE', + HealthDataType.BODY_WATER_MASS: 'BODY_WATER_MASS', + HealthDataType.DIETARY_CARBS_CONSUMED: 'DIETARY_CARBS_CONSUMED', + HealthDataType.DIETARY_CAFFEINE: 'DIETARY_CAFFEINE', + HealthDataType.DIETARY_ENERGY_CONSUMED: 'DIETARY_ENERGY_CONSUMED', + HealthDataType.DIETARY_FATS_CONSUMED: 'DIETARY_FATS_CONSUMED', + HealthDataType.DIETARY_PROTEIN_CONSUMED: 'DIETARY_PROTEIN_CONSUMED', + HealthDataType.DIETARY_FIBER: 'DIETARY_FIBER', + HealthDataType.DIETARY_SUGAR: 'DIETARY_SUGAR', + HealthDataType.DIETARY_FAT_MONOUNSATURATED: 'DIETARY_FAT_MONOUNSATURATED', + HealthDataType.DIETARY_FAT_POLYUNSATURATED: 'DIETARY_FAT_POLYUNSATURATED', + HealthDataType.DIETARY_FAT_SATURATED: 'DIETARY_FAT_SATURATED', + HealthDataType.DIETARY_CHOLESTEROL: 'DIETARY_CHOLESTEROL', + HealthDataType.DIETARY_VITAMIN_A: 'DIETARY_VITAMIN_A', + HealthDataType.DIETARY_THIAMIN: 'DIETARY_THIAMIN', + HealthDataType.DIETARY_RIBOFLAVIN: 'DIETARY_RIBOFLAVIN', + HealthDataType.DIETARY_NIACIN: 'DIETARY_NIACIN', + HealthDataType.DIETARY_PANTOTHENIC_ACID: 'DIETARY_PANTOTHENIC_ACID', + HealthDataType.DIETARY_VITAMIN_B6: 'DIETARY_VITAMIN_B6', + HealthDataType.DIETARY_BIOTIN: 'DIETARY_BIOTIN', + HealthDataType.DIETARY_VITAMIN_B12: 'DIETARY_VITAMIN_B12', + HealthDataType.DIETARY_VITAMIN_C: 'DIETARY_VITAMIN_C', + HealthDataType.DIETARY_VITAMIN_D: 'DIETARY_VITAMIN_D', + HealthDataType.DIETARY_VITAMIN_E: 'DIETARY_VITAMIN_E', + HealthDataType.DIETARY_VITAMIN_K: 'DIETARY_VITAMIN_K', + HealthDataType.DIETARY_FOLATE: 'DIETARY_FOLATE', + HealthDataType.DIETARY_CALCIUM: 'DIETARY_CALCIUM', + HealthDataType.DIETARY_CHLORIDE: 'DIETARY_CHLORIDE', + HealthDataType.DIETARY_IRON: 'DIETARY_IRON', + HealthDataType.DIETARY_MAGNESIUM: 'DIETARY_MAGNESIUM', + HealthDataType.DIETARY_PHOSPHORUS: 'DIETARY_PHOSPHORUS', + HealthDataType.DIETARY_POTASSIUM: 'DIETARY_POTASSIUM', + HealthDataType.DIETARY_SODIUM: 'DIETARY_SODIUM', + HealthDataType.DIETARY_ZINC: 'DIETARY_ZINC', + HealthDataType.DIETARY_CHROMIUM: 'DIETARY_CHROMIUM', + HealthDataType.DIETARY_COPPER: 'DIETARY_COPPER', + HealthDataType.DIETARY_IODINE: 'DIETARY_IODINE', + HealthDataType.DIETARY_MANGANESE: 'DIETARY_MANGANESE', + HealthDataType.DIETARY_MOLYBDENUM: 'DIETARY_MOLYBDENUM', + HealthDataType.DIETARY_SELENIUM: 'DIETARY_SELENIUM', + HealthDataType.FORCED_EXPIRATORY_VOLUME: 'FORCED_EXPIRATORY_VOLUME', + HealthDataType.HEART_RATE: 'HEART_RATE', + HealthDataType.HEART_RATE_VARIABILITY_SDNN: 'HEART_RATE_VARIABILITY_SDNN', + HealthDataType.HEART_RATE_VARIABILITY_RMSSD: 'HEART_RATE_VARIABILITY_RMSSD', + HealthDataType.HEIGHT: 'HEIGHT', + HealthDataType.INSULIN_DELIVERY: 'INSULIN_DELIVERY', + HealthDataType.RESTING_HEART_RATE: 'RESTING_HEART_RATE', + HealthDataType.RESPIRATORY_RATE: 'RESPIRATORY_RATE', + HealthDataType.PERIPHERAL_PERFUSION_INDEX: 'PERIPHERAL_PERFUSION_INDEX', + HealthDataType.STEPS: 'STEPS', + HealthDataType.WAIST_CIRCUMFERENCE: 'WAIST_CIRCUMFERENCE', + HealthDataType.WALKING_HEART_RATE: 'WALKING_HEART_RATE', + HealthDataType.WEIGHT: 'WEIGHT', + HealthDataType.DISTANCE_WALKING_RUNNING: 'DISTANCE_WALKING_RUNNING', + HealthDataType.DISTANCE_SWIMMING: 'DISTANCE_SWIMMING', + HealthDataType.DISTANCE_CYCLING: 'DISTANCE_CYCLING', + HealthDataType.FLIGHTS_CLIMBED: 'FLIGHTS_CLIMBED', + HealthDataType.DISTANCE_DELTA: 'DISTANCE_DELTA', + HealthDataType.WALKING_SPEED: 'WALKING_SPEED', + HealthDataType.SPEED: 'SPEED', + HealthDataType.MINDFULNESS: 'MINDFULNESS', + HealthDataType.WATER: 'WATER', + HealthDataType.SLEEP_ASLEEP: 'SLEEP_ASLEEP', + HealthDataType.SLEEP_AWAKE_IN_BED: 'SLEEP_AWAKE_IN_BED', + HealthDataType.SLEEP_AWAKE: 'SLEEP_AWAKE', + HealthDataType.SLEEP_DEEP: 'SLEEP_DEEP', + HealthDataType.SLEEP_IN_BED: 'SLEEP_IN_BED', + HealthDataType.SLEEP_LIGHT: 'SLEEP_LIGHT', + HealthDataType.SLEEP_OUT_OF_BED: 'SLEEP_OUT_OF_BED', + HealthDataType.SLEEP_REM: 'SLEEP_REM', + HealthDataType.SLEEP_SESSION: 'SLEEP_SESSION', + HealthDataType.SLEEP_UNKNOWN: 'SLEEP_UNKNOWN', + HealthDataType.EXERCISE_TIME: 'EXERCISE_TIME', + HealthDataType.WORKOUT: 'WORKOUT', + HealthDataType.HEADACHE_NOT_PRESENT: 'HEADACHE_NOT_PRESENT', + HealthDataType.HEADACHE_MILD: 'HEADACHE_MILD', + HealthDataType.HEADACHE_MODERATE: 'HEADACHE_MODERATE', + HealthDataType.HEADACHE_SEVERE: 'HEADACHE_SEVERE', + HealthDataType.HEADACHE_UNSPECIFIED: 'HEADACHE_UNSPECIFIED', + HealthDataType.NUTRITION: 'NUTRITION', + HealthDataType.UV_INDEX: 'UV_INDEX', + HealthDataType.GENDER: 'GENDER', + HealthDataType.BIRTH_DATE: 'BIRTH_DATE', + HealthDataType.BLOOD_TYPE: 'BLOOD_TYPE', + HealthDataType.MENSTRUATION_FLOW: 'MENSTRUATION_FLOW', + HealthDataType.WATER_TEMPERATURE: 'WATER_TEMPERATURE', + HealthDataType.UNDERWATER_DEPTH: 'UNDERWATER_DEPTH', + HealthDataType.HIGH_HEART_RATE_EVENT: 'HIGH_HEART_RATE_EVENT', + HealthDataType.LOW_HEART_RATE_EVENT: 'LOW_HEART_RATE_EVENT', + HealthDataType.IRREGULAR_HEART_RATE_EVENT: 'IRREGULAR_HEART_RATE_EVENT', + HealthDataType.ELECTRODERMAL_ACTIVITY: 'ELECTRODERMAL_ACTIVITY', + HealthDataType.ELECTROCARDIOGRAM: 'ELECTROCARDIOGRAM', + HealthDataType.TOTAL_CALORIES_BURNED: 'TOTAL_CALORIES_BURNED', +}; + +const _$HealthDataUnitEnumMap = { + HealthDataUnit.GRAM: 'GRAM', + HealthDataUnit.KILOGRAM: 'KILOGRAM', + HealthDataUnit.OUNCE: 'OUNCE', + HealthDataUnit.POUND: 'POUND', + HealthDataUnit.STONE: 'STONE', + HealthDataUnit.METER: 'METER', + HealthDataUnit.INCH: 'INCH', + HealthDataUnit.FOOT: 'FOOT', + HealthDataUnit.YARD: 'YARD', + HealthDataUnit.MILE: 'MILE', + HealthDataUnit.LITER: 'LITER', + HealthDataUnit.MILLILITER: 'MILLILITER', + HealthDataUnit.FLUID_OUNCE_US: 'FLUID_OUNCE_US', + HealthDataUnit.FLUID_OUNCE_IMPERIAL: 'FLUID_OUNCE_IMPERIAL', + HealthDataUnit.CUP_US: 'CUP_US', + HealthDataUnit.CUP_IMPERIAL: 'CUP_IMPERIAL', + HealthDataUnit.PINT_US: 'PINT_US', + HealthDataUnit.PINT_IMPERIAL: 'PINT_IMPERIAL', + HealthDataUnit.PASCAL: 'PASCAL', + HealthDataUnit.MILLIMETER_OF_MERCURY: 'MILLIMETER_OF_MERCURY', + HealthDataUnit.INCHES_OF_MERCURY: 'INCHES_OF_MERCURY', + HealthDataUnit.CENTIMETER_OF_WATER: 'CENTIMETER_OF_WATER', + HealthDataUnit.ATMOSPHERE: 'ATMOSPHERE', + HealthDataUnit.DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL: + 'DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL', + HealthDataUnit.SECOND: 'SECOND', + HealthDataUnit.MILLISECOND: 'MILLISECOND', + HealthDataUnit.MINUTE: 'MINUTE', + HealthDataUnit.HOUR: 'HOUR', + HealthDataUnit.DAY: 'DAY', + HealthDataUnit.JOULE: 'JOULE', + HealthDataUnit.KILOCALORIE: 'KILOCALORIE', + HealthDataUnit.LARGE_CALORIE: 'LARGE_CALORIE', + HealthDataUnit.SMALL_CALORIE: 'SMALL_CALORIE', + HealthDataUnit.DEGREE_CELSIUS: 'DEGREE_CELSIUS', + HealthDataUnit.DEGREE_FAHRENHEIT: 'DEGREE_FAHRENHEIT', + HealthDataUnit.KELVIN: 'KELVIN', + HealthDataUnit.DECIBEL_HEARING_LEVEL: 'DECIBEL_HEARING_LEVEL', + HealthDataUnit.HERTZ: 'HERTZ', + HealthDataUnit.SIEMEN: 'SIEMEN', + HealthDataUnit.VOLT: 'VOLT', + HealthDataUnit.INTERNATIONAL_UNIT: 'INTERNATIONAL_UNIT', + HealthDataUnit.COUNT: 'COUNT', + HealthDataUnit.PERCENT: 'PERCENT', + HealthDataUnit.BEATS_PER_MINUTE: 'BEATS_PER_MINUTE', + HealthDataUnit.RESPIRATIONS_PER_MINUTE: 'RESPIRATIONS_PER_MINUTE', + HealthDataUnit.MILLIGRAM_PER_DECILITER: 'MILLIGRAM_PER_DECILITER', + HealthDataUnit.METER_PER_SECOND: 'METER_PER_SECOND', + HealthDataUnit.UNKNOWN_UNIT: 'UNKNOWN_UNIT', + HealthDataUnit.NO_UNIT: 'NO_UNIT', +}; + +const _$HealthPlatformTypeEnumMap = { + HealthPlatformType.appleHealth: 'appleHealth', + HealthPlatformType.googleHealthConnect: 'googleHealthConnect', +}; + +const _$RecordingMethodEnumMap = { + RecordingMethod.unknown: 'unknown', + RecordingMethod.active: 'active', + RecordingMethod.automatic: 'automatic', + RecordingMethod.manual: 'manual', +}; + +HealthValue _$HealthValueFromJson(Map json) => + HealthValue()..$type = json['__type'] as String?; + +Map _$HealthValueToJson(HealthValue instance) => + {'__type': ?instance.$type}; + +NumericHealthValue _$NumericHealthValueFromJson(Map json) => + NumericHealthValue(numericValue: json['numericValue'] as num) + ..$type = json['__type'] as String?; + +Map _$NumericHealthValueToJson(NumericHealthValue instance) => + { + '__type': ?instance.$type, + 'numericValue': instance.numericValue, + }; + +AudiogramHealthValue _$AudiogramHealthValueFromJson( + Map json, +) => AudiogramHealthValue( + frequencies: (json['frequencies'] as List) + .map((e) => e as num) + .toList(), + leftEarSensitivities: (json['leftEarSensitivities'] as List) + .map((e) => e as num) + .toList(), + rightEarSensitivities: (json['rightEarSensitivities'] as List) + .map((e) => e as num) + .toList(), +)..$type = json['__type'] as String?; + +Map _$AudiogramHealthValueToJson( + AudiogramHealthValue instance, +) => { + '__type': ?instance.$type, + 'frequencies': instance.frequencies, + 'leftEarSensitivities': instance.leftEarSensitivities, + 'rightEarSensitivities': instance.rightEarSensitivities, +}; + +WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) => + WorkoutHealthValue( + workoutActivityType: $enumDecode( + _$HealthWorkoutActivityTypeEnumMap, + json['workoutActivityType'], + ), + totalEnergyBurned: (json['totalEnergyBurned'] as num?)?.toInt(), + totalEnergyBurnedUnit: $enumDecodeNullable( + _$HealthDataUnitEnumMap, + json['totalEnergyBurnedUnit'], + ), + totalDistance: (json['totalDistance'] as num?)?.toInt(), + totalDistanceUnit: $enumDecodeNullable( + _$HealthDataUnitEnumMap, + json['totalDistanceUnit'], + ), + totalSteps: (json['totalSteps'] as num?)?.toInt(), + totalStepsUnit: $enumDecodeNullable( + _$HealthDataUnitEnumMap, + json['totalStepsUnit'], + ), + )..$type = json['__type'] as String?; + +Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) => + { + '__type': ?instance.$type, + 'workoutActivityType': + _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!, + 'totalEnergyBurned': ?instance.totalEnergyBurned, + 'totalEnergyBurnedUnit': + ?_$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit], + 'totalDistance': ?instance.totalDistance, + 'totalDistanceUnit': ?_$HealthDataUnitEnumMap[instance.totalDistanceUnit], + 'totalSteps': ?instance.totalSteps, + 'totalStepsUnit': ?_$HealthDataUnitEnumMap[instance.totalStepsUnit], + }; + +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.CARDIO_DANCE: 'CARDIO_DANCE', + 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.GOLF: 'GOLF', + HealthWorkoutActivityType.GYMNASTICS: 'GYMNASTICS', + HealthWorkoutActivityType.HANDBALL: 'HANDBALL', + HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING: + 'HIGH_INTENSITY_INTERVAL_TRAINING', + HealthWorkoutActivityType.HIKING: 'HIKING', + HealthWorkoutActivityType.HOCKEY: 'HOCKEY', + HealthWorkoutActivityType.JUMP_ROPE: 'JUMP_ROPE', + HealthWorkoutActivityType.KICKBOXING: 'KICKBOXING', + HealthWorkoutActivityType.MARTIAL_ARTS: 'MARTIAL_ARTS', + HealthWorkoutActivityType.PILATES: 'PILATES', + HealthWorkoutActivityType.RACQUETBALL: 'RACQUETBALL', + HealthWorkoutActivityType.ROWING: 'ROWING', + HealthWorkoutActivityType.RUGBY: 'RUGBY', + HealthWorkoutActivityType.RUNNING: 'RUNNING', + HealthWorkoutActivityType.SAILING: 'SAILING', + HealthWorkoutActivityType.SKATING: 'SKATING', + HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', + HealthWorkoutActivityType.SOCCER: 'SOCCER', + HealthWorkoutActivityType.SOFTBALL: 'SOFTBALL', + HealthWorkoutActivityType.SQUASH: 'SQUASH', + HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', + HealthWorkoutActivityType.SWIMMING: 'SWIMMING', + HealthWorkoutActivityType.TABLE_TENNIS: 'TABLE_TENNIS', + HealthWorkoutActivityType.TENNIS: 'TENNIS', + HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', + HealthWorkoutActivityType.WALKING: 'WALKING', + HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', + HealthWorkoutActivityType.YOGA: 'YOGA', + HealthWorkoutActivityType.BARRE: 'BARRE', + HealthWorkoutActivityType.BOWLING: 'BOWLING', + HealthWorkoutActivityType.CLIMBING: 'CLIMBING', + HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', + HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', + HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', + HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', + HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', + HealthWorkoutActivityType.FISHING: 'FISHING', + HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', + HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', + HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: + 'FUNCTIONAL_STRENGTH_TRAINING', + 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.PLAY: 'PLAY', + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: + 'PREPARATION_AND_RECOVERY', + HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', + HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', + HealthWorkoutActivityType.STAIRS: 'STAIRS', + HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', + HealthWorkoutActivityType.SURFING: 'SURFING', + 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.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', + HealthWorkoutActivityType.WRESTLING: 'WRESTLING', + HealthWorkoutActivityType.UNDERWATER_DIVING: 'UNDERWATER_DIVING', + HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', + HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', + HealthWorkoutActivityType.DANCING: 'DANCING', + HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', + HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', + HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', + HealthWorkoutActivityType.PARAGLIDING: 'PARAGLIDING', + HealthWorkoutActivityType.ROCK_CLIMBING: 'ROCK_CLIMBING', + HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', + HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', + HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', + HealthWorkoutActivityType.SKIING: 'SKIING', + HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', + HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', + HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', + HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', + HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', + HealthWorkoutActivityType.WALKING_TREADMILL: 'WALKING_TREADMILL', + HealthWorkoutActivityType.WEIGHTLIFTING: 'WEIGHTLIFTING', + HealthWorkoutActivityType.WHEELCHAIR: 'WHEELCHAIR', + HealthWorkoutActivityType.OTHER: 'OTHER', +}; + +ElectrocardiogramHealthValue _$ElectrocardiogramHealthValueFromJson( + Map json, +) => ElectrocardiogramHealthValue( + voltageValues: (json['voltageValues'] as List) + .map( + (e) => + ElectrocardiogramVoltageValue.fromJson(e as Map), + ) + .toList(), + averageHeartRate: json['averageHeartRate'] as num?, + samplingFrequency: (json['samplingFrequency'] as num?)?.toDouble(), + classification: $enumDecodeNullable( + _$ElectrocardiogramClassificationEnumMap, + json['classification'], + ), +)..$type = json['__type'] as String?; + +Map _$ElectrocardiogramHealthValueToJson( + ElectrocardiogramHealthValue instance, +) => { + '__type': ?instance.$type, + 'voltageValues': instance.voltageValues.map((e) => e.toJson()).toList(), + 'averageHeartRate': ?instance.averageHeartRate, + 'samplingFrequency': ?instance.samplingFrequency, + 'classification': + ?_$ElectrocardiogramClassificationEnumMap[instance.classification], +}; + +const _$ElectrocardiogramClassificationEnumMap = { + ElectrocardiogramClassification.NOT_SET: 'NOT_SET', + ElectrocardiogramClassification.SINUS_RHYTHM: 'SINUS_RHYTHM', + ElectrocardiogramClassification.ATRIAL_FIBRILLATION: 'ATRIAL_FIBRILLATION', + ElectrocardiogramClassification.INCONCLUSIVE_LOW_HEART_RATE: + 'INCONCLUSIVE_LOW_HEART_RATE', + ElectrocardiogramClassification.INCONCLUSIVE_HIGH_HEART_RATE: + 'INCONCLUSIVE_HIGH_HEART_RATE', + ElectrocardiogramClassification.INCONCLUSIVE_POOR_READING: + 'INCONCLUSIVE_POOR_READING', + ElectrocardiogramClassification.INCONCLUSIVE_OTHER: 'INCONCLUSIVE_OTHER', + ElectrocardiogramClassification.UNRECOGNIZED: 'UNRECOGNIZED', +}; + +ElectrocardiogramVoltageValue _$ElectrocardiogramVoltageValueFromJson( + Map json, +) => ElectrocardiogramVoltageValue( + voltage: json['voltage'] as num, + timeSinceSampleStart: json['timeSinceSampleStart'] as num, +)..$type = json['__type'] as String?; + +Map _$ElectrocardiogramVoltageValueToJson( + ElectrocardiogramVoltageValue instance, +) => { + '__type': ?instance.$type, + 'voltage': instance.voltage, + 'timeSinceSampleStart': instance.timeSinceSampleStart, +}; + +InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson( + Map json, +) => InsulinDeliveryHealthValue( + units: (json['units'] as num).toDouble(), + reason: $enumDecode(_$InsulinDeliveryReasonEnumMap, json['reason']), +)..$type = json['__type'] as String?; + +Map _$InsulinDeliveryHealthValueToJson( + InsulinDeliveryHealthValue instance, +) => { + '__type': ?instance.$type, + 'units': instance.units, + 'reason': _$InsulinDeliveryReasonEnumMap[instance.reason]!, +}; + +const _$InsulinDeliveryReasonEnumMap = { + InsulinDeliveryReason.NOT_SET: 'NOT_SET', + InsulinDeliveryReason.BASAL: 'BASAL', + InsulinDeliveryReason.BOLUS: 'BOLUS', +}; + +NutritionHealthValue _$NutritionHealthValueFromJson( + Map json, +) => NutritionHealthValue( + name: json['name'] as String?, + mealType: json['meal_type'] as String?, + calories: (json['calories'] as num?)?.toDouble(), + protein: (json['protein'] as num?)?.toDouble(), + fat: (json['fat'] as num?)?.toDouble(), + carbs: (json['carbs'] as num?)?.toDouble(), + caffeine: (json['caffeine'] as num?)?.toDouble(), + vitaminA: (json['vitamin_a'] as num?)?.toDouble(), + b1Thiamine: (json['b1_thiamine'] as num?)?.toDouble(), + b2Riboflavin: (json['b2_riboflavin'] as num?)?.toDouble(), + b3Niacin: (json['b3_niacin'] as num?)?.toDouble(), + b5PantothenicAcid: (json['b5_pantothenic_acid'] as num?)?.toDouble(), + b6Pyridoxine: (json['b6_pyridoxine'] as num?)?.toDouble(), + b7Biotin: (json['b7_biotin'] as num?)?.toDouble(), + b9Folate: (json['b9_folate'] as num?)?.toDouble(), + b12Cobalamin: (json['b12_cobalamin'] as num?)?.toDouble(), + vitaminC: (json['vitamin_c'] as num?)?.toDouble(), + vitaminD: (json['vitamin_d'] as num?)?.toDouble(), + vitaminE: (json['vitamin_e'] as num?)?.toDouble(), + vitaminK: (json['vitamin_k'] as num?)?.toDouble(), + calcium: (json['calcium'] as num?)?.toDouble(), + chloride: (json['chloride'] as num?)?.toDouble(), + cholesterol: (json['cholesterol'] as num?)?.toDouble(), + choline: (json['choline'] as num?)?.toDouble(), + chromium: (json['chromium'] as num?)?.toDouble(), + copper: (json['copper'] as num?)?.toDouble(), + fatUnsaturated: (json['fat_unsaturated'] as num?)?.toDouble(), + fatMonounsaturated: (json['fat_monounsaturated'] as num?)?.toDouble(), + fatPolyunsaturated: (json['fat_polyunsaturated'] as num?)?.toDouble(), + fatSaturated: (json['fat_saturated'] as num?)?.toDouble(), + fatTransMonoenoic: (json['fat_trans_monoenoic'] as num?)?.toDouble(), + fiber: (json['fiber'] as num?)?.toDouble(), + iodine: (json['iodine'] as num?)?.toDouble(), + iron: (json['iron'] as num?)?.toDouble(), + magnesium: (json['magnesium'] as num?)?.toDouble(), + manganese: (json['manganese'] as num?)?.toDouble(), + molybdenum: (json['molybdenum'] as num?)?.toDouble(), + phosphorus: (json['phosphorus'] as num?)?.toDouble(), + potassium: (json['potassium'] as num?)?.toDouble(), + selenium: (json['selenium'] as num?)?.toDouble(), + sodium: (json['sodium'] as num?)?.toDouble(), + sugar: (json['sugar'] as num?)?.toDouble(), + water: (json['water'] as num?)?.toDouble(), + zinc: (json['zinc'] as num?)?.toDouble(), +)..$type = json['__type'] as String?; + +Map _$NutritionHealthValueToJson( + NutritionHealthValue instance, +) => { + '__type': ?instance.$type, + 'name': ?instance.name, + 'meal_type': ?instance.mealType, + 'calories': ?instance.calories, + 'protein': ?instance.protein, + 'fat': ?instance.fat, + 'carbs': ?instance.carbs, + 'caffeine': ?instance.caffeine, + 'vitamin_a': ?instance.vitaminA, + 'b1_thiamine': ?instance.b1Thiamine, + 'b2_riboflavin': ?instance.b2Riboflavin, + 'b3_niacin': ?instance.b3Niacin, + 'b5_pantothenic_acid': ?instance.b5PantothenicAcid, + 'b6_pyridoxine': ?instance.b6Pyridoxine, + 'b7_biotin': ?instance.b7Biotin, + 'b9_folate': ?instance.b9Folate, + 'b12_cobalamin': ?instance.b12Cobalamin, + 'vitamin_c': ?instance.vitaminC, + 'vitamin_d': ?instance.vitaminD, + 'vitamin_e': ?instance.vitaminE, + 'vitamin_k': ?instance.vitaminK, + 'calcium': ?instance.calcium, + 'chloride': ?instance.chloride, + 'cholesterol': ?instance.cholesterol, + 'choline': ?instance.choline, + 'chromium': ?instance.chromium, + 'copper': ?instance.copper, + 'fat_unsaturated': ?instance.fatUnsaturated, + 'fat_monounsaturated': ?instance.fatMonounsaturated, + 'fat_polyunsaturated': ?instance.fatPolyunsaturated, + 'fat_saturated': ?instance.fatSaturated, + 'fat_trans_monoenoic': ?instance.fatTransMonoenoic, + 'fiber': ?instance.fiber, + 'iodine': ?instance.iodine, + 'iron': ?instance.iron, + 'magnesium': ?instance.magnesium, + 'manganese': ?instance.manganese, + 'molybdenum': ?instance.molybdenum, + 'phosphorus': ?instance.phosphorus, + 'potassium': ?instance.potassium, + 'selenium': ?instance.selenium, + 'sodium': ?instance.sodium, + 'sugar': ?instance.sugar, + 'water': ?instance.water, + 'zinc': ?instance.zinc, +}; + +MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson( + Map json, +) => MenstruationFlowHealthValue( + flow: $enumDecodeNullable(_$MenstrualFlowEnumMap, json['flow']), + dateTime: DateTime.parse(json['dateTime'] as String), + isStartOfCycle: json['isStartOfCycle'] as bool?, + wasUserEntered: json['wasUserEntered'] as bool?, +)..$type = json['__type'] as String?; + +Map _$MenstruationFlowHealthValueToJson( + MenstruationFlowHealthValue instance, +) => { + '__type': ?instance.$type, + 'flow': ?_$MenstrualFlowEnumMap[instance.flow], + 'isStartOfCycle': ?instance.isStartOfCycle, + 'wasUserEntered': ?instance.wasUserEntered, + 'dateTime': instance.dateTime.toIso8601String(), +}; + +const _$MenstrualFlowEnumMap = { + MenstrualFlow.unspecified: 'unspecified', + MenstrualFlow.none: 'none', + MenstrualFlow.light: 'light', + MenstrualFlow.medium: 'medium', + MenstrualFlow.heavy: 'heavy', + MenstrualFlow.spotting: 'spotting', +}; + +WorkoutSummary _$WorkoutSummaryFromJson(Map json) => + WorkoutSummary( + workoutType: json['workoutType'] as String, + totalDistance: json['totalDistance'] as num, + totalEnergyBurned: json['totalEnergyBurned'] as num, + totalSteps: json['totalSteps'] as num, + ); + +Map _$WorkoutSummaryToJson(WorkoutSummary instance) => + { + 'workoutType': instance.workoutType, + 'totalDistance': instance.totalDistance, + 'totalEnergyBurned': instance.totalEnergyBurned, + 'totalSteps': instance.totalSteps, + }; diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index c86b2a5e8..881f238d5 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -4,7 +4,7 @@ version: 13.1.1 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ">=3.8.0 <4.0.0" flutter: ">=3.6.0" dependencies: From 7a9a6945f0ec4ee63b780e6cfc02c73c2b17577c Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 31 Aug 2025 18:34:38 +0300 Subject: [PATCH 19/25] Clean up --- packages/health/example/.metadata | 4 +- packages/health/example/README.md | 12 +- packages/health/example/analysis_options.yaml | 1 + .../health/example/android/app/build.gradle | 44 -- .../example/android/app/build.gradle.kts | 4 +- .../android/app/src/main/AndroidManifest.xml | 1 - .../health/health_example/MainActivity.kt | 2 + packages/health/example/android/build.gradle | 18 - .../health/example/android/settings.gradle | 25 - .../ios/Runner.xcodeproj/project.pbxproj | 1 - packages/health/example/lib/main.dart | 18 +- packages/health/example/pubspec.yaml | 84 +--- packages/health/lib/health.g.dart | 457 +++++++++--------- packages/health/pubspec.yaml | 2 +- 14 files changed, 258 insertions(+), 415 deletions(-) delete mode 100644 packages/health/example/android/app/build.gradle delete mode 100644 packages/health/example/android/build.gradle delete mode 100644 packages/health/example/android/settings.gradle diff --git a/packages/health/example/.metadata b/packages/health/example/.metadata index facec9677..9a674c613 100644 --- a/packages/health/example/.metadata +++ b/packages/health/example/.metadata @@ -41,5 +41,5 @@ migration: # # Files that are not part of the templates will be ignored by default. unmanaged_files: - - "lib/main.dart" - - "ios/Runner.xcodeproj/project.pbxproj" + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/health/example/README.md b/packages/health/example/README.md index 388bfb9bb..1c4dbcfc1 100644 --- a/packages/health/example/README.md +++ b/packages/health/example/README.md @@ -1,6 +1,6 @@ -# example_new +# health_example -A new Flutter project. +Demonstrates how to use the health plugin. ## Getting Started @@ -8,9 +8,9 @@ This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. diff --git a/packages/health/example/analysis_options.yaml b/packages/health/example/analysis_options.yaml index 043984832..0d2902135 100644 --- a/packages/health/example/analysis_options.yaml +++ b/packages/health/example/analysis_options.yaml @@ -23,5 +23,6 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/packages/health/example/android/app/build.gradle b/packages/health/example/android/app/build.gradle deleted file mode 100644 index bbaca37bc..000000000 --- a/packages/health/example/android/app/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id "dev.flutter.flutter-gradle-plugin" -} - -android { - namespace = "cachet.plugins.health.health_example" - compileSdk = 35 - ndkVersion = '25.1.8937393' - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "cachet.plugins.health.health_example" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 26 - targetSdk = 35 - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.debug - } - } -} - -flutter { - source = "../.." -} diff --git a/packages/health/example/android/app/build.gradle.kts b/packages/health/example/android/app/build.gradle.kts index 01b4731b7..b5cd45fa6 100644 --- a/packages/health/example/android/app/build.gradle.kts +++ b/packages/health/example/android/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "cachet.plugins.health.health_example" - compileSdk = 35 + compileSdk = flutter.compileSdkVersion // ndkVersion = flutter.ndkVersion compileOptions { @@ -25,7 +25,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 26 - targetSdk = 35 + targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml index 2d17294ac..70e81aa1e 100644 --- a/packages/health/example/android/app/src/main/AndroidManifest.xml +++ b/packages/health/example/android/app/src/main/AndroidManifest.xml @@ -88,7 +88,6 @@ - runApp(const HealthApp()); +void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { const HealthApp({super.key}); @@ -125,13 +125,12 @@ class HealthAppState extends State { try { authorized = await health.requestAuthorization(types, permissions: permissions); - + // request access to read historic data await health.requestHealthDataHistoryAuthorization(); // request access in background await health.requestHealthDataInBackgroundAuthorization(); - } catch (error) { debugPrint("Exception in authorize: $error"); } @@ -258,7 +257,8 @@ class HealthAppState extends State { type: HealthDataType.BLOOD_GLUCOSE, startTime: earlier, endTime: now); - success &= await health.writeInsulinDelivery(5, InsulinDeliveryReason.BOLUS, earlier, now); + success &= await health.writeInsulinDelivery( + 5, InsulinDeliveryReason.BOLUS, earlier, now); success &= await health.writeHealthData( value: 1.8, type: HealthDataType.WATER, @@ -382,7 +382,6 @@ class HealthAppState extends State { endTime: now, ); - if (Platform.isIOS) { success &= await health.writeHealthData( value: 30, @@ -555,8 +554,9 @@ class HealthAppState extends State { healthDataResponse.sort((a, b) => b.dateTo.compareTo(a.dateTo)); _healthDataList.clear(); - _healthDataList.addAll( - (healthDataResponse.length < 100) ? healthDataResponse : healthDataResponse.sublist(0, 100)); + _healthDataList.addAll((healthDataResponse.length < 100) + ? healthDataResponse + : healthDataResponse.sublist(0, 100)); for (var data in _healthDataList) { debugPrint(toJsonString(data)); diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml index 1b419f1d7..19f8b9007 100644 --- a/packages/health/example/pubspec.yaml +++ b/packages/health/example/pubspec.yaml @@ -1,40 +1,16 @@ -name: example_new -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +name: health_example +description: Demonstrates how to use the health plugin. +publish_to: "none" +version: 4.5.0 environment: - sdk: ^3.5.1 + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.6.0" -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 + cupertino_icons: ^1.0.2 permission_handler: ^11.3.1 carp_serializable: ^2.0.0 # polymorphic json serialization health: @@ -45,51 +21,5 @@ dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^4.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 12f2d3ed8..d12054286 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -6,29 +6,31 @@ part of 'health.dart'; // JsonSerializableGenerator // ************************************************************************** -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']), - dateFrom: DateTime.parse(json['dateFrom'] as String), - dateTo: DateTime.parse(json['dateTo'] as String), - sourcePlatform: - $enumDecode(_$HealthPlatformTypeEnumMap, json['sourcePlatform']), - sourceDeviceId: json['sourceDeviceId'] as String, - sourceId: json['sourceId'] as String, - sourceName: json['sourceName'] as String, - recordingMethod: $enumDecodeNullable( - _$RecordingMethodEnumMap, json['recordingMethod']) ?? - RecordingMethod.unknown, - workoutSummary: json['workoutSummary'] == null - ? null - : WorkoutSummary.fromJson( - json['workoutSummary'] as Map), - metadata: json['metadata'] as Map?, - deviceModel: json['deviceModel'] as String?, - ); +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']), + dateFrom: DateTime.parse(json['dateFrom'] as String), + dateTo: DateTime.parse(json['dateTo'] as String), + sourcePlatform: $enumDecode( + _$HealthPlatformTypeEnumMap, + json['sourcePlatform'], + ), + sourceDeviceId: json['sourceDeviceId'] as String, + sourceId: json['sourceId'] as String, + sourceName: json['sourceName'] as String, + recordingMethod: + $enumDecodeNullable(_$RecordingMethodEnumMap, json['recordingMethod']) ?? + RecordingMethod.unknown, + workoutSummary: json['workoutSummary'] == null + ? null + : WorkoutSummary.fromJson(json['workoutSummary'] as Map), + metadata: json['metadata'] as Map?, + deviceModel: json['deviceModel'] as String?, +); Map _$HealthDataPointToJson(HealthDataPoint instance) => { @@ -43,10 +45,9 @@ Map _$HealthDataPointToJson(HealthDataPoint instance) => 'sourceId': instance.sourceId, 'sourceName': instance.sourceName, 'recordingMethod': _$RecordingMethodEnumMap[instance.recordingMethod]!, - if (instance.workoutSummary?.toJson() case final value?) - 'workoutSummary': value, - if (instance.metadata case final value?) 'metadata': value, - if (instance.deviceModel case final value?) 'deviceModel': value, + 'workoutSummary': ?instance.workoutSummary?.toJson(), + 'metadata': ?instance.metadata, + 'deviceModel': ?instance.deviceModel, }; const _$HealthDataTypeEnumMap = { @@ -228,74 +229,76 @@ HealthValue _$HealthValueFromJson(Map json) => HealthValue()..$type = json['__type'] as String?; Map _$HealthValueToJson(HealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - }; + {'__type': ?instance.$type}; NumericHealthValue _$NumericHealthValueFromJson(Map json) => - NumericHealthValue( - numericValue: json['numericValue'] as num, - )..$type = json['__type'] as String?; + NumericHealthValue(numericValue: json['numericValue'] as num) + ..$type = json['__type'] as String?; Map _$NumericHealthValueToJson(NumericHealthValue instance) => { - if (instance.$type case final value?) '__type': value, + '__type': ?instance.$type, 'numericValue': instance.numericValue, }; AudiogramHealthValue _$AudiogramHealthValueFromJson( - Map json) => - AudiogramHealthValue( - frequencies: - (json['frequencies'] as List).map((e) => e as num).toList(), - leftEarSensitivities: (json['leftEarSensitivities'] as List) - .map((e) => e as num) - .toList(), - rightEarSensitivities: (json['rightEarSensitivities'] as List) - .map((e) => e as num) - .toList(), - )..$type = json['__type'] as String?; + Map json, +) => AudiogramHealthValue( + frequencies: (json['frequencies'] as List) + .map((e) => e as num) + .toList(), + leftEarSensitivities: (json['leftEarSensitivities'] as List) + .map((e) => e as num) + .toList(), + rightEarSensitivities: (json['rightEarSensitivities'] as List) + .map((e) => e as num) + .toList(), +)..$type = json['__type'] as String?; Map _$AudiogramHealthValueToJson( - AudiogramHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'frequencies': instance.frequencies, - 'leftEarSensitivities': instance.leftEarSensitivities, - 'rightEarSensitivities': instance.rightEarSensitivities, - }; + AudiogramHealthValue instance, +) => { + '__type': ?instance.$type, + 'frequencies': instance.frequencies, + 'leftEarSensitivities': instance.leftEarSensitivities, + 'rightEarSensitivities': instance.rightEarSensitivities, +}; WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) => WorkoutHealthValue( workoutActivityType: $enumDecode( - _$HealthWorkoutActivityTypeEnumMap, json['workoutActivityType']), + _$HealthWorkoutActivityTypeEnumMap, + json['workoutActivityType'], + ), totalEnergyBurned: (json['totalEnergyBurned'] as num?)?.toInt(), totalEnergyBurnedUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['totalEnergyBurnedUnit']), + _$HealthDataUnitEnumMap, + json['totalEnergyBurnedUnit'], + ), totalDistance: (json['totalDistance'] as num?)?.toInt(), totalDistanceUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['totalDistanceUnit']), + _$HealthDataUnitEnumMap, + json['totalDistanceUnit'], + ), totalSteps: (json['totalSteps'] as num?)?.toInt(), - totalStepsUnit: - $enumDecodeNullable(_$HealthDataUnitEnumMap, json['totalStepsUnit']), + totalStepsUnit: $enumDecodeNullable( + _$HealthDataUnitEnumMap, + json['totalStepsUnit'], + ), )..$type = json['__type'] as String?; Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) => { - if (instance.$type case final value?) '__type': value, + '__type': ?instance.$type, 'workoutActivityType': _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!, - if (instance.totalEnergyBurned case final value?) - 'totalEnergyBurned': value, - if (_$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit] - case final value?) - 'totalEnergyBurnedUnit': value, - if (instance.totalDistance case final value?) 'totalDistance': value, - if (_$HealthDataUnitEnumMap[instance.totalDistanceUnit] case final value?) - 'totalDistanceUnit': value, - if (instance.totalSteps case final value?) 'totalSteps': value, - if (_$HealthDataUnitEnumMap[instance.totalStepsUnit] case final value?) - 'totalStepsUnit': value, + 'totalEnergyBurned': ?instance.totalEnergyBurned, + 'totalEnergyBurnedUnit': + ?_$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit], + 'totalDistance': ?instance.totalDistance, + 'totalDistanceUnit': ?_$HealthDataUnitEnumMap[instance.totalDistanceUnit], + 'totalSteps': ?instance.totalSteps, + 'totalStepsUnit': ?_$HealthDataUnitEnumMap[instance.totalStepsUnit], }; const _$HealthWorkoutActivityTypeEnumMap = { @@ -405,31 +408,32 @@ const _$HealthWorkoutActivityTypeEnumMap = { }; ElectrocardiogramHealthValue _$ElectrocardiogramHealthValueFromJson( - Map json) => - ElectrocardiogramHealthValue( - voltageValues: (json['voltageValues'] as List) - .map((e) => - ElectrocardiogramVoltageValue.fromJson(e as Map)) - .toList(), - averageHeartRate: json['averageHeartRate'] as num?, - samplingFrequency: (json['samplingFrequency'] as num?)?.toDouble(), - classification: $enumDecodeNullable( - _$ElectrocardiogramClassificationEnumMap, json['classification']), - )..$type = json['__type'] as String?; + Map json, +) => ElectrocardiogramHealthValue( + voltageValues: (json['voltageValues'] as List) + .map( + (e) => + ElectrocardiogramVoltageValue.fromJson(e as Map), + ) + .toList(), + averageHeartRate: json['averageHeartRate'] as num?, + samplingFrequency: (json['samplingFrequency'] as num?)?.toDouble(), + classification: $enumDecodeNullable( + _$ElectrocardiogramClassificationEnumMap, + json['classification'], + ), +)..$type = json['__type'] as String?; Map _$ElectrocardiogramHealthValueToJson( - ElectrocardiogramHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'voltageValues': instance.voltageValues.map((e) => e.toJson()).toList(), - if (instance.averageHeartRate case final value?) - 'averageHeartRate': value, - if (instance.samplingFrequency case final value?) - 'samplingFrequency': value, - if (_$ElectrocardiogramClassificationEnumMap[instance.classification] - case final value?) - 'classification': value, - }; + ElectrocardiogramHealthValue instance, +) => { + '__type': ?instance.$type, + 'voltageValues': instance.voltageValues.map((e) => e.toJson()).toList(), + 'averageHeartRate': ?instance.averageHeartRate, + 'samplingFrequency': ?instance.samplingFrequency, + 'classification': + ?_$ElectrocardiogramClassificationEnumMap[instance.classification], +}; const _$ElectrocardiogramClassificationEnumMap = { ElectrocardiogramClassification.NOT_SET: 'NOT_SET', @@ -446,34 +450,34 @@ const _$ElectrocardiogramClassificationEnumMap = { }; ElectrocardiogramVoltageValue _$ElectrocardiogramVoltageValueFromJson( - Map json) => - ElectrocardiogramVoltageValue( - voltage: json['voltage'] as num, - timeSinceSampleStart: json['timeSinceSampleStart'] as num, - )..$type = json['__type'] as String?; + Map json, +) => ElectrocardiogramVoltageValue( + voltage: json['voltage'] as num, + timeSinceSampleStart: json['timeSinceSampleStart'] as num, +)..$type = json['__type'] as String?; Map _$ElectrocardiogramVoltageValueToJson( - ElectrocardiogramVoltageValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'voltage': instance.voltage, - 'timeSinceSampleStart': instance.timeSinceSampleStart, - }; + ElectrocardiogramVoltageValue instance, +) => { + '__type': ?instance.$type, + 'voltage': instance.voltage, + 'timeSinceSampleStart': instance.timeSinceSampleStart, +}; InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson( - Map json) => - InsulinDeliveryHealthValue( - units: (json['units'] as num).toDouble(), - reason: $enumDecode(_$InsulinDeliveryReasonEnumMap, json['reason']), - )..$type = json['__type'] as String?; + Map json, +) => InsulinDeliveryHealthValue( + units: (json['units'] as num).toDouble(), + reason: $enumDecode(_$InsulinDeliveryReasonEnumMap, json['reason']), +)..$type = json['__type'] as String?; Map _$InsulinDeliveryHealthValueToJson( - InsulinDeliveryHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - 'units': instance.units, - 'reason': _$InsulinDeliveryReasonEnumMap[instance.reason]!, - }; + InsulinDeliveryHealthValue instance, +) => { + '__type': ?instance.$type, + 'units': instance.units, + 'reason': _$InsulinDeliveryReasonEnumMap[instance.reason]!, +}; const _$InsulinDeliveryReasonEnumMap = { InsulinDeliveryReason.NOT_SET: 'NOT_SET', @@ -482,127 +486,122 @@ const _$InsulinDeliveryReasonEnumMap = { }; NutritionHealthValue _$NutritionHealthValueFromJson( - Map json) => - NutritionHealthValue( - name: json['name'] as String?, - mealType: json['meal_type'] as String?, - calories: (json['calories'] as num?)?.toDouble(), - protein: (json['protein'] as num?)?.toDouble(), - fat: (json['fat'] as num?)?.toDouble(), - carbs: (json['carbs'] as num?)?.toDouble(), - caffeine: (json['caffeine'] as num?)?.toDouble(), - vitaminA: (json['vitamin_a'] as num?)?.toDouble(), - b1Thiamine: (json['b1_thiamine'] as num?)?.toDouble(), - b2Riboflavin: (json['b2_riboflavin'] as num?)?.toDouble(), - b3Niacin: (json['b3_niacin'] as num?)?.toDouble(), - b5PantothenicAcid: (json['b5_pantothenic_acid'] as num?)?.toDouble(), - b6Pyridoxine: (json['b6_pyridoxine'] as num?)?.toDouble(), - b7Biotin: (json['b7_biotin'] as num?)?.toDouble(), - b9Folate: (json['b9_folate'] as num?)?.toDouble(), - b12Cobalamin: (json['b12_cobalamin'] as num?)?.toDouble(), - vitaminC: (json['vitamin_c'] as num?)?.toDouble(), - vitaminD: (json['vitamin_d'] as num?)?.toDouble(), - vitaminE: (json['vitamin_e'] as num?)?.toDouble(), - vitaminK: (json['vitamin_k'] as num?)?.toDouble(), - calcium: (json['calcium'] as num?)?.toDouble(), - chloride: (json['chloride'] as num?)?.toDouble(), - cholesterol: (json['cholesterol'] as num?)?.toDouble(), - choline: (json['choline'] as num?)?.toDouble(), - chromium: (json['chromium'] as num?)?.toDouble(), - copper: (json['copper'] as num?)?.toDouble(), - fatUnsaturated: (json['fat_unsaturated'] as num?)?.toDouble(), - fatMonounsaturated: (json['fat_monounsaturated'] as num?)?.toDouble(), - fatPolyunsaturated: (json['fat_polyunsaturated'] as num?)?.toDouble(), - fatSaturated: (json['fat_saturated'] as num?)?.toDouble(), - fatTransMonoenoic: (json['fat_trans_monoenoic'] as num?)?.toDouble(), - fiber: (json['fiber'] as num?)?.toDouble(), - iodine: (json['iodine'] as num?)?.toDouble(), - iron: (json['iron'] as num?)?.toDouble(), - magnesium: (json['magnesium'] as num?)?.toDouble(), - manganese: (json['manganese'] as num?)?.toDouble(), - molybdenum: (json['molybdenum'] as num?)?.toDouble(), - phosphorus: (json['phosphorus'] as num?)?.toDouble(), - potassium: (json['potassium'] as num?)?.toDouble(), - selenium: (json['selenium'] as num?)?.toDouble(), - sodium: (json['sodium'] as num?)?.toDouble(), - sugar: (json['sugar'] as num?)?.toDouble(), - water: (json['water'] as num?)?.toDouble(), - zinc: (json['zinc'] as num?)?.toDouble(), - )..$type = json['__type'] as String?; + Map json, +) => NutritionHealthValue( + name: json['name'] as String?, + mealType: json['meal_type'] as String?, + calories: (json['calories'] as num?)?.toDouble(), + protein: (json['protein'] as num?)?.toDouble(), + fat: (json['fat'] as num?)?.toDouble(), + carbs: (json['carbs'] as num?)?.toDouble(), + caffeine: (json['caffeine'] as num?)?.toDouble(), + vitaminA: (json['vitamin_a'] as num?)?.toDouble(), + b1Thiamine: (json['b1_thiamine'] as num?)?.toDouble(), + b2Riboflavin: (json['b2_riboflavin'] as num?)?.toDouble(), + b3Niacin: (json['b3_niacin'] as num?)?.toDouble(), + b5PantothenicAcid: (json['b5_pantothenic_acid'] as num?)?.toDouble(), + b6Pyridoxine: (json['b6_pyridoxine'] as num?)?.toDouble(), + b7Biotin: (json['b7_biotin'] as num?)?.toDouble(), + b9Folate: (json['b9_folate'] as num?)?.toDouble(), + b12Cobalamin: (json['b12_cobalamin'] as num?)?.toDouble(), + vitaminC: (json['vitamin_c'] as num?)?.toDouble(), + vitaminD: (json['vitamin_d'] as num?)?.toDouble(), + vitaminE: (json['vitamin_e'] as num?)?.toDouble(), + vitaminK: (json['vitamin_k'] as num?)?.toDouble(), + calcium: (json['calcium'] as num?)?.toDouble(), + chloride: (json['chloride'] as num?)?.toDouble(), + cholesterol: (json['cholesterol'] as num?)?.toDouble(), + choline: (json['choline'] as num?)?.toDouble(), + chromium: (json['chromium'] as num?)?.toDouble(), + copper: (json['copper'] as num?)?.toDouble(), + fatUnsaturated: (json['fat_unsaturated'] as num?)?.toDouble(), + fatMonounsaturated: (json['fat_monounsaturated'] as num?)?.toDouble(), + fatPolyunsaturated: (json['fat_polyunsaturated'] as num?)?.toDouble(), + fatSaturated: (json['fat_saturated'] as num?)?.toDouble(), + fatTransMonoenoic: (json['fat_trans_monoenoic'] as num?)?.toDouble(), + fiber: (json['fiber'] as num?)?.toDouble(), + iodine: (json['iodine'] as num?)?.toDouble(), + iron: (json['iron'] as num?)?.toDouble(), + magnesium: (json['magnesium'] as num?)?.toDouble(), + manganese: (json['manganese'] as num?)?.toDouble(), + molybdenum: (json['molybdenum'] as num?)?.toDouble(), + phosphorus: (json['phosphorus'] as num?)?.toDouble(), + potassium: (json['potassium'] as num?)?.toDouble(), + selenium: (json['selenium'] as num?)?.toDouble(), + sodium: (json['sodium'] as num?)?.toDouble(), + sugar: (json['sugar'] as num?)?.toDouble(), + water: (json['water'] as num?)?.toDouble(), + zinc: (json['zinc'] as num?)?.toDouble(), +)..$type = json['__type'] as String?; Map _$NutritionHealthValueToJson( - NutritionHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - if (instance.name case final value?) 'name': value, - if (instance.mealType case final value?) 'meal_type': value, - if (instance.calories case final value?) 'calories': value, - if (instance.protein case final value?) 'protein': value, - if (instance.fat case final value?) 'fat': value, - if (instance.carbs case final value?) 'carbs': value, - if (instance.caffeine case final value?) 'caffeine': value, - if (instance.vitaminA case final value?) 'vitamin_a': value, - if (instance.b1Thiamine case final value?) 'b1_thiamine': value, - if (instance.b2Riboflavin case final value?) 'b2_riboflavin': value, - if (instance.b3Niacin case final value?) 'b3_niacin': value, - if (instance.b5PantothenicAcid case final value?) - 'b5_pantothenic_acid': value, - if (instance.b6Pyridoxine case final value?) 'b6_pyridoxine': value, - if (instance.b7Biotin case final value?) 'b7_biotin': value, - if (instance.b9Folate case final value?) 'b9_folate': value, - if (instance.b12Cobalamin case final value?) 'b12_cobalamin': value, - if (instance.vitaminC case final value?) 'vitamin_c': value, - if (instance.vitaminD case final value?) 'vitamin_d': value, - if (instance.vitaminE case final value?) 'vitamin_e': value, - if (instance.vitaminK case final value?) 'vitamin_k': value, - if (instance.calcium case final value?) 'calcium': value, - if (instance.chloride case final value?) 'chloride': value, - if (instance.cholesterol case final value?) 'cholesterol': value, - if (instance.choline case final value?) 'choline': value, - if (instance.chromium case final value?) 'chromium': value, - if (instance.copper case final value?) 'copper': value, - if (instance.fatUnsaturated case final value?) 'fat_unsaturated': value, - if (instance.fatMonounsaturated case final value?) - 'fat_monounsaturated': value, - if (instance.fatPolyunsaturated case final value?) - 'fat_polyunsaturated': value, - if (instance.fatSaturated case final value?) 'fat_saturated': value, - if (instance.fatTransMonoenoic case final value?) - 'fat_trans_monoenoic': value, - if (instance.fiber case final value?) 'fiber': value, - if (instance.iodine case final value?) 'iodine': value, - if (instance.iron case final value?) 'iron': value, - if (instance.magnesium case final value?) 'magnesium': value, - if (instance.manganese case final value?) 'manganese': value, - if (instance.molybdenum case final value?) 'molybdenum': value, - if (instance.phosphorus case final value?) 'phosphorus': value, - if (instance.potassium case final value?) 'potassium': value, - if (instance.selenium case final value?) 'selenium': value, - if (instance.sodium case final value?) 'sodium': value, - if (instance.sugar case final value?) 'sugar': value, - if (instance.water case final value?) 'water': value, - if (instance.zinc case final value?) 'zinc': value, - }; + NutritionHealthValue instance, +) => { + '__type': ?instance.$type, + 'name': ?instance.name, + 'meal_type': ?instance.mealType, + 'calories': ?instance.calories, + 'protein': ?instance.protein, + 'fat': ?instance.fat, + 'carbs': ?instance.carbs, + 'caffeine': ?instance.caffeine, + 'vitamin_a': ?instance.vitaminA, + 'b1_thiamine': ?instance.b1Thiamine, + 'b2_riboflavin': ?instance.b2Riboflavin, + 'b3_niacin': ?instance.b3Niacin, + 'b5_pantothenic_acid': ?instance.b5PantothenicAcid, + 'b6_pyridoxine': ?instance.b6Pyridoxine, + 'b7_biotin': ?instance.b7Biotin, + 'b9_folate': ?instance.b9Folate, + 'b12_cobalamin': ?instance.b12Cobalamin, + 'vitamin_c': ?instance.vitaminC, + 'vitamin_d': ?instance.vitaminD, + 'vitamin_e': ?instance.vitaminE, + 'vitamin_k': ?instance.vitaminK, + 'calcium': ?instance.calcium, + 'chloride': ?instance.chloride, + 'cholesterol': ?instance.cholesterol, + 'choline': ?instance.choline, + 'chromium': ?instance.chromium, + 'copper': ?instance.copper, + 'fat_unsaturated': ?instance.fatUnsaturated, + 'fat_monounsaturated': ?instance.fatMonounsaturated, + 'fat_polyunsaturated': ?instance.fatPolyunsaturated, + 'fat_saturated': ?instance.fatSaturated, + 'fat_trans_monoenoic': ?instance.fatTransMonoenoic, + 'fiber': ?instance.fiber, + 'iodine': ?instance.iodine, + 'iron': ?instance.iron, + 'magnesium': ?instance.magnesium, + 'manganese': ?instance.manganese, + 'molybdenum': ?instance.molybdenum, + 'phosphorus': ?instance.phosphorus, + 'potassium': ?instance.potassium, + 'selenium': ?instance.selenium, + 'sodium': ?instance.sodium, + 'sugar': ?instance.sugar, + 'water': ?instance.water, + 'zinc': ?instance.zinc, +}; MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson( - Map json) => - MenstruationFlowHealthValue( - flow: $enumDecodeNullable(_$MenstrualFlowEnumMap, json['flow']), - dateTime: DateTime.parse(json['dateTime'] as String), - isStartOfCycle: json['isStartOfCycle'] as bool?, - wasUserEntered: json['wasUserEntered'] as bool?, - )..$type = json['__type'] as String?; + Map json, +) => MenstruationFlowHealthValue( + flow: $enumDecodeNullable(_$MenstrualFlowEnumMap, json['flow']), + dateTime: DateTime.parse(json['dateTime'] as String), + isStartOfCycle: json['isStartOfCycle'] as bool?, + wasUserEntered: json['wasUserEntered'] as bool?, +)..$type = json['__type'] as String?; Map _$MenstruationFlowHealthValueToJson( - MenstruationFlowHealthValue instance) => - { - if (instance.$type case final value?) '__type': value, - if (_$MenstrualFlowEnumMap[instance.flow] case final value?) - 'flow': value, - if (instance.isStartOfCycle case final value?) 'isStartOfCycle': value, - if (instance.wasUserEntered case final value?) 'wasUserEntered': value, - 'dateTime': instance.dateTime.toIso8601String(), - }; + MenstruationFlowHealthValue instance, +) => { + '__type': ?instance.$type, + 'flow': ?_$MenstrualFlowEnumMap[instance.flow], + 'isStartOfCycle': ?instance.isStartOfCycle, + 'wasUserEntered': ?instance.wasUserEntered, + 'dateTime': instance.dateTime.toIso8601String(), +}; const _$MenstrualFlowEnumMap = { MenstrualFlow.unspecified: 'unspecified', diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index c86b2a5e8..881f238d5 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -4,7 +4,7 @@ version: 13.1.1 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ">=3.8.0 <4.0.0" flutter: ">=3.6.0" dependencies: From 0eb78dd86e6927086b49bc1e1ca35ec28c21aff4 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 31 Aug 2025 18:54:07 +0300 Subject: [PATCH 20/25] Fix ECG processing in the get by UUID method for iOS --- .../health/ios/Classes/HealthDataReader.swift | 425 +++++++++++------- 1 file changed, 253 insertions(+), 172 deletions(-) diff --git a/packages/health/ios/Classes/HealthDataReader.swift b/packages/health/ios/Classes/HealthDataReader.swift index 8495ac359..c1b80183b 100644 --- a/packages/health/ios/Classes/HealthDataReader.swift +++ b/packages/health/ios/Classes/HealthDataReader.swift @@ -1,5 +1,5 @@ -import HealthKit import Flutter +import HealthKit /// Class responsible for reading health data from HealthKit class HealthDataReader { @@ -9,7 +9,7 @@ class HealthDataReader { let unitDict: [String: HKUnit] let workoutActivityTypeMap: [String: HKWorkoutActivityType] let characteristicsTypesDict: [String: HKCharacteristicType] - + /// - Parameters: /// - healthStore: The HealthKit store /// - dataTypesDict: Dictionary of data types @@ -17,12 +17,14 @@ class HealthDataReader { /// - unitDict: Dictionary of units /// - workoutActivityTypeMap: Dictionary of workout activity types /// - characteristicsTypesDict: Dictionary of characteristic types - init(healthStore: HKHealthStore, - dataTypesDict: [String: HKSampleType], - dataQuantityTypesDict: [String: HKQuantityType], - unitDict: [String: HKUnit], - workoutActivityTypeMap: [String: HKWorkoutActivityType], - characteristicsTypesDict: [String: HKCharacteristicType]) { + init( + healthStore: HKHealthStore, + dataTypesDict: [String: HKSampleType], + dataQuantityTypesDict: [String: HKQuantityType], + unitDict: [String: HKUnit], + workoutActivityTypeMap: [String: HKWorkoutActivityType], + characteristicsTypesDict: [String: HKCharacteristicType] + ) { self.healthStore = healthStore self.dataTypesDict = dataTypesDict self.dataQuantityTypesDict = dataQuantityTypesDict @@ -30,38 +32,42 @@ class HealthDataReader { self.workoutActivityTypeMap = workoutActivityTypeMap self.characteristicsTypesDict = characteristicsTypesDict } - + /// Gets health data /// - Parameters: /// - call: Flutter method call /// - result: Flutter result callback func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? NSDictionary, - let dataTypeKey = arguments["dataTypeKey"] as? String else { + let dataTypeKey = arguments["dataTypeKey"] as? String + else { DispatchQueue.main.async { - result(FlutterError(code: "ARGUMENT_ERROR", - message: "Missing required dataTypeKey argument", - details: nil)) + result( + FlutterError( + code: "ARGUMENT_ERROR", + message: "Missing required dataTypeKey argument", + details: nil)) } return } - + let dataUnitKey = arguments["dataUnitKey"] as? String let startTime = (arguments["startTime"] as? NSNumber) ?? 0 let endTime = (arguments["endTime"] as? NSNumber) ?? 0 let limit = (arguments["limit"] as? Int) ?? HKObjectQueryNoLimit let recordingMethodsToFilter = (arguments["recordingMethodsToFilter"] as? [Int]) ?? [] - let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue) - + let includeManualEntry = !recordingMethodsToFilter.contains( + HealthConstants.RecordingMethod.manual.rawValue) + // convert from milliseconds to Date() let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue) let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue) - + let sourceIdForCharacteristic = "com.apple.Health" let sourceNameForCharacteristic = "Health" - + // characteristic types checks (like GENDER, BLOOD_TYPE, etc.) - switch(dataTypeKey) { + switch dataTypeKey { case HealthConstants.BIRTH_DATE: let dateOfBirth = getBirthDate() result([ @@ -71,7 +77,7 @@ class HealthDataReader { "date_to": Int(dateTo.timeIntervalSince1970 * 1000), "source_id": sourceIdForCharacteristic, "source_name": sourceNameForCharacteristic, - "recording_method": HealthConstants.RecordingMethod.manual.rawValue + "recording_method": HealthConstants.RecordingMethod.manual.rawValue, ] ]) return @@ -84,7 +90,7 @@ class HealthDataReader { "date_to": Int(dateTo.timeIntervalSince1970 * 1000), "source_id": sourceIdForCharacteristic, "source_name": sourceNameForCharacteristic, - "recording_method": HealthConstants.RecordingMethod.manual.rawValue + "recording_method": HealthConstants.RecordingMethod.manual.rawValue, ] ]) return @@ -97,70 +103,79 @@ class HealthDataReader { "date_to": Int(dateTo.timeIntervalSince1970 * 1000), "source_id": sourceIdForCharacteristic, "source_name": sourceNameForCharacteristic, - "recording_method": HealthConstants.RecordingMethod.manual.rawValue + "recording_method": HealthConstants.RecordingMethod.manual.rawValue, ] ]) return default: break } - + guard let dataType = dataTypesDict[dataTypeKey] else { DispatchQueue.main.async { - result(FlutterError(code: "INVALID_TYPE", - message: "Invalid dataTypeKey: \(dataTypeKey)", - details: nil)) + result( + FlutterError( + code: "INVALID_TYPE", + message: "Invalid dataTypeKey: \(dataTypeKey)", + details: nil)) } return } - + var unit: HKUnit? if let dataUnitKey = dataUnitKey { unit = unitDict[dataUnitKey] } - + var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) - if (!includeManualEntry) { - let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) - predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) + if !includeManualEntry { + let manualPredicate = NSPredicate( + format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) + predicate = NSCompoundPredicate( + type: .and, subpredicates: [predicate, manualPredicate]) } let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - + let query = HKSampleQuery( - sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] + sampleType: dataType, predicate: predicate, limit: limit, + sortDescriptors: [sortDescriptor] ) { x, samplesOrNil, error in - + guard error == nil else { DispatchQueue.main.async { - result(FlutterError(code: "HEALTH_ERROR", - message: "Error getting health data: \(error!.localizedDescription)", - details: nil)) + result( + FlutterError( + code: "HEALTH_ERROR", + message: "Error getting health data: \(error!.localizedDescription)", + details: nil)) } return } - + guard let samples = samplesOrNil else { DispatchQueue.main.async { result([]) } return } - + if let quantitySamples = samples as? [HKQuantitySample] { let dictionaries = quantitySamples.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", - "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()), + "value": sample.quantity.doubleValue( + for: unit ?? HKUnit.internationalUnit()), "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue, + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, "dataUnitKey": unit?.unitString, - "metadata": HealthUtilities.sanitizeMetadata(sample.metadata) + "metadata": HealthUtilities.sanitizeMetadata(sample.metadata), ] } DispatchQueue.main.async { @@ -194,7 +209,7 @@ class HealthDataReader { default: break } - + let categories = categorySamples.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", @@ -203,10 +218,11 @@ class HealthDataReader { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue, - "metadata": HealthUtilities.sanitizeMetadata(sample.metadata) + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, + "metadata": HealthUtilities.sanitizeMetadata(sample.metadata), ] } DispatchQueue.main.async { @@ -219,7 +235,8 @@ class HealthDataReader { "workoutActivityType": self.workoutActivityTypeMap.first(where: { $0.value == sample.workoutActivityType })?.key, - "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), + "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue( + for: HKUnit.kilocalorie()), "totalEnergyBurnedUnit": "KILOCALORIE", "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), "totalDistanceUnit": "METER", @@ -227,15 +244,19 @@ class HealthDataReader { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue, + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, "workout_type": HKWorkoutActivityType.toString(sample.workoutActivityType), - "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, - "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 + "total_distance": sample.totalDistance != nil + ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, + "total_energy_burned": sample.totalEnergyBurned != nil + ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) + : 0, ] } - + DispatchQueue.main.async { result(dictionaries) } @@ -247,9 +268,11 @@ class HealthDataReader { for samplePoint in sample.sensitivityPoints { frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) leftEarSensitivities.append( - samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + samplePoint.leftEarSensitivity!.doubleValue( + for: HKUnit.decibelHearingLevel())) rightEarSensitivities.append( - samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + samplePoint.rightEarSensitivity!.doubleValue( + for: HKUnit.decibelHearingLevel())) } return [ "uuid": "\(sample.uuid)", @@ -280,24 +303,32 @@ class HealthDataReader { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, ] for sample in samples { if let quantitySample = sample as? HKQuantitySample { for (key, identifier) in HealthConstants.NUTRITION_KEYS { - if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: identifier)){ - let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram() - sampleDict[key] = quantitySample.quantity.doubleValue(for: unit) + if quantitySample.quantityType + == HKObjectType.quantityType(forIdentifier: identifier) + { + let unit = + key == "calories" + ? HKUnit.kilocalorie() + : key == "water" + ? HKUnit.literUnit(with: .milli) : HKUnit.gram() + sampleDict[key] = quantitySample.quantity.doubleValue( + for: unit) } } } } - foods.append(sampleDict as! [String : Any?]) + foods.append(sampleDict as! [String: Any?]) } } - + DispatchQueue.main.async { result(foods) } @@ -310,10 +341,10 @@ class HealthDataReader { result(nil) } } - + } } - + healthStore.execute(query) } @@ -324,27 +355,32 @@ class HealthDataReader { func getDataByUUID(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? NSDictionary, - let uuidarg = arguments["uuid"] as? String, - let dataTypeKey = arguments["dataTypeKey"] as? String else { + let uuidarg = arguments["uuid"] as? String, + let dataTypeKey = arguments["dataTypeKey"] as? String + else { DispatchQueue.main.async { - result(FlutterError(code: "HEALTH_ERROR", - message: "Invalid Arguments - UUID or DataTypeKey invalid", - details: nil)) - } + result( + FlutterError( + code: "HEALTH_ERROR", + message: "Invalid Arguments - UUID or DataTypeKey invalid", + details: nil)) + } return } - + let dataUnitKey = arguments["dataUnitKey"] as? String var unit: HKUnit? if let dataUnitKey = dataUnitKey { - unit = unitDict[dataUnitKey] // Ensure unitDict exists and contains the key + unit = unitDict[dataUnitKey] // Ensure unitDict exists and contains the key } guard let dataType = dataTypesDict[dataTypeKey] else { DispatchQueue.main.async { - result(FlutterError(code: "INVALID_TYPE", - message: "Invalid dataTypeKey: \(dataTypeKey)", - details: nil)) + result( + FlutterError( + code: "INVALID_TYPE", + message: "Invalid dataTypeKey: \(dataTypeKey)", + details: nil)) } return } @@ -355,10 +391,10 @@ class HealthDataReader { } var predicate = HKQuery.predicateForObjects(with: [uuid]) - + let sourceIdForCharacteristic = "com.apple.Health" let sourceNameForCharacteristic = "Health" - + let query = HKSampleQuery( sampleType: dataType, predicate: predicate, @@ -370,34 +406,39 @@ class HealthDataReader { guard error == nil else { DispatchQueue.main.async { - result(FlutterError(code: "HEALTH_ERROR", - message: "Error getting health data by UUID: \(error!.localizedDescription)", - details: nil)) + result( + FlutterError( + code: "HEALTH_ERROR", + message: + "Error getting health data by UUID: \(error!.localizedDescription)", + details: nil)) } return } - + guard let samples = samplesOrNil else { DispatchQueue.main.async { result(nil) } return } - + if let quantitySamples = samples as? [HKQuantitySample] { let dictionaries = quantitySamples.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", - "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()), + "value": sample.quantity.doubleValue( + for: unit ?? HKUnit.internationalUnit()), "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue, + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, "dataUnitKey": unit?.unitString, - "metadata": HealthUtilities.sanitizeMetadata(sample.metadata) + "metadata": HealthUtilities.sanitizeMetadata(sample.metadata), ] } DispatchQueue.main.async { @@ -431,7 +472,7 @@ class HealthDataReader { default: break } - + let categories = categorySamples.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", @@ -440,10 +481,11 @@ class HealthDataReader { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue, - "metadata": HealthUtilities.sanitizeMetadata(sample.metadata) + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, + "metadata": HealthUtilities.sanitizeMetadata(sample.metadata), ] } DispatchQueue.main.async { @@ -456,7 +498,8 @@ class HealthDataReader { "workoutActivityType": self.workoutActivityTypeMap.first(where: { $0.value == sample.workoutActivityType })?.key, - "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), + "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue( + for: HKUnit.kilocalorie()), "totalEnergyBurnedUnit": "KILOCALORIE", "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), "totalDistanceUnit": "METER", @@ -464,15 +507,19 @@ class HealthDataReader { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue, + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, "workout_type": HKWorkoutActivityType.toString(sample.workoutActivityType), - "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, - "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 + "total_distance": sample.totalDistance != nil + ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, + "total_energy_burned": sample.totalEnergyBurned != nil + ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) + : 0, ] } - + DispatchQueue.main.async { result(dictionaries.first) } @@ -484,9 +531,11 @@ class HealthDataReader { for samplePoint in sample.sensitivityPoints { frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) leftEarSensitivities.append( - samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + samplePoint.leftEarSensitivity!.doubleValue( + for: HKUnit.decibelHearingLevel())) rightEarSensitivities.append( - samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + samplePoint.rightEarSensitivity!.doubleValue( + for: HKUnit.decibelHearingLevel())) } return [ "uuid": "\(sample.uuid)", @@ -517,32 +566,45 @@ class HealthDataReader { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) - ? HealthConstants.RecordingMethod.manual.rawValue - : HealthConstants.RecordingMethod.automatic.rawValue + "recording_method": + (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? HealthConstants.RecordingMethod.manual.rawValue + : HealthConstants.RecordingMethod.automatic.rawValue, ] for sample in samples { if let quantitySample = sample as? HKQuantitySample { for (key, identifier) in HealthConstants.NUTRITION_KEYS { - if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: identifier)){ - let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram() - sampleDict[key] = quantitySample.quantity.doubleValue(for: unit) + if quantitySample.quantityType + == HKObjectType.quantityType(forIdentifier: identifier) + { + let unit = + key == "calories" + ? HKUnit.kilocalorie() + : key == "water" + ? HKUnit.literUnit(with: .milli) : HKUnit.gram() + sampleDict[key] = quantitySample.quantity.doubleValue( + for: unit) } } } } - foods.append(sampleDict as! [String : Any?]) + foods.append(sampleDict) } } - + DispatchQueue.main.async { result(foods.first) } } else { if #available(iOS 14.0, *), let ecgSamples = samples as? [HKElectrocardiogram] { - let dictionaries = ecgSamples.map(self.fetchEcgMeasurements) - DispatchQueue.main.async { - result(dictionaries.first) + self.fetchEcgMeasurements(ecgSamples) { ecgDictionaries in + DispatchQueue.main.async { + if let dictionaries = ecgDictionaries as? [NSDictionary] { + result(dictionaries.first) + } else { + result(nil) + } + } } } else { DispatchQueue.main.async { @@ -552,10 +614,10 @@ class HealthDataReader { } } } - + healthStore.execute(query) } - + /// Gets interval health data /// - Parameters: /// - call: Flutter method call @@ -568,32 +630,37 @@ class HealthDataReader { let endDate = (arguments?["endTime"] as? NSNumber) ?? 0 let intervalInSecond = (arguments?["interval"] as? Int) ?? 1 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] - let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue) - + let includeManualEntry = !recordingMethodsToFilter.contains( + HealthConstants.RecordingMethod.manual.rawValue) + // interval in seconds var interval = DateComponents() interval.second = intervalInSecond - + let dateFrom = HealthUtilities.dateFromMilliseconds(startDate.doubleValue) let dateTo = HealthUtilities.dateFromMilliseconds(endDate.doubleValue) - + guard let quantityType = dataQuantityTypesDict[dataTypeKey] else { DispatchQueue.main.async { - result(FlutterError(code: "INVALID_TYPE", - message: "Invalid dataTypeKey for interval query: \(dataTypeKey)", - details: nil)) + result( + FlutterError( + code: "INVALID_TYPE", + message: "Invalid dataTypeKey for interval query: \(dataTypeKey)", + details: nil)) } return } - + var predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: []) - if (!includeManualEntry) { - let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) - predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) + if !includeManualEntry { + let manualPredicate = NSPredicate( + format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) + predicate = NSCompoundPredicate( + type: .and, subpredicates: [predicate, manualPredicate]) } - + let statisticsOptions = statisticsOption(for: dataTypeKey) - + let query = HKStatisticsCollectionQuery( quantityType: quantityType, quantitySamplePredicate: predicate, @@ -601,35 +668,40 @@ class HealthDataReader { anchorDate: dateFrom, intervalComponents: interval ) - + query.initialResultsHandler = { [weak self] _, statisticCollectionOrNil, error in guard let self = self else { DispatchQueue.main.async { - result(FlutterError(code: "INTERNAL_ERROR", - message: "Internal instance reference lost", - details: nil)) + result( + FlutterError( + code: "INTERNAL_ERROR", + message: "Internal instance reference lost", + details: nil)) } return } - + if let error = error { DispatchQueue.main.async { - result(FlutterError(code: "STATISTICS_ERROR", - message: "Error getting statistics: \(error.localizedDescription)", - details: nil)) + result( + FlutterError( + code: "STATISTICS_ERROR", + message: "Error getting statistics: \(error.localizedDescription)", + details: nil)) } return } - + guard let collection = statisticCollectionOrNil else { DispatchQueue.main.async { result(nil) } return } - + var dictionaries = [[String: Any]]() - collection.enumerateStatistics(from: dateFrom, to: dateTo) { [weak self] statisticData, _ in + collection.enumerateStatistics(from: dateFrom, to: dateTo) { + [weak self] statisticData, _ in guard let self = self else { return } if let dataUnitKey = dataUnitKey, let unit = self.unitDict[dataUnitKey] { var value: Double? = nil @@ -651,7 +723,7 @@ class HealthDataReader { "date_from": Int(statisticData.startDate.timeIntervalSince1970 * 1000), "date_to": Int(statisticData.endDate.timeIntervalSince1970 * 1000), "source_id": statisticData.sources?.first?.bundleIdentifier ?? "", - "source_name": statisticData.sources?.first?.name ?? "" + "source_name": statisticData.sources?.first?.name ?? "", ] dictionaries.append(dict) } @@ -682,7 +754,7 @@ class HealthDataReader { return .cumulativeSum } } - + /// Gets total steps in interval /// - Parameters: /// - call: Flutter method call @@ -692,20 +764,23 @@ class HealthDataReader { let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] - let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue) - + let includeManualEntry = !recordingMethodsToFilter.contains( + HealthConstants.RecordingMethod.manual.rawValue) + // Convert dates from milliseconds to Date() let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue) let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue) - + let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)! var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) - if (!includeManualEntry) { - let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) - predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) + if !includeManualEntry { + let manualPredicate = NSPredicate( + format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) + predicate = NSCompoundPredicate( + type: .and, subpredicates: [predicate, manualPredicate]) } - + let query = HKStatisticsCollectionQuery( quantityType: sampleType, quantitySamplePredicate: predicate, @@ -717,13 +792,15 @@ class HealthDataReader { guard let results = results else { let errorMessage = error?.localizedDescription ?? "Unknown error" DispatchQueue.main.async { - result(FlutterError(code: "STEPS_ERROR", - message: "Error getting step count: \(errorMessage)", - details: nil)) + result( + FlutterError( + code: "STEPS_ERROR", + message: "Error getting step count: \(errorMessage)", + details: nil)) } return } - + var totalSteps = 0.0 results.enumerateStatistics(from: dateFrom, to: dateTo) { statistics, stop in if let quantity = statistics.sumQuantity() { @@ -731,15 +808,15 @@ class HealthDataReader { totalSteps += quantity.doubleValue(for: unit) } } - + DispatchQueue.main.async { result(Int(totalSteps)) } } - + healthStore.execute(query) } - + /// Gets birth date from HealthKit /// - Returns: Birth date private func getBirthDate() -> Date? { @@ -752,7 +829,7 @@ class HealthDataReader { } return dob } - + /// Gets gender from HealthKit /// - Returns: Biological sex private func getGender() -> HKBiologicalSex? { @@ -765,7 +842,7 @@ class HealthDataReader { } return bioSex } - + /// Gets blood type from HealthKit /// - Returns: Blood type private func getBloodType() -> HKBloodType? { @@ -778,12 +855,14 @@ class HealthDataReader { } return bloodType } - + /// Fetch ECG measurements from an HKElectrocardiogram sample /// - Parameter sample: ECG sample /// - Returns: Dictionary with ECG data @available(iOS 14.0, *) - private func fetchEcgMeasurements(_ ecgSample: [HKElectrocardiogram], _ result: @escaping FlutterResult) { + private func fetchEcgMeasurements( + _ ecgSample: [HKElectrocardiogram], _ result: @escaping FlutterResult + ) { let group = DispatchGroup() var dictionaries = [NSDictionary]() let lock = NSLock() @@ -792,19 +871,20 @@ class HealthDataReader { group.enter() var voltageValues = [[String: Any]]() - let expected = Int(ecg.numberOfVoltageMeasurements) - if expected > 0 { - voltageValues.reserveCapacity(expected) - } - + let expected = Int(ecg.numberOfVoltageMeasurements) + if expected > 0 { + voltageValues.reserveCapacity(expected) + } + let q = HKElectrocardiogramQuery(ecg) { _, res in switch res { case .measurement(let m): if let v = m.quantity(for: .appleWatchSimilarToLeadI)? - .doubleValue(for: HKUnit.volt()) { + .doubleValue(for: HKUnit.volt()) + { voltageValues.append([ "voltage": v, - "timeSinceSampleStart": m.timeSinceSampleStart + "timeSinceSampleStart": m.timeSinceSampleStart, ]) } case .done: @@ -812,15 +892,16 @@ class HealthDataReader { "uuid": "\(ecg.uuid)", "voltageValues": voltageValues, "averageHeartRate": ecg.averageHeartRate? - .doubleValue(for: HKUnit.count() - .unitDivided(by: HKUnit.minute())), + .doubleValue( + for: HKUnit.count() + .unitDivided(by: HKUnit.minute())), "samplingFrequency": ecg.samplingFrequency? .doubleValue(for: HKUnit.hertz()), "classification": ecg.classification.rawValue, "date_from": Int(ecg.startDate.timeIntervalSince1970 * 1000), "date_to": Int(ecg.endDate.timeIntervalSince1970 * 1000), "source_id": ecg.sourceRevision.source.bundleIdentifier, - "source_name": ecg.sourceRevision.source.name + "source_name": ecg.sourceRevision.source.name, ] lock.lock() dictionaries.append(dict) From 751aa7e44a922ffde70d6ddb26b6fc5a484a335f Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 31 Aug 2025 18:57:32 +0300 Subject: [PATCH 21/25] Bump to 13.2.0 --- packages/health/CHANGELOG.md | 4 ++++ packages/health/ios/health.podspec | 2 +- packages/health/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 153d069b9..66a2900f2 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,7 @@ +## 13.2.0 + +* Add get health data by UUID (see `getHealthDataByUUID()`) + ## 13.1.4 * Fix adding mindfulness resulted in crash in iOS diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec index f0eb7cd7a..a928ad6ad 100644 --- a/packages/health/ios/health.podspec +++ b/packages/health/ios/health.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'health' - s.version = '13.1.4' + s.version = '13.2.0' s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.' s.description = <<-DESC Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index 90d81da50..d57f32fc9 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -1,6 +1,6 @@ name: health description: Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. -version: 13.1.4 +version: 13.2.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: From aa247ab8a5b3bd967817f51b73d3433c9ab3bb4e Mon Sep 17 00:00:00 2001 From: stuart Date: Tue, 9 Sep 2025 16:58:14 -0400 Subject: [PATCH 22/25] only get the stuff we have permissions for --- .../kotlin/cachet/plugins/health/HealthDataReader.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt index cefc28a27..37eab3837 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Handler import android.util.Log import androidx.health.connect.client.HealthConnectClient +import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.records.* import androidx.health.connect.client.request.AggregateGroupByDurationRequest import androidx.health.connect.client.request.AggregateRequest @@ -53,7 +54,14 @@ class HealthDataReader( scope.launch { try { - HealthConstants.mapToType[dataType]?.let { classType -> + val grantedPermissions = healthConnectClient.permissionController.getGrantedPermissions() + + val authorizedTypeMap = HealthConstants.mapToType.filter { (typeKey, classType) -> + val requiredPermission = HealthPermission.getReadPermission(classType) + grantedPermissions.contains(requiredPermission) + } + + authorizedTypeMap[dataType]?.let { classType -> val records = mutableListOf() // Set up the initial request to read health records @@ -106,7 +114,7 @@ class HealthDataReader( "Unable to return $dataType due to the following exception:" ) Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) + result.success(emptyList>()) // Return empty list instead of null } } } From e14c8b4b8e5cf3e22f8cfccc954b50b325a4f1be Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:00:53 +0200 Subject: [PATCH 23/25] build_runner --- packages/health/lib/health.g.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index d12054286..0d8302430 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -167,6 +167,7 @@ const _$HealthDataUnitEnumMap = { HealthDataUnit.POUND: 'POUND', HealthDataUnit.STONE: 'STONE', HealthDataUnit.METER: 'METER', + HealthDataUnit.CENTIMETER: 'CENTIMETER', HealthDataUnit.INCH: 'INCH', HealthDataUnit.FOOT: 'FOOT', HealthDataUnit.YARD: 'YARD', @@ -208,6 +209,7 @@ const _$HealthDataUnitEnumMap = { HealthDataUnit.BEATS_PER_MINUTE: 'BEATS_PER_MINUTE', HealthDataUnit.RESPIRATIONS_PER_MINUTE: 'RESPIRATIONS_PER_MINUTE', HealthDataUnit.MILLIGRAM_PER_DECILITER: 'MILLIGRAM_PER_DECILITER', + HealthDataUnit.MILLIMOLES_PER_LITER: 'MILLIMOLES_PER_LITER', HealthDataUnit.METER_PER_SECOND: 'METER_PER_SECOND', HealthDataUnit.UNKNOWN_UNIT: 'UNKNOWN_UNIT', HealthDataUnit.NO_UNIT: 'NO_UNIT', From dbf5c6827218c170009a5a7fd61b0a6f8095d068 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:54:18 +0200 Subject: [PATCH 24/25] Fix int to long casting issue --- .../cachet/plugins/health/HealthDataWriter.kt | 12 +- packages/health/example/lib/main.dart | 32 +- packages/health/lib/src/health_plugin.dart | 415 +++++++++++------- 3 files changed, 289 insertions(+), 170 deletions(-) diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt index 4c09b8b0d..b7070e045 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt @@ -35,7 +35,7 @@ class HealthDataWriter( val endTime = call.argument("endTime")!! val value = call.argument("value")!! val clientRecordId: String? = call.argument("clientRecordId") - val clientRecordVersion: Long? = call.argument("clientRecordVersion") + val clientRecordVersion: Double? = call.argument("clientRecordVersion") val recordingMethod = call.argument("recordingMethod")!! Log.i( @@ -47,7 +47,7 @@ class HealthDataWriter( if ((clientRecordId != null) && (clientRecordVersion != null)) { Metadata( clientRecordId = clientRecordId, - clientRecordVersion = clientRecordVersion, + clientRecordVersion = clientRecordVersion.toLong(), recordingMethod = recordingMethod ) } else { @@ -183,7 +183,7 @@ class HealthDataWriter( val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) val recordingMethod = call.argument("recordingMethod")!! val clientRecordId: String? = call.argument("clientRecordId") - val clientRecordVersion: Long? = call.argument("clientRecordVersion") + val clientRecordVersion: Double? = call.argument("clientRecordVersion") scope.launch { try { @@ -191,7 +191,7 @@ class HealthDataWriter( if ((clientRecordId != null) && (clientRecordVersion != null)) { Metadata( clientRecordId = clientRecordId, - clientRecordVersion = clientRecordVersion, + clientRecordVersion = clientRecordVersion.toLong(), recordingMethod = recordingMethod ) } else { @@ -306,7 +306,7 @@ class HealthDataWriter( val name = call.argument("name") val mealType = call.argument("meal_type")!! val clientRecordId: String? = call.argument("clientRecordId") - val clientRecordVersion: Long? = call.argument("clientRecordVersion") + val clientRecordVersion: Double? = call.argument("clientRecordVersion") scope.launch { try { @@ -314,7 +314,7 @@ class HealthDataWriter( if ((clientRecordId != null) && (clientRecordVersion != null)) { Metadata( clientRecordId = clientRecordId, - clientRecordVersion = clientRecordVersion + clientRecordVersion = clientRecordVersion.toLong() ) } else { Metadata() diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index ed1679690..404f3ae26 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -262,11 +262,12 @@ class HealthAppState extends State { endTime: now, recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 200, - type: HealthDataType.ACTIVE_ENERGY_BURNED, - startTime: earlier, - endTime: now, - ); + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + clientRecordId: "uniqueID1234", + clientRecordVersion: 1); success &= await health.writeHealthData( value: 70, type: HealthDataType.HEART_RATE, @@ -330,12 +331,15 @@ class HealthAppState extends State { totalEnergyBurned: 400, ); success &= await health.writeBloodPressure( - systolic: 90, - diastolic: 80, - startTime: now, - ); + systolic: 90, + diastolic: 80, + startTime: now, + clientRecordId: "uniqueID1234", + clientRecordVersion: 2); success &= await health.writeMeal( mealType: MealType.SNACK, + clientRecordId: "uniqueID1234", + clientRecordVersion: 1.4, startTime: earlier, endTime: now, caloriesConsumed: 1000, @@ -681,6 +685,13 @@ class HealthAppState extends State { WidgetStatePropertyAll(Colors.blue)), child: const Text("Delete Data", style: TextStyle(color: Colors.white))), + TextButton( + onPressed: testDeleteByClientRecordId, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.red)), + child: const Text("Test Delete by Client ID", + style: TextStyle(color: Colors.white))), TextButton( onPressed: fetchStepData, style: const ButtonStyle( @@ -837,7 +848,8 @@ class HealthAppState extends State { return ListTile( title: Text("${p.typeString}: ${p.value}"), trailing: Text(p.unitString), - subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + subtitle: + Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), onTap: () { fetchDataByUUID( context, diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index ed2d69aa3..622e3a937 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -46,7 +46,7 @@ class Health { /// Get an instance of the health plugin. Health({DeviceInfoPlugin? deviceInfo}) - : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { + : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { _registerFromJsonFunctions(); } @@ -107,13 +107,17 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( - "The lists of types and permissions must be of same length."); + "The lists of types and permissions must be of same length.", + ); } final mTypes = List.from(types, growable: true); final mPermissions = permissions == null - ? List.filled(types.length, HealthDataAccess.READ.index, - growable: true) + ? 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 @@ -152,8 +156,9 @@ class Health { if (Platform.isIOS) return null; try { - final status = - await _channel.invokeMethod('getHealthConnectSdkStatus'); + final status = await _channel.invokeMethod( + 'getHealthConnectSdkStatus', + ); _healthConnectSdkStatus = status != null ? HealthConnectSdkStatus.fromNativeValue(status) : HealthConnectSdkStatus.sdkUnavailable; @@ -171,7 +176,7 @@ class Health { Future isHealthConnectAvailable() async => !Platform.isAndroid ? true : (await getHealthConnectSdkStatus() == - HealthConnectSdkStatus.sdkAvailable); + HealthConnectSdkStatus.sdkAvailable); /// Prompt the user to install the Google Health Connect app via the /// installed store (most likely Play Store). @@ -195,8 +200,9 @@ class Health { if (!(await isHealthConnectAvailable())) { throw UnsupportedError( - "Google Health Connect is not available on this Android device. " - "You may prompt the user to install it using the 'installHealthConnect' method"); + "Google Health Connect is not available on this Android device. " + "You may prompt the user to install it using the 'installHealthConnect' method", + ); } } @@ -210,12 +216,14 @@ class Health { if (Platform.isIOS) return false; try { - final status = - await _channel.invokeMethod('isHealthDataHistoryAvailable'); + final status = await _channel.invokeMethod( + 'isHealthDataHistoryAvailable', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataHistoryAvailable(): $e'); + '$runtimeType - Exception in isHealthDataHistoryAvailable(): $e', + ); return false; } } @@ -231,12 +239,14 @@ class Health { if (Platform.isIOS) return true; try { - final status = - await _channel.invokeMethod('isHealthDataHistoryAuthorized'); + final status = await _channel.invokeMethod( + 'isHealthDataHistoryAuthorized', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataHistoryAuthorized(): $e'); + '$runtimeType - Exception in isHealthDataHistoryAuthorized(): $e', + ); return false; } } @@ -254,12 +264,14 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); try { - final bool? isAuthorized = - await _channel.invokeMethod('requestHealthDataHistoryAuthorization'); + final bool? isAuthorized = await _channel.invokeMethod( + 'requestHealthDataHistoryAuthorization', + ); return isAuthorized ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in requestHealthDataHistoryAuthorization(): $e'); + '$runtimeType - Exception in requestHealthDataHistoryAuthorization(): $e', + ); return false; } } @@ -274,12 +286,14 @@ class Health { if (Platform.isIOS) return false; try { - final status = await _channel - .invokeMethod('isHealthDataInBackgroundAvailable'); + final status = await _channel.invokeMethod( + 'isHealthDataInBackgroundAvailable', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataInBackgroundAvailable(): $e'); + '$runtimeType - Exception in isHealthDataInBackgroundAvailable(): $e', + ); return false; } } @@ -295,12 +309,14 @@ class Health { if (Platform.isIOS) return true; try { - final status = await _channel - .invokeMethod('isHealthDataInBackgroundAuthorized'); + final status = await _channel.invokeMethod( + 'isHealthDataInBackgroundAuthorized', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataInBackgroundAuthorized(): $e'); + '$runtimeType - Exception in isHealthDataInBackgroundAuthorized(): $e', + ); return false; } } @@ -318,12 +334,14 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); try { - final bool? isAuthorized = await _channel - .invokeMethod('requestHealthDataInBackgroundAuthorization'); + final bool? isAuthorized = await _channel.invokeMethod( + 'requestHealthDataInBackgroundAuthorization', + ); return isAuthorized ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in requestHealthDataInBackgroundAuthorization(): $e'); + '$runtimeType - Exception in requestHealthDataInBackgroundAuthorization(): $e', + ); return false; } } @@ -355,7 +373,8 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( - 'The length of [types] must be same as that of [permissions].'); + 'The length of [types] must be same as that of [permissions].', + ); } if (permissions != null) { @@ -370,15 +389,19 @@ class Health { type == HealthDataType.ATRIAL_FIBRILLATION_BURDEN) && permission != HealthDataAccess.READ) { throw ArgumentError( - 'Requesting WRITE permission on ELECTROCARDIOGRAM / HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT / IRREGULAR_HEART_RATE_EVENT / WALKING_HEART_RATE / ATRIAL_FIBRILLATION_BURDEN is not allowed.'); + 'Requesting WRITE permission on ELECTROCARDIOGRAM / HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT / IRREGULAR_HEART_RATE_EVENT / WALKING_HEART_RATE / ATRIAL_FIBRILLATION_BURDEN is not allowed.', + ); } } } final mTypes = List.from(types, growable: true); final mPermissions = permissions == null - ? List.filled(types.length, HealthDataAccess.READ.index, - growable: true) + ? 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 @@ -386,7 +409,9 @@ class Health { List keys = mTypes.map((e) => e.name).toList(); final bool? isAuthorized = await _channel.invokeMethod( - 'requestAuthorization', {'types': keys, "permissions": mPermissions}); + 'requestAuthorization', + {'types': keys, "permissions": mPermissions}, + ); return isAuthorized ?? false; } @@ -415,17 +440,25 @@ class Health { List recordingMethodsToFilter, ) async { List heights = await _prepareQuery( - startTime, endTime, HealthDataType.HEIGHT, recordingMethodsToFilter); + startTime, + endTime, + HealthDataType.HEIGHT, + recordingMethodsToFilter, + ); if (heights.isEmpty) { return []; } List weights = await _prepareQuery( - startTime, endTime, HealthDataType.WEIGHT, recordingMethodsToFilter); + startTime, + endTime, + HealthDataType.WEIGHT, + recordingMethodsToFilter, + ); - double h = - (heights.last.value as NumericHealthValue).numericValue.toDouble(); + double h = (heights.last.value as NumericHealthValue).numericValue + .toDouble(); const dataType = HealthDataType.BODY_MASS_INDEX; final unit = dataTypeToUnit[dataType]!; @@ -434,7 +467,7 @@ class Health { for (var i = 0; i < weights.length; i++) { final bmiValue = (weights[i].value as NumericHealthValue).numericValue.toDouble() / - (h * h); + (h * h); final x = HealthDataPoint( uuid: '', value: NumericHealthValue(numericValue: bmiValue), @@ -479,20 +512,23 @@ class Health { required HealthDataType type, required DateTime startTime, String? clientRecordId, - int? clientRecordVersion, + double? clientRecordVersion, DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } if (type == HealthDataType.WORKOUT) { throw ArgumentError( - "Adding workouts should be done using the writeWorkoutData method."); + "Adding workouts should be done using the writeWorkoutData method.", + ); } // If not implemented on platform, throw an exception if (!isDataTypeAvailable(type)) { @@ -510,7 +546,8 @@ class Health { }.contains(type) && Platform.isIOS) { throw ArgumentError( - "$type - iOS does not support writing this data type in HealthKit"); + "$type - iOS does not support writing this data type in HealthKit", + ); } // Assign default unit if not specified @@ -570,7 +607,7 @@ class Health { Map args = { 'dataTypeKey': type.name, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, }; bool? success = await _channel.invokeMethod('delete', args); return success ?? false; @@ -597,13 +634,11 @@ class Health { if (Platform.isIOS && type == null) { throw ArgumentError( - "On iOS, both UUID and type are required to delete a record."); + "On iOS, both UUID and type are required to delete a record.", + ); } - Map args = { - 'uuid': uuid, - 'dataTypeKey': type?.name, - }; + Map args = {'uuid': uuid, 'dataTypeKey': type?.name}; bool? success = await _channel.invokeMethod('deleteByUUID', args); return success ?? false; @@ -619,7 +654,7 @@ class Health { Map args = { 'dataTypeKey': dataTypeKey.name, 'recordId': recordId, - 'clientRecordId': clientRecordId + 'clientRecordId': clientRecordId, }; bool? success = await _channel.invokeMethod('deleteByClientRecordId', args); return success ?? false; @@ -644,14 +679,16 @@ class Health { required int diastolic, required DateTime startTime, String? clientRecordId, - int? clientRecordVersion, + double? clientRecordVersion, DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } @@ -693,8 +730,10 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } @@ -706,11 +745,12 @@ class Health { if (Platform.isIOS) { success = await writeHealthData( - value: saturation, - type: HealthDataType.BLOOD_OXYGEN, - startTime: startTime, - endTime: endTime, - recordingMethod: recordingMethod); + value: saturation, + type: HealthDataType.BLOOD_OXYGEN, + startTime: startTime, + endTime: endTime, + recordingMethod: recordingMethod, + ); } else if (Platform.isAndroid) { Map args = { 'value': saturation, @@ -782,7 +822,7 @@ class Health { required DateTime startTime, required DateTime endTime, String? clientRecordId, - int? clientRecordVersion, + double? clientRecordVersion, double? caloriesConsumed, double? carbohydrates, double? protein, @@ -829,8 +869,10 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } @@ -912,17 +954,21 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } - var value = - Platform.isAndroid ? MenstrualFlow.toHealthConnect(flow) : flow.index; + var value = Platform.isAndroid + ? MenstrualFlow.toHealthConnect(flow) + : flow.index; if (value == -1) { throw ArgumentError( - "$flow is not a valid menstrual flow value for $platformType"); + "$flow is not a valid menstrual flow value for $platformType", + ); } Map args = { @@ -964,12 +1010,14 @@ class Health { leftEarSensitivities.isEmpty || rightEarSensitivities.isEmpty) { throw ArgumentError( - "frequencies, leftEarSensitivities and rightEarSensitivities can't be empty"); + "frequencies, leftEarSensitivities and rightEarSensitivities can't be empty", + ); } if (frequencies.length != leftEarSensitivities.length || rightEarSensitivities.length != leftEarSensitivities.length) { throw ArgumentError( - "frequencies, leftEarSensitivities and rightEarSensitivities need to be of the same length"); + "frequencies, leftEarSensitivities and rightEarSensitivities need to be of the same length", + ); } endTime ??= startTime; if (startTime.isAfter(endTime)) { @@ -1018,14 +1066,15 @@ class Health { if (Platform.isAndroid) { throw UnsupportedError( - "writeInsulinDelivery is not supported on Android"); + "writeInsulinDelivery is not supported on Android", + ); } Map args = { 'units': units, 'reason': reason.index, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, }; bool? success = await _channel.invokeMethod('writeInsulinDelivery', args); @@ -1062,10 +1111,7 @@ class Health { throw HealthException(type, 'Not available on platform $platformType'); } - final result = await _dataQueryByUUID( - uuid, - type, - ); + final result = await _dataQueryByUUID(uuid, type); debugPrint('data by UUID: ${result?.toString()}'); @@ -1087,8 +1133,12 @@ class Health { for (var type in types) { final result = await _prepareQuery( - startTime, endTime, type, recordingMethodsToFilter, - dataUnit: preferredUnits?[type]); + startTime, + endTime, + type, + recordingMethodsToFilter, + dataUnit: preferredUnits?[type], + ); dataPoints.addAll(result); } @@ -1103,18 +1153,24 @@ class Health { /// Fetch a list of health data points based on [types]. /// You can also specify the [recordingMethodsToFilter] to filter the data points. /// If not specified, all data points will be included.Vkk - Future> getHealthIntervalDataFromTypes( - {required DateTime startDate, - required DateTime endDate, - required List types, - required int interval, - List recordingMethodsToFilter = const []}) async { + Future> getHealthIntervalDataFromTypes({ + required DateTime startDate, + required DateTime endDate, + required List types, + required int interval, + List recordingMethodsToFilter = const [], + }) async { await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; for (var type in types) { final result = await _prepareIntervalQuery( - startDate, endDate, type, interval, recordingMethodsToFilter); + startDate, + endDate, + type, + interval, + recordingMethodsToFilter, + ); dataPoints.addAll(result); } @@ -1133,7 +1189,12 @@ class Health { List dataPoints = []; final result = await _prepareAggregateQuery( - startDate, endDate, types, activitySegmentDuration, includeManualEntry); + startDate, + endDate, + types, + activitySegmentDuration, + includeManualEntry, + ); dataPoints.addAll(result); return removeDuplicates(dataPoints); @@ -1141,11 +1202,12 @@ class Health { /// Prepares an interval query, i.e. checks if the types are available, etc. Future> _prepareQuery( - DateTime startTime, - DateTime endTime, - HealthDataType dataType, - List recordingMethodsToFilter, - {HealthDataUnit? dataUnit}) async { + DateTime startTime, + DateTime endTime, + HealthDataType dataType, + List recordingMethodsToFilter, { + HealthDataUnit? dataUnit, + }) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -1154,7 +1216,9 @@ class Health { // If not implemented on platform, throw an exception if (!isDataTypeAvailable(dataType)) { throw HealthException( - dataType, 'Not available on platform $platformType'); + dataType, + 'Not available on platform $platformType', + ); } // If BodyMassIndex is requested on Android, calculate this manually @@ -1162,17 +1226,22 @@ class Health { return _computeAndroidBMI(startTime, endTime, recordingMethodsToFilter); } return await _dataQuery( - startTime, endTime, dataType, recordingMethodsToFilter, - dataUnit: dataUnit); + startTime, + endTime, + dataType, + recordingMethodsToFilter, + dataUnit: dataUnit, + ); } /// Prepares an interval query, i.e. checks if the types are available, etc. Future> _prepareIntervalQuery( - DateTime startDate, - DateTime endDate, - HealthDataType dataType, - int interval, - List recordingMethodsToFilter) async { + DateTime startDate, + DateTime endDate, + HealthDataType dataType, + int interval, + List recordingMethodsToFilter, + ) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -1181,20 +1250,28 @@ class Health { // If not implemented on platform, throw an exception if (!isDataTypeAvailable(dataType)) { throw HealthException( - dataType, 'Not available on platform $platformType'); + dataType, + 'Not available on platform $platformType', + ); } return await _dataIntervalQuery( - startDate, endDate, dataType, interval, recordingMethodsToFilter); + startDate, + endDate, + dataType, + interval, + recordingMethodsToFilter, + ); } /// Prepares an aggregate query, i.e. checks if the types are available, etc. Future> _prepareAggregateQuery( - DateTime startDate, - DateTime endDate, - List dataTypes, - int activitySegmentDuration, - bool includeManualEntry) async { + DateTime startDate, + DateTime endDate, + List dataTypes, + int activitySegmentDuration, + bool includeManualEntry, + ) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -1207,22 +1284,32 @@ class Health { } } - return await _dataAggregateQuery(startDate, endDate, dataTypes, - activitySegmentDuration, includeManualEntry); + return await _dataAggregateQuery( + startDate, + endDate, + dataTypes, + activitySegmentDuration, + includeManualEntry, + ); } /// Fetches data points from Android/iOS native code. - Future> _dataQuery(DateTime startTime, DateTime endTime, - HealthDataType dataType, List recordingMethodsToFilter, - {HealthDataUnit? dataUnit}) async { + Future> _dataQuery( + DateTime startTime, + DateTime endTime, + HealthDataType dataType, + List recordingMethodsToFilter, { + HealthDataUnit? dataUnit, + }) async { String? unit = dataUnit?.name ?? dataTypeToUnit[dataType]?.name; final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': unit, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, - 'recordingMethodsToFilter': - recordingMethodsToFilter.map((e) => e.toInt()).toList(), + 'recordingMethodsToFilter': recordingMethodsToFilter + .map((e) => e.toInt()) + .toList(), }; final fetchedDataPoints = await _channel.invokeMethod('getData', args); @@ -1230,7 +1317,7 @@ class Health { final msg = { "dataType": dataType, "dataPoints": fetchedDataPoints, - "unit": unit + "unit": unit, }; const thresHold = 100; // If the no. of data points are larger than the threshold, @@ -1275,23 +1362,27 @@ class Health { /// function for fetching statistic health data Future> _dataIntervalQuery( - DateTime startDate, - DateTime endDate, - HealthDataType dataType, - int interval, - List recordingMethodsToFilter) async { + DateTime startDate, + DateTime endDate, + HealthDataType dataType, + int interval, + List recordingMethodsToFilter, + ) async { final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': dataTypeToUnit[dataType]!.name, 'startTime': startDate.millisecondsSinceEpoch, 'endTime': endDate.millisecondsSinceEpoch, 'interval': interval, - 'recordingMethodsToFilter': - recordingMethodsToFilter.map((e) => e.toInt()).toList(), + 'recordingMethodsToFilter': recordingMethodsToFilter + .map((e) => e.toInt()) + .toList(), }; - final fetchedDataPoints = - await _channel.invokeMethod('getIntervalData', args); + final fetchedDataPoints = await _channel.invokeMethod( + 'getIntervalData', + args, + ); if (fetchedDataPoints != null) { final msg = { "dataType": dataType, @@ -1304,21 +1395,24 @@ class Health { /// function for fetching statistic health data Future> _dataAggregateQuery( - DateTime startDate, - DateTime endDate, - List dataTypes, - int activitySegmentDuration, - bool includeManualEntry) async { + DateTime startDate, + DateTime endDate, + List dataTypes, + int activitySegmentDuration, + bool includeManualEntry, + ) async { final args = { 'dataTypeKeys': dataTypes.map((dataType) => dataType.name).toList(), 'startTime': startDate.millisecondsSinceEpoch, 'endTime': endDate.millisecondsSinceEpoch, 'activitySegmentDuration': activitySegmentDuration, - 'includeManualEntry': includeManualEntry + 'includeManualEntry': includeManualEntry, }; - final fetchedDataPoints = - await _channel.invokeMethod('getAggregateData', args); + final fetchedDataPoints = await _channel.invokeMethod( + 'getAggregateData', + args, + ); if (fetchedDataPoints != null) { final msg = { @@ -1336,8 +1430,10 @@ class Health { String? unit = message["unit"] as String?; return dataPoints - .map((dataPoint) => - HealthDataPoint.fromHealthDataPoint(dataType, dataPoint, unit)) + .map( + (dataPoint) => + HealthDataPoint.fromHealthDataPoint(dataType, dataPoint, unit), + ) .toList(); } @@ -1347,8 +1443,11 @@ class Health { /// Get the total number of steps within a specific time period. /// Returns null if not successful. - Future getTotalStepsInInterval(DateTime startTime, DateTime endTime, - {bool includeManualEntry = true}) async { + Future getTotalStepsInInterval( + DateTime startTime, + DateTime endTime, { + bool includeManualEntry = true, + }) async { final args = { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, @@ -1365,20 +1464,22 @@ class Health { /// Assigns numbers to specific [HealthDataType]s. int _alignValue(HealthDataType type) => switch (type) { - HealthDataType.SLEEP_IN_BED => 0, - HealthDataType.SLEEP_ASLEEP => 1, - HealthDataType.SLEEP_AWAKE => 2, - HealthDataType.SLEEP_LIGHT => 3, - HealthDataType.SLEEP_DEEP => 4, - HealthDataType.SLEEP_REM => 5, - HealthDataType.HEADACHE_UNSPECIFIED => 0, - HealthDataType.HEADACHE_NOT_PRESENT => 1, - HealthDataType.HEADACHE_MILD => 2, - HealthDataType.HEADACHE_MODERATE => 3, - HealthDataType.HEADACHE_SEVERE => 4, - _ => throw HealthException(type, - "HealthDataType was not aligned correctly - please report bug at https://github.com/cph-cachet/flutter-plugins/issues"), - }; + HealthDataType.SLEEP_IN_BED => 0, + HealthDataType.SLEEP_ASLEEP => 1, + HealthDataType.SLEEP_AWAKE => 2, + HealthDataType.SLEEP_LIGHT => 3, + HealthDataType.SLEEP_DEEP => 4, + HealthDataType.SLEEP_REM => 5, + HealthDataType.HEADACHE_UNSPECIFIED => 0, + HealthDataType.HEADACHE_NOT_PRESENT => 1, + HealthDataType.HEADACHE_MILD => 2, + HealthDataType.HEADACHE_MODERATE => 3, + HealthDataType.HEADACHE_SEVERE => 4, + _ => throw HealthException( + type, + "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 Health Connect. /// @@ -1410,18 +1511,24 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } // Check that value is on the current Platform if (Platform.isIOS && !_isOnIOS(activityType)) { - throw HealthException(activityType, - "Workout activity type $activityType is not supported on iOS"); + throw HealthException( + activityType, + "Workout activity type $activityType is not supported on iOS", + ); } else if (Platform.isAndroid && !_isOnAndroid(activityType)) { - throw HealthException(activityType, - "Workout activity type $activityType is not supported on Android"); + throw HealthException( + activityType, + "Workout activity type $activityType is not supported on Android", + ); } final args = { 'activityType': activityType.name, From 22a9e1d436b84be34c33372d59570ce7f1c294ca Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:43:57 +0200 Subject: [PATCH 25/25] Bump to 13.2.0, merge PRs and dependencies update --- packages/health/CHANGELOG.md | 9 +- packages/health/android/build.gradle | 15 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- .../plugins/health/HealthDataOperations.kt | 3 - .../cachet/plugins/health/HealthDataWriter.kt | 228 +++++++++++------- .../example/android/app/build.gradle.kts | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/android/settings.gradle.kts | 4 +- packages/health/example/lib/main.dart | 7 - packages/health/example/pubspec.yaml | 2 +- packages/health/pubspec.yaml | 6 +- 11 files changed, 171 insertions(+), 110 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 66a2900f2..44de9d899 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,6 +1,13 @@ ## 13.2.0 -* Add get health data by UUID (see `getHealthDataByUUID()`) +* Add get health data by UUID (see `getHealthDataByUUID()`) - PR [#1193](https://github.com/carp-dk/flutter-plugins/pull/1193), [#1194](https://github.com/carp-dk/flutter-plugins/pull/1194) +* Add delete by UUID (`deleteByUUID()`) +* Add support for unit conversion in `WeightRecord`, `HeightRecord`, `BodyTemperatureRecord`, and `BloodGlucoseRecord` - PR [#1212](https://github.com/carp-dk/flutter-plugins/pull/1223) +* Update `compileSDK` to 36 - Fix [#1261](https://github.com/carp-dk/flutter-plugins/issues/1261) +* Update Gradle to 8.9.1 +* Update `org.jetbrains.kotlin.android` to 2.1.0 +* Update `androidx.health.connect:connect-client` to 1.1.0-rc03 +* Update `device_info_plus` to 12.1.0 - Fix [#1264](https://github.com/carp-dk/flutter-plugins/issues/1264) ## 13.1.4 diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index 45c7b67d8..b4194b9dc 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.android.tools.build:gradle:8.13.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdk 34 + compileSdk 36 compileOptions { sourceCompatibility JavaVersion.VERSION_11 @@ -41,7 +41,7 @@ android { } defaultConfig { minSdkVersion 26 - targetSdkVersion 34 + targetSdkVersion 36 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { @@ -51,12 +51,11 @@ android { } dependencies { - def composeBom = platform('androidx.compose:compose-bom:2025.02.00') + def composeBom = platform('androidx.compose:compose-bom:2025.09.00') implementation(composeBom) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10" - implementation("androidx.health.connect:connect-client:1.1.0-alpha11") - def fragment_version = "1.8.6" - implementation "androidx.fragment:fragment-ktx:$fragment_version" + implementation("androidx.health.connect:connect-client:1.1.0-rc03") + implementation "androidx.fragment:fragment-ktx:1.8.9" } diff --git a/packages/health/android/gradle/wrapper/gradle-wrapper.properties b/packages/health/android/gradle/wrapper/gradle-wrapper.properties index a59520664..cb3cdef9c 100644 --- a/packages/health/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/health/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Sep 16 09:25:00 CEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt index f0e6d5b98..78561516b 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt @@ -3,7 +3,6 @@ package cachet.plugins.health import android.util.Log import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.HealthConnectFeatures -import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_HISTORY import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND @@ -103,7 +102,6 @@ class HealthDataOperations( * @param call Method call from Flutter (unused) * @param result Flutter result callback returning boolean availability status */ - @OptIn(ExperimentalFeatureAvailabilityApi::class) fun isHealthDataHistoryAvailable(call: MethodCall, result: Result) { scope.launch { result.success( @@ -139,7 +137,6 @@ class HealthDataOperations( * @param call Method call from Flutter (unused) * @param result Flutter result callback returning boolean availability status */ - @OptIn(ExperimentalFeatureAvailabilityApi::class) fun isHealthDataInBackgroundAvailable(call: MethodCall, result: Result) { scope.launch { result.success( diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt index b7070e045..976a9bf15 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt @@ -3,6 +3,7 @@ package cachet.plugins.health import android.util.Log import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.records.* +import androidx.health.connect.client.records.metadata.Device import androidx.health.connect.client.records.metadata.Metadata import androidx.health.connect.client.units.* import io.flutter.plugin.common.MethodCall @@ -21,6 +22,74 @@ class HealthDataWriter( private val scope: CoroutineScope ) { + // Maps incoming recordingMethod int -> Metadata factory method. + // 0: unknown, 1: manual, 2: auto, 3: active (default unknown for others) + private fun buildMetadata( + recordingMethod: Int, + clientRecordId: String? = null, + clientRecordVersion: Long? = null, + deviceType: Int? = null, + ): Metadata { + // Device is required for auto/active; optional for manual/unknown + val deviceForAutoOrActive = + when (recordingMethod) { + RECORDING_METHOD_AUTOMATICALLY_RECORDED, + RECORDING_METHOD_ACTIVELY_RECORDED -> + Device(type = deviceType ?: Device.TYPE_UNKNOWN) + else -> null + } + + return when (recordingMethod) { + RECORDING_METHOD_MANUAL_ENTRY -> { + if (clientRecordId != null && clientRecordVersion != null) { + Metadata.manualEntry( + device = null, + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion, + ) + } else { + Metadata.manualEntry() + } + } + RECORDING_METHOD_AUTOMATICALLY_RECORDED -> { + val dev = deviceForAutoOrActive!! + if (clientRecordId != null && clientRecordVersion != null) { + Metadata.autoRecorded( + device = dev, + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion, + ) + } else { + Metadata.autoRecorded(dev) + } + } + RECORDING_METHOD_ACTIVELY_RECORDED -> { + val dev = deviceForAutoOrActive!! + if (clientRecordId != null && clientRecordVersion != null) { + Metadata.activelyRecorded( + device = dev, + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion, + ) + } else { + Metadata.activelyRecorded(dev) + } + } + else -> { // unknown + if (clientRecordId != null && clientRecordVersion != null) { + Metadata.unknownRecordingMethod( + device = null, + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion, + ) + } else { + Metadata.unknownRecordingMethod() + } + } + } + } + + /** * Writes a single health data record to Health Connect. Supports most basic health metrics with * automatic type conversion and validation. @@ -37,22 +106,19 @@ class HealthDataWriter( val clientRecordId: String? = call.argument("clientRecordId") val clientRecordVersion: Double? = call.argument("clientRecordVersion") val recordingMethod = call.argument("recordingMethod")!! + val deviceType: Int? = call.argument("deviceType") Log.i( "FLUTTER_HEALTH", "Writing data for $type between $startTime and $endTime, value: $value, recording method: $recordingMethod" ) - val metadata: Metadata = - if ((clientRecordId != null) && (clientRecordVersion != null)) { - Metadata( - clientRecordId = clientRecordId, - clientRecordVersion = clientRecordVersion.toLong(), - recordingMethod = recordingMethod - ) - } else { - Metadata(recordingMethod = recordingMethod) - } + val metadata: Metadata = buildMetadata( + recordingMethod = recordingMethod, + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion?.toLong(), + deviceType = deviceType, + ) val record = createRecord(type, startTime, endTime, value, metadata) @@ -91,6 +157,8 @@ class HealthDataWriter( val totalEnergyBurned = call.argument("totalEnergyBurned") val totalDistance = call.argument("totalDistance") val recordingMethod = call.argument("recordingMethod")!! + val deviceType: Int? = call.argument("deviceType") + val workoutMetadata = buildMetadata(recordingMethod = recordingMethod, deviceType = deviceType) if (!HealthConstants.workoutTypeMap.containsKey(type)) { result.success(false) @@ -114,10 +182,7 @@ class HealthDataWriter( endZoneOffset = null, exerciseType = workoutType, title = title, - metadata = - Metadata( - recordingMethod = recordingMethod, - ), + metadata = workoutMetadata, ), ) @@ -130,10 +195,7 @@ class HealthDataWriter( endTime = endTime, endZoneOffset = null, distance = Length.meters(totalDistance.toDouble()), - metadata = - Metadata( - recordingMethod = recordingMethod, - ), + metadata = workoutMetadata, ), ) } @@ -147,10 +209,7 @@ class HealthDataWriter( endTime = endTime, endZoneOffset = null, energy = Energy.kilocalories(totalEnergyBurned.toDouble()), - metadata = - Metadata( - recordingMethod = recordingMethod, - ), + metadata = workoutMetadata, ), ) } @@ -184,19 +243,16 @@ class HealthDataWriter( val recordingMethod = call.argument("recordingMethod")!! val clientRecordId: String? = call.argument("clientRecordId") val clientRecordVersion: Double? = call.argument("clientRecordVersion") + val deviceType: Int? = call.argument("deviceType") scope.launch { try { - val metadata: Metadata = - if ((clientRecordId != null) && (clientRecordVersion != null)) { - Metadata( - clientRecordId = clientRecordId, - clientRecordVersion = clientRecordVersion.toLong(), - recordingMethod = recordingMethod - ) - } else { - Metadata(recordingMethod = recordingMethod) - } + val metadata: Metadata = buildMetadata( + recordingMethod = recordingMethod, + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion?.toLong(), + deviceType = deviceType, + ) healthConnectClient.insertRecords( listOf( BloodPressureRecord( @@ -263,68 +319,67 @@ class HealthDataWriter( 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? - 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? - val zinc = call.argument("zinc") as Double? + val protein = call.argument("protein") + val carbs = call.argument("carbs") + val fat = call.argument("fat") + val caffeine = call.argument("caffeine") + val vitaminA = call.argument("vitamin_a") + val b1Thiamine = call.argument("b1_thiamine") + val b2Riboflavin = call.argument("b2_riboflavin") + val b3Niacin = call.argument("b3_niacin") + val b5PantothenicAcid = call.argument("b5_pantothenic_acid") + val b6Pyridoxine = call.argument("b6_pyridoxine") + val b7Biotin = call.argument("b7_biotin") + val b9Folate = call.argument("b9_folate") + val b12Cobalamin = call.argument("b12_cobalamin") + val vitaminC = call.argument("vitamin_c") + val vitaminD = call.argument("vitamin_d") + val vitaminE = call.argument("vitamin_e") + val vitaminK = call.argument("vitamin_k") + val calcium = call.argument("calcium") + val chloride = call.argument("chloride") + val cholesterol = call.argument("cholesterol") + val chromium = call.argument("chromium") + val copper = call.argument("copper") + val fatUnsaturated = call.argument("fat_unsaturated") + val fatMonounsaturated = call.argument("fat_monounsaturated") + val fatPolyunsaturated = call.argument("fat_polyunsaturated") + val fatSaturated = call.argument("fat_saturated") + val fatTransMonoenoic = call.argument("fat_trans_monoenoic") + val fiber = call.argument("fiber") + val iodine = call.argument("iodine") + val iron = call.argument("iron") + val magnesium = call.argument("magnesium") + val manganese = call.argument("manganese") + val molybdenum = call.argument("molybdenum") + val phosphorus = call.argument("phosphorus") + val potassium = call.argument("potassium") + val selenium = call.argument("selenium") + val sodium = call.argument("sodium") + val sugar = call.argument("sugar") + val zinc = call.argument("zinc") val name = call.argument("name") val mealType = call.argument("meal_type")!! + val recordingMethod = call.argument("recordingMethod") ?: RECORDING_METHOD_MANUAL_ENTRY val clientRecordId: String? = call.argument("clientRecordId") val clientRecordVersion: Double? = call.argument("clientRecordVersion") + val deviceType: Int? = call.argument("deviceType") scope.launch { try { - val metaData: Metadata = - if ((clientRecordId != null) && (clientRecordVersion != null)) { - Metadata( - clientRecordId = clientRecordId, - clientRecordVersion = clientRecordVersion.toLong() - ) - } else { - Metadata() - } + val metadata: Metadata = buildMetadata( + recordingMethod = recordingMethod, + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion?.toLong(), + deviceType = deviceType, + ) val list = mutableListOf() list.add( NutritionRecord( name = name, - metadata = metaData, + metadata = metadata, energy = calories?.kilocalories, totalCarbohydrate = carbs?.grams, protein = protein?.grams, @@ -408,6 +463,7 @@ class HealthDataWriter( val endTime = call.argument("endTime")!! val samples = call.argument>>("samples")!! val recordingMethod = call.argument("recordingMethod")!! + val deviceType: Int? = call.argument("deviceType") scope.launch { try { @@ -418,6 +474,8 @@ class HealthDataWriter( speed = Velocity.metersPerSecond(sample["speed"] as Double) ) } + + val metadata = buildMetadata(recordingMethod, deviceType = deviceType) val speedRecord = SpeedRecord( @@ -426,7 +484,7 @@ class HealthDataWriter( samples = speedSamples, startZoneOffset = null, endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), + metadata = metadata, ) healthConnectClient.insertRecords(listOf(speedRecord)) @@ -789,6 +847,12 @@ class HealthDataWriter( private const val NUTRITION = "NUTRITION" private const val SPEED = "SPEED" + // Recording method mapping expected from Flutter side + private const val RECORDING_METHOD_UNKNOWN = 0 + private const val RECORDING_METHOD_MANUAL_ENTRY = 1 + private const val RECORDING_METHOD_AUTOMATICALLY_RECORDED = 2 + private const val RECORDING_METHOD_ACTIVELY_RECORDED = 3 + // Sleep types private const val SLEEP_ASLEEP = "SLEEP_ASLEEP" private const val SLEEP_LIGHT = "SLEEP_LIGHT" diff --git a/packages/health/example/android/app/build.gradle.kts b/packages/health/example/android/app/build.gradle.kts index b5cd45fa6..a9130cad1 100644 --- a/packages/health/example/android/app/build.gradle.kts +++ b/packages/health/example/android/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "cachet.plugins.health.health_example" compileSdk = flutter.compileSdkVersion - // ndkVersion = flutter.ndkVersion + ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties index afa1e8eb0..02767eb1c 100644 --- a/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/health/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip diff --git a/packages/health/example/android/settings.gradle.kts b/packages/health/example/android/settings.gradle.kts index a439442c2..43394ed5e 100644 --- a/packages/health/example/android/settings.gradle.kts +++ b/packages/health/example/android/settings.gradle.kts @@ -18,8 +18,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false } include(":app") diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 404f3ae26..ffaeec3bf 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -685,13 +685,6 @@ class HealthAppState extends State { WidgetStatePropertyAll(Colors.blue)), child: const Text("Delete Data", style: TextStyle(color: Colors.white))), - TextButton( - onPressed: testDeleteByClientRecordId, - style: const ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.red)), - child: const Text("Test Delete by Client ID", - style: TextStyle(color: Colors.white))), TextButton( onPressed: fetchStepData, style: const ButtonStyle( diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml index 19f8b9007..581869077 100644 --- a/packages/health/example/pubspec.yaml +++ b/packages/health/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 4.5.0 environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ">=3.8.0 <4.0.0" flutter: ">=3.6.0" dependencies: diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index d57f32fc9..a55907070 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -11,9 +11,9 @@ dependencies: flutter: sdk: flutter intl: '>=0.18.0 <0.21.0' - device_info_plus: '>=9.0.0 <12.0.0' - json_annotation: ^4.8.0 - carp_serializable: ^2.0.0 # polymorphic json serialization + device_info_plus: '12.1.0' + json_annotation: ^4.9.0 + carp_serializable: ^2.0.1 # polymorphic json serialization dev_dependencies: flutter_test: