Kotlin withTimeout coroutine cancellation - android

I'm trying for the first time the coroutine function withTimeout. I'm trying to fetch the current location from the device GPS in Android, and add a timeout in case no location is available. I'm not controlling the process of fetching a location so I cannot make it cancellable easily.
Update: I ended up with a custom timeout logic as the android native api is not cancellable:
suspend fun LocationManager.listenLocationUpdate(): Location? =
withTimeoutOrNull(TIMEOUT) {
locationManager.listenLocationUpdate("gps")
}
private suspend fun LocationManager.listenLocationUpdate(provider: String) =
suspendCoroutine<Location?> { continuation ->
requestLocationUpdates(provider, 1000, 0f, object: TimeoutLocationListener{
override fun onLocationChanged(location: Location?) {
continuation.resume(location)
this#listenLocationUpdate.removeUpdates(this)
}
})
}
So the process of requesting a location belongs to the sdk and I cannot make it cancellale easily. Any suggestion?

For withTimeout[OrNull] to work, you need a cooperative cancellable coroutine. If the function you call is blocking, it will not work as expected. The calling coroutine will not even resume at all, let alone stop the processing of the blocking method. You can check this playground code to confirm this.
You have to have a cancellable API in the first place if you want to build coroutine-based APIs that are cancellable. It's hard to answer your question without knowing the exact function you're calling, though.
With Android's LocationManager, you can for instance wrap getCurrentLocation into a cancellable suspending function (this function is only available in API level 30+):
#RequiresApi(Build.VERSION_CODES.R)
#RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
suspend fun LocationManager.getCurrentLocation(provider: String, executor: Executor): Location? = suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
getCurrentLocation(provider, signal, executor) { location: Location? ->
cont.resume(location)
}
cont.invokeOnCancellation {
signal.cancel()
}
}
Otherwise you could also use callbackFlow to turn the listener-based API into a cancellable Flow-based API which unsubscribes upon cancellation (by removing the listener):
#OptIn(ExperimentalCoroutinesApi::class)
#RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
fun LocationManager.locationUpdates(provider: String, minTimeMs: Long, minDistance: Float = 0f): Flow<Location> =
callbackFlow {
val listener = LocationListener { location -> sendBlocking(location) }
requestLocationUpdates(provider, minTimeMs, minDistance, listener)
awaitClose {
removeUpdates(listener)
}
}
You can use first() on the returned flow if you just want one update, and this will automatically support cancellation:
suspend fun LocationManager.listenLocationUpdate(): Location? =
withTimeoutOrNull(TIMEOUT) {
locationManager.locationUpdates("gps", 1000).first()
}
If you use numUpdates = 1 in your location request, you should also be able to wrap the listener-based API into a single-shot suspending function too. Cancellation here could be done by just removing the listener.

Related

Turn Kotlin suspend function into Callback-based function

Image, you have a Kotlin suspend function
suspend fun foo(): String { return "" }
I need to make it callback-based instead of suspend, i.e.
interface Cancellable {
fun cancel()
}
internal class CancellableImpl : Cancellable {
var cancellationCallback: (() -> Unit)? = null
override fun cancel() {
cancellationCallback?.invoke()
}
}
fun foo(callback: (String) -> Unit): Cancellable
Would GlobalScope approach work? I.e.
fun foo(callback: (String) -> Unit): Cancellable {
val task = CancellableImpl()
val job = GlobalScope.launch {
val fooResult = foo()
callback(fooResult)
}
task.cancellationCallback = {
job.cancel()
}
return task
}
Which drawbacks it has?
Whether GlobalScope is appropriate depends on your use case. If you use GlobalScope, it is up to the caller to manually cancel the task when it is no longer needed so it doesn’t hang onto resources and references longer than necessary.
Your current solution is not thread-safe and fires its callbacks on arbitrary threads from Dispatchers.Default. If you’re on Android, you may want to fire those callbacks on the Looper that the caller used to call your function.
Do you really need callbacks specifically? I would use the following so you don’t have to worry about implementing thread safety yourself.
GlobalScope.async { foo() }.asCompletableFuture()

LocationManager.requestSingleUpdate and kotlin suspendCoroutine

