23

I'm quite new to Kotlin coroutine and Android development in general. While playing around to understand how it worked, I faced an error I can't seem to solve.

From a basic activity i try to connect to the googleApiClient. The permissions are ok. I wish to use kotlin coroutines to get location updates from LocationManager in a direct style to use this Location object later on. The first time I changed my position in the emulator it works fine, the second time I change my position, It crashes with an exception like this:

FATAL EXCEPTION: main
    Process: com.link_value.eventlv, PID: 32404
    java.lang.IllegalStateException: Already resumed, but got value Location[gps 48.783000,2.516180 acc=20 et=+59m16s372ms alt=0.0 {Bundle[mParcelledData.dataSize=40]}]
    at kotlinx.coroutines.experimental.AbstractContinuation.resumeImpl(AbstractContinuation.kt:79)
    at kotlinx.coroutines.experimental.AbstractContinuation.resume(AbstractContinuation.kt:72)
    at com.link_value.eventlv.View.Create.NewEventLvActivity$await$2$1.onLocationChanged(NewEventLvActivity.kt:100)
    at android.location.LocationManager$ListenerTransport._handleMessage(LocationManager.java:297)
    at android.location.LocationManager$ListenerTransport.-wrap0(LocationManager.java)
    at android.location.LocationManager$ListenerTransport$1.handleMessage(LocationManager.java:242)
     at android.os.Handler.dispatchMessage(Handler.java:102)
     at android.os.Looper.loop(Looper.java:154)
     at android.app.ActivityThread.main(ActivityThread.java:6077)
     at java.lang.reflect.Method.invoke(Native Method)
     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)



override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_new_event_lv)

    askForUserLocation()
    val locationManager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    val presenter = CreateEventPresenterImpl(this@NewEventLvActivity)

    googleApiClient = GoogleApiClient.Builder(this@NewEventLvActivity)
            .enableAutoManage(this /* FragmentActivity */,
                    this /* OnConnectionFailedListener */)
            .addApi(Places.GEO_DATA_API)
            .addConnectionCallbacks(this)
            .build()
}
override fun onConnected(p0: Bundle?) {
    val locationManager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    input_address.addTextChangedListener(object: TextWatcher{
        override fun afterTextChanged(p0: Editable?) {
        }

        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        }

        override fun onTextChanged(query: CharSequence?, p1: Int, p2: Int, p3: Int) {
            if (query.toString().length >= 4) {
                launch(UI) {
                    val locationUpdated = locationManager.await(LocationManager.GPS_PROVIDER)
                    input_name.text = Editable.Factory.getInstance().newEditable(locationUpdated.toString())
                }
            }
        }
    })
}

private suspend fun LocationManager.await(locationProvider: String): Location? = suspendCoroutine { cont ->
    try {
        requestLocationUpdates(locationProvider, 0, 0.toFloat(), object : LocationListener {
            override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) {
            }

            override fun onProviderEnabled(p0: String?) {
            }

            override fun onProviderDisabled(p0: String?) {
                cont.resumeWithException(UnknownLocationException())
            }

            override fun onLocationChanged(location: Location?) {
                cont.resume(location)
            }
        })
    } catch (ex: SecurityException) {
        cont.resumeWithException(ex)
    }
}

It's as if Kotlin use the same Continuation. I don't know what I'm doing wrong and why it crashes the second time. Can someone enlighten me. Thks in advance.

Shinmen
  • 329
  • 1
  • 2
  • 6
  • 1
    You have register LocationListener, which had been resumed multiple times. I think, for your purpose is more preferred to use Channels communication, in which you will put new location value, and from which you will read values and update after text in UI. – kurt Jan 12 '18 at 13:48

3 Answers3

40

According to documentation, it's possible to resume Continuation only single time. Second resume will throw IllegalStateException with "Already resumed, but got $proposedUpdate" message. You can add additional checking continuation.isActive to prevent this exception. It's better to use Channels for multiple callbacks like location updates.

Mikhail Sharin
  • 3,661
  • 3
  • 27
  • 36
7

I had this issue recently with one of 3rd party APIs we use in our project. The problem was solely on the side of the API as the callback was called more than once in the underlying implementation and unsubscribe method wasn't immedeately removing callback from the API.

So, approach I picked was just simply to add a boolean flag. Although, it could've been solved in a bit more elegant way with Kotlin's Flow.mapLatest/collectLatest or so.

suspend fun singularContentUpdate(): Data =
    suspendCancellableCoroutine { continuation ->
        var resumed = false

        val callback = object : Callback {
            override fun onDataReady(d: Data) {
                api.unsubscribe(this)

                if (!resumed) {
                    continuation.resume(d)
                    resumed = true
                }
            }
        }

        continuation.invokeOnCancellation {
            api.unsubscribe(callback)
        }

        api.subscribe(subscriber)

        api.refresh()
    }
Ivan
  • 1,446
  • 2
  • 11
  • 25
  • 3
    You don't need to maintain that resumed state yourself, it's done for you by the `suspendCancellableCoroutine`. Use `continuation.isActive` to check if the corouine is resumed or not – bensadiku Aug 27 '20 at 14:25
  • 1
    @bensadiku I have tried that, unfortunately it becomes inactive a bit too late for me. And the `api` second callback call occures too early. So, there was no other option for me rather than to use this ugly flag :( – Ivan Sep 02 '20 at 13:20
6

You can use this function to prevent this exception.

import kotlinx.coroutines.CancellableContinuation
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume

inline fun <T> Continuation<T>.safeResume(value: T, onExceptionCalled: () -> Unit) {
    if (this is CancellableContinuation) {
      if (isActive)
        resume(value)
      else
        onExceptionCalled()
    } else throw Exception("Must use suspendCancellableCoroutine instead of suspendCoroutine")
}

Notice that I'll throw an exception if you're using suspendCoroutine instead of suspendCancellableCoroutine.

Usage:

suspend fun getId() : Result<String> = suspendCancellableCoroutine { continuation ->

   ...

   continuation.safeResume(Result.success(adjustID)) { Timber.e("Job has already done") }
}
Mattia Ferigutti
  • 2,608
  • 1
  • 18
  • 22