6

I've got much of my app working fine with "by lazy" initializers because everything magically happens in the order that is necessary.

But not all of the initializers are synchronous. Some of them are wrapping callbacks, which means I need to wait until the callback happens, which means I need runBlocking and suspendCoroutine.

But after refactoring everything, I get this IllegalStateException: runBlocking is not allowed in Android main looper thread

What? You can't block? You're killing me here. What is the right way if my "by lazy" happens to be a blocking function?

private val cameraCaptureSession: CameraCaptureSession by lazy {
    runBlocking(Background) {
        suspendCoroutine { cont: Continuation<CameraCaptureSession> ->
            cameraDevice.createCaptureSession(Arrays.asList(readySurface, imageReader.surface), object : CameraCaptureSession.StateCallback() {
                override fun onConfigured(session: CameraCaptureSession) {
                    cont.resume(session).also {
                        Log.i(TAG, "Created cameraCaptureSession through createCaptureSession.onConfigured")
                    }
                }

                override fun onConfigureFailed(session: CameraCaptureSession) {
                    cont.resumeWithException(Exception("createCaptureSession.onConfigureFailed")).also {
                        Log.e(TAG, "onConfigureFailed: Could not configure capture session.")
                    }
                }
            }, backgroundHandler)
        }
    }
}

Full GIST of the class, for getting an idea of what I was originally trying to accomplish: https://gist.github.com/salamanders/aae560d9f72289d5e4b49011fd2ce62b

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
Benjamin H
  • 5,164
  • 6
  • 34
  • 42

1 Answers1

5

It is a well-known fact that performing a blocking call on the UI thread results in a completely frozen app for the duration of the call. The documentation of createCaptureSession specifically states

It can take several hundred milliseconds for the session's configuration to complete, since camera hardware may need to be powered on or reconfigured.

It may very easily result in an Application Not Responding dialog and your app being killed. That's why Kotlin has introduced an explicit guard against runBlocking on the UI thread.

Therefore your idea to start this process just in time, when you have already tried to access cameraCaptureSession, cannot work. What you must do instead is wrap the code that accesses it into launch(UI) and turn your val into a suspend fun.

In a nutshell:

private var savedSession: CameraCaptureSession? = null

private suspend fun cameraCaptureSession(): CameraCaptureSession {
    savedSession?.also { return it }
    return suspendCoroutine { cont ->
        cameraDevice.createCaptureSession(listOf(readySurface, imageReader.surface), object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) {
                savedSession = session
                Log.i(TAG, "Created cameraCaptureSession through createCaptureSession.onConfigured")
                cont.resume(session)
            }

            override fun onConfigureFailed(session: CameraCaptureSession) {
                Log.e(TAG, "onConfigureFailed: Could not configure capture session.")
                cont.resumeWithException(Exception("createCaptureSession.onConfigureFailed"))
            }
        })
    }
}

fun useCamera() {
    launch(UI) {
        cameraCaptureSession().also { session ->
            session.capture(...)
        }
    }
}

Note that session.capture() is another target for wrapping into a suspend fun.

Also be sure to note that the code I gave is only safe if you can ensure that you won't call cameraCaptureSession() again before the first call has resumed. Check out the followup thread for a more general solution that takes care of that.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • I'm 80% towards understanding, bear with me. I tried wrapping all of the cam class (initialization and taking a picture) with `launch(UI) { ... camstuff... }` but got the same error. I'd be fine with doing all the setup before the final "take a picture" call earlier (like in the onCreate call) but am not sure how to avoid that error. – Benjamin H Aug 14 '18 at 16:02
  • Did you remove `runBlocking` then? – Marko Topolnik Aug 14 '18 at 16:07
  • In your `suspend fun cameraCaptureSession() = suspendCoroutine...` example, it looks like it would re-run the function each time it was called, which I was happy to let the `by lazy` take care of for me so it would initialize each object once. But if there is really no way to use by lazy along with blocking, then I guess I'll have to! – Benjamin H Aug 14 '18 at 18:08
  • May be related to https://github.com/Kotlin/kotlin-coroutines/issues/52 – Benjamin H Aug 14 '18 at 18:13
  • You can use `runBlocking` with `by lazy`, that's not the issue. The issue is initializing it on the UI thread and freezing the app. You have to give up the idea that you can pretend such a thing is a regular `val` that you can simply access like any other. – Marko Topolnik Aug 14 '18 at 18:47
  • ... drat. I can't run the whole thing in some background process and have the main app sitting around checking for it every so often? I was just so close to having all the API callbacks in the right order. Dangit. – Benjamin H Aug 14 '18 at 19:20
  • Ok, I get what you are saying. It just seems odd to me that you have to manually provide the "savedSession" var, which caches the cameraCaptureSession() result, so it only does it once - which is, at a high level, what "by lazy" is supposed to do. – Benjamin H Aug 14 '18 at 22:54
  • `by lazy` is a synchronous construct, it's totally different from this. With the code I gave, the `cameraCaptureSession()` call doesn't return the created session, it returns the special `COROUTINE_SUSPENDED` object. Then, at some arbitrary later time, Android calls the `onConfigured` callback, which pushes the result into the continuation object, and that makes your code that originally called `cameraCaptureSession()` to resume. – Marko Topolnik Aug 15 '18 at 06:44
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/178091/discussion-between-benjamin-h-and-marko-topolnik). – Benjamin H Aug 15 '18 at 16:57