Kotlin/Anko/OpenCV/CameraView can't create handler because thread hasn't called Looper.prepare()

488 views Asked by At

So, I'm currently attempting to convert a picture taken using CameraView into Grey Scale using OpenCV. After the picture is taken, I use Anko in order to process it asynchronously. After the processing is done, I write the image to the MediaStore. The Activity has a CameraView and a Button to take the picture.

However, when I press the button, the following exception appears:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
   at android.os.Handler.<init>(Handler.java:200)
   at android.os.Handler.<init>(Handler.java:114)
   at com.otaliastudios.cameraview.CameraUtils.decodeBitmap(CameraUtils.java:101)
   at com.otaliastudios.cameraview.CameraUtils.decodeBitmap(CameraUtils.java:83)
   at com.example.joao.waterprototype.MainActivity$onCreate$2$onPictureTaken$1.invoke(MainActivity.kt:55)
   at com.example.joao.waterprototype.MainActivity$onCreate$2$onPictureTaken$1.invoke(MainActivity.kt:46)
   at org.jetbrains.anko.AsyncKt$doAsync$1.invoke(Async.kt:143)
   at org.jetbrains.anko.AsyncKt$doAsync$1.invoke(Async.kt)
   at org.jetbrains.anko.AsyncKt$sam$java_util_concurrent_Callable$0.call(Async.kt)
   at java.util.concurrent.FutureTask.run(FutureTask.java:237)
   at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272)
   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
   at java.lang.Thread.run(Thread.java:761)

the place where said problem happens is described in the following snippet:

camera.addCameraListener(object : CameraListener() {

        override fun onPictureTaken(jpeg: ByteArray?) {

            Toast.makeText(applicationContext, "Image Taken", Toast.LENGTH_SHORT).show()

            doAsync() {

                lateinit var result : Bitmap
                CameraUtils.decodeBitmap(jpeg) {

                    val mat = Mat()
                    val bmp32 = it.copy(Bitmap.Config.ARGB_8888, true)
                    Utils.bitmapToMat(bmp32, mat)

                    val greyMat = Mat()
                    cvtColor(mat, greyMat, COLOR_BGRA2GRAY)

                    result = Bitmap.createBitmap(greyMat.cols(), greyMat.rows(), Bitmap.Config.ARGB_8888)
                    Utils.matToBitmap(greyMat, result)

                }

                uiThread {

                    val title = "${UUID.randomUUID()}.jpg"
                    val savedImageURL = MediaStore.Images.Media.insertImage(
                            contentResolver,
                            result,
                            title,
                            "Image of $title"
                    )

                    val uri = Uri.parse(savedImageURL)
                    Toast.makeText(applicationContext, "Image Created at ${uri.path}", Toast.LENGTH_SHORT).show()

                }
            }
        }

    })

A common theme for those exceptions is the usage of Toasts when using Async contexts, but I've double checked it, and I'm being careful in not using them in there. In fact said error occurs on the decodeBitmap invocation. Which is weird, since the CameraView documentation insists that said function is called on a worker thread. Can any of you explain to me what's going on and how to fix it?

In order to make it more easier to reproduce, here's the full MainActivity.kt:

package com.example.joao.waterprototype

import android.graphics.Bitmap
import android.net.Uri
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.WindowManager
import android.widget.Toast
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.CameraUtils
import com.otaliastudios.cameraview.Gesture
import com.otaliastudios.cameraview.GestureAction
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import org.opencv.android.OpenCVLoader
import org.opencv.android.Utils
import org.opencv.core.Mat
import org.opencv.imgproc.Imgproc.COLOR_BGRA2GRAY
import org.opencv.imgproc.Imgproc.cvtColor
import java.util.*

class MainActivity : AppCompatActivity(){


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
        camera.setLifecycleOwner(this)

        take_photo.setOnClickListener {
            camera.capturePicture()
        }

        camera.mapGesture(Gesture.PINCH, GestureAction.ZOOM); // Pinch to zoom!
        camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); // Tap to focus!
        camera.mapGesture(Gesture.LONG_TAP, GestureAction.CAPTURE); // Long tap to shoot!

        camera.addCameraListener(object : CameraListener() {

            override fun onPictureTaken(jpeg: ByteArray?) {

                Toast.makeText(applicationContext, "Image Taken", Toast.LENGTH_SHORT).show()

                doAsync() {

                    lateinit var result : Bitmap
                    CameraUtils.decodeBitmap(jpeg) {

                        val mat = Mat()
                        val bmp32 = it.copy(Bitmap.Config.ARGB_8888, true)
                        Utils.bitmapToMat(bmp32, mat)

                        val greyMat = Mat()
                        cvtColor(mat, greyMat, COLOR_BGRA2GRAY)

                        result = Bitmap.createBitmap(greyMat.cols(), greyMat.rows(), Bitmap.Config.ARGB_8888)
                        Utils.matToBitmap(greyMat, result)

                    }

                    uiThread {

                        val title = "${UUID.randomUUID()}.jpg"
                        val savedImageURL = MediaStore.Images.Media.insertImage(
                                contentResolver,
                                result,
                                title,
                                "Image of $title"
                        )

                        val uri = Uri.parse(savedImageURL)
                        Toast.makeText(applicationContext, "Image Created at ${uri.path}", Toast.LENGTH_SHORT).show()

                    }
                }
            }

        })

    }


    companion object {

        // Used to load the 'native-lib' library on application startup.
        init {

            if (!OpenCVLoader.initDebug()) {
                Log.e("OpenCV", "Cannot connect to OpenCV Manager")
            } else {
                Log.e("OpenCV", "Connected Successfully")
            }


        }
    }
}

And here's the activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.otaliastudios.cameraview.CameraView
        android:id="@+id/camera"
        android:keepScreenOn="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/take_photo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Take Photo"
        android:textAlignment="center"/>

</LinearLayout>
1

There are 1 answers

0
Duane On

I believe there is a presumption that decodeBitmmap is being initiated from the UI thread, even though the actual work is done on a worker thread. This is the code in question ... executed on the callers thread (line 101):

final Handler ui = new Handler();

There is a little bit of discussion on a similar issue here