Android runtime permissions in one (suspending) function
This post offers a basic implementation of a single suspending function managing the runtime permission process.
Runtime permission API
If you are reading this, you should already know this API. It is only composed of requestPermissions
method, and its related callback onRequestPermissionsResult
. But this is a View-level API, not really convenient.
We still are lucky, this API is accessible from fragments, and that will allow us to make it way easier to use.
Headless Fragment
The trick is to make it a headless fragment, i.e. a fragment without a view. It’s still bound to its activity and can fire dialogs, that’s what we want.
override fun onCreate(savedInstanceState: Bundle?) {
retainInstance = true
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_STORAGE_CODE)
}
// No onCreateView() overriding
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: PermissionResults) {
when (requestCode) {
PERMISSION_STORAGE_CODE -> {
if (grantResults.granted()) {
deferredGrant.complete(true)
exit()
return
} else if (shouldShowRequestPermissionRationale(...)) {
// We insist
return
}
deferredGrant.complete(false)
exit()
}
}
}
protected fun exit() {
retainInstance = false
activity?.run {
if (!isFinishing) supportFragmentManager
.beginTransaction()
.remove(this@BaseHeadlessFragment)
.commitAllowingStateLoss()
}
}
Our runtime permission implementation has become modular, it can be called from any activity just by starting this fragment. That’s a first step!
The permission grant result is handled by deferredGrant.complete(boolean)
, that’s how we can get a suspend
function controlling our fragment.
Deferred behavior
We will now leverage it, and create a single function to launch this permission granting process and get its result.
For this, we use a CompletableDeferred
, which is a Deferred
like the one returned by the async
function, but we can complete it by ourselves once we have our result.
All we have to do is to launch our fragment, prepare a CompletableDeferred
and await for it.
Voilà! We have our function:
protected val deferredGrant = CompletableDeferred<Boolean>()
suspend fun awaitGrant() = deferredGrant.await()
companion object {
suspend fun FragmentActivity.getStoragePermission() : Boolean {
if (canReadStorage()) return true
return launchFragment().awaitGrant()
}
}
Profit
Usage is straghtforward:
From a coroutine, any activity can call getStoragePermission()
, and it suspends until user has made its choice.
No callback anymore, and it’s callable from any Activity or Fragment in your app \o/
if (getStoragePermission()) {
proceed()
} else {
retreat()
}
While the user is asked for the permission you want for your app, coroutine execution is suspended by getStoragePermission()
condition. Then, the relevant branch of this if
expression will be executed.
Leave a comment