AndroidのWebViewの「ファイルを選択」ボタン(input type=”file”)で選択されたファイルを取得する

以前、「AndroidのWebViewの「ファイルを選択」ボタン(input typ=”file”)で写真を撮るかファイルを選択する」を書いた。
この方法はカメラアプリが起動したときにアクティビティが破棄されると、選択した画像が表示されない問題があった。

アクティビティが破棄されても、選択された画像を取得する方法を紹介する。

WebViewが表示していたページが破棄されると、選択画像をWebViewで選択状態にすることはできない。
画像をサーバーに保存してWebViewに通知するなど、Web側での対応が必要になるだろう。

ActivityのonCreateでWebViewを作成する。
WebChromeClientのonShowFileChooserをオーバーライドして、<input type=”file”>をクリックした時のイベントを処理する。
onShowFileChooserでは、startForResultでインテントを起動し、写真の撮影か画像ファイルの選択を促す。
写真が撮影されるか画像ファイルが選択されると、fileChooserのgetSelectedPictureで画像データを取得する。

アクティビティが破棄されたり復元されたときに、fileChooserの状態を復元することも必要だ。

class MainActivity : AppCompatActivity() {
    private val fileChooser = FileChooser()
    private val startForResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            this.fileChooser.getSelectedPicture(this, result.resultCode, result.data)?.let { jpg ->
                // 取得した画像を処理する
            }
        }

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

    @SuppressLint("SetJavaScriptEnabled")
    private fun setupWebView() {
        webView = WebView(this)
        setContentView(webView)
        webView.loadUrl(URL)
        // JavaScriptを有効にする
        webView.settings.javaScriptEnabled = true
        webView.webChromeClient = object : WebChromeClient() {
            // <input type="file">をクリックした時のイベント
            override fun onShowFileChooser(
                webView: WebView?,
                filePathCallback: ValueCallback<Array<Uri>>?,
                fileChooserParams: FileChooserParams?
            ): Boolean {
                fileChooser.createPictureChooseIntent(
                    this@MainActivity,
                    filePathCallback,
                    fileChooserParams
                )?.let { intent ->
                    startForResult.launch(intent)
                }
                return true
            }
        }
    }
    override fun onSaveInstanceState(outState: Bundle) {
        this.fileChooser.onSaveInstanceState(outState)
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        this.fileChooser.onRestoreInstanceState(savedInstanceState)
    }
}

FileChooserクラスは、「ファイルを選択」ボタン(input typ=”file”)が押された時に、写真の撮影か画像の選択を促し、画像を取得する処理を行う。

「ファイルを選択」ボタン(input typ=”file”)が押された時、createPictureChooseIntentで写真の撮影または選択のインテントを作成する。
写真の撮影にはカメラの権限が必要なため、権限の確認を忘れないようにする。
撮影された写真のURLはimageUriに保持するため、アクティビティが復元されたときにBundleから復元している。

写真が撮影、または画像が選択されると、getSelectedPictureで画像を取得する。
撮影または選択されたファイルを読み込み、JPEGに変換する。
filePathCallbackはブラウザのファイルを選択ボタンのコールバック関数で、処理待ちの状態になっているので、onReceiveValueを実行して処理を完了する。

private const val REQUEST_ID_MULTIPLE_PERMISSIONS = 122
private const val TAG = "FileChooser"

class FileChooser {
    private var filePathCallback: ValueCallback<Array<Uri>>? = null
    private var imageUri: Uri? = null

    /**
     * 写真の撮影または選択のインテントを作成する
     */
    fun createPictureChooseIntent(
        activity: Activity,
        filePathCallback: ValueCallback<Array<Uri>>?,
        fileChooserParams: FileChooserParams?
    ): Intent? {
        this.filePathCallback?.onReceiveValue(null)
        this.filePathCallback = filePathCallback
        return createPictureChooseIntent(activity, fileChooserParams)
    }

    private fun createPictureChooseIntent(
        activity: Activity,
        fileChooserParams: FileChooserParams?
    ): Intent? {
        // 権限がないときは、権限を要求する
        if (!checkAndRequestPermissions(activity, REQUEST_ID_MULTIPLE_PERMISSIONS)) {
            this.filePathCallback?.onReceiveValue(null)
            this.filePathCallback = null
            return null
        }
        val chooserIntent = Intent.createChooser(fileChooserParams?.createIntent(), "写真の選択")
        this.imageUri = createImageFile(activity)
        val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        imageCaptureIntent.putExtra(MediaStore.EXTRA_OUTPUT, this.imageUri)
        chooserIntent.putExtra(
            Intent.EXTRA_INITIAL_INTENTS,
            arrayOf<Parcelable>(imageCaptureIntent)
        )
        return chooserIntent
    }

    private fun createImageFile(activity: Activity): Uri? {
        val folder = activity.getExternalFilesDir(Environment.DIRECTORY_DCIM)
        val date = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
        val fileName = String.format("_%s.jpg", date)
        val cameraFile = File(folder, fileName)
        return FileProvider.getUriForFile(
            activity,
            activity.applicationContext.packageName + ".fileprovider",
            cameraFile
        )
    }

    fun onSaveInstanceState(outState: Bundle) {
        outState.putParcelable("imageUri", this.imageUri)
    }

    fun onRestoreInstanceState(savedInstanceState: Bundle) {
        this.imageUri = savedInstanceState.getParcelable("imageUri")
    }

    /**
     * 選択された画像を取得する
     * @param context
     * @param resultCode
     * @param data
     */
    fun getSelectedPicture(
        context: Context,
        resultCode: Int,
        data: Intent?
    ): ByteArrayOutputStream? {
        var bmp: Bitmap?
        try {
            if (resultCode == Activity.RESULT_OK) {
                val result = FileChooserParams.parseResult(resultCode, data)
                val uri = result?.get(0) ?: imageUri
                uri?.let {
                    // 撮影された写真のJPGデータを作成
                    bmp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        ImageDecoder.decodeBitmap(
                            ImageDecoder.createSource(
                                context.contentResolver,
                                uri
                            )
                        )
                    } else {
                        MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
                    }
                    val jpg = ByteArrayOutputStream()
                    bmp?.compress(Bitmap.CompressFormat.JPEG, 100, jpg)
                    return jpg
                }
            }
        } finally {
            this.imageUri = null
            this.filePathCallback?.onReceiveValue(null)
            this.filePathCallback = null
        }
        return null
    }
}

/**
 * 権限があるか確認し、権限がなければ要求する
 *
 * @return 権限があるときはtrue
 */
private fun checkAndRequestPermissions(activity: Activity, requestCode: Int): Boolean {
    val permissionNeeded = permissionNeeded(activity, arrayOf(Manifest.permission.CAMERA))
    if (permissionNeeded.isNotEmpty()) {
        ActivityCompat.requestPermissions(activity, permissionNeeded, requestCode)
        return false
    }
    return true
}

private fun permissionNeeded(activity: Activity, permissions: Array<String>): Array<String> {
    val listPermissionsNeeded: MutableList<String> = ArrayList()
    for (permission in permissions) {
        if (ContextCompat.checkSelfPermission(
                activity,
                permission
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            listPermissionsNeeded.add(permission)
        }
    }
    return listPermissionsNeeded.toTypedArray()
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください