Android: ActivityResultContract を使って画像選択アプリで画像を複数選択する (EXTRA_ALLOW_MULTIPLE)

ActivityResultContract を使って外部の画像選択アプリを起動し、ユーザーに複数の画像を選択させます。

単一の画像選択で構わない場合は以下の記事も参照してください。

https://qiita.com/irgaly/items/b9bbef4de73669113ddf

Android 標準の画像選択アプリではなく、Google Photos を起動して複数画像選択をしたい場合は以下の記事を参照してください。

https://qiita.com/irgaly/items/a1967f0fa4b505ab83e8

## 必要な権限

外部の画像選択アプリによって画像を選択し、その画像を読み取るためには **READ_EXTERNAL_STORAGE 権限は不要** です。

画像選択アプリが端末に保存されている画像を読み取る権限を取得しており、ユーザーが選択した画像は Intent 発行元のアプリが読み取れるパスとして返却されるためです。

## 前提

AndroidX や AndroidX Fragment が導入されている前提です。

## 実装

複数画像選択に対応した GetContentsContract を実装します。

```kotlin
class GetContentsContract : ActivityResultContract<String, List<String>?>() {
    override fun createIntent(context: Context, input: String): Intent {
        return Intent(Intent.ACTION_GET_CONTENT)
            .addCategory(Intent.CATEGORY_OPENABLE)
            .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
            .setType(input)
    }

    override fun parseResult(resultCode: Int, intent: Intent?): List<String>? {
        return if (resultCode == Activity.RESULT_OK) {
            intent?.clipData?.let { clipData ->
                (0 until clipData.itemCount).mapNotNull {
                    clipData.getItemAt(it).uri?.toString()
                }
            } ?: intent?.data?.toString()?.let { listOf(it) }
        } else null
    }
}
```

GetContentsContract を使用して複数の画像選択を実行します。

```kotlin
class MyFragment: Fragment() {
    private val getContentsLauncher = registerForActivityResult(
        GetContentsContract()
    ) { uris ->
        if (uris != null) {
            // 画像が選択された
            // content://com.android.providers.media.documents/...
            // のような Content Provider 形式の URI を受け取ります
        } else {
            // 画像選択がキャンセルされた
        }
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = /* ... */
        binding.button.setOnClickListener {
            getContentsLauncher.launch("image/*")
        }
    }
// ...
```

## 解説

[ACTION_GET_CONTENT](https://developer.android.com/reference/kotlin/android/content/Intent#action_get_content) は複数の画像選択に対応しています。

ACTION_GET_CONTENT に加えて、[EXTRA_ALLOW_MULTIPLE](https://developer.android.com/reference/kotlin/android/content/Intent#extra_allow_multiple) に true を指定すると複数選択となります。

複数選択の結果は [Intent.clipData](https://developer.android.com/reference/kotlin/android/content/Intent#getclipdata) に格納されています。 ClipData.getItemAt() で ClipData.itemCount まで取り出せます。 [ClipData.Item](https://developer.android.com/reference/kotlin/android/content/ClipData.Item) の uri に選択したファイルのパスが含まれています。

画像選択アプリでユーザーが一つの画像を選択した場合は clipData ではなく intent.data にファイルパスが格納されていることがあります。intent.clipData から値を取得できなければ単一ファイル選択とみなし、 inetnet.data からファイルパス取得するようにしています。

## 実行結果

以下のように Android 標準の画像選択アプリが起動します。

画像選択アプリの見た目や動作は OS のバージョンや端末の種類によります。

以下の標準アプリでは複数画像選択できるようにしていても、画像をタップすると単一のファイルを選択して確定してしまうようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/0ba096d2-b76b-f8ee-47ce-9e266508e57c.png)

ユーザーが画像を長押しすると、以下のように複数選択 UI となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/a63f1ef5-6c36-889a-1707-2098a4b22592.png)

標準の画像選択アプリは、左側のドロワーメニューから Google Photos アプリも呼び出すことができます。

Google Photos アプリの選択画面は、この画面を開いた状態ですでに複数画像選択表示となっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/7d3264db-69c5-6c54-f57a-7f7aefbdba78.png)

## 注意事項

EXTRA_ALLOW_MULTIPLE による複数画像選択 Intent は簡単に実装できますが、以下の制限があります。

複数画像選択 Intent は便利ですがあまり融通が効かず、実際に活躍する場面は少ないかもしれません。

### 選択できるファイル数の上限を指定できない

Android 標準の画像選択アプリでも Google Photos でも、選択できる画像の上限数を指定することはできません。ユーザーが選択した分がすべて Intent に返されます。

画像選択 Intent の呼び出し側のアプリで、Intent の結果からファイル数を削るのか、無限のファイル選択に対応できるようにするのかを検討し、対処できるようにしておく必要があります。

### すでに選択済みのファイル一覧を渡して、選択済みの状態から開始することはできない

Android 標準の画像選択アプリでも Google Photos でも、画像選択を開始するときに任意の画像を選択済みのような表示にすることはできません。Intent を発行するたびにすべて未選択の状態から開始します。特定の画像を非表示とするような機能もありません。