From d2c831dd656eafa0a70c8577233103e395ab034a Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Mon, 3 Feb 2025 08:30:39 +0100 Subject: [PATCH 1/8] [Health] Add HC health history permission Created two new methods isHealthDataHistoryAuthorized() and requestHealthDataHistoryAuthorization(). Both methods are no-op on iOS and return true since Apple Health does not restrict history length. Closes #1126 --- packages/health/android/build.gradle | 2 +- .../cachet/plugins/health/HealthPlugin.kt | 38 +++++++++++++++++ packages/health/lib/src/health_plugin.dart | 42 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index 1f96a1fb1..bed498fd2 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -55,7 +55,7 @@ dependencies { implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation("androidx.health.connect:connect-client:1.1.0-alpha07") + implementation("androidx.health.connect:connect-client:1.1.0-alpha11") def fragment_version = "1.6.2" implementation "androidx.fragment:fragment-ktx:$fragment_version" 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 1d6ee83dc..4bba159e1 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 @@ -12,6 +12,7 @@ import androidx.annotation.NonNull import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.PermissionController import androidx.health.connect.client.permission.HealthPermission +import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_HISTORY import androidx.health.connect.client.records.* import androidx.health.connect.client.records.MealType.MEAL_TYPE_BREAKFAST import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER @@ -147,6 +148,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : when (call.method) { "installHealthConnect" -> installHealthConnect(call, result) "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) + "isHealthDataHistoryAuthorized" -> isHealthDataHistoryAuthorized(call, result) + "requestHealthDataHistoryAuthorization" -> requestHealthDataHistoryAuthorization(call, result) "hasPermissions" -> hasPermissions(call, result) "requestAuthorization" -> requestAuthorization(call, result) "revokePermissions" -> revokePermissions(call, result) @@ -512,6 +515,41 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } + /** + * Checks if PERMISSION_READ_HEALTH_DATA_HISTORY has been granted + */ + private fun isHealthDataHistoryAuthorized(call: MethodCall, result: Result) { + scope.launch { + result.success( + healthConnectClient + .permissionController + .getGrantedPermissions() + .containsAll(listOf(PERMISSION_READ_HEALTH_DATA_HISTORY)), + ) + } + } + + /** + * Requests authorization for PERMISSION_READ_HEALTH_DATA_HISTORY + */ + private fun requestHealthDataHistoryAuthorization(call: MethodCall, result: Result) { + if (context == null) { + result.success(false) + return + } + + if (healthConnectRequestPermissionsLauncher == null) { + result.success(false) + Log.i("FLUTTER_HEALTH", "Permission launcher not found") + return + } + + // Store the result to be called in [onHealthConnectPermissionCallback] + mResult = result + isReplySubmitted = false + healthConnectRequestPermissionsLauncher!!.launch(setOf(PERMISSION_READ_HEALTH_DATA_HISTORY)) + } + private fun hasPermissions(call: MethodCall, result: Result) { val args = call.arguments as HashMap<*, *> val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index cc62e3ac7..7ed39cb7b 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -200,6 +200,48 @@ class Health { } } + /// Checks the current status of the Health Data History permission. + /// + /// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_HISTORY() + /// + /// + /// Android only. Returns true on iOS or false if an error occurs. + Future isHealthDataHistoryAuthorized() async { + if (Platform.isIOS) return true; + + try { + final status = + await _channel.invokeMethod('isHealthDataHistoryAuthorized'); + return status ?? false; + } catch (e) { + debugPrint('$runtimeType - Exception in getHealthConnectSdkStatus(): $e'); + return false; + } + } + + /// Requests the Health Data History permission. + /// + /// Returns true if successful, false otherwise. + /// + /// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_HISTORY() + /// + /// + /// Android only. Returns true on iOS or false if an error occurs. + Future requestHealthDataHistoryAuthorization() async { + if (Platform.isIOS) return true; + + await _checkIfHealthConnectAvailableOnAndroid(); + try { + final bool? isAuthorized = + await _channel.invokeMethod('requestHealthDataHistoryAuthorization'); + return isAuthorized ?? false; + } catch (e) { + debugPrint( + '$runtimeType - Exception in requestHealthDataHistoryAuthorization(): $e'); + return false; + } + } + /// Requests permissions to access health data [types]. /// /// Returns true if successful, false otherwise. From 94b1e423677200fb8360b39fa6e5ed316a5be4f3 Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Mon, 3 Feb 2025 11:44:27 +0100 Subject: [PATCH 2/8] Add documentation --- packages/health/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/health/README.md b/packages/health/README.md index ffa41fc89..6eb8836dd 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -74,6 +74,16 @@ An example of asking for permission to read and write heart rate data is shown b ``` +By default, Health Connect restricts read data to 30 days from when permission has been granted. + +You can check and request access to historical data using the `isHealthDataHistoryAuthorized` and `requestHealthDataHistoryAuthorization` methods, respectively. + +The above methods require the following permission to be declared: + +```xml + +``` + Accessing fitness data (e.g. Steps) requires permission to access the "Activity Recognition" API. To set it add the following line to your `AndroidManifest.xml` file. ```xml From 9160f0406f0c276654b391547efd24bec802d3df Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Tue, 4 Feb 2025 11:58:05 +0100 Subject: [PATCH 3/8] Implement check for data history feature --- .../cachet/plugins/health/HealthPlugin.kt | 17 +++++++++++++++ packages/health/lib/src/health_plugin.dart | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+) 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 4bba159e1..89f403722 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 @@ -9,7 +9,9 @@ import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.annotation.NonNull +import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi import androidx.health.connect.client.HealthConnectClient +import androidx.health.connect.client.HealthConnectFeatures import androidx.health.connect.client.PermissionController import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_HISTORY @@ -148,6 +150,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : when (call.method) { "installHealthConnect" -> installHealthConnect(call, result) "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) + "isHealthDataHistoryAvailable" -> isHealthDataHistoryAvailable(call, result) "isHealthDataHistoryAuthorized" -> isHealthDataHistoryAuthorized(call, result) "requestHealthDataHistoryAuthorization" -> requestHealthDataHistoryAuthorization(call, result) "hasPermissions" -> hasPermissions(call, result) @@ -515,6 +518,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } + /** + * Checks if the health data history feature is available on this device + */ + @OptIn(ExperimentalFeatureAvailabilityApi::class) + private fun isHealthDataHistoryAvailable(call: MethodCall, result: Result) { + scope.launch { + result.success( + healthConnectClient + .features + .getFeatureStatus(HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_HISTORY) == + HealthConnectFeatures.FEATURE_STATUS_AVAILABLE) + } + } + /** * Checks if PERMISSION_READ_HEALTH_DATA_HISTORY has been granted */ diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 7ed39cb7b..453672d7a 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -200,7 +200,28 @@ class Health { } } + /// Checks if the Health Data History feature is available. + /// + /// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_HISTORY() + /// + /// + /// Android only. Returns false on iOS or if an error occurs. + Future isHealthDataHistoryAvailable() async { + if (Platform.isIOS) return false; + + try { + final status = + await _channel.invokeMethod('isHealthDataHistoryAvailable'); + return status ?? false; + } catch (e) { + debugPrint( + '$runtimeType - Exception in isHealthDataHistoryAvailable(): $e'); + return false; + } + } + /// Checks the current status of the Health Data History permission. + /// Make sure to check [isHealthConnectAvailable] before calling this method. /// /// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_HISTORY() /// From a0b03dd9a1d3b7e4bdcd12f27795a07311405d37 Mon Sep 17 00:00:00 2001 From: Daniel Cachapa Date: Tue, 4 Feb 2025 11:58:24 +0100 Subject: [PATCH 4/8] Fix exception message --- packages/health/lib/src/health_plugin.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 453672d7a..c734533f0 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -235,7 +235,8 @@ class Health { await _channel.invokeMethod('isHealthDataHistoryAuthorized'); return status ?? false; } catch (e) { - debugPrint('$runtimeType - Exception in getHealthConnectSdkStatus(): $e'); + debugPrint( + '$runtimeType - Exception in isHealthDataHistoryAuthorized(): $e'); return false; } } From 31e97500a5484a052602a6f1589c6651b32c2e7e Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 1 Mar 2025 07:16:17 +0100 Subject: [PATCH 5/8] Update Java build to 11, Fragment and Compose-BOM to latest --- packages/health/android/build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index bed498fd2..45c7b67d8 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -25,15 +25,15 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 34 + compileSdk 34 compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '11' } sourceSets { @@ -51,12 +51,12 @@ android { } dependencies { - def composeBom = platform('androidx.compose:compose-bom:2022.10.00') + def composeBom = platform('androidx.compose:compose-bom:2025.02.00') implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation("androidx.health.connect:connect-client:1.1.0-alpha11") - def fragment_version = "1.6.2" + def fragment_version = "1.8.6" implementation "androidx.fragment:fragment-ktx:$fragment_version" } From 84e3bb74df02650d7c73034962339bd92ce38411 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:10:09 +0100 Subject: [PATCH 6/8] Bump version to 12.1.0, update CHANGELOG; add permissions for reading historical health data and request access in example app --- packages/health/CHANGELOG.md | 8 ++++++- .../android/app/src/main/AndroidManifest.xml | 3 +++ packages/health/example/lib/main.dart | 22 +++++++++++-------- packages/health/ios/health.podspec | 2 +- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 5b8107dd5..8681284b4 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,7 +1,13 @@ -## 12.0.2 +## 12.1.0 * iOS: Parse metadata to remove unsupported types - PR [#1120](https://github.com/cph-cachet/flutter-plugins/pull/1120) * iOS: Add UV Index Types +* Android: Add request access to historic data [#1126](https://github.com/cph-cachet/flutter-plugins/issues/1126) - PR [#1120](https://github.com/cph-cachet/flutter-plugins/pull/1120) +* Android: + * Update `androidx.compose:compose-bom` to `2025.02.00` + * Update `androidx.health.connect:connect-client` to `1.1.0-alpha11` + * Update `androidx.fragment:fragment-ktx` to `1.8.6` + * Update to Java 11 * Update example apps ## 12.0.1 diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml index d76e5bb77..1b559506e 100644 --- a/packages/health/example/android/app/src/main/AndroidManifest.xml +++ b/packages/health/example/android/app/src/main/AndroidManifest.xml @@ -54,6 +54,9 @@ + + + { try { authorized = await health.requestAuthorization(types, permissions: permissions); + + // request access to read historic data + await health.requestHealthDataHistoryAuthorization(); + } catch (error) { debugPrint("Exception in authorize: $error"); } @@ -289,10 +293,10 @@ class HealthAppState extends State { startTime: earlier, endTime: now); success &= await health.writeHealthData( - value: 22, - type: HealthDataType.LEAN_BODY_MASS, - startTime: earlier, - endTime: now); + value: 22, + type: HealthDataType.LEAN_BODY_MASS, + startTime: earlier, + endTime: now); // specialized write methods success &= await health.writeBloodOxygen( @@ -400,11 +404,11 @@ class HealthAppState extends State { endTime: now, recordingMethod: RecordingMethod.manual); success &= await health.writeHealthData( - value: 4.3, - type: HealthDataType.UV_INDEX, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + value: 4.3, + type: HealthDataType.UV_INDEX, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); } setState(() { diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec index 68bb13cf3..aba1806e8 100644 --- a/packages/health/ios/health.podspec +++ b/packages/health/ios/health.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'health' - s.version = '12.0.2' + s.version = '12.1.0' s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.' s.description = <<-DESC Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. From ae60d932a02de45a3c069652fc223eaed9179852 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:03:11 +0100 Subject: [PATCH 7/8] Update CHANGELOG --- packages/health/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 8681284b4..b1aa31ba8 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -2,7 +2,7 @@ * iOS: Parse metadata to remove unsupported types - PR [#1120](https://github.com/cph-cachet/flutter-plugins/pull/1120) * iOS: Add UV Index Types -* Android: Add request access to historic data [#1126](https://github.com/cph-cachet/flutter-plugins/issues/1126) - PR [#1120](https://github.com/cph-cachet/flutter-plugins/pull/1120) +* Android: Add request access to historic data [#1126](https://github.com/cph-cachet/flutter-plugins/issues/1126) - PR [#1127](https://github.com/cph-cachet/flutter-plugins/pull/1127) * Android: * Update `androidx.compose:compose-bom` to `2025.02.00` * Update `androidx.health.connect:connect-client` to `1.1.0-alpha11` From 3b745089089cd548d47785cfd704166545cfefb6 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:09:43 +0100 Subject: [PATCH 8/8] Update CHANGELOG --- packages/health/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index b1aa31ba8..7e5a22404 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -3,6 +3,10 @@ * iOS: Parse metadata to remove unsupported types - PR [#1120](https://github.com/cph-cachet/flutter-plugins/pull/1120) * iOS: Add UV Index Types * Android: Add request access to historic data [#1126](https://github.com/cph-cachet/flutter-plugins/issues/1126) - PR [#1127](https://github.com/cph-cachet/flutter-plugins/pull/1127) +```XML + + +``` * Android: * Update `androidx.compose:compose-bom` to `2025.02.00` * Update `androidx.health.connect:connect-client` to `1.1.0-alpha11`