Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android WebView Handling input type "File" (File Explorer and Camera separate)

I have a webform that's fairly simple. The only people accessing the webform are using mobile devices so I built the form with mobile in use.

To make it simpler for the user, I have two file upload buttons (the HTML here isn't too important other than to explain what I'm trying to do and why I haven't been able to resolve it). One upload button is a simple upload button where the user would navigate their files. The second upload button should open up the camera straight away.
This is working fine when I open the form on a mobile browser.

<div class = "col-6-6">
    <label class = "image-label label-upload">
        <span class = "icons i-upload"></span>
        Upload
        <input name="image-upload" type = "file" accept="image/*">
    </label>
</div>
<div class = "col-6-6">
    <label class = "image-label label-upload">
        <span class = "icons i-camera"></span>
        Capture
        <input name="image-capture" type = "file" accept="image/*" capture = "environment">
    </label>
</div>

What I'm now attempting to do is simulate the exact same behavior the browser has as an App using webview.

package com.example.testapp

import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportActionBar?.hide()

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val myWebView: WebView = findViewById(R.id.wv)

        myWebView.loadUrl("https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture")

        myWebView.webViewClient = WebViewClient()
        myWebView.webChromeClient = WebChromeClient()

        myWebView.settings.javaScriptEnabled = true
        myWebView.settings.allowFileAccess = true
        myWebView.settings.allowContentAccess = true
        myWebView.settings.javaScriptCanOpenWindowsAutomatically = true
        myWebView.settings.mediaPlaybackRequiresUserGesture = false

    }
    override fun onBackPressed() {
        val myWebView: WebView = findViewById(R.id.wv)
        myWebView.webViewClient = WebViewClient()
        if (myWebView.canGoBack()) myWebView.goBack() else super.onBackPressed()
    }
}

I also have the following in my Android manifest:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA2" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-feature android:name="android.hardware.camera" android:required="true"/>

I've seen some solutions online and tried them out but they don't seem to work exactly how I'm expecting them to...
Solutions are often in Java, trying to do this in Kotlin. As for the Kotlin solutions I've tried out, they either don't give enough information (so unable to understand), it defaults to just choosing an image (no camera option), you get a pop-up menu asking if you want to use the camera or file explorer (it should know which one to used based off of the HTML use of capture), or it will have both options and the camera option doesn't work properly.

Also, should be said that all users of this app are on Android 8 and higher. So there's no need for legacy solutions.

Any and all help would be greatly appreciated. I'm trying to keep it simple, not sure if there's something already built in that can be accessed to make this happen.

Regards,

Alex

like image 582
Alex Avatar asked Mar 07 '26 21:03

Alex


1 Answers

I figured it out! Actually took a bit of playing around with some other solutions and asking for some external help.

Under AndroidManifest.xml I added the following inside <Application>

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

Then I created a new xml file (app > res > xml), file_paths.xml

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="camera_files"
        path="DCIM/Camera" />
</paths>

And finally... my updated Kotlin file (MainActivity.kt)

package com.example.testapp

import android.Manifest
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.view.KeyEvent
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainActivity : AppCompatActivity() {
    private lateinit var myWebView: WebView
    private var fileUploadCallback: ValueCallback<Array<Uri>>? = null
    private lateinit var currentPhotoUri: Uri

    companion object {
        private const val FILE_CHOOSER_REQUEST_CODE = 1
        private const val CAMERA_PERMISSION_REQUEST_CODE = 2
    }

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

        myWebView = findViewById(R.id.wv)
        myWebView.loadUrl("https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture")
        myWebView.webViewClient = WebViewClient()
        myWebView.webChromeClient = object : WebChromeClient() {
            override fun onShowFileChooser(
                webView: WebView?,
                filePathCallback: ValueCallback<Array<Uri>>?,
                fileChooserParams: FileChooserParams?
            ): Boolean {
                fileUploadCallback?.onReceiveValue(null)
                fileUploadCallback = filePathCallback

                if (fileChooserParams?.acceptTypes?.contains("image/*") == true && fileChooserParams.isCaptureEnabled) {
                    // Launch camera
                    if (ContextCompat.checkSelfPermission(
                            this@MainActivity,
                            Manifest.permission.CAMERA
                        ) == PackageManager.PERMISSION_GRANTED
                    ) {
                        launchCamera()
                    } else {
                        ActivityCompat.requestPermissions(
                            this@MainActivity,
                            arrayOf(Manifest.permission.CAMERA),
                            CAMERA_PERMISSION_REQUEST_CODE
                        )
                    }
                } else {
                    // Use file picker
                    val intent = Intent(Intent.ACTION_GET_CONTENT)
                    intent.addCategory(Intent.CATEGORY_OPENABLE)
                    intent.type = "image/*"
                    val chooserIntent = Intent.createChooser(intent, "Choose File")
                    startActivityForResult(chooserIntent, FILE_CHOOSER_REQUEST_CODE)
                }

                return true
            }
        }

        val webSettings: WebSettings = myWebView.settings
        with(webSettings) {
            javaScriptEnabled = true
            allowFileAccess = true
            allowContentAccess = true
            javaScriptCanOpenWindowsAutomatically = true
            mediaPlaybackRequiresUserGesture = false
            domStorageEnabled = true
        }
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (keyCode == KeyEvent.KEYCODE_BACK && myWebView.canGoBack()) {
            myWebView.goBack()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
            if (fileUploadCallback == null) {
                super.onActivityResult(requestCode, resultCode, data)
                return
            }

            val results: Array<Uri>? = when {
                resultCode == RESULT_OK && data?.data != null -> arrayOf(data.data!!)
                resultCode == RESULT_OK -> arrayOf(currentPhotoUri)
                else -> null
            }

            fileUploadCallback?.onReceiveValue(results)
            fileUploadCallback = null
        } else {
            super.onActivityResult(requestCode, resultCode, data)
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission granted, launch camera
                launchCamera()
            } else {
                // Permission denied, show an error or request permission again
                Toast.makeText(this, "Camera permission denied", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun launchCamera() {
        val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        currentPhotoUri = createImageFileUri()
        captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentPhotoUri)
        startActivityForResult(captureIntent, FILE_CHOOSER_REQUEST_CODE)
    }

    private fun createImageFileUri(): Uri {
        val fileName = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + ".jpg"
        val contentValues = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
            }
        }
        val resolver: ContentResolver = contentResolver
        val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        return imageUri ?: throw RuntimeException("ImageUri is null")
    }
}

It's not as "simple" as I had hoped it would be. But it works! Also it does save the picture to the cameraroll, I believe it's also possible to save to temporary storage by using something like applicationContext.externalCacheDir.
I hope this saves someone from having a headache, as for me. I am done with this and don't want to think about it any longer lol.

Alex

like image 101
Alex Avatar answered Mar 09 '26 10:03

Alex



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!