'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.



Solution 1:[1]

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)
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1