From 9880767e541a868251ab9396bcfdbcf96c3659f8 Mon Sep 17 00:00:00 2001 From: Eric Bouchard Date: Thu, 11 Jan 2024 18:50:05 +0700 Subject: [PATCH 1/2] Android 14 compatibility Support new Android sleep stage record format for Health Connect Add namespace Update versions for general compatibility Fixed a few typos in the README.md --- packages/health/README.md | 10 +-- packages/health/android/build.gradle | 6 +- .../cachet/plugins/health/HealthPlugin.kt | 61 +++++++++++++------ packages/health/example/lib/main.dart | 16 ++--- packages/health/example/lib/util.dart | 12 ++-- 5 files changed, 66 insertions(+), 39 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 7c1827fdc..72170b035 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -150,7 +150,7 @@ In the Health Connect permissions activity there is a link to your privacy polic ### Android Permissions -Starting from API level 28 (Android 9.0) acessing some fitness data (e.g. Steps) requires a special permission. +Starting from API level 28 (Android 9.0) accessing some fitness data (e.g. Steps) requires a special permission. To set it add the following line to your `AndroidManifest.xml` file. @@ -160,7 +160,7 @@ To set it add the following line to your `AndroidManifest.xml` file. #### Health Connect -If using Health Connect on Android it requires speciel permissions in the `AndroidManifest.xml` file. +If using Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file. The permissions can be found here: https://developer.android.com/guide/health-and-fitness/health-connect/data-and-data-types/data-types Example shown here (can also be found in the example app): @@ -181,7 +181,7 @@ Additionally, for Workouts: If the distance of a workout is requested then the l There's a `debug`, `main` and `profile` version which are chosen depending on how you start your app. In general, it's sufficient to add permission only to the `main` version. -Beacuse this is labled as a `dangerous` protection level, the permission system will not grant it automaticlly and it requires the user's action. +Because this is labeled as a `dangerous` protection level, the permission system will not grant it automaticlly and it requires the user's action. You can prompt the user for it using the [permission_handler](https://pub.dev/packages/permission_handler) plugin. Follow the plugin setup instructions and add the following line before requsting the data: @@ -192,7 +192,9 @@ await Permission.location.request(); ### Android 14 -This plugin uses the new `registerForActivityResult` when requesting permissions from Health Connect. In order for that to work, the Main app's activity should extend `FlutterFragmentActivity` instead of `FlutterActivity`. This adjustment allows casting from `Activity` to `ComponentActivity` for accessing `registerForActivityResult`. +This plugin uses the new `registerForActivityResult` when requesting permissions from Health Connect. +In order for that to work, the Main app's activity should extend `FlutterFragmentActivity` instead of `FlutterActivity`. +This adjustment allows casting from `Activity` to `ComponentActivity` for accessing `registerForActivityResult`. In your MainActivity.kt file, update the `MainActivity` class so that it extends `FlutterFragmentActivity` instead of the default `FlutterActivity`: diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index 950f6d210..66f8d2933 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -2,14 +2,14 @@ group 'cachet.plugins.health' version '1.2' buildscript { - ext.kotlin_version = '1.8.0' + ext.kotlin_version = '1.7.22' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -27,6 +27,8 @@ apply plugin: 'kotlin-android' android { compileSdkVersion 34 + namespace "cachet.plugins.health" + sourceSets { main.java.srcDirs += 'src/main/kotlin' } 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 ad0b3c29f..23bf9dfb5 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 @@ -456,6 +456,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : SLEEP_ASLEEP -> DataType.TYPE_SLEEP_SEGMENT SLEEP_AWAKE -> DataType.TYPE_SLEEP_SEGMENT SLEEP_IN_BED -> DataType.TYPE_SLEEP_SEGMENT + SLEEP_LIGHT -> DataType.TYPE_SLEEP_SEGMENT + SLEEP_REM -> DataType.TYPE_SLEEP_SEGMENT + SLEEP_DEEP -> DataType.TYPE_SLEEP_SEGMENT WORKOUT -> DataType.TYPE_ACTIVITY_SEGMENT else -> throw IllegalArgumentException("Unsupported dataType: $type") } @@ -480,6 +483,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : SLEEP_ASLEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE SLEEP_AWAKE -> Field.FIELD_SLEEP_SEGMENT_TYPE SLEEP_IN_BED -> Field.FIELD_SLEEP_SEGMENT_TYPE + SLEEP_LIGHT -> Field.FIELD_SLEEP_SEGMENT_TYPE + SLEEP_REM -> Field.FIELD_SLEEP_SEGMENT_TYPE + SLEEP_DEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE WORKOUT -> Field.FIELD_ACTIVITY else -> throw IllegalArgumentException("Unsupported dataType: $type") } @@ -1641,12 +1647,25 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ) } // Filter sleep stages for requested stage - } else if (classType == SleepStageRecord::class) { + } + else if (classType == SleepSessionRecord::class) { for (rec in response.records) { - if (rec is SleepStageRecord) { - if (dataType == MapSleepStageToType[rec.stage]) { + if (rec is SleepSessionRecord) { + if (dataType == SLEEP_SESSION) { healthConnectData.addAll(convertRecord(rec, dataType)) } + else { + for (recStage in rec.stages) { + if (dataType == MapSleepStageToType[recStage.stage]) { + healthConnectData.addAll( + convertRecordStage( + recStage, dataType, + rec.metadata.dataOrigin.packageName + ) + ) + } + } + } } } } @@ -1660,6 +1679,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } + fun convertRecordStage(stage: SleepSessionRecord.Stage, dataType: String, sourceName: String): + List> { + return listOf( + mapOf( + "stage" to stage.stage, + "value" to ChronoUnit.MINUTES.between(stage.startTime, stage.endTime), + "date_from" to stage.startTime.toEpochMilli(), + "date_to" to stage.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to sourceName, + ), + ); + } + // TODO: Find alternative to SOURCE_ID or make it nullable? fun convertRecord(record: Any, dataType: String): List> { val metadata = (record as Record).metadata @@ -1781,16 +1814,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) - is SleepStageRecord -> return listOf( - mapOf( - "stage" to record.stage, - "value" to ChronoUnit.MINUTES.between(record.startTime, record.endTime), - "date_from" to record.startTime.toEpochMilli(), - "date_to" to record.endTime.toEpochMilli(), - "source_id" to "", - "source_name" to metadata.dataOrigin.packageName, - ), - ) is RestingHeartRateRecord -> return listOf( mapOf( "value" to record.beatsPerMinute, @@ -2138,12 +2161,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : BLOOD_GLUCOSE to BloodGlucoseRecord::class, DISTANCE_DELTA to DistanceRecord::class, WATER to HydrationRecord::class, - SLEEP_ASLEEP to SleepStageRecord::class, - SLEEP_AWAKE to SleepStageRecord::class, - SLEEP_LIGHT to SleepStageRecord::class, - SLEEP_DEEP to SleepStageRecord::class, - SLEEP_REM to SleepStageRecord::class, - SLEEP_OUT_OF_BED to SleepStageRecord::class, + SLEEP_ASLEEP to SleepSessionRecord::class, + SLEEP_AWAKE to SleepSessionRecord::class, + SLEEP_LIGHT to SleepSessionRecord::class, + SLEEP_DEEP to SleepSessionRecord::class, + SLEEP_REM to SleepSessionRecord::class, + SLEEP_OUT_OF_BED to SleepSessionRecord::class, SLEEP_SESSION to SleepSessionRecord::class, WORKOUT to ExerciseSessionRecord::class, RESTING_HEART_RATE to RestingHeartRateRecord::class, diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 445171b8a..7e570a2fd 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -160,14 +160,14 @@ class _HealthAppState extends State { totalDistance: 2430, totalEnergyBurned: 400); success &= await health.writeBloodPressure(90, 80, earlier, now); - // success &= await health.writeHealthData( - // 0.0, HealthDataType.SLEEP_REM, earlier, now); - // success &= await health.writeHealthData( - // 0.0, HealthDataType.SLEEP_ASLEEP, earlier, now); - // success &= await health.writeHealthData( - // 0.0, HealthDataType.SLEEP_AWAKE, earlier, now); - // success &= await health.writeHealthData( - // 0.0, HealthDataType.SLEEP_DEEP, earlier, now); + success &= await health.writeHealthData( + 0.0, HealthDataType.SLEEP_REM, earlier, now); + success &= await health.writeHealthData( + 0.0, HealthDataType.SLEEP_ASLEEP, earlier, now); + success &= await health.writeHealthData( + 0.0, HealthDataType.SLEEP_AWAKE, earlier, now); + success &= await health.writeHealthData( + 0.0, HealthDataType.SLEEP_DEEP, earlier, now); // Store an Audiogram // Uncomment these on iOS - only available on iOS diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index 4a9299568..e2756e09e 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -68,12 +68,12 @@ const List dataTypesAndroid = [ // HealthDataType.MOVE_MINUTES, // TODO: Find alternative for Health Connect HealthDataType.DISTANCE_DELTA, HealthDataType.RESPIRATORY_RATE, - // HealthDataType.SLEEP_AWAKE, - // HealthDataType.SLEEP_ASLEEP, - // HealthDataType.SLEEP_LIGHT, - // HealthDataType.SLEEP_DEEP, - // HealthDataType.SLEEP_REM, - // HealthDataType.SLEEP_SESSION, + HealthDataType.SLEEP_AWAKE, + HealthDataType.SLEEP_ASLEEP, + HealthDataType.SLEEP_LIGHT, + HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_REM, + HealthDataType.SLEEP_SESSION, HealthDataType.WATER, HealthDataType.WORKOUT, HealthDataType.RESTING_HEART_RATE, From 58eaa11e6ce3358bf29060660d97f186713ad064 Mon Sep 17 00:00:00 2001 From: Eric Bouchard Date: Mon, 22 Jan 2024 15:35:36 +0700 Subject: [PATCH 2/2] Remove package from manifest for Android 14 --- packages/health/android/src/main/AndroidManifest.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/health/android/src/main/AndroidManifest.xml b/packages/health/android/src/main/AndroidManifest.xml index 5648f6800..a2f47b605 100644 --- a/packages/health/android/src/main/AndroidManifest.xml +++ b/packages/health/android/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - +