AndroidのACTION_IMAGE_CAPTUREやACTION_VIDEO_CAPTUREはカメラ権限が必要?その複雑な仕様について

こんにちは、kintone開発チームのAndroid担当のトニオ(@tonionagauzzi)です。
本日は、Androidアプリ開発中にカメラの権限を調査していてわかったことを共有します。

概要

Androidアプリで写真や動画を撮影する手段として、ACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTUREのインテントを用いてカメラアプリで撮影する方法が公式ドキュメントに書かれています。
その場合、Manifest.permission.CAMERAの権限は宣言せずインテントを呼び出すべきことも公式ドキュメントに書かれています。

そこだけ読むと、ACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTURECAMERAの権限は無関係のように読めるのですが、実はそうではありません。
他機能のためにCAMERAの権限を宣言しており、CAMERAの権限をユーザーが許可していない場合、ACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTUREのインテントでカメラアプリを起動しようとするとSecurityExceptionが発生してしまうのです。

背景

Androidアプリで写真や動画を撮影したい場合、MediaStoreACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTUREのアクションを使えます。

private fun takePicture(savePictureUri: Uri) {
    val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, savePictureUri)
    startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
}

private fun takeVideo(saveVideoUri: Uri) {
    val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
    takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, saveVideoUri)
    startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE)
}

このインテントを呼ぶと、プリインのカメラアプリが起動します。
写真や動画を撮影すると、撮影データがsavePictureUrisaveVideoUriに保存されます。
撮影後はonActivityResultが呼ばれ、撮影したデータが受け取れます。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) {
        val pictureUri = data?.data
        // ここで写真URIを使って何かを行うか、保存するなどの処理を行う
    } else if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == Activity.RESULT_OK) {
        val videoUri = data?.data
        // ここで動画URIを使って何かを行うか、保存するなどの処理を行う
    }
}

ActivityResultContractを使う実装も可能ですが、今回は省略します。

本題

さて、この実装は以下のようなManifest.permission.CAMERAの実装を必要とするのでしょうか。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.yourpackage">
    <uses-permission android:name="android.permission.CAMERA" />
    <application
        ...
    </application>
</manifest>
private fun checkCameraPermission() {
    if (
        ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.CAMERA
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        // 権限が許可されていない場合、ダイアログを表示してユーザーに許可を求める
        ActivityCompat.requestPermissions(
            this,
            arrayOf(Manifest.permission.CAMERA),
            CAMERA_PERMISSION_REQUEST_CODE
        )
    } else {
        // 権限がすでに許可されている場合、カメラの処理を実行する
        takePicture(savePictureUri = createSavePictureUri())
    }
}

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray,
) {
    if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // ユーザーが権限を許可した場合、カメラの処理を実行する
            takePicture(savePictureUri = createSavePictureUri())
        } else {
            // ユーザーが権限を拒否した場合、適切なエラーハンドリングを行うか、
            // 権限要求を再試行する方法を提供する
            ...
        }
    }
}

結論から言えば、他の箇所でandroid.permission.CAMERAを使っていなければ、AndroidManifestに書く必要はなく、requestPermissionも必要ありません
公式ドキュメントでは、以下のように書かれています。

プリインストールされているシステム カメラアプリを使用して、ユーザーがアプリで写真を撮影できる場合があります。 このような場合、CAMERA 権限を宣言しないでください。代わりに、ACTION_IMAGE_CAPTURE インテントのアクションを呼び出します。

ところが、他の箇所のためにandroid.permission.CAMERAをAndroidManifestで宣言していて、引き続き宣言が必要な場合、事情が違います。ACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTUREでもrequestPermissionが必要です
それはMediaStoreのドキュメントに書いてあります。

Note: if you app targets M and above and declares as using the Manifest.permission.CAMERA permission which is not granted, then attempting to use this action will result in a SecurityException.

カメラの権限を宣言していてユーザーが許可していない場合、ACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTUREを呼ぶとSecurityExceptionが発生してしまうのです。

GoogleのIssueTrackerによると、ユーザーがカメラについて意思表示をしていない場合とは異なり、ユーザーが明示的にカメラを拒否しているのであれば、カメラを起動するような処理は一切してはならないという判断のようです。

拒否しているはずのカメラが起動できてしまった、と考えるユーザーもいるのですね。

であれば初期状態ではユーザーはまだ意思表示していないから起動できるのでは、と思いますが、AndroidManifestにandroid.permission.CAMERAを宣言している時点で、初期状態は「許可しない」、つまり拒否状態と同じようです。
アプリインストール時にアプリに与えられた権限を読んで、それに同意していないということは、許可しない意思表示だと扱われているんですね。
なので、許可状態にするためにrequestPermissionが必要なのです。

まとめ

他の箇所でandroid.permission.CAMERAを必要としない場合、ACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTUREのためにandroid.permission.CAMERAをAndroidManifestに書く必要もrequestPermissionする必要もありません。
しかし、他の箇所のためにandroid.permission.CAMERAの使用を宣言している場合、初期状態ではACTION_IMAGE_CAPTUREACTION_VIDEO_CAPTUREを含むカメラに関する操作が拒否されているので、requestPermissionする必要があります。