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 1 commit
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
5 changes: 4 additions & 1 deletion packages/health/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,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 @@ -69,6 +71,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 @@ -421,6 +424,18 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
return false
}

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 @@ -1412,6 +1427,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 @@ -1427,6 +1452,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
return
}
activity = null
healthConnectRequestPermissionsLauncher = null;
}

/**
Expand Down Expand Up @@ -1537,9 +1563,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
10 changes: 10 additions & 0 deletions packages/health/example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>
<activity-alias
eliasteeny marked this conversation as resolved.
Show resolved Hide resolved
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>
<meta-data android:name="flutterEmbedding"
android:value="2"/>
</application>
Expand Down
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);

// Store an Audiogram
// Uncomment these on iOS - only available on iOS
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 @@ -68,12 +68,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