LoginSignup
2
1

【Android】WebView の input type file で カメラを起動する

Last updated at Posted at 2023-12-15

Android WebViewでは、なぜかカメラが起動しない!!

webViewで、<input type="file"> でのカメラ起動は、IOSは何もしなくともカメラが起動するのですが、Androidだと、カメラがぜーんぜん起動しない・・・😢

かなり手こずったので、私がカメラ起動に成功した方法をまとめてみました。

ゼロから作っていく

Web側

<input type="file" multiple>

ここからアプリ
空っぽアクティビティです
MainActivity.kt

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

レイアウトは、webViewが使えるようにしておく
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivityでWebViewが表示できるようにしていく
MainActivity.kt

package com.example.qiitacamera

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

class MainActivity : AppCompatActivity() {
    /** WebView */
    private lateinit var webView: WebView

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

        // webViewのセット
        webView = findViewById<View>(R.id.web_view) as WebView
        val webSettings = webView.settings
        // WebView内でJavaScriptを有効にするための設定
        webSettings.javaScriptEnabled = true
        // WebViewがファイルへのアクセスを許可するための設定
        webSettings.allowFileAccess = true

        // WebViewをより安全に使いつつ、パフォーマンスも向上するためのの設定たち
        webSettings.mixedContentMode = 0
        webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)

        // WebView内のUIイベントを処理
        webView.webChromeClient = object : WebChromeClient() {
            // 中身は後から書きます
        }

        // WebView内のページの読み込みとかのイベントを処理
        webView.webViewClient = object : WebViewClient() {
            // 中身は後から書きます
        }

        // 表示させたいwebのURL (私はDockerでローカルにたちあげているのでエミュレーターが参照するlocal)
        webView.loadUrl("http://10.0.2.2:8000/")
    }
}

マニフェストの設定がめっちゃ重要!!!

AndroidManifest.xml

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


    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />

		<!--  ↓コレ、最初許可してなくて、解決するのにめっちゃ時間かかりました。
            これらの宣言により、アプリは他のアプリケーションのカメラ機能やギャラリーへのアクセスを要求する際に、
            システムがそれらのアクセス権を持つアプリをユーザーに提示できます。
            ユーザーが同意した場合、アプリはそれらの機能を使用することができます。-->
    <queries>
        <!-- カメラ -->
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
        <!-- ギャラリー -->
        <intent>
            <action android:name="android.intent.action.GET_CONTENT" />
        </intent>
    </queries>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true" <!-- httpをwebViewにしたいならコレ必須 -->
        android:theme="@style/Theme.QiitaCamera"
        tools:targetApi="31">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

httpsの描画なら、android:usesCleartextTraffic="true" は書かなくてOK

まず、この時点でbuildしてみましょう。
run app成功したら、次へGO!

続いて、端末内のファイルと、カメラの写真を格納するためのクラス定数を定義していきます。
MainActiivty.kt

   /** WebView */
   private lateinit var webView: WebView
   
   /** 画像のパスを保存する */
   private var mCameraPhotoPath: String? = null

   /** ファイル選択ダイアログが表示された際に、選択されたファイルのURLを受け取る
    * アップロードされたファイルの取得や操作する
    * */
   private var mUM: ValueCallback<Uri>? = null

   /** ファイル選択ダイアログが表示された際に、選択されたファイルのURLを受け取る
    * 複数のファイルが選択された場合
    *  */
   private var mUMA: ValueCallback<Array<Uri>>? = null

   /** ファイル選択リクエストの識別子 なんでもOK */
   private val FCR = 1

つづいて、onCreateの中で、ユーザーにパーミッション許可を要求していきます。
MainActivity.kt

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

       if (Build.VERSION.SDK_INT >= 23 && (ContextCompat.checkSelfPermission(
               this,
               Manifest.permission.WRITE_EXTERNAL_STORAGE
           ) != PackageManager.PERMISSION_GRANTED ||
                   ContextCompat.checkSelfPermission(
                       this,
                       Manifest.permission.CAMERA
                   ) != PackageManager.PERMISSION_GRANTED)
       ) {
           ActivityCompat.requestPermissions(
               this@MainActivity, arrayOf(
                   Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA
               ), 1
           )
       }

念の為、buildして、パーミッション許可ダイアログが出ることを確認すると吉
こんな感じ↓
パーミッション許可.png

つづいて、WebView内でのページ読み込み時にエラーが発生した際の動作をカスタマイズするために、KotlinでWebViewClientを拡張していきます。
MainActivity.kt

