[Android] 写真選択はPhoto Pickerへ ― READ_MEDIA権限とスコープドストレージ対応

画像を1枚選ばせたいだけなのに、READ_MEDIA_IMAGESREAD_EXTERNAL_STORAGE の権限をリクエストして、ユーザーに許可ダイアログを出していませんか?実はその実装、今ではもう古い作法かもしれません。

Android 13 から導入された Photo Picker を使えば、ストレージ権限をいっさい取らずに画像・動画を選べます。プライバシー的にも安心ですし、Google Play の審査でも「その権限、本当に必要?」と弾かれるリスクを避けられます。この記事では、Photo Picker の基本実装から、古い端末への対応、そして「それでも権限が必要なケース」までを Kotlin のコード付きで整理していきます。

スポンサーリンク

なぜ Photo Picker なのか

従来の ACTION_PICKGetContent、あるいは自前で MediaStore を読む方式は、多くの場合ストレージ権限を必要としました。一方 Photo Picker は、OS 側の画面で写真を選び、選ばれた URI だけがアプリに渡る仕組みです。アプリはユーザーのストレージを直接覗かないので、権限が要りません。

Google Play のポリシー的にも流れははっきりしています。「写真・動画へのアクセスが一回限り、またはたまにしか発生しない」用途なら、Photo Picker を使うべきという方針で、READ_MEDIA_IMAGES のような広範な権限は「コア機能として恒常的にメディア全体へアクセスする必要があるアプリだけ」に絞られてきています。プロフィール画像やチャットの添付など、たいていのアプリはこちらで十分です。

準備:依存関係

Photo Picker の Activity Result Contract は androidx.activity に含まれています。新しめのバージョンを入れておきましょう。

dependencies {
    // バージョンは最新の安定版に置き換えてください
    implementation("androidx.activity:activity-ktx:X.Y.Z")
}

実装1:画像を1枚選ぶ

基本形はとてもシンプルです。registerForActivityResult でランチャーを登録し、ボタンなどから launch() するだけ。

class PhotoSelectFragment : Fragment() {
 
    // ランチャーは初期化時に登録する(後述の注意点あり)
    private val pickImage = registerForActivityResult(
        ActivityResultContracts.PickVisualMedia()
    ) { uri: Uri? ->
        if (uri != null) {
            // 選択された画像の URI が渡ってくる
            binding.previewImage.setImageURI(uri)
        } else {
            // 何も選ばずに閉じた
        }
    }
 
    private fun onPickButtonClick() {
        // 画像だけを選ばせる
        pickImage.launch(
            PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
        )
    }
}

返ってきた uri には、その画像への読み取りアクセスが付与されています。アップロード用に InputStream で開いたり、ImageView に表示したりがそのままできます。権限リクエストのコードは一切不要です。

注意:registerForActivityResult は、Fragment / Activity の初期化のタイミングで呼ぶ必要があります。ボタンのクリックリスナーの中など、画面が再開(resume)した後で登録しようとすると例外になります。プロパティとして宣言しておくのが安全です。

実装2:複数選択する

複数選ばせたいときは PickMultipleVisualMedia を使います。最大選択数を渡せます。

// 最大5枚まで選べるようにする
private val pickMultiple = registerForActivityResult(
    ActivityResultContracts.PickMultipleVisualMedia(5)
) { uris: List<Uri> ->
    if (uris.isNotEmpty()) {
        uris.forEach { uri ->
            // 選択された各画像を処理する
        }
    }
}
 
private fun onPickMultipleClick() {
    pickMultiple.launch(
        PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
    )
}

メディアタイプの指定

PickVisualMediaRequest に渡すタイプで、選べるものを絞れます。

  • PickVisualMedia.ImageOnly ― 画像のみ
  • PickVisualMedia.VideoOnly ― 動画のみ
  • PickVisualMedia.ImageAndVideo ― 画像と動画の両方
  • PickVisualMedia.SingleMimeType("image/gif") ― 特定の MIME タイプのみ(例: GIF)
// GIF だけを選ばせたい場合
pickImage.launch(
    PickVisualMediaRequest(
        ActivityResultContracts.PickVisualMedia.SingleMimeType("image/gif")
    )
)

古い端末への対応(バックポート)

「Android 13 未満はどうするの?」という点ですが、心配いりません。Photo Picker は Google Play services 経由でAndroid 4.4(API 19)あたりまでバックポートされています。さらに、Photo Picker が使えない端末では、Contract が自動的に ACTION_OPEN_DOCUMENT(システムのファイル選択)へフォールバックしてくれます。

バックポートを確実に効かせるには、AndroidManifest.xml に次のサービスエントリを追加しておきます。これは Google Play services にバックポート版モジュールをインストールさせるためのトリガーです。

<!-- Photo Picker のバックポートモジュールを有効化する -->
<service
    android:name="com.google.android.gms.metadata.ModuleDependencies"
    android:enabled="false"
    android:exported="false"
    tools:ignore="MissingClass">
    <intent-filter>
        <action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
    </intent-filter>
    <meta-data
        android:name="photopicker_activity:0:required"
        android:value="" />
</service>

端末側で Photo Picker が本当に使えるかは、isPhotoPickerAvailable() で明示的に確認することもできます。

val available = ActivityResultContracts.PickVisualMedia
    .isPhotoPickerAvailable(requireContext())
// available が false の環境向けの分岐を書きたい場合に使う

とはいえ、前述のとおり Contract 自体が自動フォールバックしてくれるので、多くのケースではバージョン分岐を自分で書かずに済むのが Photo Picker の気楽なところです。

じゃあ READ_MEDIA_IMAGES はいつ使う?

Photo Picker が万能かというと、そうではありません。アプリが端末内のメディアを自前でスキャン・一覧表示したい(ギャラリーアプリ、写真管理アプリ、バックアップアプリなど)ような、恒常的な全アクセスが必要なケースでは、やはり MediaStore を直接読む必要があり、そのときは権限が要ります。

  • Android 13(API 33)以降 ― READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIO(種類ごとに分割)
  • Android 12(API 32)以前 ― READ_EXTERNAL_STORAGE

さらに Android 14(API 34)からは、ユーザーが「一部の写真だけ許可」を選べる部分アクセス(READ_MEDIA_VISUAL_USER_SELECTED)が加わりました。権限を使う場合でも、ユーザーが選んだ写真だけにアクセスが絞られることがあるので、「全件見えている前提」でコードを書かないよう注意が必要です。

裏を返せば、こうした複雑さを丸ごと回避できるのが Photo Picker のありがたさ、とも言えますね。

まとめ:判断はシンプルに

  • 画像・動画を「選ばせたい」だけ → Photo Picker(PickVisualMedia)。権限不要、これが第一選択。
  • 端末内のメディアを自前で一覧・スキャンしたいMediaStoreREAD_MEDIA_* 権限。部分アクセスにも配慮する。
  • 古い端末は自動フォールバック+バックポートでだいたいカバーできる。

「とりあえず権限を取っておく」という発想は、今の Android ではむしろリスクになりがちです。まずは Photo Picker で足りないか?を最初に考えるクセをつけておくと、実装もシンプルになり、審査もプライバシーも安心です。似たようなストレージ・権限まわりの話はまだまだあるので、また少しずつ書いていければと思います。

コメント

タイトルとURLをコピーしました