Health Connect: Using getGrantedPermissions() with async coroutine freeze the process when await()

184 views Asked by At

HealthConnectClient.permissionController.getGrantedPermissions() freeze the process when called inside async coroutine with await()

This bug doesn't occure if an Intent using PermissionController.createRequestPermissionResultContract() is launch before.

A simple code like this reproduces the bug :

    private val PERMISSIONS = setOf(HealthPermission.getReadPermission(StepsRecord::class))
    private fun syncCheckPermissions(healthConnectClient:HealthConnectClient): Boolean {
        val granted: Boolean = runBlocking {
            try {
                withTimeout(5000) {
                    GlobalScope.async {
                        val granted = healthConnectClient.permissionController.getGrantedPermissions()
                        granted.containsAll(PERMISSIONS)
                    }.await()
                }
            } catch (e: TimeoutCancellationException) {
                return@runBlocking true
            }
        }
        return granted
    }

When getGrantedPermissions() is replaced by any mock function, there is no timeout triggered.

I'm not sure if the bug come from the lib or my code. Is there another way to get non-suspend result from suspend function ?

Edit : I know runBlocking block the process until await done and there is the bug, await never return a result and in the end I got an ANR from the system.

2

There are 2 answers

0
Tenfour04 On

The answer is no, there is no way to get the result of a suspend function outside of a coroutine without blocking. A suspend function is typically a time-consuming function, which is why it's marked suspend. So you have a function that takes a long time to return a result. You can either block to wait for it (which you must never do on the main thread), or you can call it from a coroutine.

You should never be using runBlocking in this way even for suspend functions that won't hang, because they will still temporarily hang the UI thread and make your app seem frozen and janky.

Do this kind of stuff in a regular launched coroutine. You can show a UI progress indicator at the start and hide it at the end.

Also, it is pointless to call async and then immediately call await() on it. It's no different than just running the code directly. And you can use withTimeoutOrNull to simplify your code:

// Inside a launched coroutine:
val granted = withTimeoutOrNull(5000) {
    healthConnectClient.permissionController.getGrantedPermissions()
        .containsAll(PERMISSIONS)
} ?: true

although I think making true your default is risky. You probably don't have the permissions if it isn't able to return within 5 seconds.

1
Maxim Shadrin On

Inside the runBlocking block, you call the await() function.

The runBlocking block - Runs a new coroutine and blocks the current thread until its completion.

This is the reason why your syncCheckPermissions function blocks the thread and waits for the result from await().

So syncCheckPermissions isn't async because runBlocking, you can declare syncCheckPermissions as a suspend function or use callback approach.

I guess there you can find more detailed explanation