diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 5b8107dd5..7e5a22404 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,7 +1,17 @@ -## 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 [#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` + * Update `androidx.fragment:fragment-ktx` to `1.8.6` + * Update to Java 11 * Update example apps ## 12.0.1 diff --git a/packages/health/README.md b/packages/health/README.md index f9c1ba9e6..559ec09c4 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 diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index 1f96a1fb1..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-alpha07") - def fragment_version = "1.6.2" + implementation("androidx.health.connect:connect-client:1.1.0-alpha11") + def fragment_version = "1.8.6" 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..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,9 +9,12 @@ 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 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 +150,9 @@ 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) "requestAuthorization" -> requestAuthorization(call, result) "revokePermissions" -> revokePermissions(call, result) @@ -512,6 +518,55 @@ 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 + */ + 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/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. diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index cc62e3ac7..c734533f0 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -200,6 +200,70 @@ 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() + /// + /// + /// 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 isHealthDataHistoryAuthorized(): $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.