FusedLocationProviderClient with Kotlin coroutines
Asked Answered
F

3

8

I am trying to request a new location with FusedLocationProviderClient and Kotlin Coroutines. This is my current setup:

class LocationProviderImpl(context: Context) : LocationProvider, CoroutineScope {

    private val TAG = this::class.java.simpleName

    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.IO

    private val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
    private val locationRequest = LocationRequest().apply {
        numUpdates = 1
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
    }

    override suspend fun getLocation(): LatLng = suspendCoroutine {
        val locationCallback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation.run {
                    val latLng = latitude at longitude
                    it.resume(latLng)
                }

                fusedLocationProviderClient.removeLocationUpdates(this)
            }
        }

        try {
            fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
        } catch (e: SecurityException) {
            throw NoLocationPermissionException()
        }
    }
}

But when trying to request a new location, I get the following exception:

java.lang.IllegalStateException: Can't create handler inside thread that has not called Looper.prepare()

However, if I would call Looper.prepare() (and Looper.quit() eventually) wouldn't it mean that I can call the function only once?

Any help is appreciated.

Flange answered 28/3, 2019 at 15:28 Comment(0)
L
3

The problem is in the way you set up your coroutineContext. Use this instead:

override val coroutineContext = Dispatchers.Main + job

If you ever need the IO dispatcher, you can require it explicitly:

withContext(Dispatchers.IO) { ... blocking IO code ... }

To suspend the coroutine, call suspendCancellableCoroutine, otherwise you won't get any benefit from structured concurrency.

Another detail, don't write any code after it.resume in the suspendCancellableCoroutine block. If the dispatcher chooses to resume the coroutine immediately, within the resume call, that code won't execute until all the code of the coroutine has run (or at least until the next suspension point).

override fun onLocationResult(result: LocationResult) {
    fusedLocationProviderClient.removeLocationUpdates(this)
    it.resume(result.lastLocation.run { latitude to longitude })
}
Lightman answered 29/3, 2019 at 8:13 Comment(2)
It's better to use Dispatchers.Default rather then IO for non blocking operations e.g. calculationsMarc
I don't see the relevance of this comment. OP used the IO dispatcher, the answer is explicit in stating that it is only for blocking IO operations, and nobody mentioned CPU-intensive operations.Lightman
J
2
private val locationRequestGPS by lazy {
    LocationRequest.create()
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
            .setNumUpdates(1)
            .setExpirationDuration(1000)
}

private val locationRequestNETWORK by lazy {
    LocationRequest.create()
            .setPriority(LocationRequest.PRIORITY_LOW_POWER)
            .setNumUpdates(1)
            .setExpirationDuration(1000)
}



suspend fun getLocation(context: Context, offsetMinutes: Int = 15): Location? = suspendCoroutine { task ->
    val ctx = context.applicationContext
    if (!ctx.isPermissionValid(Manifest.permission.ACCESS_COARSE_LOCATION)
            && !ctx.isPermissionValid(Manifest.permission.ACCESS_FINE_LOCATION)) {
        task.resume(null)
    } else {
        val manager = ctx.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        if (!LocationManagerCompat.isLocationEnabled(manager)) {
            task.resume(null)
        } else {
            val service = LocationServices.getFusedLocationProviderClient(ctx)
            service.lastLocation
                    .addOnCompleteListener { locTask ->
                        if (locTask.result == null || System.currentTimeMillis() - locTask.result!!.time > offsetMinutes.minute) {
                            GlobalScope.launch(Dispatchers.Main) {
                                task.resume(locationRequest(manager, service))
                            }
                        } else {
                            task.resume(locTask.result)
                        }

                    }
        }
    }
}

suspend fun getLocationLast(context: Context): Location? = suspendCoroutine { task ->
    val ctx = context.applicationContext
    if (!ctx.isPermissionValid(Manifest.permission.ACCESS_COARSE_LOCATION)
            && !ctx.isPermissionValid(Manifest.permission.ACCESS_FINE_LOCATION)) {
        task.resume(null)
    } else {
        if (!LocationManagerCompat.isLocationEnabled(ctx.getSystemService(Context.LOCATION_SERVICE) as LocationManager)) {
            task.resume(null)
        } else {
            LocationServices.getFusedLocationProviderClient(ctx)
                    .lastLocation
                    .addOnCompleteListener { locTask ->
                        task.resume(locTask.result)
                    }
        }

    }
}

suspend fun locationRequest(locationManager: LocationManager, service: FusedLocationProviderClient): Location? = suspendCoroutine { task ->
    val callback = object : LocationCallback() {
        override fun onLocationResult(p0: LocationResult?) {
            service.removeLocationUpdates(this)
            task.resume(p0?.lastLocation)
        }
    }

    when {
        locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) -> {
            service.requestLocationUpdates(locationRequestGPS, callback, Looper.getMainLooper())
        }
        locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> {
            service.requestLocationUpdates(locationRequestNETWORK, callback, Looper.getMainLooper())
        }
        else -> {
            task.resume(null)
        }
    }
}
Jackinthepulpit answered 23/4, 2020 at 11:16 Comment(1)
Not found ctx.isPermissionValid and offsetMinutes.minuteAmericano
V
0

By using suspendCoroutine you invoke the provided code on the dispatcher that called the suspended function during runtime. Since most dispatchers don't run on Looper threads (pretty much only Dispatchers.MAIN does) the call to Looper.myLooper() fails.

The documentation says you can replace Looper.myLooper() with null to invoke the callback on an unspecified thread. The built-in coroutine dispatcher will then make sure it is routed to the correct thread for resuming the execution.

EDIT: You might need to call it.intercepted().resume(latLng) to ensure the result is dispatched to the correct thread. I'm not entirely sure if the suspendCoroutine continuation is intercepted by default.

In addition you don't need to call fusedLocationProviderClient.removeLocationUpdates(this) because you already set the number of updates in the LocationRequest to 1.

Veradis answered 28/3, 2019 at 17:33 Comment(2)
Thanks for your reply! When passing in null I get the same exception again... The it.intercept() method is not available to me?!Flange
"IllegalStateException If looper is null and this method is executed in a thread that has not called Looper.prepare()." You'll need to wrap the requestLocationUpdates call inside of an withContext(Dispatchers.MAIN) { ... } block.Veradis

© 2022 - 2024 — McMap. All rights reserved.