From 122b3aec2ba73f9d279d887bde43cd04b27f7d62 Mon Sep 17 00:00:00 2001 From: bardram Date: Wed, 3 Apr 2024 13:48:03 +0200 Subject: [PATCH] PR #919 & support for HealthPlatformType --- packages/health/CHANGELOG.md | 4 +- packages/health/README.md | 28 ++++++++-- packages/health/example/lib/main.dart | 34 +++++------ packages/health/lib/health.g.dart | 16 +++--- packages/health/lib/src/functions.dart | 3 - .../health/lib/src/health_data_point.dart | 29 +++++----- packages/health/lib/src/health_plugin.dart | 56 +++++++++---------- packages/health/lib/src/heath_data_types.dart | 8 +-- 8 files changed, 98 insertions(+), 80 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index da6965245..0d464dcb2 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,10 +1,12 @@ ## 10.2.0 * Using named parameters in most methods for consistency. -* Improved support for Google Health Connect +* Added a `HealthPlatformType` to save which health platform the data originates from (Apple Health, Google Fit, or Google Health Connect). +* Android: Improved support for Google Health Connect * getHealthConnectSdkStatus, PR [#941](https://github.com/cph-cachet/flutter-plugins/pull/941) * installHealthConnect, PR [#943](https://github.com/cph-cachet/flutter-plugins/pull/943) * workout title, PR [#938](https://github.com/cph-cachet/flutter-plugins/pull/938) +* iOS: Add support for saving blood pressure as a correlation, PR [#919](https://github.com/cph-cachet/flutter-plugins/pull/919) ## 10.1.1 diff --git a/packages/health/README.md b/packages/health/README.md index 9a6ebc06c..30f87c6ca 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -234,17 +234,35 @@ HealthDataType type; HealthDataUnit unit; DateTime dateFrom; DateTime dateTo; -PlatformType platform; -String uuid, deviceId; +HealthPlatformType sourcePlatform; +String sourceDeviceId; String sourceId; String sourceName; bool isManualEntry; WorkoutSummary? workoutSummary; ``` -where a [HealthValue](https://pub.dev/documentation/health/latest/health/HealthValue-class.html) can be any type of `AudiogramHealthValue`, `ElectrocardiogramHealthValue`, `ElectrocardiogramVoltageValue`, `NumericHealthValue`, `NutritionHealthValue`, or `WorkoutHealthValue`. - -A `HealthDataPoint` object can be serialized to and from JSON using the `toJson()` and `fromJson()` methods. JSON serialization is using camel_case notation. +where a [`HealthValue`](https://pub.dev/documentation/health/latest/health/HealthValue-class.html) can be any type of `AudiogramHealthValue`, `ElectrocardiogramHealthValue`, `ElectrocardiogramVoltageValue`, `NumericHealthValue`, `NutritionHealthValue`, or `WorkoutHealthValue`. + +A `HealthDataPoint` object can be serialized to and from JSON using the `toJson()` and `fromJson()` methods. JSON serialization is using camel_case notation. Null values are not serialized. For example; + +```json +{ + "value": { + "__type": "NumericHealthValue", + "numeric_value": 141.0 + }, + "type": "STEPS", + "unit": "COUNT", + "date_from": "2024-04-03T10:06:57.736", + "date_to": "2024-04-03T10:12:51.724", + "source_platform": "appleHealth", + "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", + "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", + "source_name": "iPhone12.bardram.net", + "is_manual_entry": false +} +``` ### Fetch health data diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 1dd7d8133..84e93cf07 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -7,10 +7,6 @@ import 'package:health_example/util.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:carp_serializable/carp_serializable.dart'; -// /// A connivent function to convert a Dart object into a formatted JSON string. -// String toJsonString(Object? object) => -// const JsonEncoder.withIndent(' ').convert(object); - void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { @@ -135,23 +131,23 @@ class _HealthAppState extends State { // Clear old data points _healthDataList.clear(); - try { - // fetch health data - List healthData = await Health().getHealthDataFromTypes( - types: types, - startTime: yesterday, - endTime: now, - ); + // try { + // fetch health data + List healthData = await Health().getHealthDataFromTypes( + types: types, + startTime: yesterday, + endTime: now, + ); - debugPrint('Total number of data points: ${healthData.length}. ' - '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); + debugPrint('Total number of data points: ${healthData.length}. ' + '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); - // save all the new data points (only the first 100) - _healthDataList.addAll( - (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); - } catch (error) { - debugPrint("Exception in getHealthDataFromTypes: $error"); - } + // save all the new data points (only the first 100) + _healthDataList.addAll( + (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); + // } catch (error) { + // debugPrint("Exception in getHealthDataFromTypes: $error"); + // } // filter out duplicates _healthDataList = Health().removeDuplicates(_healthDataList); diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 1e52a3762..8b23ccc0e 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -13,8 +13,9 @@ HealthDataPoint _$HealthDataPointFromJson(Map json) => unit: $enumDecode(_$HealthDataUnitEnumMap, json['unit']), dateFrom: DateTime.parse(json['date_from'] as String), dateTo: DateTime.parse(json['date_to'] as String), - platform: $enumDecode(_$PlatformTypeEnumMap, json['platform']), - deviceId: json['device_id'] as String, + sourcePlatform: + $enumDecode(_$HealthPlatformTypeEnumMap, json['source_platform']), + sourceDeviceId: json['source_device_id'] as String, sourceId: json['source_id'] as String, sourceName: json['source_name'] as String, isManualEntry: json['is_manual_entry'] as bool? ?? false, @@ -31,8 +32,8 @@ Map _$HealthDataPointToJson(HealthDataPoint instance) { 'unit': _$HealthDataUnitEnumMap[instance.unit]!, 'date_from': instance.dateFrom.toIso8601String(), 'date_to': instance.dateTo.toIso8601String(), - 'platform': _$PlatformTypeEnumMap[instance.platform]!, - 'device_id': instance.deviceId, + 'source_platform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, + 'source_device_id': instance.sourceDeviceId, 'source_id': instance.sourceId, 'source_name': instance.sourceName, 'is_manual_entry': instance.isManualEntry, @@ -163,9 +164,10 @@ const _$HealthDataUnitEnumMap = { HealthDataUnit.NO_UNIT: 'NO_UNIT', }; -const _$PlatformTypeEnumMap = { - PlatformType.IOS: 'IOS', - PlatformType.ANDROID: 'ANDROID', +const _$HealthPlatformTypeEnumMap = { + HealthPlatformType.appleHealth: 'appleHealth', + HealthPlatformType.googleFit: 'googleFit', + HealthPlatformType.googleHealthConnect: 'googleHealthConnect', }; HealthValue _$HealthValueFromJson(Map json) => diff --git a/packages/health/lib/src/functions.dart b/packages/health/lib/src/functions.dart index 4cb6a2b1c..cc17b988c 100644 --- a/packages/health/lib/src/functions.dart +++ b/packages/health/lib/src/functions.dart @@ -16,9 +16,6 @@ class HealthException implements Exception { "Error requesting health data type '$dataType' - cause: $cause"; } -/// A list of supported platforms. -enum PlatformType { IOS, ANDROID } - /// The status of Google Health Connect. /// /// **NOTE** - The enum order is arbitrary. If you need the native value, diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index a734aa690..3a6f65f95 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -1,5 +1,8 @@ part of '../health.dart'; +/// Types of health platforms. +enum HealthPlatformType { appleHealth, googleFit, googleHealthConnect } + /// A [HealthDataPoint] object corresponds to a data point capture from /// Apple HealthKit or Google Fit or Google Health Connect with a [HealthValue] /// as value. @@ -26,11 +29,11 @@ class HealthDataPoint { /// The end of the time interval. DateTime dateTo; - /// The software platform of the data point. - PlatformType platform; + /// The health platform that this data point was fetched. + HealthPlatformType sourcePlatform; /// The id of the device from which the data point was fetched. - String deviceId; + String sourceDeviceId; /// The id of the source from which the data point was fetched. String sourceId; @@ -50,8 +53,8 @@ class HealthDataPoint { required this.unit, required this.dateFrom, required this.dateTo, - required this.platform, - required this.deviceId, + required this.sourcePlatform, + required this.sourceDeviceId, required this.sourceId, required this.sourceName, this.isManualEntry = false, @@ -131,8 +134,8 @@ class HealthDataPoint { unit: unit, dateFrom: from, dateTo: to, - platform: Health().platformType, - deviceId: Health().deviceId, + sourcePlatform: Health().platformType, + sourceDeviceId: Health().deviceId, sourceId: sourceId, sourceName: sourceName, isManualEntry: isManualEntry, @@ -147,8 +150,8 @@ class HealthDataPoint { dateFrom: $dateFrom, dateTo: $dateTo, dataType: ${type.name}, - platform: $platform, - deviceId: $deviceId, + platform: $sourcePlatform, + deviceId: $sourceDeviceId, sourceId: $sourceId, sourceName: $sourceName isManualEntry: $isManualEntry @@ -162,13 +165,13 @@ class HealthDataPoint { dateFrom == other.dateFrom && dateTo == other.dateTo && type == other.type && - platform == other.platform && - deviceId == other.deviceId && + sourcePlatform == other.sourcePlatform && + sourceDeviceId == other.sourceDeviceId && sourceId == other.sourceId && sourceName == other.sourceName && isManualEntry == other.isManualEntry; @override - int get hashCode => Object.hash(value, unit, dateFrom, dateTo, type, platform, - deviceId, sourceId, sourceName); + int get hashCode => Object.hash(value, unit, dateFrom, dateTo, type, + sourcePlatform, sourceDeviceId, sourceId, sourceName); } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 317e9db4e..cb755553c 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -37,8 +37,11 @@ class Health { factory Health() => _instance; /// The type of platform of this device. - PlatformType get platformType => - Platform.isAndroid ? PlatformType.ANDROID : PlatformType.IOS; + HealthPlatformType get platformType => Platform.isIOS + ? HealthPlatformType.appleHealth + : useHealthConnectIfAvailable + ? HealthPlatformType.googleHealthConnect + : HealthPlatformType.googleFit; /// The id of this device. /// @@ -51,7 +54,7 @@ class Health { /// If [useHealthConnectIfAvailable] is true, Google Health Connect on /// Android will be used. Has no effect on iOS. Future configure({bool useHealthConnectIfAvailable = false}) async { - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -67,10 +70,9 @@ class Health { bool get useHealthConnectIfAvailable => _useHealthConnectIfAvailable; /// Check if a given data type is available on the platform - bool isDataTypeAvailable(HealthDataType dataType) => - platformType == PlatformType.ANDROID - ? dataTypeKeysAndroid.contains(dataType) - : dataTypeKeysIOS.contains(dataType); + bool isDataTypeAvailable(HealthDataType dataType) => Platform.isAndroid + ? dataTypeKeysAndroid.contains(dataType) + : dataTypeKeysIOS.contains(dataType); /// Determines if the health data [types] have been granted with the specified /// access rights [permissions]. @@ -112,7 +114,7 @@ class Health { : permissions.map((permission) => permission.index).toList(); /// On Android, if BMI is requested, then also ask for weight and height - if (platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions); + if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); return await _channel.invokeMethod('hasPermissions', { "types": mTypes.map((type) => type.name).toList(), @@ -127,7 +129,7 @@ class Health { /// Not implemented on iOS as there is no way to programmatically remove access. Future revokePermissions() async { try { - if (platformType == PlatformType.IOS) { + if (Platform.isIOS) { throw UnsupportedError( 'Revoke permissions is not supported on iOS. Please revoke permissions manually in the settings.'); } @@ -146,7 +148,7 @@ class Health { /// Android only. Future getHealthConnectSdkStatus() async { try { - if (platformType == PlatformType.IOS) { + if (Platform.isIOS) { throw UnsupportedError('Health Connect is not available on iOS.'); } final int status = @@ -164,7 +166,7 @@ class Health { /// Android only. Future installHealthConnect() async { try { - if (platformType != PlatformType.ANDROID) { + if (!Platform.isAndroid) { throw UnsupportedError( 'installHealthConnect is only available on Android'); } @@ -193,7 +195,7 @@ class Health { : permissions.map((permission) => permission.index).toList(); // on Android, if BMI is requested, then also ask for weight and height - if (platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions); + if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); List keys = mTypes.map((dataType) => dataType.name).toList(); @@ -253,7 +255,7 @@ class Health { : permissions.map((permission) => permission.index).toList(); // on Android, if BMI is requested, then also ask for weight and height - if (platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions); + if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); List keys = mTypes.map((e) => e.name).toList(); final bool? isAuthorized = await _channel.invokeMethod( @@ -265,7 +267,7 @@ class Health { void _handleBMI(List mTypes, List mPermissions) { final index = mTypes.indexOf(HealthDataType.BODY_MASS_INDEX); - if (index != -1 && platformType == PlatformType.ANDROID) { + if (index != -1 && Platform.isAndroid) { if (!mTypes.contains(HealthDataType.WEIGHT)) { mTypes.add(HealthDataType.WEIGHT); mPermissions.add(mPermissions[index]); @@ -312,8 +314,8 @@ class Health { unit: unit, dateFrom: weights[i].dateFrom, dateTo: weights[i].dateTo, - platform: platformType, - deviceId: _deviceId!, + sourcePlatform: platformType, + sourceDeviceId: _deviceId!, sourceId: '', sourceName: '', isManualEntry: !includeManualEntry, @@ -362,7 +364,7 @@ class Health { HealthDataType.IRREGULAR_HEART_RATE_EVENT, HealthDataType.ELECTROCARDIOGRAM, }.contains(type) && - platformType == PlatformType.IOS) { + Platform.isIOS) { throw ArgumentError( "$type - iOS does not support writing this data type in HealthKit"); } @@ -487,13 +489,13 @@ class Health { } bool? success; - if (platformType == PlatformType.IOS) { + if (Platform.isIOS) { success = await writeHealthData( value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, endTime: endTime); - } else if (platformType == PlatformType.ANDROID) { + } else if (Platform.isAndroid) { Map args = { 'value': saturation, 'flowRate': flowRate, @@ -590,7 +592,7 @@ class Health { if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } - if (platformType == PlatformType.ANDROID) { + if (Platform.isAndroid) { throw UnsupportedError("writeAudiogram is not supported on Android"); } @@ -672,7 +674,7 @@ class Health { bool includeManualEntry, ) async { // Ask for device ID only once - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -683,8 +685,7 @@ class Health { } // If BodyMassIndex is requested on Android, calculate this manually - if (dataType == HealthDataType.BODY_MASS_INDEX && - platformType == PlatformType.ANDROID) { + if (dataType == HealthDataType.BODY_MASS_INDEX && Platform.isAndroid) { return _computeAndroidBMI(startTime, endTime, includeManualEntry); } return await _dataQuery(startTime, endTime, dataType, includeManualEntry); @@ -698,7 +699,7 @@ class Health { int interval, bool includeManualEntry) async { // Ask for device ID only once - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -720,7 +721,7 @@ class Health { int activitySegmentDuration, bool includeManualEntry) async { // Ask for device ID only once - _deviceId ??= platformType == PlatformType.ANDROID + _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; @@ -899,11 +900,10 @@ class Health { String? title, }) async { // Check that value is on the current Platform - if (platformType == PlatformType.IOS && !_isOnIOS(activityType)) { + if (Platform.isIOS && !_isOnIOS(activityType)) { throw HealthException(activityType, "Workout activity type $activityType is not supported on iOS"); - } else if (platformType == PlatformType.ANDROID && - !_isOnAndroid(activityType)) { + } else if (Platform.isAndroid && !_isOnAndroid(activityType)) { throw HealthException(activityType, "Workout activity type $activityType is not supported on Android"); } diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index 37ca9b5cd..dfd35efce 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -239,10 +239,10 @@ const Map dataTypeToUnit = { HealthDataType.TOTAL_CALORIES_BURNED: HealthDataUnit.KILOCALORIE, }; -const PlatformTypeJsonValue = { - PlatformType.IOS: 'ios', - PlatformType.ANDROID: 'android', -}; +// const PlatformTypeJsonValue = { +// PlatformType.IOS: 'ios', +// PlatformType.ANDROID: 'android', +// }; /// List of all [HealthDataUnit]s. enum HealthDataUnit {