I want to get the current GPS location of the device using LocationManager.requestSingleUpdate(). The following code is used to wrap the call to LocationManager in a kotlin suspending function:
private var locationManager =
context.getSystemService(LOCATION_SERVICE) as LocationManager
#RequiresPermission("android.permission.ACCESS_FINE_LOCATION")
suspend fun getCurrentLocationPreS(): Coordinate? = suspendCoroutine {
val handlerThread = HandlerThread("getCurrentLocation() HandlerThread")
handlerThread.start()
try {
// Use of deprecated function is ok because we are pre android S
locationManager.requestSingleUpdate(
LocationManager.GPS_PROVIDER,
{ location ->
handlerThread.quit()
it.resume(
Coordinate(
location.latitude,
location.longitude
)
)
},
handlerThread.looper
)
}
catch (ex: Exception) {
ex.printStackTrace()
it.resumeWithException(ex)
}
}
As you can see, I use suspendCoroutine to make the asynchronous location call. This implementation works for some devices but I have problems on other devices. Sometimes the supending function never returns and waits forever because the location update callback is not called. The app also has the needed permissions and GPS is enabled.
What edge case leads to a state that the function never returns?
Logcat does not indicate any exception or other error. The app also does not crash. The only symptom is that the getCurrentLocationPreS() never returns.
Just because GPS is enabled doesn't mean that it is working properly. You might have a poor signal when being indoors or in areas packed with tall buildings. If you look at the implementation of requestSingleUpdate you will see it uses a timeout of 30s, so if the timeout expires, your callback will never be executed and your coroutine gets stuck indefinitely.
I would suggest to either to use a timeout for this call as well or consider using FusedLocationProviderClient which allows you to get the last known location in a safer way.
I would also suggest using Looper.getMainLooper(), the runtime overhead from temporarily switching to the main thread is negligible compared to the effort of making sure you are properly managing the HandlerThread
So my take on this would look something like this:
suspend fun getCurrentLocationPreS(): Coordinate? = withTimeoutOrNull(30.seconds){
suspendCoroutine { cont ->
try {
// Use of deprecated function is ok because we are pre android S
locationManager.requestSingleUpdate(
LocationManager.GPS_PROVIDER,
{ location ->
cont.resume(
Coordinate(
location.latitude,
location.longitude
)
)
},
Looper.getMainLooper()
)
}
catch (ex: Exception) {
ex.printStackTrace()
cont.resumeWithException(ex)
}
}
}

Run evaluateJavascript() function sequentially using Kotlin Corutines

I'm working on a pet project where I'm trying to create a hybrid app using a WebView. The web platform that I run in the WebView sends events to the WebView/App through a #JavascriptInterface object. I can also command the web navigation by running a set of javascript functions against the web platform via the WebView using the evaluateJavascript(String, (String) -> Unit) function.
What I'm trying to achieve right now is that these commands that I execute through the evaluateJavascript(String, (String) -> Unit) function run sequentially. I might execute these commands from many different places at the same time, so I want them to run, wait for the callback from the evaluateJavascript() function to get called, and then execute the next command in the queue.
This is what I have in my custom WebView class:
val scriptQueue = mutableListOf<String>()
fun queueEvaluateJavascript(script: String) {
if (webViewIsLoading) {
scriptQueue.add(script)
} else {
scriptQueue.add(script)
runScriptQueue()
}
}
fun runScriptQueue() {
for (script in scriptQueue) {
evaluateJavascript(script, { })
}
scriptQueue.clear()
}
As you can see this is a super basic approach, and I don't really account for the evaluateJavascript() callback. Ideally, I'd like to find a way to flat map each of this evaluateJavascript() calls so we execute one after another, but waiting for the callback to go through.
With RxJava I think I'd create an Observable and then have the evaluateJavascript() callback trigger the subscriber's onNext(). Since, I'm using Kotlin Coroutines I wanted to do something with Coroutines, so I can queue these evaulateJavascript() calls. But I'm not 100% sure what would be the equivalent here.
That would be a nice problem to approach with coroutines.
The usual way to convert callback based APIs to suspend functions is the following:
suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
evaluateJavascript(script) { result ->
cont.resume(result)
}
}
You can then use that in combination maybe with a Channel (to serve as a queue) and a coroutine that processes this channel:
class MyWebView(context: Context) : WebView(context) {
private val jsQueue = Channel<String>(BUFFERED)
fun startJsProcessingLoopIn(scope: CoroutineScope) {
scope.launch {
for (script in jsQueue) {
evaluateJs(script)
}
}
}
// you could also make this function non-suspend if necessary by calling
// sendBlocking (or trySend depending on coroutines version)
suspend fun queueEvaluateJavascript(script: String) {
jsQueue.send(script)
}
private suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
evaluateJavascript(script) { result ->
cont.resume(result)
}
}
}
Alternatively you can create your own coroutine scope and make sure to tie it with some sort of lifecycle of your webview (I'm not familiar with WebView so I'll let you judge which kind of method is correct):
class MyWebView2(context: Context) : WebView(context) {
// you can even further customize the exact thread pool used here
// by providing a particular dispatcher
private val jsProcessingScope = CoroutineScope(CoroutineName("js-processing"))
private val jsQueue = Channel<String>(BUFFERED)
// this starts the loop right away but you can also put this in a method
// to start it at a more appropriate moment
init {
jsProcessingScope.launch {
for (script in jsQueue) {
evaluateJs(script)
}
}
}
// you could also make this function non-suspend if necessary by calling
// sendBlocking (or trySend depending on coroutines version)
suspend fun queueEvaluateJavascript(script: String) {
jsQueue.send(script)
}
private suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
evaluateJavascript(script) { result ->
cont.resume(result)
}
}
fun someCloseOrDisposeCallback() {
jsProcessingScope.cancel()
}
}

Android kotlin task to be executed using coroutines

