From 0cefe4c81e47f554fa22501c2899eba167a7e71d Mon Sep 17 00:00:00 2001 From: Strime Date: Thu, 5 Jun 2025 08:48:36 +0400 Subject: [PATCH 1/9] [HEALTH] Distance data is always fetched for workouts regardless of need --- .../cachet/plugins/health/HealthDataReader.kt | 59 ++++++++++++------- .../plugins/health/HealthPermissionChecker.kt | 39 ++++++++++++ 2 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt 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..53b60568b 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 @@ -29,6 +29,7 @@ class HealthDataReader( private val dataConverter: HealthDataConverter ) { private val recordingFilter = HealthRecordingFilter() + private val permissionChecker = HealthPermissionChecker(context) /** * Retrieves all health data points of a specified type within a given time range. @@ -306,33 +307,47 @@ class HealthDataReader( val record = rec as ExerciseSessionRecord // Get distance data - 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 + if (permissionChecker.isLocationPermissionGranted() && permissionChecker.isHealthDistancePermissionGranted()) { + val distanceRequest = healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = DistanceRecord::class, + timeRangeFilter = TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), + ) + for (distanceRec in distanceRequest.records) { + totalDistance += distanceRec.distance.inMeters + } + } else { + Log.i( + "FLUTTER_HEALTH", + "Skipping distance data retrieval for workout due to missing permissions (location or health distance)" + ) } // Get energy burned data - 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 + if (permissionChecker.isHealthTotalCaloriesBurnedPermissionGranted()) { + val energyBurnedRequest = healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = TotalCaloriesBurnedRecord::class, + timeRangeFilter = TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), + ) + for (energyBurnedRec in energyBurnedRequest.records) { + totalEnergyBurned += energyBurnedRec.energy.inKilocalories + } + } else { + Log.i( + "FLUTTER_HEALTH", + "Skipping total calories burned data retrieval for workout due to missing permissions" + ) } // Get steps data diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt new file mode 100644 index 000000000..8b890e20c --- /dev/null +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt @@ -0,0 +1,39 @@ +package cachet.plugins.health + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat + +class HealthPermissionChecker(private val context: Context) { + + fun isLocationPermissionGranted(): Boolean { + val fineLocationGranted = ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + val coarseLocationGranted = ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + return fineLocationGranted || coarseLocationGranted + } + + fun isHealthDistancePermissionGranted(): Boolean { + val healthDistancePermission = "android.permission.health.READ_DISTANCE" + return ContextCompat.checkSelfPermission( + context, + healthDistancePermission + ) == PackageManager.PERMISSION_GRANTED + } + + fun isHealthTotalCaloriesBurnedPermissionGranted(): Boolean { + val healthCaloriesPermission = "android.permission.health.READ_TOTAL_CALORIES_BURNED" + return ContextCompat.checkSelfPermission( + context, + healthCaloriesPermission + ) == PackageManager.PERMISSION_GRANTED + } +} \ No newline at end of file From 735750d2a08c8ef641dbd1555c379f7717560f7a Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:37:23 +0200 Subject: [PATCH 2/9] Committing open changes to undo them --- .zed/settings.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 000000000..ce0e34474 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,19 @@ +{ + "lsp": { + "dart": { + "binary": { + "path": "/opt/homebrew/bin/fvm", + "arguments": ["dart", "language-server", "--protocol=lsp"] + } + } + }, + "auto_install_extensions": { + "dart": true + }, + "debugger": { + "stepping_granularity": "line", + "save_breakpoints": true, + "show_button": true, + "timeout": 3000 + } +} From 143ba45ea7d091105fc3995eaef38c9be15fadec Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:38:07 +0200 Subject: [PATCH 3/9] Ignore zed --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f92bbe5dc..bf39584a4 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,4 @@ app.*.symbols .sdkmanrc **/.cxx/ +.zed/settings.json From 2399311d8b2cd0b11571cbe9e05441b18f7b1bc2 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:30:37 +0200 Subject: [PATCH 4/9] Fix [health: 12.2.0] vitaminC on NutritionHealthValue always null #1207 --- packages/health/CHANGELOG.md | 5 + packages/health/example/lib/main.dart | 2 +- packages/health/ios/health.podspec | 2 +- packages/health/lib/health.g.dart | 132 +++++++++--------- .../health/lib/src/health_value_types.dart | 45 ++++-- packages/health/pubspec.yaml | 2 +- 6 files changed, 107 insertions(+), 81 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 41a7a6956..aca7de9ee 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,8 @@ +## 13.1.1 + +* Fix [#1207](https://github.com/cph-cachet/flutter-plugins/issues/1207) - (**Important**: Some property names might have changed compared to before for `Nutrition`) +* + ## 13.1.0 * Refactored Android native implementation (No Flutter API changes) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 9558a097f..2af993c7f 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -756,7 +756,7 @@ class HealthAppState extends State { if (p.value is NutritionHealthValue) { return ListTile( title: Text( - "${p.typeString} ${(p.value as NutritionHealthValue).meal_type}: ${(p.value as NutritionHealthValue).name}"), + "${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}'), diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec index 4a717a697..e3cb939ad 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.0' + s.version = '13.1.1' 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/lib/health.g.dart b/packages/health/lib/health.g.dart index 97b6d1a5f..3d478f782 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -482,36 +482,36 @@ NutritionHealthValue _$NutritionHealthValueFromJson( Map json) => NutritionHealthValue( name: json['name'] as String?, - meal_type: json['meal_type'] 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['vitaminA'] as num?)?.toDouble(), - b1Thiamine: (json['b1Thiamine'] as num?)?.toDouble(), - b2Riboflavin: (json['b2Riboflavin'] as num?)?.toDouble(), - b3Niacin: (json['b3Niacin'] as num?)?.toDouble(), - b5PantothenicAcid: (json['b5PantothenicAcid'] as num?)?.toDouble(), - b6Pyridoxine: (json['b6Pyridoxine'] as num?)?.toDouble(), - b7Biotin: (json['b7Biotin'] as num?)?.toDouble(), - b9Folate: (json['b9Folate'] as num?)?.toDouble(), - b12Cobalamin: (json['b12Cobalamin'] as num?)?.toDouble(), - vitaminC: (json['vitaminC'] as num?)?.toDouble(), - vitaminD: (json['vitaminD'] as num?)?.toDouble(), - vitaminE: (json['vitaminE'] as num?)?.toDouble(), - vitaminK: (json['vitaminK'] 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['fatUnsaturated'] as num?)?.toDouble(), - fatMonounsaturated: (json['fatMonounsaturated'] as num?)?.toDouble(), - fatPolyunsaturated: (json['fatPolyunsaturated'] as num?)?.toDouble(), - fatSaturated: (json['fatSaturated'] as num?)?.toDouble(), - fatTransMonoenoic: (json['fatTransMonoenoic'] 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(), @@ -530,55 +530,51 @@ NutritionHealthValue _$NutritionHealthValueFromJson( Map _$NutritionHealthValueToJson( NutritionHealthValue instance) => { - if (instance.$type case final value?) '__type': value, - if (instance.name case final value?) 'name': value, - if (instance.meal_type 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?) 'vitaminA': value, - if (instance.b1Thiamine case final value?) 'b1Thiamine': value, - if (instance.b2Riboflavin case final value?) 'b2Riboflavin': value, - if (instance.b3Niacin case final value?) 'b3Niacin': value, - if (instance.b5PantothenicAcid case final value?) - 'b5PantothenicAcid': value, - if (instance.b6Pyridoxine case final value?) 'b6Pyridoxine': value, - if (instance.b7Biotin case final value?) 'b7Biotin': value, - if (instance.b9Folate case final value?) 'b9Folate': value, - if (instance.b12Cobalamin case final value?) 'b12Cobalamin': value, - if (instance.vitaminC case final value?) 'vitaminC': value, - if (instance.vitaminD case final value?) 'vitaminD': value, - if (instance.vitaminE case final value?) 'vitaminE': value, - if (instance.vitaminK case final value?) 'vitaminK': value, - if (instance.calcium case final value?) 'calcium': value, - if (instance.chloride case final value?) 'chloride': value, - if (instance.cholesterol case final value?) 'cholesterol': value, - if (instance.choline case final value?) 'choline': value, - if (instance.chromium case final value?) 'chromium': value, - if (instance.copper case final value?) 'copper': value, - if (instance.fatUnsaturated case final value?) 'fatUnsaturated': value, - if (instance.fatMonounsaturated case final value?) - 'fatMonounsaturated': value, - if (instance.fatPolyunsaturated case final value?) - 'fatPolyunsaturated': value, - if (instance.fatSaturated case final value?) 'fatSaturated': value, - if (instance.fatTransMonoenoic case final value?) - 'fatTransMonoenoic': value, - if (instance.fiber case final value?) 'fiber': value, - if (instance.iodine case final value?) 'iodine': value, - if (instance.iron case final value?) 'iron': value, - if (instance.magnesium case final value?) 'magnesium': value, - if (instance.manganese case final value?) 'manganese': value, - if (instance.molybdenum case final value?) 'molybdenum': value, - if (instance.phosphorus case final value?) 'phosphorus': value, - if (instance.potassium case final value?) 'potassium': value, - if (instance.selenium case final value?) 'selenium': value, - if (instance.sodium case final value?) 'sodium': value, - if (instance.sugar case final value?) 'sugar': value, - if (instance.water case final value?) 'water': value, - if (instance.zinc case final value?) 'zinc': value, + '__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( diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index 6558af1c2..6a8b28819 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -398,7 +398,7 @@ class InsulinDeliveryHealthValue extends HealthValue { /// * [fatMonounsaturated] - the amount of monounsaturated fat in grams /// * [fatPolyunsaturated] - the amount of polyunsaturated fat in grams /// * [fatSaturated] - the amount of saturated fat in grams -/// * [fatTransMonoenoic] - the amount of +/// * [fatTransMonoenoic] - the amount of trans-monoenoic fat in grams /// * [fatUnsaturated] - the amount of unsaturated fat in grams /// * [fiber] - the amount of fiber in grams /// * [iodine] - the amount of iodine in grams @@ -426,7 +426,8 @@ class NutritionHealthValue extends HealthValue { String? name; /// The type of meal. - String? meal_type; + @JsonKey(name: 'meal_type') + String? mealType; /// The amount of calories in kcal. double? calories; @@ -444,42 +445,55 @@ class NutritionHealthValue extends HealthValue { double? caffeine; /// The amount of vitamin A in grams. + @JsonKey(name: 'vitamin_a') double? vitaminA; /// The amount of thiamine (B1) in grams. + @JsonKey(name: 'b1_thiamine') double? b1Thiamine; /// The amount of riboflavin (B2) in grams. + @JsonKey(name: 'b2_riboflavin') double? b2Riboflavin; /// The amount of niacin (B3) in grams. + @JsonKey(name: 'b3_niacin') double? b3Niacin; /// The amount of pantothenic acid (B5) in grams. + @JsonKey(name: 'b5_pantothenic_acid') double? b5PantothenicAcid; /// The amount of pyridoxine (B6) in grams. + @JsonKey(name: 'b6_pyridoxine') double? b6Pyridoxine; /// The amount of biotin (B7) in grams. + @JsonKey(name: 'b7_biotin') double? b7Biotin; /// The amount of folate (B9) in grams. + @JsonKey(name: 'b9_folate') double? b9Folate; /// The amount of cobalamin (B12) in grams. + @JsonKey(name: 'b12_cobalamin') double? b12Cobalamin; /// The amount of vitamin C in grams. + @JsonKey(name: 'vitamin_c') double? vitaminC; /// The amount of vitamin D in grams. + @JsonKey(name: 'vitamin_d') double? vitaminD; /// The amount of vitamin E in grams. + @JsonKey(name: 'vitamin_e') double? vitaminE; /// The amount of vitamin K in grams. + @JsonKey(name: 'vitamin_k') double? vitaminK; /// The amount of calcium in grams. @@ -501,18 +515,23 @@ class NutritionHealthValue extends HealthValue { double? copper; /// The amount of unsaturated fat in grams. + @JsonKey(name: 'fat_unsaturated') double? fatUnsaturated; /// The amount of monounsaturated fat in grams. + @JsonKey(name: 'fat_monounsaturated') double? fatMonounsaturated; /// The amount of polyunsaturated fat in grams. + @JsonKey(name: 'fat_polyunsaturated') double? fatPolyunsaturated; /// The amount of saturated fat in grams. + @JsonKey(name: 'fat_saturated') double? fatSaturated; /// The amount of trans-monoenoic fat in grams. + @JsonKey(name: 'fat_trans_monoenoic') double? fatTransMonoenoic; /// The amount of fiber in grams. @@ -556,7 +575,7 @@ class NutritionHealthValue extends HealthValue { NutritionHealthValue({ this.name, - this.meal_type, + this.mealType, this.calories, this.protein, this.fat, @@ -604,17 +623,23 @@ class NutritionHealthValue extends HealthValue { @override Function get fromJsonFunction => _$NutritionHealthValueFromJson; factory NutritionHealthValue.fromJson(Map json) => - (json) as NutritionHealthValue; + _$NutritionHealthValueFromJson(json); @override Map toJson() => _$NutritionHealthValueToJson(this); /// Create a [NutritionHealthValue] based on a health data point from native data format. factory NutritionHealthValue.fromHealthDataPoint(dynamic dataPoint) { dataPoint = dataPoint as Map; - // where key is not null - final Map dataPointMap = Map.fromEntries(dataPoint.entries - .where((entry) => entry.key != null) - .map((entry) => MapEntry(entry.key as String, entry.value))); + // Convert to Map and ensure all expected fields are present + final Map dataPointMap = {}; + + // Add all entries from the native data + dataPoint.forEach((key, value) { + if (key != null) { + dataPointMap[key as String] = value; + } + }); + return _$NutritionHealthValueFromJson(dataPointMap); } @@ -625,7 +650,7 @@ class NutritionHealthValue extends HealthValue { name: ${name.toString()}, carbs: ${carbs.toString()}, caffeine: ${caffeine.toString()}, - mealType: $meal_type, + mealType: $mealType, vitaminA: ${vitaminA.toString()}, b1Thiamine: ${b1Thiamine.toString()}, b2Riboflavin: ${b2Riboflavin.toString()}, @@ -668,7 +693,7 @@ class NutritionHealthValue extends HealthValue { bool operator ==(Object other) => other is NutritionHealthValue && other.name == name && - other.meal_type == meal_type && + other.mealType == mealType && other.calories == calories && other.protein == protein && other.fat == fat && diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index f0658bc8d..c86b2a5e8 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.0 +version: 13.1.1 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: From 0449709f1f9d7ce2c44ecfae007d4a42a4343e98 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 22 Jun 2025 01:56:53 +0200 Subject: [PATCH 5/9] Fix crash for fetching data using interval in iOS [Health 12.2.0] iOS - Crash when using health.getHealthIntervalDataFromTypes #1201 --- packages/health/CHANGELOG.md | 2 +- packages/health/example/lib/main.dart | 42 +++++++++++++ .../health/ios/Classes/HealthDataReader.swift | 59 +++++++++++++++---- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index aca7de9ee..3060fafa6 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,7 +1,7 @@ ## 13.1.1 * Fix [#1207](https://github.com/cph-cachet/flutter-plugins/issues/1207) - (**Important**: Some property names might have changed compared to before for `Nutrition`) -* +* Fix [#1201](https://github.com/cph-cachet/flutter-plugins/issues/1201) ## 13.1.0 diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 2af993c7f..65fd93dea 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -530,6 +530,41 @@ class HealthAppState extends State { }); } + Future getIntervalBasedData() async { + final startDate = DateTime.now().subtract(const Duration(days: 7)); + final endDate = DateTime.now(); + + List healthDataResponse = + await health.getHealthIntervalDataFromTypes( + startDate: startDate, + endDate: endDate, + types: [HealthDataType.BLOOD_OXYGEN, HealthDataType.STEPS], + interval: 86400, // 86400 seconds = 1 day + // recordingMethodsToFilter: recordingMethodsToFilter, + ); + debugPrint( + 'Total number of interval data points: ${healthDataResponse.length}. ' + '${healthDataResponse.length > 100 ? 'Only showing the first 100.' : ''}'); + + debugPrint("Interval data points: "); + for (var data in healthDataResponse) { + debugPrint(toJsonString(data)); + } + healthDataResponse.sort((a, b) => b.dateTo.compareTo(a.dateTo)); + + _healthDataList.clear(); + _healthDataList.addAll( + (healthDataResponse.length < 100) ? healthDataResponse : healthDataResponse.sublist(0, 100)); + + for (var data in _healthDataList) { + debugPrint(toJsonString(data)); + } + + setState(() { + _state = _healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY; + }); + } + // UI building below @override @@ -607,6 +642,13 @@ class HealthAppState extends State { WidgetStatePropertyAll(Colors.blue)), child: const Text("Revoke Access", style: TextStyle(color: Colors.white))), + TextButton( + onPressed: getIntervalBasedData, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text('Get Interval Data (7 days)', + style: TextStyle(color: Colors.white))), ]), ], ), diff --git a/packages/health/ios/Classes/HealthDataReader.swift b/packages/health/ios/Classes/HealthDataReader.swift index baf380f9e..49d6ef7ad 100644 --- a/packages/health/ios/Classes/HealthDataReader.swift +++ b/packages/health/ios/Classes/HealthDataReader.swift @@ -355,10 +355,12 @@ class HealthDataReader { predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } + let statisticsOptions = statisticsOption(for: dataTypeKey) + let query = HKStatisticsCollectionQuery( quantityType: quantityType, quantitySamplePredicate: predicate, - options: [.cumulativeSum, .separateBySource], + options: statisticsOptions, anchorDate: dateFrom, intervalComponents: interval ) @@ -392,18 +394,30 @@ class HealthDataReader { var dictionaries = [[String: Any]]() collection.enumerateStatistics(from: dateFrom, to: dateTo) { [weak self] statisticData, _ in guard let self = self else { return } - - if let quantity = statisticData.sumQuantity(), - let dataUnitKey = dataUnitKey, - let unit = self.unitDict[dataUnitKey] { - let dict = [ - "value": quantity.doubleValue(for: unit), - "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 ?? "" - ] - dictionaries.append(dict) + if let dataUnitKey = dataUnitKey, let unit = self.unitDict[dataUnitKey] { + var value: Double? = nil + switch statisticsOptions { + case .cumulativeSum: + value = statisticData.sumQuantity()?.doubleValue(for: unit) + case .discreteAverage: + value = statisticData.averageQuantity()?.doubleValue(for: unit) + case .discreteMin: + value = statisticData.minimumQuantity()?.doubleValue(for: unit) + case .discreteMax: + value = statisticData.maximumQuantity()?.doubleValue(for: unit) + default: + value = statisticData.sumQuantity()?.doubleValue(for: unit) + } + if let value = value { + let dict = [ + "value": value, + "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 ?? "" + ] + dictionaries.append(dict) + } } } DispatchQueue.main.async { @@ -412,6 +426,25 @@ class HealthDataReader { } healthStore.execute(query) } + + /// Helper to select correct HKStatisticsOptions for a given dataTypeKey + private func statisticsOption(for dataTypeKey: String) -> HKStatisticsOptions { + guard let quantityType = dataQuantityTypesDict[dataTypeKey] else { + // Default to cumulativeSum for backward compatibility + return .cumulativeSum + } + switch quantityType.aggregationStyle { + case .cumulative: + return .cumulativeSum + case .discreteArithmetic: + return .discreteAverage + case .discrete: + // Other options are .discreteAverage, .discreteMin, or .discreteMax + return .discreteAverage + @unknown default: + return .cumulativeSum + } + } /// Gets total steps in interval /// - Parameters: From b312ba57f38f5180f4003bcd1b0fcfe5605d40b1 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 22 Jun 2025 18:24:01 +0200 Subject: [PATCH 6/9] Add `APPLE_STAND_TIME`, `APPLE_STAND_HOUR`, and `APPLE_MOVE_TIME` health data types (READ ONLY) #1190 --- packages/health/CHANGELOG.md | 1 + packages/health/example/lib/main.dart | 61 ++++++------ packages/health/example/lib/util.dart | 3 + .../health/ios/Classes/HealthConstants.swift | 3 + .../ios/Classes/SwiftHealthPlugin.swift | 21 ++-- packages/health/lib/health.g.dart | 97 ++++++++++--------- packages/health/lib/src/heath_data_types.dart | 9 ++ 7 files changed, 113 insertions(+), 82 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 3060fafa6..986e322fd 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -2,6 +2,7 @@ * Fix [#1207](https://github.com/cph-cachet/flutter-plugins/issues/1207) - (**Important**: Some property names might have changed compared to before for `Nutrition`) * Fix [#1201](https://github.com/cph-cachet/flutter-plugins/issues/1201) +* iOS: Add `APPLE_STAND_TIME`, `APPLE_STAND_HOUR`, and `APPLE_MOVE_TIME` health data types (READ ONLY) [#1190](https://github.com/cph-cachet/flutter-plugins/issues/1190) ## 13.1.0 diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 65fd93dea..698f125c5 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -74,6 +74,9 @@ class HealthAppState extends State { .map((type) => // can only request READ permissions to the following list of types on iOS [ + HealthDataType.APPLE_MOVE_TIME, + HealthDataType.APPLE_STAND_HOUR, + HealthDataType.APPLE_STAND_TIME, HealthDataType.WALKING_HEART_RATE, HealthDataType.ELECTROCARDIOGRAM, HealthDataType.HIGH_HEART_RATE_EVENT, @@ -245,19 +248,6 @@ class HealthAppState extends State { type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now); - if (Platform.isIOS) { - success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, - startTime: earlier, - endTime: now); - } else { - success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, - startTime: earlier, - endTime: now); - } success &= await health.writeHealthData( value: 37, type: HealthDataType.BODY_TEMPERATURE, @@ -301,22 +291,6 @@ class HealthAppState extends State { startTime: earlier, endTime: now); - if (Platform.isIOS) { - 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); - } 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); - } - // specialized write methods success &= await health.writeBloodOxygen( saturation: 98, @@ -407,7 +381,34 @@ class HealthAppState extends State { endTime: now, ); - // Available on iOS 16.0+ only + + if (Platform.isIOS) { + success &= await health.writeHealthData( + 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); + } 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); + success &= await health.writeHealthData( + 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, diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index 8bb37fda8..be08a31ec 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -3,6 +3,9 @@ import 'package:health/health.dart'; /// Data types available on iOS via Apple Health. const List dataTypesIOS = [ HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.APPLE_STAND_TIME, + HealthDataType.APPLE_STAND_HOUR, + HealthDataType.APPLE_MOVE_TIME, HealthDataType.AUDIOGRAM, HealthDataType.BASAL_ENERGY_BURNED, HealthDataType.BLOOD_GLUCOSE, diff --git a/packages/health/ios/Classes/HealthConstants.swift b/packages/health/ios/Classes/HealthConstants.swift index 49d56788f..444096fd8 100644 --- a/packages/health/ios/Classes/HealthConstants.swift +++ b/packages/health/ios/Classes/HealthConstants.swift @@ -14,6 +14,9 @@ enum HealthConstants { static let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" static let ATRIAL_FIBRILLATION_BURDEN = "ATRIAL_FIBRILLATION_BURDEN" static let AUDIOGRAM = "AUDIOGRAM" + static let APPLE_STAND_HOUR = "APPLE_STAND_HOUR" + static let APPLE_MOVE_TIME = "APPLE_MOVE_TIME" + static let APPLE_STAND_TIME = "APPLE_STAND_TIME" static let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" static let BLOOD_GLUCOSE = "BLOOD_GLUCOSE" static let BLOOD_OXYGEN = "BLOOD_OXYGEN" diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 21cab1556..52e68af12 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -274,13 +274,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { HealthConstants.DIETARY_SELENIUM, ] - // Set up iOS 13 specific types (ordinary health data types) - if #available(iOS 13.0, *) { - initializeIOS13Types() - healthDataTypes = Array(dataTypesDict.values) - characteristicsDataTypes = Array(characteristicsTypesDict.values) - } - // Set up iOS 11 specific types (ordinary health data quantity types) if #available(iOS 11.0, *) { initializeIOS11Types() @@ -292,6 +285,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { initializeIOS12Types() } + // Set up iOS 13 specific types (ordinary health data types) + if #available(iOS 13.0, *) { + initializeIOS13Types() + healthDataTypes = Array(dataTypesDict.values) + characteristicsDataTypes = Array(characteristicsTypesDict.values) + } + if #available(iOS 13.6, *) { initializeIOS13_6Types() } @@ -312,6 +312,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { /// Initialize iOS 11 specific data types @available(iOS 11.0, *) private func initializeIOS11Types() { + dataTypesDict[HealthConstants.APPLE_STAND_HOUR] = HKSampleType.categoryType(forIdentifier: .appleStandHour)! + dataQuantityTypesDict[HealthConstants.ACTIVE_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)! dataQuantityTypesDict[HealthConstants.BASAL_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned)! dataQuantityTypesDict[HealthConstants.BLOOD_GLUCOSE] = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)! @@ -390,6 +392,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { @available(iOS 13.0, *) private func initializeIOS13Types() { dataTypesDict[HealthConstants.ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)! + dataTypesDict[HealthConstants.APPLE_STAND_TIME] = HKSampleType.quantityType(forIdentifier: .appleStandTime)! dataTypesDict[HealthConstants.AUDIOGRAM] = HKSampleType.audiogramSampleType() dataTypesDict[HealthConstants.BASAL_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)! dataTypesDict[HealthConstants.BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)! @@ -525,6 +528,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown + + if #available(iOS 14.5, *) { + dataTypesDict[HealthConstants.APPLE_MOVE_TIME] = HKSampleType.quantityType(forIdentifier: .appleMoveTime)! + } } /// Initialize iOS 16 specific data types diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 3d478f782..12f2d3ed8 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -52,6 +52,9 @@ Map _$HealthDataPointToJson(HealthDataPoint instance) => 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', @@ -530,51 +533,55 @@ NutritionHealthValue _$NutritionHealthValueFromJson( 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, + 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( diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index cd36c89d7..ba605eb33 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -4,6 +4,9 @@ part of '../health.dart'; enum HealthDataType { ACTIVE_ENERGY_BURNED, ATRIAL_FIBRILLATION_BURDEN, + APPLE_STAND_HOUR, + APPLE_MOVE_TIME, + APPLE_STAND_TIME, AUDIOGRAM, BASAL_ENERGY_BURNED, BLOOD_GLUCOSE, @@ -124,6 +127,9 @@ enum HealthDataAccess { const List dataTypeKeysIOS = [ HealthDataType.ACTIVE_ENERGY_BURNED, HealthDataType.ATRIAL_FIBRILLATION_BURDEN, + HealthDataType.APPLE_STAND_HOUR, + HealthDataType.APPLE_MOVE_TIME, + HealthDataType.APPLE_STAND_TIME, HealthDataType.AUDIOGRAM, HealthDataType.BASAL_ENERGY_BURNED, HealthDataType.BLOOD_GLUCOSE, @@ -263,6 +269,9 @@ const List dataTypeKeysAndroid = [ const Map dataTypeToUnit = { HealthDataType.ACTIVE_ENERGY_BURNED: HealthDataUnit.KILOCALORIE, HealthDataType.ATRIAL_FIBRILLATION_BURDEN: HealthDataUnit.PERCENT, + HealthDataType.APPLE_STAND_HOUR: HealthDataUnit.HOUR, + HealthDataType.APPLE_MOVE_TIME: HealthDataUnit.SECOND, + HealthDataType.APPLE_STAND_TIME: HealthDataUnit.SECOND, HealthDataType.AUDIOGRAM: HealthDataUnit.DECIBEL_HEARING_LEVEL, HealthDataType.BASAL_ENERGY_BURNED: HealthDataUnit.KILOCALORIE, HealthDataType.BLOOD_GLUCOSE: HealthDataUnit.MILLIGRAM_PER_DECILITER, From f2d0e2d914fddd4a44c92895b464287c4820f8d2 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:21:22 +0200 Subject: [PATCH 7/9] Revert "[health] Fix: Only fetch distance data for workouts when explicitly requested" --- .../cachet/plugins/health/HealthDataReader.kt | 59 +++++++------------ .../plugins/health/HealthPermissionChecker.kt | 39 ------------ 2 files changed, 22 insertions(+), 76 deletions(-) delete mode 100644 packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt 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 53b60568b..ba0db624c 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 @@ -29,7 +29,6 @@ class HealthDataReader( private val dataConverter: HealthDataConverter ) { private val recordingFilter = HealthRecordingFilter() - private val permissionChecker = HealthPermissionChecker(context) /** * Retrieves all health data points of a specified type within a given time range. @@ -307,47 +306,33 @@ class HealthDataReader( val record = rec as ExerciseSessionRecord // Get distance data - var totalDistance = 0.0 - if (permissionChecker.isLocationPermissionGranted() && permissionChecker.isHealthDistancePermissionGranted()) { - val distanceRequest = healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = DistanceRecord::class, - timeRangeFilter = TimeRangeFilter.between( - record.startTime, - record.endTime, - ), + val distanceRequest = healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = DistanceRecord::class, + timeRangeFilter = TimeRangeFilter.between( + record.startTime, + record.endTime, ), - ) - for (distanceRec in distanceRequest.records) { - totalDistance += distanceRec.distance.inMeters - } - } else { - Log.i( - "FLUTTER_HEALTH", - "Skipping distance data retrieval for workout due to missing permissions (location or health distance)" - ) + ), + ) + var totalDistance = 0.0 + for (distanceRec in distanceRequest.records) { + totalDistance += distanceRec.distance.inMeters } // Get energy burned data - var totalEnergyBurned = 0.0 - if (permissionChecker.isHealthTotalCaloriesBurnedPermissionGranted()) { - val energyBurnedRequest = healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = TotalCaloriesBurnedRecord::class, - timeRangeFilter = TimeRangeFilter.between( - record.startTime, - record.endTime, - ), + val energyBurnedRequest = healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = TotalCaloriesBurnedRecord::class, + timeRangeFilter = TimeRangeFilter.between( + record.startTime, + record.endTime, ), - ) - for (energyBurnedRec in energyBurnedRequest.records) { - totalEnergyBurned += energyBurnedRec.energy.inKilocalories - } - } else { - Log.i( - "FLUTTER_HEALTH", - "Skipping total calories burned data retrieval for workout due to missing permissions" - ) + ), + ) + var totalEnergyBurned = 0.0 + for (energyBurnedRec in energyBurnedRequest.records) { + totalEnergyBurned += energyBurnedRec.energy.inKilocalories } // Get steps data diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt deleted file mode 100644 index 8b890e20c..000000000 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPermissionChecker.kt +++ /dev/null @@ -1,39 +0,0 @@ -package cachet.plugins.health - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat - -class HealthPermissionChecker(private val context: Context) { - - fun isLocationPermissionGranted(): Boolean { - val fineLocationGranted = ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - - val coarseLocationGranted = ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_COARSE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - - return fineLocationGranted || coarseLocationGranted - } - - fun isHealthDistancePermissionGranted(): Boolean { - val healthDistancePermission = "android.permission.health.READ_DISTANCE" - return ContextCompat.checkSelfPermission( - context, - healthDistancePermission - ) == PackageManager.PERMISSION_GRANTED - } - - fun isHealthTotalCaloriesBurnedPermissionGranted(): Boolean { - val healthCaloriesPermission = "android.permission.health.READ_TOTAL_CALORIES_BURNED" - return ContextCompat.checkSelfPermission( - context, - healthCaloriesPermission - ) == PackageManager.PERMISSION_GRANTED - } -} \ No newline at end of file From f0ef7e4b1e6d2f247fc661970a13b054eaac2a2e Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:26:10 +0200 Subject: [PATCH 8/9] Update README.md --- packages/health/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/health/README.md b/packages/health/README.md index 916fdb786..17d8c71d7 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -386,6 +386,9 @@ The plugin supports the following [`HealthDataType`](https://pub.dev/documentati | UV_INDEX | COUNT | yes | | | | LEAN_BODY_MASS | KILOGRAMS | yes | yes | | | WALKING_SPEED | METER_PER_SECOND | yes | (yes) | On Android this will be recorded as `SPEED` with similar unit | +| APPLE_MOVE_TIME | SECOND | yes | | READ Only | +| APPLE_STAND_HOUR | HOUR | yes | | READ Only | +| APPLE_MOVE_TIME | SECOND | yes | | READ Only | ## Workout Types From 03f065939c876bf34d740f9b94f5fe980cc7c1af Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:09:17 +0200 Subject: [PATCH 9/9] Add Insulin samples to example --- packages/health/example/lib/main.dart | 1 + packages/health/example/lib/util.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 698f125c5..6df1a70e3 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -258,6 +258,7 @@ class HealthAppState extends State { 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, diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index be08a31ec..1b1c6cfa0 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -25,6 +25,7 @@ const List dataTypesIOS = [ HealthDataType.HEART_RATE, HealthDataType.HEART_RATE_VARIABILITY_SDNN, HealthDataType.HEIGHT, + HealthDataType.INSULIN_DELIVERY, HealthDataType.RESPIRATORY_RATE, HealthDataType.PERIPHERAL_PERFUSION_INDEX, HealthDataType.STEPS,