diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt index 7cbd7f951..6d288170b 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt @@ -21,20 +21,34 @@ class HealthDataConverter { * @return List> List of converted records (some records may split into multiple entries) * @throws IllegalArgumentException If the record type is not supported */ - fun convertRecord(record: Any, dataType: String): List> { + fun convertRecord(record: Any, dataType: String, dataUnit: String? = null): List> { val metadata = (record as Record).metadata return when (record) { // Single-value instant records - is WeightRecord -> listOf(createInstantRecord(metadata, record.time, record.weight.inKilograms)) - is HeightRecord -> listOf(createInstantRecord(metadata, record.time, record.height.inMeters)) + is WeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "POUND" -> record.weight.inPounds + else -> record.weight.inKilograms + })) + is HeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "CENTIMETER" -> (record.height.inMeters * 100) + "INCH" -> record.height.inInches + else -> record.height.inMeters + })) is BodyFatRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value)) is LeanBodyMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms)) is HeartRateVariabilityRmssdRecord -> listOf(createInstantRecord(metadata, record.time, record.heartRateVariabilityMillis)) - is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, record.temperature.inCelsius)) + is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "DEGREE_FAHRENHEIT" -> record.temperature.inFahrenheit + "KELVIN" -> record.temperature.inCelsius + 273.15 + else -> record.temperature.inCelsius + })) is BodyWaterMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms)) is OxygenSaturationRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value)) - is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, record.level.inMilligramsPerDeciliter)) + is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) { + "MILLIMOLES_PER_LITER" -> record.level.inMillimolesPerLiter + else -> record.level.inMilligramsPerDeciliter + })) is BasalMetabolicRateRecord -> listOf(createInstantRecord(metadata, record.time, record.basalMetabolicRate.inKilocaloriesPerDay)) is RestingHeartRateRecord -> listOf(createInstantRecord(metadata, record.time, record.beatsPerMinute)) is RespiratoryRateRecord -> listOf(createInstantRecord(metadata, record.time, record.rate)) @@ -236,7 +250,7 @@ class HealthDataConverter { ) ) } - + companion object { private const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" private const val MEAL_UNKNOWN = "UNKNOWN" diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt index e04b2cb8a..f0e6d5b98 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt @@ -247,6 +247,45 @@ class HealthDataOperations( } } + /** + * Deletes a specific health record by its client record ID and data type. Allows precise + * deletion of individual health records using client-side IDs. + * + * @param call Method call containing 'dataTypeKey', 'recordId', and 'clientRecordId' + * @param result Flutter result callback returning boolean success status + */ + fun deleteByClientRecordId(call: MethodCall, result: Result) { + val arguments = call.arguments as? HashMap<*, *> + val dataTypeKey = (arguments?.get("dataTypeKey") as? String)!! + val recordId = listOfNotNull(arguments["recordId"] as? String) + val clientRecordId = listOfNotNull(arguments["clientRecordId"] as? String) + if (!HealthConstants.mapToType.containsKey(dataTypeKey)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataTypeKey not found in HC") + result.success(false) + return + } + val classType = HealthConstants.mapToType[dataTypeKey]!! + + scope.launch { + try { + healthConnectClient.deleteRecords( + classType, + recordId, + clientRecordId + ) + result.success(true) + } catch (e: Exception) { + Log.e( + "FLUTTER_HEALTH::ERROR", + "Error deleting record with ClientRecordId: $clientRecordId" + ) + Log.e("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.e("FLUTTER_HEALTH::ERROR", e.stackTraceToString()) + result.success(false) + } + } + } + /** * Internal helper method to prepare Health Connect permission strings. Converts data type names * and access levels into proper permission format. 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 8a82d3cf2..37eab3837 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataReader.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Handler import android.util.Log import androidx.health.connect.client.HealthConnectClient +import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.records.* import androidx.health.connect.client.request.AggregateGroupByDurationRequest import androidx.health.connect.client.request.AggregateRequest @@ -40,6 +41,7 @@ class HealthDataReader( */ fun getData(call: MethodCall, result: Result) { val dataType = call.argument("dataTypeKey")!! + val dataUnit: String? = call.argument("dataUnitKey") val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) val healthConnectData = mutableListOf>() @@ -47,12 +49,19 @@ class HealthDataReader( Log.i( "FLUTTER_HEALTH", - "Getting data for $dataType between $startTime and $endTime, filtering by $recordingMethodsToFilter" + "Getting data for $dataType with unit $dataUnit between $startTime and $endTime, filtering by $recordingMethodsToFilter" ) scope.launch { try { - HealthConstants.mapToType[dataType]?.let { classType -> + val grantedPermissions = healthConnectClient.permissionController.getGrantedPermissions() + + val authorizedTypeMap = HealthConstants.mapToType.filter { (typeKey, classType) -> + val requiredPermission = HealthPermission.getReadPermission(classType) + grantedPermissions.contains(requiredPermission) + } + + authorizedTypeMap[dataType]?.let { classType -> val records = mutableListOf() // Set up the initial request to read health records @@ -92,7 +101,7 @@ class HealthDataReader( ) for (rec in filteredRecords) { healthConnectData.addAll( - dataConverter.convertRecord(rec, dataType) + dataConverter.convertRecord(rec, dataType, dataUnit) ) } } @@ -105,7 +114,7 @@ class HealthDataReader( "Unable to return $dataType due to the following exception:" ) Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) + result.success(emptyList>()) // Return empty list instead of null } } } diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt index a3e47beee..b7070e045 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt @@ -7,25 +7,26 @@ import androidx.health.connect.client.records.metadata.Metadata import androidx.health.connect.client.units.* import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel.Result +import java.time.Instant import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import java.time.Instant /** - * Handles writing health data to Health Connect. - * Manages data insertion for various health metrics, specialized records like workouts and nutrition, - * and proper data type conversion from Flutter to Health Connect format. + * Handles writing health data to Health Connect. Manages data insertion for various health metrics, + * specialized records like workouts and nutrition, and proper data type conversion from Flutter to + * Health Connect format. */ class HealthDataWriter( - private val healthConnectClient: HealthConnectClient, - private val scope: CoroutineScope + private val healthConnectClient: HealthConnectClient, + private val scope: CoroutineScope ) { - + /** - * Writes a single health data record to Health Connect. - * Supports most basic health metrics with automatic type conversion and validation. - * - * @param call Method call containing 'dataTypeKey', 'startTime', 'endTime', 'value', 'recordingMethod' + * Writes a single health data record to Health Connect. Supports most basic health metrics with + * automatic type conversion and validation. + * + * @param call Method call containing 'dataTypeKey', 'startTime', 'endTime', 'value', + * 'recordingMethod' * @param result Flutter result callback returning boolean success status */ fun writeData(call: MethodCall, result: Result) { @@ -33,15 +34,28 @@ class HealthDataWriter( val startTime = call.argument("startTime")!! val endTime = call.argument("endTime")!! val value = call.argument("value")!! + val clientRecordId: String? = call.argument("clientRecordId") + val clientRecordVersion: Double? = call.argument("clientRecordVersion") val recordingMethod = call.argument("recordingMethod")!! Log.i( - "FLUTTER_HEALTH", - "Writing data for $type between $startTime and $endTime, value: $value, recording method: $recordingMethod" + "FLUTTER_HEALTH", + "Writing data for $type between $startTime and $endTime, value: $value, recording method: $recordingMethod" ) - val record = createRecord(type, startTime, endTime, value, recordingMethod) - + val metadata: Metadata = + if ((clientRecordId != null) && (clientRecordVersion != null)) { + Metadata( + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion.toLong(), + recordingMethod = recordingMethod + ) + } else { + Metadata(recordingMethod = recordingMethod) + } + + val record = createRecord(type, startTime, endTime, value, metadata) + if (record == null) { result.success(false) return @@ -59,13 +73,16 @@ class HealthDataWriter( } /** - * Writes a comprehensive workout session with optional distance and calorie data. - * Creates an ExerciseSessionRecord with associated DistanceRecord and TotalCaloriesBurnedRecord - * if supplementary data is provided. - * - * @param call Method call containing workout details: 'activityType', 'startTime', 'endTime', + * Writes a comprehensive workout session with optional distance and calorie data. Creates an + * ExerciseSessionRecord with associated DistanceRecord and TotalCaloriesBurnedRecord if + * supplementary data is provided. + * + * @param call Method call containing workout details: 'activityType', 'startTime', 'endTime', + * ``` * 'totalEnergyBurned', 'totalDistance', 'recordingMethod', 'title' - * @param result Flutter result callback returning boolean success status + * @param result + * ``` + * Flutter result callback returning boolean success status */ fun writeWorkoutData(call: MethodCall, result: Result) { val type = call.argument("activityType")!! @@ -74,80 +91,77 @@ class HealthDataWriter( val totalEnergyBurned = call.argument("totalEnergyBurned") val totalDistance = call.argument("totalDistance") val recordingMethod = call.argument("recordingMethod")!! - + if (!HealthConstants.workoutTypeMap.containsKey(type)) { result.success(false) - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] Workout type not supported" - ) + Log.w("FLUTTER_HEALTH::ERROR", "[Health Connect] Workout type not supported") return } - + val workoutType = HealthConstants.workoutTypeMap[type]!! val title = call.argument("title") ?: type scope.launch { try { val list = mutableListOf() - + // Add exercise session record list.add( - ExerciseSessionRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - exerciseType = workoutType, - title = title, - metadata = Metadata( - recordingMethod = recordingMethod, + ExerciseSessionRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + exerciseType = workoutType, + title = title, + metadata = + Metadata( + recordingMethod = recordingMethod, + ), ), - ), ) - + // Add distance record if provided if (totalDistance != null) { list.add( - DistanceRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - distance = Length.meters(totalDistance.toDouble()), - metadata = Metadata( - recordingMethod = recordingMethod, + DistanceRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + distance = Length.meters(totalDistance.toDouble()), + metadata = + Metadata( + recordingMethod = recordingMethod, + ), ), - ), ) } - + // Add energy burned record if provided if (totalEnergyBurned != null) { list.add( - TotalCaloriesBurnedRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - energy = Energy.kilocalories(totalEnergyBurned.toDouble()), - metadata = Metadata( - recordingMethod = recordingMethod, + TotalCaloriesBurnedRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + energy = Energy.kilocalories(totalEnergyBurned.toDouble()), + metadata = + Metadata( + recordingMethod = recordingMethod, + ), ), - ), ) } - + healthConnectClient.insertRecords(list) result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Workout was successfully added!" - ) + Log.i("FLUTTER_HEALTH::SUCCESS", "[Health Connect] Workout was successfully added!") } catch (e: Exception) { Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the workout", + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the workout", ) Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) @@ -157,10 +171,9 @@ class HealthDataWriter( } /** - * Writes blood pressure measurement with both systolic and diastolic values. - * Creates a single BloodPressureRecord containing both pressure readings - * taken at the same time point. - * + * Writes blood pressure measurement with both systolic and diastolic values. Creates a single + * BloodPressureRecord containing both pressure readings taken at the same time point. + * * @param call Method call containing 'systolic', 'diastolic', 'startTime', 'recordingMethod' * @param result Flutter result callback returning boolean success status */ @@ -169,31 +182,41 @@ class HealthDataWriter( val diastolic = call.argument("diastolic")!! val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) val recordingMethod = call.argument("recordingMethod")!! + val clientRecordId: String? = call.argument("clientRecordId") + val clientRecordVersion: Double? = call.argument("clientRecordVersion") scope.launch { try { + val metadata: Metadata = + if ((clientRecordId != null) && (clientRecordVersion != null)) { + Metadata( + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion.toLong(), + recordingMethod = recordingMethod + ) + } else { + Metadata(recordingMethod = recordingMethod) + } healthConnectClient.insertRecords( - listOf( - BloodPressureRecord( - time = startTime, - systolic = Pressure.millimetersOfMercury(systolic), - diastolic = Pressure.millimetersOfMercury(diastolic), - zoneOffset = null, - metadata = Metadata( - recordingMethod = recordingMethod, - ), + listOf( + BloodPressureRecord( + time = startTime, + systolic = Pressure.millimetersOfMercury(systolic), + diastolic = Pressure.millimetersOfMercury(diastolic), + zoneOffset = null, + metadata = metadata, + ), ), - ), ) result.success(true) Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Blood pressure was successfully added!", + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Blood pressure was successfully added!", ) } catch (e: Exception) { Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the blood pressure", + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the blood pressure", ) Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) @@ -203,9 +226,9 @@ class HealthDataWriter( } /** - * Writes blood oxygen saturation measurement. - * Delegates to standard writeData method for OxygenSaturationRecord handling. - * + * Writes blood oxygen saturation measurement. Delegates to standard writeData method for + * OxygenSaturationRecord handling. + * * @param call Method call with blood oxygen data * @param result Flutter result callback returning success status */ @@ -214,9 +237,9 @@ class HealthDataWriter( } /** - * Writes menstrual flow data. - * Delegates to standard writeData method for MenstruationFlowRecord handling. - * + * Writes menstrual flow data. Delegates to standard writeData method for MenstruationFlowRecord + * handling. + * * @param call Method call with menstruation flow data * @param result Flutter result callback returning success status */ @@ -225,13 +248,16 @@ class HealthDataWriter( } /** - * Writes comprehensive nutrition/meal data with detailed nutrient breakdown. - * Creates NutritionRecord with extensive nutrient information including vitamins, - * minerals, macronutrients, and meal classification. - * - * @param call Method call containing nutrition data: calories, macronutrients, vitamins, + * Writes comprehensive nutrition/meal data with detailed nutrient breakdown. Creates + * NutritionRecord with extensive nutrient information including vitamins, minerals, + * macronutrients, and meal classification. + * + * @param call Method call containing nutrition data: calories, macronutrients, vitamins, + * ``` * minerals, meal details, timing information - * @param result Flutter result callback returning boolean success status + * @param result + * ``` + * Flutter result callback returning boolean success status */ fun writeMeal(call: MethodCall, result: Result) { val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) @@ -279,71 +305,81 @@ class HealthDataWriter( val name = call.argument("name") val mealType = call.argument("meal_type")!! + val clientRecordId: String? = call.argument("clientRecordId") + val clientRecordVersion: Double? = call.argument("clientRecordVersion") scope.launch { try { + val metaData: Metadata = + if ((clientRecordId != null) && (clientRecordVersion != null)) { + Metadata( + clientRecordId = clientRecordId, + clientRecordVersion = clientRecordVersion.toLong() + ) + } else { + Metadata() + } val list = mutableListOf() + list.add( - NutritionRecord( - name = name, - energy = calories?.kilocalories, - totalCarbohydrate = carbs?.grams, - protein = protein?.grams, - totalFat = fat?.grams, - caffeine = caffeine?.grams, - vitaminA = vitaminA?.grams, - thiamin = b1Thiamine?.grams, - riboflavin = b2Riboflavin?.grams, - niacin = b3Niacin?.grams, - pantothenicAcid = b5PantothenicAcid?.grams, - vitaminB6 = b6Pyridoxine?.grams, - biotin = b7Biotin?.grams, - folate = b9Folate?.grams, - vitaminB12 = b12Cobalamin?.grams, - vitaminC = vitaminC?.grams, - vitaminD = vitaminD?.grams, - vitaminE = vitaminE?.grams, - vitaminK = vitaminK?.grams, - calcium = calcium?.grams, - chloride = chloride?.grams, - cholesterol = cholesterol?.grams, - chromium = chromium?.grams, - copper = copper?.grams, - unsaturatedFat = fatUnsaturated?.grams, - monounsaturatedFat = fatMonounsaturated?.grams, - polyunsaturatedFat = fatPolyunsaturated?.grams, - saturatedFat = fatSaturated?.grams, - transFat = fatTransMonoenoic?.grams, - dietaryFiber = fiber?.grams, - iodine = iodine?.grams, - iron = iron?.grams, - magnesium = magnesium?.grams, - manganese = manganese?.grams, - molybdenum = molybdenum?.grams, - phosphorus = phosphorus?.grams, - potassium = potassium?.grams, - selenium = selenium?.grams, - sodium = sodium?.grams, - sugar = sugar?.grams, - zinc = zinc?.grams, - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - mealType = HealthConstants.mapMealTypeToType[mealType] - ?: MealType.MEAL_TYPE_UNKNOWN, - ), + NutritionRecord( + name = name, + metadata = metaData, + energy = calories?.kilocalories, + totalCarbohydrate = carbs?.grams, + protein = protein?.grams, + totalFat = fat?.grams, + caffeine = caffeine?.grams, + vitaminA = vitaminA?.grams, + thiamin = b1Thiamine?.grams, + riboflavin = b2Riboflavin?.grams, + niacin = b3Niacin?.grams, + pantothenicAcid = b5PantothenicAcid?.grams, + vitaminB6 = b6Pyridoxine?.grams, + biotin = b7Biotin?.grams, + folate = b9Folate?.grams, + vitaminB12 = b12Cobalamin?.grams, + vitaminC = vitaminC?.grams, + vitaminD = vitaminD?.grams, + vitaminE = vitaminE?.grams, + vitaminK = vitaminK?.grams, + calcium = calcium?.grams, + chloride = chloride?.grams, + cholesterol = cholesterol?.grams, + chromium = chromium?.grams, + copper = copper?.grams, + unsaturatedFat = fatUnsaturated?.grams, + monounsaturatedFat = fatMonounsaturated?.grams, + polyunsaturatedFat = fatPolyunsaturated?.grams, + saturatedFat = fatSaturated?.grams, + transFat = fatTransMonoenoic?.grams, + dietaryFiber = fiber?.grams, + iodine = iodine?.grams, + iron = iron?.grams, + magnesium = magnesium?.grams, + manganese = manganese?.grams, + molybdenum = molybdenum?.grams, + phosphorus = phosphorus?.grams, + potassium = potassium?.grams, + selenium = selenium?.grams, + sodium = sodium?.grams, + sugar = sugar?.grams, + zinc = zinc?.grams, + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + mealType = HealthConstants.mapMealTypeToType[mealType] + ?: MealType.MEAL_TYPE_UNKNOWN + ), ) healthConnectClient.insertRecords(list) result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Meal was successfully added!" - ) + Log.i("FLUTTER_HEALTH::SUCCESS", "[Health Connect] Meal was successfully added!") } catch (e: Exception) { Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the meal", + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the meal", ) Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) @@ -353,16 +389,19 @@ class HealthDataWriter( } /** - * Writes speed/velocity data with multiple samples to Health Connect. - * Creates a SpeedRecord containing time-series speed measurements captured during - * activities like running, cycling, or walking. Each sample represents the user's - * instantaneous speed at a specific moment within the recording period. + * Writes speed/velocity data with multiple samples to Health Connect. Creates a SpeedRecord + * containing time-series speed measurements captured during activities like running, cycling, + * or walking. Each sample represents the user's instantaneous speed at a specific moment within + * the recording period. * * @param call Method call containing startTime, endTime, recordingMethod, + * ``` * samples: List> List of speed measurements, each * containing: time, speed (m/s) * - * @param result Flutter result callback returning boolean success status + * @param result + * ``` + * Flutter result callback returning boolean success status */ fun writeMultipleSpeedData(call: MethodCall, result: Result) { val startTime = call.argument("startTime")!! @@ -372,27 +411,29 @@ class HealthDataWriter( scope.launch { try { - val speedSamples = samples.map { sample -> - SpeedRecord.Sample( - time = Instant.ofEpochMilli(sample["time"] as Long), - speed = Velocity.metersPerSecond(sample["speed"] as Double) - ) - } + val speedSamples = + samples.map { sample -> + SpeedRecord.Sample( + time = Instant.ofEpochMilli(sample["time"] as Long), + speed = Velocity.metersPerSecond(sample["speed"] as Double) + ) + } - val speedRecord = SpeedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - samples = speedSamples, - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) + val speedRecord = + SpeedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + samples = speedSamples, + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata(recordingMethod = recordingMethod), + ) healthConnectClient.insertRecords(listOf(speedRecord)) result.success(true) Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Successfully wrote ${speedSamples.size} speed samples" + "FLUTTER_HEALTH::SUCCESS", + "Successfully wrote ${speedSamples.size} speed samples" ) } catch (e: Exception) { Log.e("FLUTTER_HEALTH::ERROR", "Error writing speed data: ${e.message}") @@ -401,13 +442,12 @@ class HealthDataWriter( } } - // ---------- Private Methods ---------- + // ---------- Private Methods ---------- /** - * Creates appropriate Health Connect record objects based on data type. - * Factory method that instantiates the correct record type with proper unit conversion - * and metadata assignment. - * + * Creates appropriate Health Connect record objects based on data type. Factory method that + * instantiates the correct record type with proper unit conversion and metadata assignment. + * * @param type Health data type string identifier * @param startTime Record start time in milliseconds * @param endTime Record end time in milliseconds @@ -416,254 +456,308 @@ class HealthDataWriter( * @return Record? Properly configured Health Connect record, or null if type unsupported */ private fun createRecord( - type: String, - startTime: Long, - endTime: Long, - value: Double, - recordingMethod: Int + type: String, + startTime: Long, + endTime: Long, + value: Double, + metadata: Metadata ): Record? { return when (type) { - BODY_FAT_PERCENTAGE -> BodyFatRecord( - time = Instant.ofEpochMilli(startTime), - percentage = Percentage(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - LEAN_BODY_MASS -> LeanBodyMassRecord( - time = Instant.ofEpochMilli(startTime), - mass = Mass.kilograms(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - HEIGHT -> HeightRecord( - time = Instant.ofEpochMilli(startTime), - height = Length.meters(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - WEIGHT -> WeightRecord( - time = Instant.ofEpochMilli(startTime), - weight = Mass.kilograms(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - STEPS -> StepsRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - count = value.toLong(), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - ACTIVE_ENERGY_BURNED -> ActiveCaloriesBurnedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - energy = Energy.kilocalories(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - HEART_RATE -> HeartRateRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - samples = listOf( - HeartRateRecord.Sample( - time = Instant.ofEpochMilli(startTime), - beatsPerMinute = value.toLong(), - ), - ), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BODY_TEMPERATURE -> BodyTemperatureRecord( - time = Instant.ofEpochMilli(startTime), - temperature = Temperature.celsius(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BODY_WATER_MASS -> BodyWaterMassRecord( - time = Instant.ofEpochMilli(startTime), - mass = Mass.kilograms(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BLOOD_OXYGEN -> OxygenSaturationRecord( - time = Instant.ofEpochMilli(startTime), - percentage = Percentage(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BLOOD_GLUCOSE -> BloodGlucoseRecord( - time = Instant.ofEpochMilli(startTime), - level = BloodGlucose.milligramsPerDeciliter(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - HEART_RATE_VARIABILITY_RMSSD -> HeartRateVariabilityRmssdRecord( - time = Instant.ofEpochMilli(startTime), - heartRateVariabilityMillis = value, - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - DISTANCE_DELTA -> DistanceRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - distance = Length.meters(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - WATER -> HydrationRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - volume = Volume.liters(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - SLEEP_ASLEEP -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_SLEEPING, recordingMethod) - SLEEP_LIGHT -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_LIGHT, recordingMethod) - SLEEP_DEEP -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_DEEP, recordingMethod) - SLEEP_REM -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_REM, recordingMethod) - SLEEP_OUT_OF_BED -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_OUT_OF_BED, recordingMethod) - SLEEP_AWAKE -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_AWAKE, recordingMethod) - SLEEP_AWAKE_IN_BED -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_AWAKE_IN_BED, recordingMethod) - SLEEP_UNKNOWN -> createSleepRecord(startTime, endTime, SleepSessionRecord.STAGE_TYPE_UNKNOWN, recordingMethod) - - SLEEP_SESSION -> SleepSessionRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - RESTING_HEART_RATE -> RestingHeartRateRecord( - time = Instant.ofEpochMilli(startTime), - beatsPerMinute = value.toLong(), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - BASAL_ENERGY_BURNED -> BasalMetabolicRateRecord( - time = Instant.ofEpochMilli(startTime), - basalMetabolicRate = Power.kilocaloriesPerDay(value), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - FLIGHTS_CLIMBED -> FloorsClimbedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - floors = value, - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - RESPIRATORY_RATE -> RespiratoryRateRecord( - time = Instant.ofEpochMilli(startTime), - rate = value, - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - TOTAL_CALORIES_BURNED -> TotalCaloriesBurnedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - energy = Energy.kilocalories(value), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - MENSTRUATION_FLOW -> MenstruationFlowRecord( - time = Instant.ofEpochMilli(startTime), - flow = value.toInt(), - zoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - - SPEED -> SpeedRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - samples = listOf( - SpeedRecord.Sample( - time = Instant.ofEpochMilli(startTime), - speed = Velocity.metersPerSecond(value), + BODY_FAT_PERCENTAGE -> + BodyFatRecord( + time = Instant.ofEpochMilli(startTime), + percentage = Percentage(value), + zoneOffset = null, + metadata = metadata, + ) + LEAN_BODY_MASS -> + LeanBodyMassRecord( + time = Instant.ofEpochMilli(startTime), + mass = Mass.kilograms(value), + zoneOffset = null, + metadata = metadata, + ) + HEIGHT -> + HeightRecord( + time = Instant.ofEpochMilli(startTime), + height = Length.meters(value), + zoneOffset = null, + metadata = metadata, + ) + WEIGHT -> + WeightRecord( + time = Instant.ofEpochMilli(startTime), + weight = Mass.kilograms(value), + zoneOffset = null, + metadata = metadata, + ) + STEPS -> + StepsRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + count = value.toLong(), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + ACTIVE_ENERGY_BURNED -> + ActiveCaloriesBurnedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + energy = Energy.kilocalories(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + HEART_RATE -> + HeartRateRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + samples = + listOf( + HeartRateRecord.Sample( + time = Instant.ofEpochMilli(startTime), + beatsPerMinute = value.toLong(), + ), + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + BODY_TEMPERATURE -> + BodyTemperatureRecord( + time = Instant.ofEpochMilli(startTime), + temperature = Temperature.celsius(value), + zoneOffset = null, + metadata = metadata, + ) + BODY_WATER_MASS -> + BodyWaterMassRecord( + time = Instant.ofEpochMilli(startTime), + mass = Mass.kilograms(value), + zoneOffset = null, + metadata = metadata, + ) + BLOOD_OXYGEN -> + OxygenSaturationRecord( + time = Instant.ofEpochMilli(startTime), + percentage = Percentage(value), + zoneOffset = null, + metadata = metadata, + ) + BLOOD_GLUCOSE -> + BloodGlucoseRecord( + time = Instant.ofEpochMilli(startTime), + level = BloodGlucose.milligramsPerDeciliter(value), + zoneOffset = null, + metadata = metadata, + ) + HEART_RATE_VARIABILITY_RMSSD -> + HeartRateVariabilityRmssdRecord( + time = Instant.ofEpochMilli(startTime), + heartRateVariabilityMillis = value, + zoneOffset = null, + metadata = metadata, + ) + DISTANCE_DELTA -> + DistanceRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + distance = Length.meters(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + WATER -> + HydrationRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + volume = Volume.liters(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + SLEEP_ASLEEP -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_SLEEPING, + metadata + ) + SLEEP_LIGHT -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_LIGHT, + metadata + ) + SLEEP_DEEP -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_DEEP, + metadata + ) + SLEEP_REM -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_REM, + metadata + ) + SLEEP_OUT_OF_BED -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_OUT_OF_BED, + metadata + ) + SLEEP_AWAKE -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_AWAKE, + metadata + ) + SLEEP_AWAKE_IN_BED -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_AWAKE_IN_BED, + metadata + ) + SLEEP_UNKNOWN -> + createSleepRecord( + startTime, + endTime, + SleepSessionRecord.STAGE_TYPE_UNKNOWN, + metadata + ) + SLEEP_SESSION -> + SleepSessionRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + RESTING_HEART_RATE -> + RestingHeartRateRecord( + time = Instant.ofEpochMilli(startTime), + beatsPerMinute = value.toLong(), + zoneOffset = null, + metadata = metadata, + ) + BASAL_ENERGY_BURNED -> + BasalMetabolicRateRecord( + time = Instant.ofEpochMilli(startTime), + basalMetabolicRate = Power.kilocaloriesPerDay(value), + zoneOffset = null, + metadata = metadata, + ) + FLIGHTS_CLIMBED -> + FloorsClimbedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + floors = value, + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + RESPIRATORY_RATE -> + RespiratoryRateRecord( + time = Instant.ofEpochMilli(startTime), + rate = value, + zoneOffset = null, + metadata = metadata, + ) + TOTAL_CALORIES_BURNED -> + TotalCaloriesBurnedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + energy = Energy.kilocalories(value), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, + ) + MENSTRUATION_FLOW -> + MenstruationFlowRecord( + time = Instant.ofEpochMilli(startTime), + flow = value.toInt(), + zoneOffset = null, + metadata = metadata, + ) + SPEED -> + SpeedRecord( + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + samples = + listOf( + SpeedRecord.Sample( + time = Instant.ofEpochMilli(startTime), + speed = Velocity.metersPerSecond(value), + ) + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = metadata, ) - ), - startZoneOffset = null, - endZoneOffset = null, - metadata = Metadata(recordingMethod = recordingMethod), - ) - BLOOD_PRESSURE_SYSTOLIC -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeBloodPressure] API") null } - BLOOD_PRESSURE_DIASTOLIC -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeBloodPressure] API") null } - WORKOUT -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeWorkoutData] API") null } - NUTRITION -> { Log.e("FLUTTER_HEALTH::ERROR", "You must use the [writeMeal] API") null } - else -> { - Log.e("FLUTTER_HEALTH::ERROR", "The type $type was not supported by the Health plugin or you must use another API") + Log.e( + "FLUTTER_HEALTH::ERROR", + "The type $type was not supported by the Health plugin or you must use another API" + ) null } } } /** - * Creates sleep session records with stage information. - * Builds SleepSessionRecord with appropriate sleep stage data and timing. - * + * Creates sleep session records with stage information. Builds SleepSessionRecord with + * appropriate sleep stage data and timing. + * * @param startTime Sleep period start time in milliseconds * @param endTime Sleep period end time in milliseconds * @param stageType Sleep stage type constant * @param recordingMethod How sleep data was recorded * @return SleepSessionRecord Configured sleep session record */ - private fun createSleepRecord(startTime: Long, endTime: Long, stageType: Int, recordingMethod: Int): SleepSessionRecord { + private fun createSleepRecord( + startTime: Long, + endTime: Long, + stageType: Int, + metadata: Metadata + ): SleepSessionRecord { return SleepSessionRecord( - startTime = Instant.ofEpochMilli(startTime), - endTime = Instant.ofEpochMilli(endTime), - startZoneOffset = null, - endZoneOffset = null, - stages = listOf( - SleepSessionRecord.Stage( - Instant.ofEpochMilli(startTime), - Instant.ofEpochMilli(endTime), - stageType - ) - ), - metadata = Metadata(recordingMethod = recordingMethod), + startTime = Instant.ofEpochMilli(startTime), + endTime = Instant.ofEpochMilli(endTime), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord.Stage( + Instant.ofEpochMilli(startTime), + Instant.ofEpochMilli(endTime), + stageType + ) + ), + metadata = metadata, ) } @@ -694,7 +788,7 @@ class HealthDataWriter( private const val WORKOUT = "WORKOUT" private const val NUTRITION = "NUTRITION" private const val SPEED = "SPEED" - + // Sleep types private const val SLEEP_ASLEEP = "SLEEP_ASLEEP" private const val SLEEP_LIGHT = "SLEEP_LIGHT" @@ -706,4 +800,4 @@ class HealthDataWriter( private const val SLEEP_UNKNOWN = "SLEEP_UNKNOWN" private const val SLEEP_SESSION = "SLEEP_SESSION" } -} \ No newline at end of file +} diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 83a861487..888240eee 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -169,6 +169,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // Deleting data "delete" -> dataOperations.deleteData(call, result) "deleteByUUID" -> dataOperations.deleteByUUID(call, result) + "deleteByClientRecordId" -> dataOperations.deleteByClientRecordId(call, result) else -> result.notImplemented() } } diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index ed1679690..404f3ae26 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -262,11 +262,12 @@ class HealthAppState extends State { endTime: now, recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 200, - type: HealthDataType.ACTIVE_ENERGY_BURNED, - startTime: earlier, - endTime: now, - ); + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + clientRecordId: "uniqueID1234", + clientRecordVersion: 1); success &= await health.writeHealthData( value: 70, type: HealthDataType.HEART_RATE, @@ -330,12 +331,15 @@ class HealthAppState extends State { totalEnergyBurned: 400, ); success &= await health.writeBloodPressure( - systolic: 90, - diastolic: 80, - startTime: now, - ); + systolic: 90, + diastolic: 80, + startTime: now, + clientRecordId: "uniqueID1234", + clientRecordVersion: 2); success &= await health.writeMeal( mealType: MealType.SNACK, + clientRecordId: "uniqueID1234", + clientRecordVersion: 1.4, startTime: earlier, endTime: now, caloriesConsumed: 1000, @@ -681,6 +685,13 @@ class HealthAppState extends State { WidgetStatePropertyAll(Colors.blue)), child: const Text("Delete Data", style: TextStyle(color: Colors.white))), + TextButton( + onPressed: testDeleteByClientRecordId, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.red)), + child: const Text("Test Delete by Client ID", + style: TextStyle(color: Colors.white))), TextButton( onPressed: fetchStepData, style: const ButtonStyle( @@ -837,7 +848,8 @@ class HealthAppState extends State { return ListTile( title: Text("${p.typeString}: ${p.value}"), trailing: Text(p.unitString), - subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + subtitle: + Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), onTap: () { fetchDataByUUID( context, diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index d12054286..0d8302430 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -167,6 +167,7 @@ const _$HealthDataUnitEnumMap = { HealthDataUnit.POUND: 'POUND', HealthDataUnit.STONE: 'STONE', HealthDataUnit.METER: 'METER', + HealthDataUnit.CENTIMETER: 'CENTIMETER', HealthDataUnit.INCH: 'INCH', HealthDataUnit.FOOT: 'FOOT', HealthDataUnit.YARD: 'YARD', @@ -208,6 +209,7 @@ const _$HealthDataUnitEnumMap = { HealthDataUnit.BEATS_PER_MINUTE: 'BEATS_PER_MINUTE', HealthDataUnit.RESPIRATIONS_PER_MINUTE: 'RESPIRATIONS_PER_MINUTE', HealthDataUnit.MILLIGRAM_PER_DECILITER: 'MILLIGRAM_PER_DECILITER', + HealthDataUnit.MILLIMOLES_PER_LITER: 'MILLIMOLES_PER_LITER', HealthDataUnit.METER_PER_SECOND: 'METER_PER_SECOND', HealthDataUnit.UNKNOWN_UNIT: 'UNKNOWN_UNIT', HealthDataUnit.NO_UNIT: 'NO_UNIT', diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 374eb00a1..1255e7048 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -112,9 +112,7 @@ class HealthDataPoint { /// Create a [HealthDataPoint] based on a health data point from native data format. factory HealthDataPoint.fromHealthDataPoint( - HealthDataType dataType, - dynamic dataPoint, - ) { + HealthDataType dataType, dynamic dataPoint, String? unitName) { // Handling different [HealthValue] types HealthValue value = switch (dataType) { HealthDataType.AUDIOGRAM => @@ -141,7 +139,9 @@ class HealthDataPoint { final Map? metadata = dataPoint["metadata"] == null ? null : Map.from(dataPoint['metadata'] as Map); - final unit = dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT; + final HealthDataUnit unit = HealthDataUnit.values.firstWhere( + (value) => value.name == unitName, + orElse: () => dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT); final String? uuid = dataPoint["uuid"] as String?; final String? deviceModel = dataPoint["device_model"] as String?; diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index d92671d25..622e3a937 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -46,7 +46,7 @@ class Health { /// Get an instance of the health plugin. Health({DeviceInfoPlugin? deviceInfo}) - : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { + : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { _registerFromJsonFunctions(); } @@ -107,13 +107,17 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( - "The lists of types and permissions must be of same length."); + "The lists of types and permissions must be of same length.", + ); } final mTypes = List.from(types, growable: true); final mPermissions = permissions == null - ? List.filled(types.length, HealthDataAccess.READ.index, - growable: true) + ? List.filled( + types.length, + HealthDataAccess.READ.index, + growable: true, + ) : permissions.map((permission) => permission.index).toList(); /// On Android, if BMI is requested, then also ask for weight and height @@ -152,8 +156,9 @@ class Health { if (Platform.isIOS) return null; try { - final status = - await _channel.invokeMethod('getHealthConnectSdkStatus'); + final status = await _channel.invokeMethod( + 'getHealthConnectSdkStatus', + ); _healthConnectSdkStatus = status != null ? HealthConnectSdkStatus.fromNativeValue(status) : HealthConnectSdkStatus.sdkUnavailable; @@ -171,7 +176,7 @@ class Health { Future isHealthConnectAvailable() async => !Platform.isAndroid ? true : (await getHealthConnectSdkStatus() == - HealthConnectSdkStatus.sdkAvailable); + HealthConnectSdkStatus.sdkAvailable); /// Prompt the user to install the Google Health Connect app via the /// installed store (most likely Play Store). @@ -195,8 +200,9 @@ class Health { if (!(await isHealthConnectAvailable())) { throw UnsupportedError( - "Google Health Connect is not available on this Android device. " - "You may prompt the user to install it using the 'installHealthConnect' method"); + "Google Health Connect is not available on this Android device. " + "You may prompt the user to install it using the 'installHealthConnect' method", + ); } } @@ -210,12 +216,14 @@ class Health { if (Platform.isIOS) return false; try { - final status = - await _channel.invokeMethod('isHealthDataHistoryAvailable'); + final status = await _channel.invokeMethod( + 'isHealthDataHistoryAvailable', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataHistoryAvailable(): $e'); + '$runtimeType - Exception in isHealthDataHistoryAvailable(): $e', + ); return false; } } @@ -231,12 +239,14 @@ class Health { if (Platform.isIOS) return true; try { - final status = - await _channel.invokeMethod('isHealthDataHistoryAuthorized'); + final status = await _channel.invokeMethod( + 'isHealthDataHistoryAuthorized', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataHistoryAuthorized(): $e'); + '$runtimeType - Exception in isHealthDataHistoryAuthorized(): $e', + ); return false; } } @@ -254,12 +264,14 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); try { - final bool? isAuthorized = - await _channel.invokeMethod('requestHealthDataHistoryAuthorization'); + final bool? isAuthorized = await _channel.invokeMethod( + 'requestHealthDataHistoryAuthorization', + ); return isAuthorized ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in requestHealthDataHistoryAuthorization(): $e'); + '$runtimeType - Exception in requestHealthDataHistoryAuthorization(): $e', + ); return false; } } @@ -274,12 +286,14 @@ class Health { if (Platform.isIOS) return false; try { - final status = await _channel - .invokeMethod('isHealthDataInBackgroundAvailable'); + final status = await _channel.invokeMethod( + 'isHealthDataInBackgroundAvailable', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataInBackgroundAvailable(): $e'); + '$runtimeType - Exception in isHealthDataInBackgroundAvailable(): $e', + ); return false; } } @@ -295,12 +309,14 @@ class Health { if (Platform.isIOS) return true; try { - final status = await _channel - .invokeMethod('isHealthDataInBackgroundAuthorized'); + final status = await _channel.invokeMethod( + 'isHealthDataInBackgroundAuthorized', + ); return status ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in isHealthDataInBackgroundAuthorized(): $e'); + '$runtimeType - Exception in isHealthDataInBackgroundAuthorized(): $e', + ); return false; } } @@ -318,12 +334,14 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); try { - final bool? isAuthorized = await _channel - .invokeMethod('requestHealthDataInBackgroundAuthorization'); + final bool? isAuthorized = await _channel.invokeMethod( + 'requestHealthDataInBackgroundAuthorization', + ); return isAuthorized ?? false; } catch (e) { debugPrint( - '$runtimeType - Exception in requestHealthDataInBackgroundAuthorization(): $e'); + '$runtimeType - Exception in requestHealthDataInBackgroundAuthorization(): $e', + ); return false; } } @@ -355,7 +373,8 @@ class Health { await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( - 'The length of [types] must be same as that of [permissions].'); + 'The length of [types] must be same as that of [permissions].', + ); } if (permissions != null) { @@ -370,15 +389,19 @@ class Health { type == HealthDataType.ATRIAL_FIBRILLATION_BURDEN) && permission != HealthDataAccess.READ) { throw ArgumentError( - 'Requesting WRITE permission on ELECTROCARDIOGRAM / HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT / IRREGULAR_HEART_RATE_EVENT / WALKING_HEART_RATE / ATRIAL_FIBRILLATION_BURDEN is not allowed.'); + 'Requesting WRITE permission on ELECTROCARDIOGRAM / HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT / IRREGULAR_HEART_RATE_EVENT / WALKING_HEART_RATE / ATRIAL_FIBRILLATION_BURDEN is not allowed.', + ); } } } final mTypes = List.from(types, growable: true); final mPermissions = permissions == null - ? List.filled(types.length, HealthDataAccess.READ.index, - growable: true) + ? List.filled( + types.length, + HealthDataAccess.READ.index, + growable: true, + ) : permissions.map((permission) => permission.index).toList(); // on Android, if BMI is requested, then also ask for weight and height @@ -386,7 +409,9 @@ class Health { List keys = mTypes.map((e) => e.name).toList(); final bool? isAuthorized = await _channel.invokeMethod( - 'requestAuthorization', {'types': keys, "permissions": mPermissions}); + 'requestAuthorization', + {'types': keys, "permissions": mPermissions}, + ); return isAuthorized ?? false; } @@ -415,17 +440,25 @@ class Health { List recordingMethodsToFilter, ) async { List heights = await _prepareQuery( - startTime, endTime, HealthDataType.HEIGHT, recordingMethodsToFilter); + startTime, + endTime, + HealthDataType.HEIGHT, + recordingMethodsToFilter, + ); if (heights.isEmpty) { return []; } List weights = await _prepareQuery( - startTime, endTime, HealthDataType.WEIGHT, recordingMethodsToFilter); + startTime, + endTime, + HealthDataType.WEIGHT, + recordingMethodsToFilter, + ); - double h = - (heights.last.value as NumericHealthValue).numericValue.toDouble(); + double h = (heights.last.value as NumericHealthValue).numericValue + .toDouble(); const dataType = HealthDataType.BODY_MASS_INDEX; final unit = dataTypeToUnit[dataType]!; @@ -434,7 +467,7 @@ class Health { for (var i = 0; i < weights.length; i++) { final bmiValue = (weights[i].value as NumericHealthValue).numericValue.toDouble() / - (h * h); + (h * h); final x = HealthDataPoint( uuid: '', value: NumericHealthValue(numericValue: bmiValue), @@ -478,19 +511,24 @@ class Health { HealthDataUnit? unit, required HealthDataType type, required DateTime startTime, + String? clientRecordId, + double? clientRecordVersion, DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } if (type == HealthDataType.WORKOUT) { throw ArgumentError( - "Adding workouts should be done using the writeWorkoutData method."); + "Adding workouts should be done using the writeWorkoutData method.", + ); } // If not implemented on platform, throw an exception if (!isDataTypeAvailable(type)) { @@ -508,7 +546,8 @@ class Health { }.contains(type) && Platform.isIOS) { throw ArgumentError( - "$type - iOS does not support writing this data type in HealthKit"); + "$type - iOS does not support writing this data type in HealthKit", + ); } // Assign default unit if not specified @@ -537,6 +576,8 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), + 'clientRecordId': clientRecordId, + 'clientRecordVersion': clientRecordVersion, }; bool? success = await _channel.invokeMethod('writeData', args); return success ?? false; @@ -566,7 +607,7 @@ class Health { Map args = { 'dataTypeKey': type.name, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, }; bool? success = await _channel.invokeMethod('delete', args); return success ?? false; @@ -593,18 +634,32 @@ class Health { if (Platform.isIOS && type == null) { throw ArgumentError( - "On iOS, both UUID and type are required to delete a record."); + "On iOS, both UUID and type are required to delete a record.", + ); } - Map args = { - 'uuid': uuid, - 'dataTypeKey': type?.name, - }; + Map args = {'uuid': uuid, 'dataTypeKey': type?.name}; bool? success = await _channel.invokeMethod('deleteByUUID', args); return success ?? false; } + Future deleteByClientRecordId({ + required HealthDataType dataTypeKey, + required String clientRecordId, + String? recordId, + }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + + Map args = { + 'dataTypeKey': dataTypeKey.name, + 'recordId': recordId, + 'clientRecordId': clientRecordId, + }; + bool? success = await _channel.invokeMethod('deleteByClientRecordId', args); + return success ?? false; + } + /// Saves a blood pressure record. /// /// Returns true if successful, false otherwise. @@ -623,13 +678,17 @@ class Health { required int systolic, required int diastolic, required DateTime startTime, + String? clientRecordId, + double? clientRecordVersion, DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } @@ -644,6 +703,8 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'recordingMethod': recordingMethod.toInt(), + 'clientRecordId': clientRecordId, + 'clientRecordVersion': clientRecordVersion, }; return await _channel.invokeMethod('writeBloodPressure', args) == true; } @@ -669,8 +730,10 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } @@ -682,11 +745,12 @@ class Health { if (Platform.isIOS) { success = await writeHealthData( - value: saturation, - type: HealthDataType.BLOOD_OXYGEN, - startTime: startTime, - endTime: endTime, - recordingMethod: recordingMethod); + value: saturation, + type: HealthDataType.BLOOD_OXYGEN, + startTime: startTime, + endTime: endTime, + recordingMethod: recordingMethod, + ); } else if (Platform.isAndroid) { Map args = { 'value': saturation, @@ -757,6 +821,8 @@ class Health { required MealType mealType, required DateTime startTime, required DateTime endTime, + String? clientRecordId, + double? clientRecordVersion, double? caloriesConsumed, double? carbohydrates, double? protein, @@ -803,8 +869,10 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } @@ -817,6 +885,8 @@ class Health { 'meal_type': mealType.name, 'start_time': startTime.millisecondsSinceEpoch, 'end_time': endTime.millisecondsSinceEpoch, + 'clientRecordId': clientRecordId, + 'clientRecordVersion': clientRecordVersion, 'calories': caloriesConsumed, 'carbs': carbohydrates, 'protein': protein, @@ -884,17 +954,21 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } - var value = - Platform.isAndroid ? MenstrualFlow.toHealthConnect(flow) : flow.index; + var value = Platform.isAndroid + ? MenstrualFlow.toHealthConnect(flow) + : flow.index; if (value == -1) { throw ArgumentError( - "$flow is not a valid menstrual flow value for $platformType"); + "$flow is not a valid menstrual flow value for $platformType", + ); } Map args = { @@ -936,12 +1010,14 @@ class Health { leftEarSensitivities.isEmpty || rightEarSensitivities.isEmpty) { throw ArgumentError( - "frequencies, leftEarSensitivities and rightEarSensitivities can't be empty"); + "frequencies, leftEarSensitivities and rightEarSensitivities can't be empty", + ); } if (frequencies.length != leftEarSensitivities.length || rightEarSensitivities.length != leftEarSensitivities.length) { throw ArgumentError( - "frequencies, leftEarSensitivities and rightEarSensitivities need to be of the same length"); + "frequencies, leftEarSensitivities and rightEarSensitivities need to be of the same length", + ); } endTime ??= startTime; if (startTime.isAfter(endTime)) { @@ -990,14 +1066,15 @@ class Health { if (Platform.isAndroid) { throw UnsupportedError( - "writeInsulinDelivery is not supported on Android"); + "writeInsulinDelivery is not supported on Android", + ); } Map args = { 'units': units, 'reason': reason.index, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, }; bool? success = await _channel.invokeMethod('writeInsulinDelivery', args); @@ -1034,10 +1111,7 @@ class Health { throw HealthException(type, 'Not available on platform $platformType'); } - final result = await _dataQueryByUUID( - uuid, - type, - ); + final result = await _dataQueryByUUID(uuid, type); debugPrint('data by UUID: ${result?.toString()}'); @@ -1049,6 +1123,7 @@ class Health { /// If not specified, all data points will be included. Future> getHealthDataFromTypes({ required List types, + Map? preferredUnits, required DateTime startTime, required DateTime endTime, List recordingMethodsToFilter = const [], @@ -1058,7 +1133,12 @@ class Health { for (var type in types) { final result = await _prepareQuery( - startTime, endTime, type, recordingMethodsToFilter); + startTime, + endTime, + type, + recordingMethodsToFilter, + dataUnit: preferredUnits?[type], + ); dataPoints.addAll(result); } @@ -1073,18 +1153,24 @@ class Health { /// Fetch a list of health data points based on [types]. /// You can also specify the [recordingMethodsToFilter] to filter the data points. /// If not specified, all data points will be included.Vkk - Future> getHealthIntervalDataFromTypes( - {required DateTime startDate, - required DateTime endDate, - required List types, - required int interval, - List recordingMethodsToFilter = const []}) async { + Future> getHealthIntervalDataFromTypes({ + required DateTime startDate, + required DateTime endDate, + required List types, + required int interval, + List recordingMethodsToFilter = const [], + }) async { await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; for (var type in types) { final result = await _prepareIntervalQuery( - startDate, endDate, type, interval, recordingMethodsToFilter); + startDate, + endDate, + type, + interval, + recordingMethodsToFilter, + ); dataPoints.addAll(result); } @@ -1103,7 +1189,12 @@ class Health { List dataPoints = []; final result = await _prepareAggregateQuery( - startDate, endDate, types, activitySegmentDuration, includeManualEntry); + startDate, + endDate, + types, + activitySegmentDuration, + includeManualEntry, + ); dataPoints.addAll(result); return removeDuplicates(dataPoints); @@ -1114,8 +1205,9 @@ class Health { DateTime startTime, DateTime endTime, HealthDataType dataType, - List recordingMethodsToFilter, - ) async { + List recordingMethodsToFilter, { + HealthDataUnit? dataUnit, + }) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -1124,7 +1216,9 @@ class Health { // If not implemented on platform, throw an exception if (!isDataTypeAvailable(dataType)) { throw HealthException( - dataType, 'Not available on platform $platformType'); + dataType, + 'Not available on platform $platformType', + ); } // If BodyMassIndex is requested on Android, calculate this manually @@ -1132,16 +1226,22 @@ class Health { return _computeAndroidBMI(startTime, endTime, recordingMethodsToFilter); } return await _dataQuery( - startTime, endTime, dataType, recordingMethodsToFilter); + startTime, + endTime, + dataType, + recordingMethodsToFilter, + dataUnit: dataUnit, + ); } /// Prepares an interval query, i.e. checks if the types are available, etc. Future> _prepareIntervalQuery( - DateTime startDate, - DateTime endDate, - HealthDataType dataType, - int interval, - List recordingMethodsToFilter) async { + DateTime startDate, + DateTime endDate, + HealthDataType dataType, + int interval, + List recordingMethodsToFilter, + ) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -1150,20 +1250,28 @@ class Health { // If not implemented on platform, throw an exception if (!isDataTypeAvailable(dataType)) { throw HealthException( - dataType, 'Not available on platform $platformType'); + dataType, + 'Not available on platform $platformType', + ); } return await _dataIntervalQuery( - startDate, endDate, dataType, interval, recordingMethodsToFilter); + startDate, + endDate, + dataType, + interval, + recordingMethodsToFilter, + ); } /// Prepares an aggregate query, i.e. checks if the types are available, etc. Future> _prepareAggregateQuery( - DateTime startDate, - DateTime endDate, - List dataTypes, - int activitySegmentDuration, - bool includeManualEntry) async { + DateTime startDate, + DateTime endDate, + List dataTypes, + int activitySegmentDuration, + bool includeManualEntry, + ) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -1176,23 +1284,32 @@ class Health { } } - return await _dataAggregateQuery(startDate, endDate, dataTypes, - activitySegmentDuration, includeManualEntry); + return await _dataAggregateQuery( + startDate, + endDate, + dataTypes, + activitySegmentDuration, + includeManualEntry, + ); } /// Fetches data points from Android/iOS native code. Future> _dataQuery( - DateTime startTime, - DateTime endTime, - HealthDataType dataType, - List recordingMethodsToFilter) async { + DateTime startTime, + DateTime endTime, + HealthDataType dataType, + List recordingMethodsToFilter, { + HealthDataUnit? dataUnit, + }) async { + String? unit = dataUnit?.name ?? dataTypeToUnit[dataType]?.name; final args = { 'dataTypeKey': dataType.name, - 'dataUnitKey': dataTypeToUnit[dataType]!.name, + 'dataUnitKey': unit, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, - 'recordingMethodsToFilter': - recordingMethodsToFilter.map((e) => e.toInt()).toList(), + 'recordingMethodsToFilter': recordingMethodsToFilter + .map((e) => e.toInt()) + .toList(), }; final fetchedDataPoints = await _channel.invokeMethod('getData', args); @@ -1200,6 +1317,7 @@ class Health { final msg = { "dataType": dataType, "dataPoints": fetchedDataPoints, + "unit": unit, }; const thresHold = 100; // If the no. of data points are larger than the threshold, @@ -1244,23 +1362,27 @@ class Health { /// function for fetching statistic health data Future> _dataIntervalQuery( - DateTime startDate, - DateTime endDate, - HealthDataType dataType, - int interval, - List recordingMethodsToFilter) async { + DateTime startDate, + DateTime endDate, + HealthDataType dataType, + int interval, + List recordingMethodsToFilter, + ) async { final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': dataTypeToUnit[dataType]!.name, 'startTime': startDate.millisecondsSinceEpoch, 'endTime': endDate.millisecondsSinceEpoch, 'interval': interval, - 'recordingMethodsToFilter': - recordingMethodsToFilter.map((e) => e.toInt()).toList(), + 'recordingMethodsToFilter': recordingMethodsToFilter + .map((e) => e.toInt()) + .toList(), }; - final fetchedDataPoints = - await _channel.invokeMethod('getIntervalData', args); + final fetchedDataPoints = await _channel.invokeMethod( + 'getIntervalData', + args, + ); if (fetchedDataPoints != null) { final msg = { "dataType": dataType, @@ -1273,21 +1395,24 @@ class Health { /// function for fetching statistic health data Future> _dataAggregateQuery( - DateTime startDate, - DateTime endDate, - List dataTypes, - int activitySegmentDuration, - bool includeManualEntry) async { + DateTime startDate, + DateTime endDate, + List dataTypes, + int activitySegmentDuration, + bool includeManualEntry, + ) async { final args = { 'dataTypeKeys': dataTypes.map((dataType) => dataType.name).toList(), 'startTime': startDate.millisecondsSinceEpoch, 'endTime': endDate.millisecondsSinceEpoch, 'activitySegmentDuration': activitySegmentDuration, - 'includeManualEntry': includeManualEntry + 'includeManualEntry': includeManualEntry, }; - final fetchedDataPoints = - await _channel.invokeMethod('getAggregateData', args); + final fetchedDataPoints = await _channel.invokeMethod( + 'getAggregateData', + args, + ); if (fetchedDataPoints != null) { final msg = { @@ -1302,10 +1427,13 @@ class Health { List _parse(Map message) { final dataType = message["dataType"] as HealthDataType; final dataPoints = message["dataPoints"] as List; + String? unit = message["unit"] as String?; return dataPoints - .map((dataPoint) => - HealthDataPoint.fromHealthDataPoint(dataType, dataPoint)) + .map( + (dataPoint) => + HealthDataPoint.fromHealthDataPoint(dataType, dataPoint, unit), + ) .toList(); } @@ -1315,8 +1443,11 @@ class Health { /// Get the total number of steps within a specific time period. /// Returns null if not successful. - Future getTotalStepsInInterval(DateTime startTime, DateTime endTime, - {bool includeManualEntry = true}) async { + Future getTotalStepsInInterval( + DateTime startTime, + DateTime endTime, { + bool includeManualEntry = true, + }) async { final args = { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, @@ -1333,20 +1464,22 @@ class Health { /// Assigns numbers to specific [HealthDataType]s. int _alignValue(HealthDataType type) => switch (type) { - HealthDataType.SLEEP_IN_BED => 0, - HealthDataType.SLEEP_ASLEEP => 1, - HealthDataType.SLEEP_AWAKE => 2, - HealthDataType.SLEEP_LIGHT => 3, - HealthDataType.SLEEP_DEEP => 4, - HealthDataType.SLEEP_REM => 5, - HealthDataType.HEADACHE_UNSPECIFIED => 0, - HealthDataType.HEADACHE_NOT_PRESENT => 1, - HealthDataType.HEADACHE_MILD => 2, - HealthDataType.HEADACHE_MODERATE => 3, - HealthDataType.HEADACHE_SEVERE => 4, - _ => throw HealthException(type, - "HealthDataType was not aligned correctly - please report bug at https://github.com/cph-cachet/flutter-plugins/issues"), - }; + HealthDataType.SLEEP_IN_BED => 0, + HealthDataType.SLEEP_ASLEEP => 1, + HealthDataType.SLEEP_AWAKE => 2, + HealthDataType.SLEEP_LIGHT => 3, + HealthDataType.SLEEP_DEEP => 4, + HealthDataType.SLEEP_REM => 5, + HealthDataType.HEADACHE_UNSPECIFIED => 0, + HealthDataType.HEADACHE_NOT_PRESENT => 1, + HealthDataType.HEADACHE_MILD => 2, + HealthDataType.HEADACHE_MODERATE => 3, + HealthDataType.HEADACHE_SEVERE => 4, + _ => throw HealthException( + type, + "HealthDataType was not aligned correctly - please report bug at https://github.com/cph-cachet/flutter-plugins/issues", + ), + }; /// Write workout data to Apple Health or Google Health Connect. /// @@ -1378,18 +1511,24 @@ class Health { }) async { await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && - [RecordingMethod.active, RecordingMethod.unknown] - .contains(recordingMethod)) { + [ + RecordingMethod.active, + RecordingMethod.unknown, + ].contains(recordingMethod)) { throw ArgumentError("recordingMethod must be manual or automatic on iOS"); } // Check that value is on the current Platform if (Platform.isIOS && !_isOnIOS(activityType)) { - throw HealthException(activityType, - "Workout activity type $activityType is not supported on iOS"); + throw HealthException( + activityType, + "Workout activity type $activityType is not supported on iOS", + ); } else if (Platform.isAndroid && !_isOnAndroid(activityType)) { - throw HealthException(activityType, - "Workout activity type $activityType is not supported on Android"); + throw HealthException( + activityType, + "Workout activity type $activityType is not supported on Android", + ); } final args = { 'activityType': activityType.name, diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index ba605eb33..3d23592e3 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -404,6 +404,7 @@ enum HealthDataUnit { // Length units METER, + CENTIMETER, INCH, FOOT, YARD, @@ -468,6 +469,7 @@ enum HealthDataUnit { BEATS_PER_MINUTE, RESPIRATIONS_PER_MINUTE, MILLIGRAM_PER_DECILITER, + MILLIMOLES_PER_LITER, METER_PER_SECOND, UNKNOWN_UNIT, NO_UNIT,