// WebView内のページの読み込みとかのイベントを処理
    webView.webViewClient = object : WebViewClient() {
        override fun onReceivedError(
            view: WebView?,
            errorCode: Int,
            description: String?,
            failingUrl: String?
        ) {
                            // ページ読み込みでエラーが発生したらトーストを出す
            Toast.makeText(
                applicationContext, "Failed loading app!",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

つづいて、カメラ動作に必要なメソッドを作成していきます。

カメラで撮影した写真を一旦外部ストレージに退避させるためのメソッドです。

MainActivity.kt

// 一時画像ファイルを外部ストレージに作成する
   private fun createImageFile(): File? {
       @SuppressLint("SimpleDateFormat") val timeStamp = SimpleDateFormat(
           "yyyyMMdd_HHmmss"
       ).format(Date())
       val imageFileName = "img_" + timeStamp + "_"
       // 外部ストレージのディレクトリを取得し、storageDir変数に格納
       val storageDir = Environment.getExternalStorageDirectory()
       // 一時的なJPEGファイルを作成し、外部ストレージ内の一時ディレクトリに作成
       return File.createTempFile(imageFileName, ".jpg", storageDir)
   }

外部ストレージのアクセスに、Environment.getExternalStorageDirectory()を使用している人もいるのではないでしょうか?
でも、私は、Environment.getExternalStorageDirectory()だと、端末の権限系のダイアログが表示されてしまい、カメラ起動ができませんでした。(許可方法もちょっとわからず・・・)

続いて、Webページからファイルを選択する際に呼び出されるonShowFileChooserメソッドをオーバーライドします。

カメラでの写真撮影やファイル選択ダイアログをカスタマイズしています。

以下のプログラムだと、カメラ起動か、ファイル選択かが選べるようになりますが、もしファイル選択のみにしたい場合は、var takePictureIntent = Intent(MediaStore.ACTION_PICK_IMAGES)

などにして、アクションを変更してください。
MainActivity.kt

// WebView内のUIイベントを処理
    webView.webChromeClient = object : WebChromeClient() {
        override fun onShowFileChooser(
            webView: WebView?,
            filePathCallback: ValueCallback<Array<Uri>>?,
            fileChooserParams: FileChooserParams?
        ): Boolean {
            if (mUMA != null) {
                mUMA!!.onReceiveValue(null)
            }

            mUMA = filePathCallback
            var takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)

            if (takePictureIntent.resolveActivity(this@MainActivity.packageManager) != null) {
                var photoUri: Uri? = null
                try {
                    photoUri = createImageFile()
                    takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath)
                } catch (ex: IOException) {
                    android.util.Log.e("WebActivity", "Image file creation failed", ex)
                }
                // ファイルが正常に作成された場合にのみ続行
                if (photoUri != null) {
                    mCameraPhotoPath = photoUri.toString()
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
                } else {
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, "null")
                }
            }

            val contentSelectionIntent = Intent(Intent.ACTION_GET_CONTENT)
            contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE)
            contentSelectionIntent.type = "*/*"

            var intentArray: Array<Intent?>

            if (takePictureIntent != null) {
                intentArray = arrayOf(takePictureIntent)
            } else {
                intentArray = arrayOfNulls(0)
            }

            val chooserIntent = Intent(Intent.ACTION_CHOOSER)
            chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent)
            chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser")
            chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)
            startActivityForResult(chooserIntent, FCR)
            return true;
        }
    }

ここまでで、Buildしてみましょう。
web側のinputタグをタップすると、カメラorファイル選択の画面が表示されるはずです。
選択画面.png

しかし、このままだと、
カメラのIntentが終了したら、そのまま次の処理が走りません。

startActivityForResult(chooserIntent, FCR)
ここで起動した、カメラのインテントの終了を受け取る処理を書いていきます。

WebViewでファイルの選択が完了した後に実行され、選択されたファイルのURIを適切な形式でWebViewに渡す役割を果たします。

MainActivity

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == FCR) {
        if (mUMA != null) {
            val resultUri: Uri? = if (data?.data == null) {
                if (mCameraPhotoPath != null) {
                    Uri.parse(mCameraPhotoPath)
                } else {
                    null
                }
            } else {
                data.data
            }

            val results = if (resultUri != null) {
                arrayOf(resultUri)
            } else {
                null
            }

            mUMA?.onReceiveValue(results)
            mUMA = null
        } else if (requestCode == FCR && mUM != null) {
            val result: Uri? = if (resultCode != Activity.RESULT_OK || data == null) {
                null
            } else {
                data.data
            }

            mUM?.onReceiveValue(result)
            mUM = null
        }
    }
}

完成!!これで、<input type="file" multiple>が正常に動くようになりました!!

エミュレーターのセルフィーかわいい。
エミュレータ.png

全体のソース

MainActivity.kt

package com.example.qiitacamera

import android.Manifest
import android.app.Activity
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.View
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
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.io.IOException
import java.text.SimpleDateFormat
import java.util.Date

class MainActivity : AppCompatActivity() {
    /** WebView */
    private lateinit var webView: WebView

    /** 画像のパスを保存する */
    private var mCameraPhotoPath: String? = null

    /** ファイル選択ダイアログが表示された際に、選択されたファイルのURLを受け取る
     * アップロードされたファイルの取得や操作する
     * */
    private var mUM: ValueCallback<Uri>? = null

    /** ファイル選択ダイアログが表示された際に、選択されたファイルのURLを受け取る
     * 複数のファイルが選択された場合
     *  */
    private var mUMA: ValueCallback<Array<Uri>>? = null