As an example, I'm using FusedLocationProviderClient to access the current location, which returns a task which callback will eventually return the location. The method looks something like follows:
fun getLocation(callback: MyCallback){
val flpc = LocationServices.getFusedLocationProviderClient(it)
flpc.lastLocation.addOnSuccessListener {
callback.onLocation(it)
}
}
Is it possible to transform this so that I can use corroutines to suspend this function and wait for the task returned by flpc.lastLocation so I can return it in this method and this way get rid of that callback? For example something like this:
suspend fun getLocation(): Location? =
withContext(Dispachers.IO){
val flpc = LocationServices.getFusedLocationProviderClient(it)
return#withContext flpc.lastLocation.result()
}
My question is if there is something around coroutines where I can return the result of a Task (in this example, a Task<Location>)
Thanks in advance!
The kotlinx-coroutines-play-services library has a Task<T>.await(): T helper.
import kotlinx.coroutines.tasks.await
suspend fun getLocation(): Location? =
LocationServices.getFusedLocationProviderClient(context).lastLocation.await()
Alternatively take a look at Blocking Tasks
It would be used the next way:
suspend fun getLocation(): Location? =
withContext(Dispachers.IO){
val flpc = LocationServices.getFusedLocationProviderClient(context)
try{
return#withContext Tasks.await(flpc.lastLocation)
catch(ex: Exception){
ex.printStackTrace()
}
return#withContext null
}
Just to add to this example, for completion purposes, the call to getLocation() would be done the next way:
coroutineScope.launch(Dispatchers.Main) {
val location = LocationReceiver.getLocation(context)
...
}
However this negates the benefits of coroutines by not leveraging the available callback and blocking a thread on the IO dispatcher and should not be used if the alternative is available.
Another way that I have done this that can also be used with any callback type interface is to use suspendCoroutine<T> {}.
So for this example it would be:
suspend fun getLocation(): Location? {
return suspendCoroutine<Location?> { continuation ->
val flpc = LocationServices.getFusedLocationProviderClient(it)
flpc.lastLocation.addOnSuccessListener { location ->
continuation.resume(location)
}
// you should add error listener and call 'continuation.resume(null)'
// or 'continuation.resumeWith(Result.failure(exception))'
}
}

Kotlin Coroutine to escape callback hell

I'm trying to use Kotlin's coroutines to avoid callback hell, but it doesnt look like I can in this specific situation, I would like some thougths about it.
I have this SyncService class which calls series of different methods to send data to the server like the following:
SyncService calls Sync Student, which calls Student Repository, which calls DataSource that makes a server request sending the data through Apollo's Graphql Client.
The same pattern follows in each of my features:
SyncService -> Sync Feature -> Feature Repository -> DataSource
So every one of the method that I call has this signature:
fun save(onSuccess: ()-> Unit, onError:()->Unit) {
//To Stuff here
}
The problem is:
When I sync and successfully save the Student on server, I need to sync his enrollment, and if I successfully save the enrollment, I need to sync another object and so on.
It all depends on each other and I need to do it sequentially, that's why I was using callbacks.
But as you can imagine, the code result is not very friendly, and me and my team starting searching for alternatives to keep it better. And we ended up with this extension function:
suspend fun <T> ApolloCall<T>.execute() = suspendCoroutine<Response<T>> { cont ->
enqueue(object: ApolloCall.Callback<T>() {
override fun onResponse(response: Response<T>) {
cont.resume(response)
}
override fun onFailure(e: ApolloException) {
cont.resumeWithException(e)
}
})
}
But the function in DataSource still has a onSuccess() and onError() as callbacks that needs to be passed to whoever call it.
fun saveStudents(
students: List<StudentInput>,
onSuccess: () -> Unit,
onError: (errorMessage: String) -> Unit) {
runBlocking {
try {
val response = GraphQLClient.apolloInstance
.mutate(CreateStudentsMutation
.builder()
.students(students)
.build())
.execute()
if (!response.hasErrors())
onSuccess()
else
onError("Response has errors!")
} catch (e: ApolloException) {
e.printStackTrace()
onError("Server error occurred!")
}
}
}
The SyncService class code changed to be like:
private fun runSync(onComplete: () -> Unit) = async(CommonPool) {
val syncStudentProcess = async(coroutineContext, start = CoroutineStart.LAZY) {
syncStudents()
}
val syncEnrollmentProcess = async(coroutineContext, start = CoroutineStart.LAZY) {
syncEnrollments()
}
syncStudentProcess.await()
syncEnrollmentProcess.await()
onComplete()
}
It does execute it sequentially, but I need a way to stop every other coroutine if any got any errors. Error that might come only from Apollo's
So I've been trying a lot to find a way to simplify this code, but didn't get any good result. I don't even know if this chaining of callbacks can be simplify at all. That's why I came here to see some thoughts on it.
TLDR: I want a way to execute all of my functions sequentially, and still be able to stop all coroutines if any got an exception without a lot o chaining callbacks.

Categories

Resources