Android WebView 处理输入类型“文件”(文件资源管理器和相机分开)

Ale*_*lex 2 android webview android-webview kotlin

我有一个相当简单的网络表单。访问网络表单的唯一人是使用移动设备,因此我使用移动设备构建了表单。

为了让用户更简单,我有两个文件上传按钮(这里的 HTML 并不是太重要,除了解释我正在尝试做什么以及为什么我无法解决它)。一个上传按钮是一个简单的上传按钮,用户可以在其中导航他们的文件。第二个上传按钮应该立即打开相机。
当我在移动浏览器上打开表单时,这工作正常。

<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>
Run Code Online (Sandbox Code Playgroud)

我现在尝试做的是使用 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()
    }
}
Run Code Online (Sandbox Code Playgroud)

我的 Android 清单中还有以下内容:

<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"/>
Run Code Online (Sandbox Code Playgroud)

我在网上看到了一些解决方案并尝试了它们,但它们似乎并没有完全按照我期望的方式工作...
解决方案通常是用 Java 编写的,试图在 Kotlin 中做到这一点。至于我尝试过的 Kotlin 解决方案,它们要么没有提供足够的信息(所以无法理解),它默认只选择图像(没有相机选项),你会得到一个弹出菜单,询问你是否想要使用相机或文件资源管理器(它应该根据捕获的 HTML 使用情况知道使用哪一个),否则它将同时具有这两个选项,并且相机选项无法正常工作。

另外,应该说这个应用程序的所有用户都使用 Android 8 及更高版本。因此不需要遗留解决方案。

任何和所有的帮助将不胜感激。我试图保持简单,不确定是否已经内置了一些可以访问的东西来实现这一点。

问候,

亚历克斯

Ale*_*lex 5

我想到了!实际上花了一些时间尝试其他一些解决方案并寻求一些外部帮助。

在 AndroidManifest.xml 下我添加了以下内容<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>
Run Code Online (Sandbox Code Playgroud)

然后我创建了一个新的xml文件(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>
Run Code Online (Sandbox Code Playgroud)

最后...我更新的 Kotlin 文件 (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")
    }
}
Run Code Online (Sandbox Code Playgroud)

这并不像我希望的那么“简单”。但它有效!此外,它确实将图片保存到相机胶卷中,我相信也可以通过使用类似applicationContext.externalCacheDir.
我希望这可以帮助某人免于头痛,就像我一样。我已经完成了这件事,不想再考虑它了,哈哈。

亚历克斯