Android: ActivityResultContract を使って Google Photos アプリで画像を選択する(複数選択にも対応)

ActivityResultContract を使って、Google Photos アプリを起動し、ユーザーに画像を選択させます。

ActivityResultContracts.GetContent を使って、Android 標準の画像選択アプリから画像選択をする実装は以下の記事を参照してください。Android 標準の画像選択アプリであれば Google Drive や Google Photos や Dropbox など多くのストレージサービスとの連携が可能であるため、Google Photos アプリ起動 Intent よりも Android 標準の画像選択を利用することをお勧めします。

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

## 必要な権限

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

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

## 前提

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

## 実装

Google Photos アプリが端末にインストールされているか調べるために、resolveActivity() を使用しています。Android 11 以上の [Package Visibility](https://developer.android.com/training/basics/intents/package-visibility?hl=ja) に対応するため、AndroidManifest.xml に `<queries>` タグを追加します。

Android 11 で試したところ Google Photos アプリの検出には `<data>` タグの指定は必須です。 `<data>` タグの指定がない場合は resolveActivity() が null となってしまうようです。

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="...">
    <queries>
        <!--
        Intent Action ではなく package で指定することもできる
        <package android:name="com.google.android.apps.photos" />
        -->
        <intent>
            <action android:name="android.intent.action.PICK" />
            <data android:mimeType="image/*" />
        </intent>
    </queries>
    <application android:theme="@stylve/AppTheme">
        <!-- ... -->
    </application>
</manifest>
```

Google Photos アプリの起動 Intent に対応した PickImageContract を実装します。

```kotlin
class PickImageContract : ActivityResultContract<Unit, String?>() {
    companion object {
        fun canPickImage(context: Context): Boolean {
            return (createIntentInternal().resolveActivity(context.packageManager) != null)
        }

        private fun createIntentInternal(): Intent {
            return Intent(Intent.ACTION_PICK).setType("image/*")
            // 厳密に Google Photos アプリだけに絞るなら Package 指定を追加しても良い
            // return Intent(Intent.ACTION_PICK).setType("image/*")
            //    .setPackage("com.google.android.apps.photos")
        }
    }

    override fun createIntent(context: Context, input: Unit): Intent {
        return createIntentInternal()
    }

    override fun parseResult(resultCode: Int, intent: Intent?): String? {
        return if (resultCode == Activity.RESULT_OK) intent?.data?.toString() else null
    }
}
```

PickImageContract を使用して画像選択を実行します。

```kotlin
class MyFragment: Fragment() {
    private val pickImageLauncher = registerForActivityResult(PickImageContract()) { uri ->
        if (uri != null) {
            // 画像が選択された
            // content://com.google.android.apps.photos.contentprovider/...
            // のような Content Provider 形式の URI を受け取ります
        } else {
            // 画像選択がキャンセルされた
        }
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = /* ... */
        binding.button.setOnClickListener {
            if (PickImageContract.canPickImage(this)) {
                launcher.launch(Unit)
            }
        }
    }
// ...
```

## 解説

ACTION_PICK Intent と `mimeType "image/*"` を組み合わせることで Google Photos アプリを画像選択アプリとして起動することができます。

端末に Google Photos アプリがインストールされていなければ ActivityNotFoundException が発生するため、PickImageContract の実行前に canPickImage() で Google Photos アプリのインストール確認をしています。

## Google Photos アプリで複数画像選択をできるようにする

以下の記事と同じ事が ACTION_PICK Intent でも可能です。

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

PickImagesContract を以下の通りに実装します。

[ACTION_PICK](https://developer.android.com/reference/kotlin/android/content/Intent#ACTION_PICK:kotlin.String) ドキュメントには EXTRA_ALLOW_MULTIPLE の記載はありませんが、 Google Photos アプリが独自に複数選択に対応しているようです。

```kotlin
class PickImagesContract : ActivityResultContract<Unit, List<String>?>() {
    companion object {
        fun canPickImage(context: Context): Boolean {
            return (createIntentInternal().resolveActivity(context.packageManager) != null)
        }

        private fun createIntentInternal(): Intent {
            return Intent(Intent.ACTION_PICK)
                .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
                .setType("image/*")
            }
        }
    }

    override fun createIntent(context: Context, input: Unit): Intent {
        return createIntentInternal()
    }

    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
    }
}
```

## Intent Chooser で Android 標準画像選択アプリと Google Photos アプリをユーザーが選択できるようにする

ActivityResultContract で Intent Chooser に Android 標準画像選択アプリと Google Photos アプリの両方を表示する方法は以下の記事を参照してください。

https://qiita.com/irgaly/items/7b55c7f9e5996d5df924