diff --git a/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt b/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt index 976a9bf1..c7605e51 100644 --- a/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt +++ b/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt @@ -5,6 +5,7 @@ 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.response.InsertRecordsResponse import androidx.health.connect.client.units.* import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel.Result @@ -96,7 +97,7 @@ class HealthDataWriter( * * @param call Method call containing 'dataTypeKey', 'startTime', 'endTime', 'value', * 'recordingMethod' - * @param result Flutter result callback returning boolean success status + * @param result Flutter result callback returning string inserted record UUID or empty string on failure */ fun writeData(call: MethodCall, result: Result) { val type = call.argument("dataTypeKey")!! @@ -123,17 +124,31 @@ class HealthDataWriter( val record = createRecord(type, startTime, endTime, value, metadata) if (record == null) { - result.success(false) + result.success("") return } scope.launch { try { - healthConnectClient.insertRecords(listOf(record)) - result.success(true) + // Insert records into Health Connect + val insertResponse: InsertRecordsResponse = healthConnectClient.insertRecords(listOf(record)) + + // Extract UUID from the first inserted record + val insertedUUID = insertResponse.recordIdsList.firstOrNull() ?: "" + + if (insertedUUID.isEmpty()) { + Log.e("FLUTTER_HEALTH::ERROR", "UUID is empty! No records were inserted.") + } else { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Workout $insertedUUID was successfully added!" + ) + } + + result.success(insertedUUID) } catch (e: Exception) { Log.e("FLUTTER_HEALTH::ERROR", "Error writing $type: ${e.message}") - result.success(false) + result.success("") } } } @@ -148,7 +163,7 @@ class HealthDataWriter( * 'totalEnergyBurned', 'totalDistance', 'recordingMethod', 'title' * @param result * ``` - * Flutter result callback returning boolean success status + * Flutter result callback returning string inserted record UUID or empty string on failure */ fun writeWorkoutData(call: MethodCall, result: Result) { val type = call.argument("activityType")!! @@ -161,8 +176,11 @@ class HealthDataWriter( val workoutMetadata = buildMetadata(recordingMethod = recordingMethod, deviceType = deviceType) if (!HealthConstants.workoutTypeMap.containsKey(type)) { - result.success(false) - Log.w("FLUTTER_HEALTH::ERROR", "[Health Connect] Workout type not supported") + result.success("") + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] Workout type not supported" + ) return } @@ -213,10 +231,23 @@ class HealthDataWriter( ), ) } + + // Insert records into Health Connect + val insertResponse: InsertRecordsResponse = healthConnectClient.insertRecords(list) - healthConnectClient.insertRecords(list) - result.success(true) - Log.i("FLUTTER_HEALTH::SUCCESS", "[Health Connect] Workout was successfully added!") + // Extract UUID from the first inserted record + val insertedUUID = insertResponse.recordIdsList.firstOrNull() ?: "" + + if (insertedUUID.isEmpty()) { + Log.e("FLUTTER_HEALTH::ERROR", "UUID is empty! No records were inserted.") + } + + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Workout $insertedUUID was successfully added!" + ) + + result.success(insertedUUID) } catch (e: Exception) { Log.w( "FLUTTER_HEALTH::ERROR", @@ -224,7 +255,7 @@ class HealthDataWriter( ) Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) + result.success("") } } } diff --git a/example/lib/main.dart b/example/lib/main.dart index ffaeec3b..ca3f9474 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -224,6 +224,45 @@ class HealthAppState extends State { } } + // Add single steps data (health data) + Future addSingleHealthData() async { + final now = DateTime.now(); + final earlier = now.subtract(const Duration(minutes: 20)); + + final healthDataPoint = await health.writeHealthData( + value: 2130, + type: HealthDataType.STEPS, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ); + + bool success = healthDataPoint != null; + setState(() { + _state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED; + }); + } + + // Add single running data (workout data) + Future addSingleWorkoutData() async { + final now = DateTime.now(); + final earlier = now.subtract(const Duration(minutes: 20)); + + final healthDataPoint = await health.writeWorkoutData( + activityType: HealthWorkoutActivityType.RUNNING, + title: "New RUNNING activity", + start: earlier, + end: now, + totalDistance: 2430, + totalEnergyBurned: 400, + ); + + bool success = healthDataPoint != null; + setState(() { + _state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED; + }); + } + /// Add some random health data. /// Note that you should ensure that you have permissions to add the /// following data types. @@ -238,83 +277,125 @@ class HealthAppState extends State { bool success = true; // 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); - success &= await health.writeHealthData( - 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); - success &= await health.writeHealthData( - 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, - clientRecordId: "uniqueID1234", - clientRecordVersion: 1); - success &= await health.writeHealthData( - value: 70, - type: HealthDataType.HEART_RATE, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 37, - type: HealthDataType.BODY_TEMPERATURE, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 105, - type: HealthDataType.BLOOD_GLUCOSE, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 1.8, - type: HealthDataType.WATER, - startTime: earlier, - endTime: now); + success &= + await health.writeHealthData( + 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, + ) != + null; + success &= + await health.writeHealthData( + 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, + ) != + null; + success &= + await health.writeHealthData( + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + clientRecordId: "uniqueID1234", + clientRecordVersion: 1, + ) != + null; + success &= + await health.writeHealthData( + 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, + ) != + null; + success &= + await health.writeHealthData( + value: 105, + type: HealthDataType.BLOOD_GLUCOSE, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + 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); - success &= await health.writeHealthData( - 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); - success &= await health.writeHealthData( - 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); + success &= + await health.writeHealthData( + 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, + ) != + null; + success &= + await health.writeHealthData( + 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, + ) != + null; + success &= + await health.writeHealthData( + value: 22, + type: HealthDataType.LEAN_BODY_MASS, + startTime: earlier, + endTime: now, + ) != + null; // specialized write methods success &= await health.writeBloodOxygen( @@ -322,14 +403,16 @@ class HealthAppState extends State { startTime: earlier, endTime: now, ); - success &= await health.writeWorkoutData( - activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, - title: "Random workout name that shows up in Health Connect", - start: now.subtract(const Duration(minutes: 15)), - end: now, - totalDistance: 2430, - totalEnergyBurned: 400, - ); + success &= + await health.writeWorkoutData( + activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, + title: "Random workout name that shows up in Health Connect", + start: now.subtract(const Duration(minutes: 15)), + end: now, + totalDistance: 2430, + totalEnergyBurned: 400, + ) != + null; success &= await health.writeBloodPressure( systolic: 90, diastolic: 80, @@ -411,62 +494,89 @@ class HealthAppState extends State { if (Platform.isIOS) { success &= await health.writeInsulinDelivery( - 5, InsulinDeliveryReason.BOLUS, earlier, now); - 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); + 5, + InsulinDeliveryReason.BOLUS, + earlier, + now, + ); + success &= + await health.writeHealthData( + 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, + ) != + 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); - success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, - startTime: earlier, - endTime: now); + 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; + success &= + await health.writeHealthData( + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + startTime: earlier, + endTime: now, + ) != + null; // Mindfulness value should be counted based on start and end time - success &= await health.writeHealthData( - value: 10, - type: HealthDataType.MINDFULNESS, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.automatic, - ); + success &= + await health.writeHealthData( + value: 10, + type: HealthDataType.MINDFULNESS, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.automatic, + ) != + 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); - - success &= await health.writeHealthData( - 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); + success &= + await health.writeHealthData( + 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, + ) != + null; + success &= + await health.writeHealthData( + value: 4.3, + type: HealthDataType.UV_INDEX, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; } setState(() { @@ -678,6 +788,26 @@ class HealthAppState extends State { WidgetStatePropertyAll(Colors.blue)), child: const Text("Add Data", style: TextStyle(color: Colors.white))), + TextButton( + onPressed: addSingleHealthData, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + "Add Steps Data", + style: TextStyle(color: Colors.white), + ), + ), + TextButton( + onPressed: addSingleWorkoutData, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + "Add Running Data", + style: TextStyle(color: Colors.white), + ), + ), TextButton( onPressed: deleteData, style: const ButtonStyle( diff --git a/ios/Classes/HealthDataWriter.swift b/ios/Classes/HealthDataWriter.swift index df19a505..18805f7f 100644 --- a/ios/Classes/HealthDataWriter.swift +++ b/ios/Classes/HealthDataWriter.swift @@ -73,7 +73,17 @@ class HealthDataWriter { print("Error Saving \(type) Sample: \(err.localizedDescription)") } DispatchQueue.main.async { - result(success) + if success { + // Return the UUID of the saved object + if let savedSample = sample as? HKObject { + print("Saved: \(savedSample.uuid.uuidString)") + result(savedSample.uuid.uuidString) // Return UUID as String + } else { + result("") + } + } + + result("") } }) } @@ -433,7 +443,18 @@ class HealthDataWriter { print("Error Saving Workout. Sample: \(err.localizedDescription)") } DispatchQueue.main.async { - result(success) + + if success { + // Return the UUID of the saved object + if let savedSample = workout as? HKWorkout { + print("Saved: \(savedSample.uuid.uuidString)") + result(savedSample.uuid.uuidString) // Return UUID as String + } else { + result("") + } + } + + result("") } }) } diff --git a/lib/src/health_plugin.dart b/lib/src/health_plugin.dart index 62fac251..d4f196ae 100644 --- a/lib/src/health_plugin.dart +++ b/lib/src/health_plugin.dart @@ -489,7 +489,7 @@ class Health { /// Write health data. /// - /// Returns true if successful, false otherwise. + /// Returns created HealthDataPoint UUID if successful, null otherwise. /// /// Parameters: /// * [value] - the health data's value in double @@ -506,7 +506,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, @@ -579,8 +579,10 @@ class Health { 'clientRecordId': clientRecordId, 'clientRecordVersion': clientRecordVersion, }; - bool? success = await _channel.invokeMethod('writeData', args); - return success ?? false; + + String uuid = '${await _channel.invokeMethod('writeData', args)}'; + + return uuid; } /// Deletes all records of the given [type] for a given period of time. @@ -744,13 +746,15 @@ class Health { bool? success; if (Platform.isIOS) { - success = await writeHealthData( + final healthPointUUID = await writeHealthData( value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, endTime: endTime, recordingMethod: recordingMethod, ); + + success = healthPointUUID != null; } else if (Platform.isAndroid) { Map args = { 'value': saturation, @@ -759,7 +763,9 @@ class Health { 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, 'recordingMethod': recordingMethod.toInt(), }; - success = await _channel.invokeMethod('writeBloodOxygen', args); + // Check if UUID is not empty + success = + '${await _channel.invokeMethod('writeBloodOxygen', args)}'.isNotEmpty; } return success ?? false; } @@ -1483,7 +1489,7 @@ class Health { /// Write workout data to Apple Health or Google Health Connect. /// - /// Returns true if the workout data was successfully added. + /// Returns created HealthDataPoint UUID if the workout data was successfully added, null otherwise. /// /// Parameters: /// - [activityType] The type of activity performed. @@ -1498,7 +1504,7 @@ class Health { /// - [title] The title of the workout. /// *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING". /// - [recordingMethod] The recording method of the data point, automatic by default (on iOS this can only be automatic or manual). - Future writeWorkoutData({ + Future writeWorkoutData({ required HealthWorkoutActivityType activityType, required DateTime start, required DateTime end, @@ -1541,7 +1547,10 @@ class Health { 'title': title, 'recordingMethod': recordingMethod.toInt(), }; - return await _channel.invokeMethod('writeWorkoutData', args) == true; + + String uuid = '${await _channel.invokeMethod('writeWorkoutData', args)}'; + + return uuid; } /// Check if the given [HealthWorkoutActivityType] is supported on the iOS platform