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.

3 Answers3

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 })
}
Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • It's better to use `Dispatchers.Default` rather then IO for non blocking operations e.g. calculations – Vlad Apr 22 '20 at 12:08
  • 1
    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. – Marko Topolnik Apr 22 '20 at 16:27
3
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)
        }
    }
}
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.

Kiskae
  • 24,655
  • 2
  • 77
  • 74
  • Thanks for your reply! When passing in null I get the same exception again... The it.intercept() method is not available to me?! –  Mar 28 '19 at 19:01
  • 1
    "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. – Kiskae Mar 28 '19 at 21:00