'Android intent always has null in callback (using registerForActivityResult)

I am using this code and I am missing something, because almost everything is working, but I get a null in the data when the callback responds:

private inner class JavascriptInterface {
    @android.webkit.JavascriptInterface
    fun image_capture() {
        val photoFileName = "photo.jpg"
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        var photoFile = getPhotoFileUri(photoFileName)
        if (photoFile != null) {
            fileProvider = FileProvider.getUriForFile(applicationContext, "com.codepath.fileprovider", photoFile!!)
            intent.putExtra(EXTRA_OUTPUT, fileProvider)
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            if (intent.resolveActivity(packageManager) != null) {
                getContent.launch(intent)
            }
        }
    }
}

val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
    if (result.resultCode == Activity.RESULT_OK) {
        val intent:Intent? = result.data // <- PROBLEM: data is ALWAYS null
    }
}

My manifest snippet related to this looks like this:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

and my fileprovider.xml looks like this:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-files-path name="images" path="Pictures" />
</paths>

Any help is appreciated. Thanks!



Solution 1:[1]

So, I ended up checking out the TakePicture contract @ian (thanks for that tip!) and after a lot of cobbling together various resources I found, I finally got it to work. This is the pertinent kotlin code for the webview Activity:

class WebViewShell : AppCompatActivity() {
    val APP_TAG = "MyApp"

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

        // Storing data into SharedPreferences
        val sharedPreferences = getSharedPreferences("MySharedPrefs", MODE_PRIVATE)
        val storedurl: String = sharedPreferences.getString("url", "").toString()

        val myWebView: WebView = findViewById(R.id.webview_webview)
        myWebView.clearCache(true)

        myWebView.settings.setJavaScriptCanOpenWindowsAutomatically(true)
        myWebView.settings.setJavaScriptEnabled(true)
        myWebView.settings.setAppCacheEnabled(true)
        myWebView.settings.setAppCacheMaxSize(10 * 1024 * 1024)
        myWebView.settings.setAppCachePath("")
        myWebView.settings.setDomStorageEnabled(true)
        myWebView.settings.setRenderPriority(android.webkit.WebSettings.RenderPriority.HIGH)
        WebView.setWebContentsDebuggingEnabled(true)

        myWebView.addJavascriptInterface(JavascriptInterface(),"Android")
        myWebView.loadUrl(storedurl)
    }

    private inner class JavascriptInterface {
        @android.webkit.JavascriptInterface
        fun image_capture() { // opens Camera
            takeImage()
        }
    }
    
    private fun takeImage() {
        try {
            val uri = getTmpFileUri()
            lifecycleScope.launchWhenStarted {
                takeImageResult.launch(uri)
            }
        }
        catch (e: Exception) {
            android.widget.Toast.makeText(applicationContext, e.message, android.widget.Toast.LENGTH_LONG).show()
        }
    }
    
    private val takeImageResult = registerForActivityResult(TakePictureWithUriReturnContract()) { (isSuccess, imageUri) ->
        val myWebView: android.webkit.WebView = findViewById(R.id.webview_webview)
        if (isSuccess) {
            val imageStream: InputStream? = contentResolver.openInputStream(imageUri)
            val selectedImage = BitmapFactory.decodeStream(imageStream)
            val scaledImage = scaleDown(selectedImage, 800F, true)
            val baos = ByteArrayOutputStream()
            scaledImage?.compress(Bitmap.CompressFormat.JPEG, 100, baos)
            val byteArray: ByteArray = baos.toByteArray()
            val dataURL: String = Base64.encodeToString(byteArray, Base64.DEFAULT)
            myWebView.loadUrl( "JavaScript:fnWebAppReceiveImage('" + dataURL + "')" )
        }
        else {
            android.widget.Toast.makeText(applicationContext, "Image capture failed", android.widget.Toast.LENGTH_LONG).show()
        }
    }
    
    private inner class TakePictureWithUriReturnContract : ActivityResultContract<Uri, Pair<Boolean, Uri>>() {
        private lateinit var imageUri: Uri
        @CallSuper
        override fun createIntent(context: Context, input: Uri): Intent {
            imageUri = input
            return Intent(MediaStore.ACTION_IMAGE_CAPTURE).putExtra(MediaStore.EXTRA_OUTPUT, input)
        }
        override fun getSynchronousResult(
            context: Context,
            input: Uri
        ): SynchronousResult<Pair<Boolean, Uri>>? = null
        @Suppress("AutoBoxing")
        override fun parseResult(resultCode: Int, intent: Intent?): Pair<Boolean, Uri> {
            return (resultCode == Activity.RESULT_OK) to imageUri
        }
    }

    private fun getTmpFileUri(): Uri? {
        val mediaStorageDir = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), APP_TAG)
        if (!mediaStorageDir.exists() && !mediaStorageDir.mkdirs()) {
            throw Exception("Failed to create directory to store media temp file")
        }
        return FileProvider.getUriForFile(applicationContext, getApplicationContext().getPackageName() + ".provider", File(mediaStorageDir.path + File.separator + "photo.jpg"))
    }
    
    fun scaleDown(realImage: Bitmap, maxImageSize: Float, filter: Boolean): Bitmap? {
        val ratio = Math.min(maxImageSize / realImage.width, maxImageSize / realImage.height)
        val width = Math.round(ratio * realImage.width)
        val height = Math.round(ratio * realImage.height)
        return Bitmap.createScaledBitmap(realImage, width, height, filter)
    }
}

To round things out, here is the pertinent JavaScript code - which the Activity is loading via the myWebView.loadUrl(storedurl) statement.

This is the JavaScript code which calls the Android code:

if (window.Android) {
    Android.image_capture();
}

And when the picture has been taken, and sized by the Android code, it sends the Base64 back to JavaScript with:

myWebView.loadUrl("JavaScript:fnWebAppReceiveImage('" + dataURL + "')")

Note how weirdly you have to specify function arguments. There probably is a better way, but this code works. If there are any suggestions about how to specify a function argument easier than this, please let me know.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.MyApp">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        ...
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>
    </application>
</manifest>

And the provider_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-files-path name="external_files" path="." />
</paths>

Hope this helps someone - it took me days of research to figure this one out!

Solution 2:[2]

That is supposed to be null, as ACTION_IMAGE_CAPTURE is not documented to return a Uri. You are using EXTRA_OUTPUT. The image should be stored in the location that you specified using EXTRA_OUTPUT.

Note, though, that you should be adding both FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION to the Intent, as the camera app needs to be able to write the image to the desired location.

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 WagsMax
Solution 2 CommonsWare