Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempt to fix Health Connect on Android 14 #834

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ Health Connect requires the following lines in the `AndroidManifest.xml` file (a
</queries>
```

In the Health Connect permissions activity there is a link to your privacy policy. You need to grant the Health Connect app access in order to link back to your privacy policy. In the example below, you should either replace `.MainActivity` with an activity that presents the privacy policy or have the Main Activity route the user to the policy. This step may be required to pass Google app review when requesting access to sensitive permissions.

```
<activity-alias
android:name="ViewPermissionUsageActivity"
android:exported="true"
android:targetActivity=".MainActivity"
android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
</intent-filter>
</activity-alias>
```

### Android Permissions

Starting from API level 28 (Android 9.0) acessing some fitness data (e.g. Steps) requires a special permission.
Expand Down Expand Up @@ -177,6 +192,22 @@ await Permission.activityRecognition.request();
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`.

In your MainActivity.kt file, update the `MainActivity` class so that it extends `FlutterFragmentActivity` instead of the default `FlutterActivity`:

```
...
import io.flutter.embedding.android.FlutterFragmentActivity
...

class MainActivity: FlutterFragmentActivity() {
...
}
```

### Android X

Replace the content of the `android/gradle.properties` file with the following lines:
Expand Down
5 changes: 4 additions & 1 deletion packages/health/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,8 @@ dependencies {
implementation("com.google.android.gms:play-services-auth:20.2.0")

// The new health connect api
implementation("androidx.health.connect:connect-client:1.1.0-alpha03")
implementation("androidx.health.connect:connect-client:1.1.0-alpha06")

def fragment_version = "1.6.2"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.NonNull
import androidx.core.content.ContextCompat
import androidx.health.connect.client.HealthConnectClient
Expand Down Expand Up @@ -74,6 +76,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
private var context: Context? = null
private var threadPoolExecutor: ExecutorService? = null
private var useHealthConnectIfAvailable: Boolean = false
private var healthConnectRequestPermissionsLauncher: ActivityResultLauncher<Set<String>>? = null
private lateinit var healthConnectClient: HealthConnectClient
private lateinit var scope: CoroutineScope

Expand Down Expand Up @@ -418,6 +421,19 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}


private fun onHealthConnectPermissionCallback(permissionGranted: Set<String>)
{
if(permissionGranted.isEmpty()) {
mResult?.success(false);
Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect)!")

}else {
mResult?.success(true);
Log.i("FLUTTER_HEALTH", "Access Granted (to Health Connect)!")
}

}

private fun keyToHealthDataType(type: String): DataType {
return when (type) {
BODY_FAT_PERCENTAGE -> DataType.TYPE_BODY_FAT_PERCENTAGE
Expand Down Expand Up @@ -1554,6 +1570,16 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
binding.addActivityResultListener(this)
activity = binding.activity


if ( healthConnectAvailable) {
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()

healthConnectRequestPermissionsLauncher =(activity as ComponentActivity).registerForActivityResult(requestPermissionActivityContract) { granted ->

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use FlutterFragmentActivity instead of ComponentActivity

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the ComponentActivity in order to use the new api function registerForActivityResult, FlutterFragmentActivity is built upon the new Android apis which can be casted to a ComponentActivity. If you try to cast FlutterActivity to a ComponentActivity it will throw an error.

onHealthConnectPermissionCallback(granted);
}
}

}

override fun onDetachedFromActivityForConfigChanges() {
Expand All @@ -1569,6 +1595,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
return
}
activity = null
healthConnectRequestPermissionsLauncher = null;
}

/**
Expand Down Expand Up @@ -1679,9 +1706,15 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
}
}
val contract = PermissionController.createRequestPermissionResultContract()
val intent = contract.createIntent(activity!!, permList.toSet())
activity!!.startActivityForResult(intent, HEALTH_CONNECT_RESULT_CODE)

if(healthConnectRequestPermissionsLauncher == null) {
result.success(false)
Log.i("FLUTTER_HEALTH", "Permission launcher not found")
return;
}


healthConnectRequestPermissionsLauncher!!.launch(permList.toSet());
}

fun getHCData(call: MethodCall, result: Result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>
<!-- For versions starting Android 14, create an activity alias to show the rationale
of Health Connect permissions once users click the privacy policy link.
<activity-alias
eliasteeny marked this conversation as resolved.
Show resolved Hide resolved
android:name="ViewPermissionUsageActivity"
android:exported="true"
Expand All @@ -92,8 +90,7 @@
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
</intent-filter>
</activity-alias>

--> <meta-data android:name="flutterEmbedding"
<meta-data android:name="flutterEmbedding"
android:value="2"/>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cachet.plugins.example_app

import android.os.Bundle

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
eliasteeny marked this conversation as resolved.
Show resolved Hide resolved

class MainActivity: FlutterActivity() {
class MainActivity: FlutterFragmentActivity() {
}
16 changes: 8 additions & 8 deletions packages/health/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,14 @@ class _HealthAppState extends State<HealthApp> {
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes relevant to the PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah for some reason adding the sleep records is failing stating that it is an unsupported type. Although it is working on Android 13 and below.

// 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.writeMeal(
earlier, now, 1000, 50, 25, 50, "Banana", MealType.SNACK);
Expand Down
12 changes: 6 additions & 6 deletions packages/health/example/lib/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ const List<HealthDataType> 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes relevant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetching the SLEEP data is crashing the app with the same error as writing.

Copy link
Contributor

@eric-nextsense eric-nextsense Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked out this PR, uncommented the SLEEP lines, and i can read sleep data without a crash. I am running Android 14.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eliasteeny could you please double check if this error still occurs on your device and if so, tell us your OS version and setup?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got a closer look. I did not have a wearable so was reading only sleep session data (time asleep, one data point) and that worked fine. I get the crash now that i have a wearable and i try to read the stages. The issue seems to be that SleepStageRecord object no longer exists in Android 14. Instead there is a list in the SleepSessionRecord. You can see this sample code in https://developer.android.com/health-and-fitness/guides/health-connect/develop/sessions

suspend fun readSleepSessions(
healthConnectClient: HealthConnectClient,
startTime: Instant,
endTime: Instant
) {
val response =
healthConnectClient.readRecords(
ReadRecordsRequest(
SleepSessionRecord::class,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
)
for (sleepRecord in response.records) {
// Retrieve relevant sleep stages from each sleep record
val sleepStages = sleepRecord.stages
}
}

The convertRecord function returns a single record and now it should return many, not sure how to handle this with the way it works now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a pull request on your fork @eliasteeny. I tested it on Android 13 and Android 14, both were able to pull both SLEEP_SESSION (total sleep time only) and individual sleep stages.

I did not test writing though, i don't have an app that would do that.

// HealthDataType.SLEEP_ASLEEP,
// HealthDataType.SLEEP_LIGHT,
// HealthDataType.SLEEP_DEEP,
// HealthDataType.SLEEP_REM,
// HealthDataType.SLEEP_SESSION,
HealthDataType.WATER,
HealthDataType.WORKOUT,
HealthDataType.RESTING_HEART_RATE,
Expand Down