Skip to content

Commit fb2e6ec

Browse files
authored
Merge pull request #1267 from carp-dk/health13/prs
Health 13.2.0
2 parents d52bf66 + 22a9e1d commit fb2e6ec

23 files changed

Lines changed: 1820 additions & 797 deletions

packages/health/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 13.2.0
2+
3+
* Add get health data by UUID (see `getHealthDataByUUID()`) - PR [#1193](https://github.com/carp-dk/flutter-plugins/pull/1193), [#1194](https://github.com/carp-dk/flutter-plugins/pull/1194)
4+
* Add delete by UUID (`deleteByUUID()`)
5+
* Add support for unit conversion in `WeightRecord`, `HeightRecord`, `BodyTemperatureRecord`, and `BloodGlucoseRecord` - PR [#1212](https://github.com/carp-dk/flutter-plugins/pull/1223)
6+
* Update `compileSDK` to 36 - Fix [#1261](https://github.com/carp-dk/flutter-plugins/issues/1261)
7+
* Update Gradle to 8.9.1
8+
* Update `org.jetbrains.kotlin.android` to 2.1.0
9+
* Update `androidx.health.connect:connect-client` to 1.1.0-rc03
10+
* Update `device_info_plus` to 12.1.0 - Fix [#1264](https://github.com/carp-dk/flutter-plugins/issues/1264)
11+
112
## 13.1.4
213

314
* Fix adding mindfulness resulted in crash in iOS

packages/health/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,22 @@ flutter: Health Plugin Error:
270270
flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible}))
271271
```
272272

273+
### Fetch single health data by UUID
274+
275+
In order to retrieve a single record, it is required to provide `String uuid` and `HealthDataType type`.
276+
277+
Please see example below:
278+
```dart
279+
HealthDataPoint? healthPoint = await health.getHealthDataByUUID(
280+
uuid: 'random-uuid-string',
281+
type: HealthDataType.STEPS,
282+
);
283+
```
284+
```
285+
I/FLUTTER_HEALTH( 9161): Success: {uuid=random-uuid-string, value=12, date_from=1742259061009, date_to=1742259092888, source_id=, source_name=com.google.android.apps.fitness, recording_method=0}
286+
```
287+
> Assuming that the `uuid` and `type` are coming from your database.
288+
273289
### Filtering by recording method
274290

275291
Google Health Connect and Apple HealthKit both provide ways to distinguish samples collected "automatically" and manually entered data by the user.
@@ -322,6 +338,43 @@ Furthermore, the plugin now exposes three new functions to help you check and re
322338
2. `isHealthDataInBackgroundAuthorized()`: Checks the current status of the Health Data in Background permission
323339
3. `requestHealthDataInBackgroundAuthorization()`: Requests the Health Data in Background permission.
324340

341+
### Fetch single health data by UUID
342+
343+
In order to retrieve a single record, it is required to provide `String uuid` and `HealthDataType type`.
344+
345+
Please see example below:
346+
```dart
347+
HealthDataPoint? healthPoint = await health.getHealthDataByUUID(
348+
uuid: 'E9F2EEAD-8FC5-4CE5-9FF5-7C4E535FB8B8',
349+
type: HealthDataType.WORKOUT,
350+
);
351+
```
352+
```
353+
data by UUID: HealthDataPoint -
354+
uuid: E9F2EEAD-8FC5-4CE5-9FF5-7C4E535FB8B8,
355+
value: WorkoutHealthValue - workoutActivityType: RUNNING,
356+
totalEnergyBurned: null,
357+
totalEnergyBurnedUnit: KILOCALORIE,
358+
totalDistance: 2400,
359+
totalDistanceUnit: METER
360+
totalSteps: null,
361+
totalStepsUnit: null,
362+
unit: NO_UNIT,
363+
dateFrom: 2025-05-02 07:31:00.000,
364+
dateTo: 2025-05-02 08:25:00.000,
365+
dataType: WORKOUT,
366+
platform: HealthPlatformType.appleHealth,
367+
deviceId: unknown,
368+
sourceId: com.apple.Health,
369+
sourceName: Health
370+
recordingMethod: RecordingMethod.manual
371+
workoutSummary: WorkoutSummary - workoutType: runningtotalDistance: 2400, totalEnergyBurned: 0, totalSteps: 0
372+
metadata: null
373+
deviceModel: null
374+
```
375+
> Assuming that the `uuid` and `type` are coming from your database.
376+
377+
325378
## Data Types
326379

327380
The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html).

packages/health/android/build.gradle

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ buildscript {
99
}
1010

1111
dependencies {
12-
classpath 'com.android.tools.build:gradle:8.1.4'
12+
classpath 'com.android.tools.build:gradle:8.13.0'
1313
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1414
}
1515
}
@@ -25,7 +25,7 @@ apply plugin: 'com.android.library'
2525
apply plugin: 'kotlin-android'
2626

2727
android {
28-
compileSdk 34
28+
compileSdk 36
2929

3030
compileOptions {
3131
sourceCompatibility JavaVersion.VERSION_11
@@ -41,7 +41,7 @@ android {
4141
}
4242
defaultConfig {
4343
minSdkVersion 26
44-
targetSdkVersion 34
44+
targetSdkVersion 36
4545
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
4646
}
4747
lintOptions {
@@ -51,12 +51,11 @@ android {
5151
}
5252

5353
dependencies {
54-
def composeBom = platform('androidx.compose:compose-bom:2025.02.00')
54+
def composeBom = platform('androidx.compose:compose-bom:2025.09.00')
5555
implementation(composeBom)
56-
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
56+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10"
5757

58-
implementation("androidx.health.connect:connect-client:1.1.0-alpha11")
59-
def fragment_version = "1.8.6"
60-
implementation "androidx.fragment:fragment-ktx:$fragment_version"
58+
implementation("androidx.health.connect:connect-client:1.1.0-rc03")
59+
implementation "androidx.fragment:fragment-ktx:1.8.9"
6160

6261
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
#Tue Sep 16 09:25:00 CEST 2025
12
distributionBase=GRADLE_USER_HOME
23
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
4+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
45
zipStoreBase=GRADLE_USER_HOME
56
zipStorePath=wrapper/dists

packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataConverter.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,34 @@ class HealthDataConverter {
2121
* @return List<Map<String, Any?>> List of converted records (some records may split into multiple entries)
2222
* @throws IllegalArgumentException If the record type is not supported
2323
*/
24-
fun convertRecord(record: Any, dataType: String): List<Map<String, Any?>> {
24+
fun convertRecord(record: Any, dataType: String, dataUnit: String? = null): List<Map<String, Any?>> {
2525
val metadata = (record as Record).metadata
2626

2727
return when (record) {
2828
// Single-value instant records
29-
is WeightRecord -> listOf(createInstantRecord(metadata, record.time, record.weight.inKilograms))
30-
is HeightRecord -> listOf(createInstantRecord(metadata, record.time, record.height.inMeters))
29+
is WeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
30+
"POUND" -> record.weight.inPounds
31+
else -> record.weight.inKilograms
32+
}))
33+
is HeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
34+
"CENTIMETER" -> (record.height.inMeters * 100)
35+
"INCH" -> record.height.inInches
36+
else -> record.height.inMeters
37+
}))
3138
is BodyFatRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value))
3239
is LeanBodyMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms))
3340
is HeartRateVariabilityRmssdRecord -> listOf(createInstantRecord(metadata, record.time, record.heartRateVariabilityMillis))
34-
is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, record.temperature.inCelsius))
41+
is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
42+
"DEGREE_FAHRENHEIT" -> record.temperature.inFahrenheit
43+
"KELVIN" -> record.temperature.inCelsius + 273.15
44+
else -> record.temperature.inCelsius
45+
}))
3546
is BodyWaterMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms))
3647
is OxygenSaturationRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value))
37-
is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, record.level.inMilligramsPerDeciliter))
48+
is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
49+
"MILLIMOLES_PER_LITER" -> record.level.inMillimolesPerLiter
50+
else -> record.level.inMilligramsPerDeciliter
51+
}))
3852
is BasalMetabolicRateRecord -> listOf(createInstantRecord(metadata, record.time, record.basalMetabolicRate.inKilocaloriesPerDay))
3953
is RestingHeartRateRecord -> listOf(createInstantRecord(metadata, record.time, record.beatsPerMinute))
4054
is RespiratoryRateRecord -> listOf(createInstantRecord(metadata, record.time, record.rate))
@@ -236,7 +250,7 @@ class HealthDataConverter {
236250
)
237251
)
238252
}
239-
253+
240254
companion object {
241255
private const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC"
242256
private const val MEAL_UNKNOWN = "UNKNOWN"

packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataOperations.kt

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cachet.plugins.health
33
import android.util.Log
44
import androidx.health.connect.client.HealthConnectClient
55
import androidx.health.connect.client.HealthConnectFeatures
6-
import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
76
import androidx.health.connect.client.permission.HealthPermission
87
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_HISTORY
98
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
@@ -103,7 +102,6 @@ class HealthDataOperations(
103102
* @param call Method call from Flutter (unused)
104103
* @param result Flutter result callback returning boolean availability status
105104
*/
106-
@OptIn(ExperimentalFeatureAvailabilityApi::class)
107105
fun isHealthDataHistoryAvailable(call: MethodCall, result: Result) {
108106
scope.launch {
109107
result.success(
@@ -139,7 +137,6 @@ class HealthDataOperations(
139137
* @param call Method call from Flutter (unused)
140138
* @param result Flutter result callback returning boolean availability status
141139
*/
142-
@OptIn(ExperimentalFeatureAvailabilityApi::class)
143140
fun isHealthDataInBackgroundAvailable(call: MethodCall, result: Result) {
144141
scope.launch {
145142
result.success(
@@ -247,6 +244,45 @@ class HealthDataOperations(
247244
}
248245
}
249246

247+
/**
248+
* Deletes a specific health record by its client record ID and data type. Allows precise
249+
* deletion of individual health records using client-side IDs.
250+
*
251+
* @param call Method call containing 'dataTypeKey', 'recordId', and 'clientRecordId'
252+
* @param result Flutter result callback returning boolean success status
253+
*/
254+
fun deleteByClientRecordId(call: MethodCall, result: Result) {
255+
val arguments = call.arguments as? HashMap<*, *>
256+
val dataTypeKey = (arguments?.get("dataTypeKey") as? String)!!
257+
val recordId = listOfNotNull(arguments["recordId"] as? String)
258+
val clientRecordId = listOfNotNull(arguments["clientRecordId"] as? String)
259+
if (!HealthConstants.mapToType.containsKey(dataTypeKey)) {
260+
Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataTypeKey not found in HC")
261+
result.success(false)
262+
return
263+
}
264+
val classType = HealthConstants.mapToType[dataTypeKey]!!
265+
266+
scope.launch {
267+
try {
268+
healthConnectClient.deleteRecords(
269+
classType,
270+
recordId,
271+
clientRecordId
272+
)
273+
result.success(true)
274+
} catch (e: Exception) {
275+
Log.e(
276+
"FLUTTER_HEALTH::ERROR",
277+
"Error deleting record with ClientRecordId: $clientRecordId"
278+
)
279+
Log.e("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error")
280+
Log.e("FLUTTER_HEALTH::ERROR", e.stackTraceToString())
281+
result.success(false)
282+
}
283+
}
284+
}
285+
250286
/**
251287
* Internal helper method to prepare Health Connect permission strings. Converts data type names
252288
* and access levels into proper permission format.

0 commit comments

Comments
 (0)