From a07cf84a6766d6623e847240419ee929be96fe5c Mon Sep 17 00:00:00 2001 From: cancng <me@mahmutcan.dev> Date: Fri, 15 Sep 2023 11:48:09 +0300 Subject: [PATCH] implement apple healthkit mindfulness data addition --- .../ios/Classes/SwiftHealthPlugin.swift | 40 ++++++ packages/health/lib/src/health_factory.dart | 126 ++++++++---------- 2 files changed, 98 insertions(+), 68 deletions(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 2c7586d51..edacfabcd 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -147,6 +147,11 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { getTotalStepsInInterval(call: call, result: result) } + /// Handle writeMindfulnessData + else if call.method.elementsEqual("writeMindfulnessData") { + try! writeMindfulnessData(call: call, result: result) + } + /// Handle writeData else if call.method.elementsEqual("writeData") { try! writeData(call: call, result: result) @@ -258,6 +263,41 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } + func writeMindfulnessData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let startTime = arguments["startTime"] as? NSNumber, + let endTime = arguments["endTime"] as? NSNumber + else { + throw PluginError(message: "Invalid Arguments") + } + + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else { + result( + FlutterError( + code: "UNAVAILABLE", message: "Mindful Session type is not available", details: nil)) + return + } + + let sample = HKCategorySample( + type: mindfulType, + value: 0, + start: dateFrom, + end: dateTo + ) + + HKHealthStore().save(sample) { (success, error) in + if let error = error { + print("Error Saving Mindfulness Sample: \(error.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + } + } + func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let value = (arguments["value"] as? Double), diff --git a/packages/health/lib/src/health_factory.dart b/packages/health/lib/src/health_factory.dart index 58ccc85ed..e6c011116 100644 --- a/packages/health/lib/src/health_factory.dart +++ b/packages/health/lib/src/health_factory.dart @@ -16,23 +16,20 @@ class HealthFactory { final _deviceInfo = DeviceInfoPlugin(); late bool _useHealthConnectIfAvailable; - static PlatformType _platformType = - Platform.isAndroid ? PlatformType.ANDROID : PlatformType.IOS; + static PlatformType _platformType = Platform.isAndroid ? PlatformType.ANDROID : PlatformType.IOS; /// The plugin was created to use Health Connect (if true) or Google Fit (if false). bool get useHealthConnectIfAvailable => _useHealthConnectIfAvailable; HealthFactory({bool useHealthConnectIfAvailable = false}) { _useHealthConnectIfAvailable = useHealthConnectIfAvailable; - if (_useHealthConnectIfAvailable) - _channel.invokeMethod('useHealthConnectIfAvailable'); + if (_useHealthConnectIfAvailable) _channel.invokeMethod('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) => _platformType == PlatformType.ANDROID + ? _dataTypeKeysAndroid.contains(dataType) + : _dataTypeKeysIOS.contains(dataType); /// Determines if the data types have been granted with the specified access rights. /// @@ -60,13 +57,11 @@ class HealthFactory { Future<bool?> hasPermissions(List<HealthDataType> types, {List<HealthDataAccess>? permissions}) async { if (permissions != null && permissions.length != types.length) - throw ArgumentError( - "The lists of types and permissions must be of same length."); + throw ArgumentError("The lists of types and permissions must be of same length."); final mTypes = List<HealthDataType>.from(types, growable: true); final mPermissions = permissions == null - ? List<int>.filled(types.length, HealthDataAccess.READ.index, - growable: true) + ? List<int>.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 @@ -117,8 +112,7 @@ class HealthFactory { List<HealthDataAccess>? permissions, }) async { if (permissions != null && permissions.length != types.length) { - throw ArgumentError( - 'The length of [types] must be same as that of [permissions].'); + throw ArgumentError('The length of [types] must be same as that of [permissions].'); } if (permissions != null) { @@ -139,16 +133,15 @@ class HealthFactory { final mTypes = List<HealthDataType>.from(types, growable: true); final mPermissions = permissions == null - ? List<int>.filled(types.length, HealthDataAccess.READ.index, - growable: true) + ? List<int>.filled(types.length, HealthDataAccess.READ.index, growable: true) : permissions.map((permission) => permission.index).toList(); // on Android, if BMI is requested, then also ask for weight and height if (_platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions); List<String> keys = mTypes.map((e) => e.name).toList(); - final bool? isAuthorized = await _channel.invokeMethod( - 'requestAuthorization', {'types': keys, "permissions": mPermissions}); + final bool? isAuthorized = await _channel + .invokeMethod('requestAuthorization', {'types': keys, "permissions": mPermissions}); return isAuthorized ?? false; } @@ -171,39 +164,25 @@ class HealthFactory { } /// Calculate the BMI using the last observed height and weight values. - Future<List<HealthDataPoint>> _computeAndroidBMI( - DateTime startTime, DateTime endTime) async { - List<HealthDataPoint> heights = - await _prepareQuery(startTime, endTime, HealthDataType.HEIGHT); + Future<List<HealthDataPoint>> _computeAndroidBMI(DateTime startTime, DateTime endTime) async { + List<HealthDataPoint> heights = await _prepareQuery(startTime, endTime, HealthDataType.HEIGHT); if (heights.isEmpty) { return []; } - List<HealthDataPoint> weights = - await _prepareQuery(startTime, endTime, HealthDataType.WEIGHT); + List<HealthDataPoint> weights = await _prepareQuery(startTime, endTime, HealthDataType.WEIGHT); - 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]!; final bmiHealthPoints = <HealthDataPoint>[]; for (var i = 0; i < weights.length; i++) { - final bmiValue = - (weights[i].value as NumericHealthValue).numericValue.toDouble() / - (h * h); - final x = HealthDataPoint( - NumericHealthValue(bmiValue), - dataType, - unit, - weights[i].dateFrom, - weights[i].dateTo, - _platformType, - _deviceId!, - '', - ''); + final bmiValue = (weights[i].value as NumericHealthValue).numericValue.toDouble() / (h * h); + final x = HealthDataPoint(NumericHealthValue(bmiValue), dataType, unit, weights[i].dateFrom, + weights[i].dateTo, _platformType, _deviceId!, '', ''); bmiHealthPoints.add(x); } @@ -233,8 +212,7 @@ class HealthFactory { HealthDataUnit? unit, }) async { if (type == HealthDataType.WORKOUT) - throw ArgumentError( - "Adding workouts should be done using the writeWorkoutData method."); + throw ArgumentError("Adding workouts should be done using the writeWorkoutData method."); if (startTime.isAfter(endTime)) throw ArgumentError("startTime must be equal or earlier than endTime"); if ({ @@ -244,8 +222,7 @@ class HealthFactory { HealthDataType.ELECTROCARDIOGRAM, }.contains(type) && _platformType == PlatformType.IOS) - throw ArgumentError( - "$type - iOS doesnt support writing this data type in HealthKit"); + throw ArgumentError("$type - iOS doesnt support writing this data type in HealthKit"); // Assign default unit if not specified unit ??= _dataTypeToUnit[type]!; @@ -274,6 +251,31 @@ class HealthFactory { return success ?? false; } + /// Saves mindfulness health data into Apple Health + /// + /// Returns true if successful, false otherwise. + /// + /// Parameters: + /// * [startTime] - the start time when this [value] is measured. + /// + It must be equal to or earlier than [endTime]. + /// * [endTime] - the end time when this [value] is measured. + /// + It must be equal to or later than [startTime]. + /// + Simply set [endTime] equal to [startTime] if the [value] is measured only at a specific point in time. + Future<bool> writeMindfulnessData( + DateTime startTime, + DateTime endTime, + ) async { + if (startTime.isAfter(endTime)) + throw ArgumentError("startTime must be equal or earlier than endTime"); + + Map<String, dynamic> args = { + 'startTime': startTime.millisecondsSinceEpoch, + 'endTime': endTime.millisecondsSinceEpoch + }; + bool? success = await _channel.invokeMethod('writeMindfulnessData', args); + return success ?? false; + } + /// Deletes all records of the given type for a given period of time /// /// Returns true if successful, false otherwise. @@ -284,8 +286,7 @@ class HealthFactory { /// + It must be equal to or earlier than [endTime]. /// * [endTime] - the end time when this [value] is measured. /// + It must be equal to or later than [startTime]. - Future<bool> delete( - HealthDataType type, DateTime startTime, DateTime endTime) async { + Future<bool> delete(HealthDataType type, DateTime startTime, DateTime endTime) async { if (startTime.isAfter(endTime)) throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -337,16 +338,14 @@ class HealthFactory { /// * [endTime] - the end time when this [value] is measured. /// + It must be equal to or later than [startTime]. /// + Simply set [endTime] equal to [startTime] if the blood oxygen saturation is measured only at a specific point in time. - Future<bool> writeBloodOxygen( - double saturation, DateTime startTime, DateTime endTime, + Future<bool> writeBloodOxygen(double saturation, DateTime startTime, DateTime endTime, {double flowRate = 0.0}) async { if (startTime.isAfter(endTime)) throw ArgumentError("startTime must be equal or earlier than endTime"); bool? success; if (_platformType == PlatformType.IOS) { - success = await writeHealthData( - saturation, HealthDataType.BLOOD_OXYGEN, startTime, endTime); + success = await writeHealthData(saturation, HealthDataType.BLOOD_OXYGEN, startTime, endTime); } else if (_platformType == PlatformType.ANDROID) { Map<String, dynamic> args = { 'value': saturation, @@ -374,16 +373,10 @@ class HealthFactory { /// + It must be equal to or later than [startTime]. /// + Simply set [endTime] equal to [startTime] if the audiogram is measured only at a specific point in time. /// * [metadata] - optional map of keys, both HKMetadataKeyExternalUUID and HKMetadataKeyDeviceName are required - Future<bool> writeAudiogram( - List<double> frequencies, - List<double> leftEarSensitivities, - List<double> rightEarSensitivities, - DateTime startTime, - DateTime endTime, + Future<bool> writeAudiogram(List<double> frequencies, List<double> leftEarSensitivities, + List<double> rightEarSensitivities, DateTime startTime, DateTime endTime, {Map<String, dynamic>? metadata}) async { - if (frequencies.isEmpty || - leftEarSensitivities.isEmpty || - rightEarSensitivities.isEmpty) + if (frequencies.isEmpty || leftEarSensitivities.isEmpty || rightEarSensitivities.isEmpty) throw ArgumentError( "frequencies, leftEarSensitivities and rightEarSensitivities can't be empty"); if (frequencies.length != leftEarSensitivities.length || @@ -435,13 +428,11 @@ class HealthFactory { // If not implemented on platform, throw an exception if (!isDataTypeAvailable(dataType)) { - throw HealthException( - dataType, 'Not available on platform $_platformType'); + throw HealthException(dataType, 'Not available on platform $_platformType'); } // If BodyMassIndex is requested on Android, calculate this manually - if (dataType == HealthDataType.BODY_MASS_INDEX && - _platformType == PlatformType.ANDROID) { + if (dataType == HealthDataType.BODY_MASS_INDEX && _platformType == PlatformType.ANDROID) { return _computeAndroidBMI(startTime, endTime); } return await _dataQuery(startTime, endTime, dataType); @@ -591,12 +582,11 @@ class HealthFactory { }) async { // Check that value is on the current Platform if (_platformType == PlatformType.IOS && !_isOnIOS(activityType)) { - throw HealthException(activityType, - "Workout activity type $activityType is not supported on iOS"); - } else if (_platformType == PlatformType.ANDROID && - !_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 iOS"); + } else if (_platformType == PlatformType.ANDROID && !_isOnAndroid(activityType)) { + throw HealthException( + activityType, "Workout activity type $activityType is not supported on Android"); } final args = <String, dynamic>{ 'activityType': activityType.name,