    /** ファイル選択リクエストの識別子 なんでもOK */
    private val FCR = 1

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == FCR) {
            if (mUMA != null) {
                val resultUri: Uri? = if (data?.data == null) {
                    if (mCameraPhotoPath != null) {
                        Uri.parse(mCameraPhotoPath)
                    } else {
                        null
                    }
                } else {
                    data.data
                }

                val results = if (resultUri != null) {
                    arrayOf(resultUri)
                } else {
                    null
                }

                mUMA?.onReceiveValue(results)
                mUMA = null
            } else if (requestCode == FCR && mUM != null) {
                val result: Uri? = if (resultCode != Activity.RESULT_OK || data == null) {
                    null
                } else {
                    data.data
                }

                mUM?.onReceiveValue(result)
                mUM = null
            }
        }
    }

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

        if (Build.VERSION.SDK_INT >= 23 && (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED ||
                    ContextCompat.checkSelfPermission(
                        this,
                        Manifest.permission.CAMERA
                    ) != PackageManager.PERMISSION_GRANTED)
        ) {
            ActivityCompat.requestPermissions(
                this@MainActivity, arrayOf(
                    Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA
                ), 1
            )
        }

        // webViewのセット
        webView = findViewById<View>(R.id.web_view) as WebView
        val webSettings = webView.settings
        // WebView内でJavaScriptを有効にするための設定
        webSettings.javaScriptEnabled = true
        // WebViewがファイルへのアクセスを許可するための設定
        webSettings.allowFileAccess = true

        // WebViewをより安全に使いつつ、パフォーマンスも向上するためのの設定たち
        webSettings.mixedContentMode = 0
        webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)

        // WebView内のUIイベントを処理
        webView.webChromeClient = object : WebChromeClient() {
            override fun onShowFileChooser(
                webView: WebView?,
                filePathCallback: ValueCallback<Array<Uri>>?,
                fileChooserParams: FileChooserParams?
            ): Boolean {
                if (mUMA != null) {
                    mUMA!!.onReceiveValue(null)
                }

                mUMA = filePathCallback
                var takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)

                if (takePictureIntent.resolveActivity(this@MainActivity.packageManager) != null) {
                    var photoUri: Uri? = null
                    try {
                        photoUri = createImageFile()
                        takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath)
                    } catch (ex: IOException) {
                        android.util.Log.e("WebActivity", "Image file creation failed", ex)
                    }
                    // ファイルが正常に作成された場合にのみ続行
                    if (photoUri != null) {
                        mCameraPhotoPath = photoUri.toString()
                        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
                    } else {
                        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, "null")
                    }
                }

                val contentSelectionIntent = Intent(Intent.ACTION_GET_CONTENT)
                contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE)
                contentSelectionIntent.type = "*/*"

                var intentArray: Array<Intent?>

                if (takePictureIntent != null) {
                    intentArray = arrayOf(takePictureIntent)
                } else {
                    intentArray = arrayOfNulls(0)
                }

                val chooserIntent = Intent(Intent.ACTION_CHOOSER)
                chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent)
                chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser")
                chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)
                startActivityForResult(chooserIntent, FCR)
                return true;
            }
        }

        // WebView内のページの読み込みとかのイベントを処理
        webView.webViewClient = object : WebViewClient() {
            override fun onReceivedError(
                view: WebView?,
                errorCode: Int,
                description: String?,
                failingUrl: String?
            ) {
                Toast.makeText(
                    applicationContext, "Failed loading app!",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }

        // 表示させたいwebのURL (私はDockerでローカルにたちあげているのでエミュレーターが参照するlocal)
        webView.loadUrl("http://10.0.2.2:8000/")
    }

    // insert()メソッドを使って画像ファイルのメタデータを含むContentValuesを使用して、
    // 外部ストレージの画像コンテンツURI(MediaStore.Images.Media.EXTERNAL_CONTENT_URI)に新しい画像ファイルを挿入する。
    private fun createImageFile(): Uri? {
        // Create an image file name
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val imageFileName = "JPEG_" + timeStamp + "_"

        val contentValues = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, "$imageFileName.jpg")
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
        }

        val resolver = applicationContext.contentResolver
        val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)

        return imageUri
    }
}

ちなみに、AndroidManifest.ktで以下の許可を書かないと、

<queries>
    <!-- カメラ -->
    <intent>
        <action android:name="android.media.action.IMAGE_CAPTURE" />
    </intent>
    <!-- ギャラリー -->
    <intent>
        <action android:name="android.intent.action.GET_CONTENT" />
    </intent>
</queries>

MainActivityのここが、永遠にtrueになりませんのでご注意を!(私はここに詰まりました・・・)

if (takePictureIntent.resolveActivity(this@MainActivity.packageManager) != null) {

おわりに

AndroidのWebViewでカメラが起動できない問題、調べたらみーんな困ってたので、私が動いた方法を記事にしてみました!!

参考になれば嬉しいです!

がんばるぞい!!

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1