写真と動画への部分的なアクセス権を付与する

Android 14 では、Selected Photos Access が導入され、特定のタイプのすべてのメディアへのアクセス権を付与するのではなく、ライブラリ内の特定の画像および動画へのアクセス権をアプリに付与できるようになりました。

この変更は、アプリが Android 14(API レベル 34)以降をターゲットとする場合にのみ有効になります。写真選択ツールをまだ使用していない場合は、アプリに実装して、ストレージの権限をリクエストしなくても、画像や動画を選択する際に一貫性のあるエクスペリエンスを提供することをおすすめします。これにより、ユーザーのプライバシーも強化されます。

ストレージの権限を使用して独自のギャラリー選択ツールを管理しており、実装を完全に制御する必要がある場合は、新しい READ_MEDIA_VISUAL_USER_SELECTED 権限を使用するように実装を調整します。アプリが新しい権限を使用しない場合、システムはアプリを互換モードで実行します。

対象 SDK READ_MEDIA_VISUAL_USER_SELECTED を宣言しました 選択した写真へのアクセスが有効になりました UX の動作
SDK 33 × × なし
アプリで制御
SDK 34 × システムによって制御されている(互換動作)
アプリで制御

独自のギャラリー選択ツールを作成するには、大規模な開発とメンテナンスが必要です。また、ユーザーから明示的な同意を得るために、アプリでストレージ権限をリクエストする必要があります。ユーザーはこれらのリクエストを拒否できます。また、Android 14(API レベル 34)以降をターゲットとするアプリが Android 14 を搭載したデバイスで実行されている場合は、選択したメディアへのアクセスを制限できます。次の図は、権限をリクエストし、新しいオプションを使用してメディアを選択する例を示しています。

.
図 1.新しいダイアログでは、フルアクセス権を付与するかすべてのアクセスを拒否する通常のオプションに加えて、アプリで使用できるようにする特定の写真や動画をユーザーが選択できます。

このセクションでは、MediaStore を使用して独自のギャラリー選択ツールを作成する場合におすすめのアプローチについて説明します。すでにアプリのギャラリー選択ツールを管理しており、完全な制御を維持する必要がある場合は、これらの例を使用して実装を調整できます。選択されたフォトアクセスを処理するように実装を更新しない場合、システムはアプリを互換モードで実行します。

権限をリクエストする

まず、OS のバージョンに応じて、Android マニフェストで適切なストレージ権限をリクエストします。

<!-- Devices running Android 12L (API level 32) or lower  -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- To handle the reselection within the app on devices running Android 14
     or higher if your app targets Android 14 (API level 34) or higher.  -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

次に、OS のバージョンに応じて、適切な実行時の権限をリクエストします。

// Register ActivityResult handler
val requestPermissions = registerForActivityResult(RequestMultiplePermissions()) { results ->
    // Handle permission requests results
    // See the permission example in the Android platform samples: https://github.com/android/platform-samples
}

// Permission request logic
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED))
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO))
} else {
    requestPermissions.launch(arrayOf(READ_EXTERNAL_STORAGE))
}

権限が必要なアプリもあります

Android 10(API レベル 29)以降、共有ストレージにファイルを追加するためのストレージ権限がアプリで不要になりました。つまり、アプリはストレージ権限をリクエストしなくても、ギャラリーに画像を追加したり、動画を記録して共有ストレージに保存したり、PDF 請求書をダウンロードしたりできます。アプリが共有ストレージにファイルを追加するだけで、画像や動画のクエリを行わない場合は、ストレージの権限のリクエストを停止し、AndroidManifest.xml で API 28 の maxSdkVersion を設定する必要があります。

<!-- No permission is needed to add files to shared storage on Android 10 (API level 29) or higher  -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

メディアの再選択を処理する

Android 14 の「選択中の写真へのアクセス」機能では、メディアの再選択を制御する新しい READ_MEDIA_VISUAL_USER_SELECTED 権限をアプリで採用し、ユーザーが別の画像や動画のセットにアクセスできるようにアプリのインターフェースを更新する必要があります。次の図は、権限をリクエストしてメディアを再選択する例を示しています。

.
図 2.新しいダイアログでは、アプリで使用できるようにする写真や動画をユーザーが再選択することもできます。

選択ダイアログを開くと、リクエストされた権限に応じて写真、動画、またはその両方が表示されます。たとえば、READ_MEDIA_IMAGES 権限なしで READ_MEDIA_VIDEO 権限をリクエストした場合、ユーザーがファイルを選択するための UI には動画のみが表示されます。

// Allow the user to select only videos
requestPermissions.launch(arrayOf(READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED))

アプリがデバイスのフォト ライブラリに対する完全アクセス権、部分アクセス権、または拒否されたアクセス権を確認し、それに応じてインターフェースを更新できます。これらの権限をリクエストするのは、アプリの起動時ではなく、ストレージへのアクセスが必要な場合です。権限の付与は、アプリのライフサイクル コールバックで onStartonResume の間で変更できます。ユーザーはアプリを閉じずに設定でアクセス権を変更できます。

if (
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
    (
        ContextCompat.checkSelfPermission(context, READ_MEDIA_IMAGES) == PERMISSION_GRANTED ||
        ContextCompat.checkSelfPermission(context, READ_MEDIA_VIDEO) == PERMISSION_GRANTED
    )
) {
    // Full access on Android 13 (API level 33) or higher
} else if (
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
    ContextCompat.checkSelfPermission(context, READ_MEDIA_VISUAL_USER_SELECTED) == PERMISSION_GRANTED
) {
    // Partial access on Android 14 (API level 34) or higher
}  else if (ContextCompat.checkSelfPermission(context, READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
    // Full access up to Android 12 (API level 32)
} else {
    // Access denied
}

デバイス ライブラリに対してクエリを実行する

適切なストレージ権限にアクセスできることを確認したら、MediaStore を操作してデバイス ライブラリをクエリできます(付与されたアクセス権が部分的か完全かにかかわらず、同じ方法を使用できます)。

data class Media(
    val uri: Uri,
    val name: String,
    val size: Long,
    val mimeType: String,
)

// Run the querying logic in a coroutine outside of the main thread to keep the app responsive.
// Keep in mind that this code snippet is querying only images of the shared storage.
suspend fun getImages(contentResolver: ContentResolver): List<Media> = withContext(Dispatchers.IO) {
    val projection = arrayOf(
        Images.Media._ID,
        Images.Media.DISPLAY_NAME,
        Images.Media.SIZE,
        Images.Media.MIME_TYPE,
    )

    val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Query all the device storage volumes instead of the primary only
        Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
    } else {
        Images.Media.EXTERNAL_CONTENT_URI
    }

    val images = mutableListOf<Media>()

    contentResolver.query(
        collectionUri,
        projection,
        null,
        null,
        "${Images.Media.DATE_ADDED} DESC"
    )?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(Images.Media._ID)
        val displayNameColumn = cursor.getColumnIndexOrThrow(Images.Media.DISPLAY_NAME)
        val sizeColumn = cursor.getColumnIndexOrThrow(Images.Media.SIZE)
        val mimeTypeColumn = cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)

        while (cursor.moveToNext()) {
            val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
            val name = cursor.getString(displayNameColumn)
            val size = cursor.getLong(sizeColumn)
            val mimeType = cursor.getString(mimeTypeColumn)

            val image = Media(uri, name, size, mimeType)
            images.add(image)
        }
    }

    return@withContext images
}

このコード スニペットは、MediaStore の操作方法を示すために簡略化されています。本番環境対応のアプリでは、ページング ライブラリなどでページ分割を使用すると、優れたパフォーマンスを確保できます。

デバイスのアップグレード後も、写真と動画へのアクセスは保持される

以前のバージョンの Android から Android 14 にアップグレードするデバイスにアプリがインストールされている場合、システムではそのユーザーの写真と動画への完全アクセス権は維持され、アプリに一部の権限が自動的に付与されます。デバイスが Android 14 にアップグレードされる前にアプリに付与されている権限のセットによって、具体的な動作は異なります。

Android 13 の権限

次のような状況を考えてみましょう。

  1. アプリは Android 13 を搭載したデバイスにインストールされています。
  2. ユーザーがアプリに READ_MEDIA_IMAGES 権限と READ_MEDIA_VIDEO 権限を付与しています。
  3. アプリがインストールされている状態で、デバイスが Android 14 にアップグレードされます。
  4. アプリが Android 14(API レベル 34)以降をターゲットとしている。

この場合、アプリは引き続きそのユーザーの写真と動画への完全アクセス権を持ちます。また、アプリに付与されている READ_MEDIA_IMAGES 権限と READ_MEDIA_VIDEO 権限も自動的に保持されます。

Android 12 以前の権限

次のような状況を考えてみましょう。

  1. アプリは Android 13 を搭載したデバイスにインストールされています。
  2. ユーザーがアプリに READ_EXTERNAL_STORAGE 権限または WRITE_EXTERNAL_STORAGE 権限を付与しています。
  3. アプリがインストールされている状態で、デバイスが Android 14 にアップグレードされます。
  4. アプリが Android 14(API レベル 34)以降をターゲットとしている。

この場合、アプリは引き続きそのユーザーの写真と動画への完全アクセス権を持ちます。また、READ_MEDIA_IMAGES 権限と READ_MEDIA_VIDEO 権限もアプリに自動的に付与されます。

おすすめの方法

このセクションでは、READ_MEDIA_VISUAL_USER_SELECTED 権限を使用する際のベスト プラクティスについて説明します。詳しくは、権限に関するベスト プラクティスをご覧ください。

権限の状態を永続的に保存しない

SharedPreferencesDataStore を含め、権限の状態を永続的に保存しないでください。保存された状態は、実際の状態と同期していない可能性があります。権限の状態は、権限のリセット後、アプリの休止状態後、ユーザーが開始したアプリの設定変更後、またはアプリがバックグラウンドに移行したときに変化することがあります。代わりに、ContextCompat.checkSelfPermission() を使用してストレージの権限を確認します。

写真と動画への完全アクセス権を持っていることを前提としない

Android 14 で導入された変更により、アプリはデバイスのフォト ライブラリに部分的にしかアクセスできない場合があります。ContentResolver を使用してクエリが行われたときにアプリが MediaStore データをキャッシュに保存していた場合、キャッシュが最新ではない可能性があります。

  • 保存されたキャッシュに依存するのではなく、常に ContentResolver を使用して MediaStore をクエリしてください。
  • アプリがフォアグラウンドにある間、結果をメモリに保持してください。
  • ユーザーが権限の設定で完全アクセス権から部分アクセス権に切り替える可能性があるため、アプリが onResume アプリのライフサイクルを通過したら、結果を更新します。

URI アクセスを一時的なものとして扱う

ユーザーがシステム権限ダイアログで [写真と動画を選択] を選択すると、選択した写真と動画へのアプリのアクセス権が最終的に期限切れになります。アプリは、権限に関係なく、Uri にアクセスできないケースを常に処理する必要があります。

選択可能なメディアタイプを権限でフィルタ

選択ダイアログは、リクエストされた権限タイプに応じて異なります。

  • READ_MEDIA_IMAGES のみをリクエストすると、選択可能な画像のみが表示されます。
  • READ_MEDIA_VIDEO のみをリクエストすると、動画のみが選択可能になります。
  • READ_MEDIA_IMAGESREAD_MEDIA_VIDEO の両方をリクエストすると、フォト ライブラリ全体が選択可能になります。

アプリのユースケースに基づいて、ユーザー エクスペリエンスの低下を避けるため、適切な権限をリクエストする必要があります。機能が動画の選択のみを想定している場合は、READ_MEDIA_VIDEO のみをリクエストするようにしてください。

1 回の操作で権限をリクエストする

ユーザーに複数のシステム ランタイム ダイアログ ボックスが表示されないようにするには、1 回のオペレーションで READ_MEDIA_VISUAL_USER_SELECTEDACCESS_MEDIA_LOCATION、「読み取りメディア」権限(READ_MEDIA_IMAGESREAD_MEDIA_VIDEO のいずれかまたは両方)をリクエストします。

ユーザーに選択の管理を許可する

ユーザーが部分アクセスモードを選択した場合、アプリはデバイスのフォト ライブラリが空であると想定せず、ユーザーが追加のファイルへのアクセスを許可できるようにする必要があります。

ユーザーは、一部のビジュアル メディア ファイルへのアクセス権を付与せずに、権限設定を使用して完全アクセス権から部分アクセス権に切り替えることもできます。

互換性モード

ストレージの権限を使用して独自のギャラリー選択ツールを維持しているが、新しい READ_MEDIA_VISUAL_USER_SELECTED 権限を使用するようにアプリを適応していない場合は、ユーザーがメディアを選択または再選択する必要があるたびに、システムはアプリを互換モードで実行します。

最初のメディア選択時の動作

最初の選択時に、ユーザーが [写真と動画を選択](図 1 を参照)を選択すると、アプリ セッション中に READ_MEDIA_IMAGES 権限と READ_MEDIA_VIDEO 権限が付与され、ユーザーが選択した写真と動画への一時的な権限付与と一時的なアクセス権が付与されます。アプリがバックグラウンドに移動するか、ユーザーが積極的にアプリを強制終了すると、システムは最終的にこれらの権限を拒否します。この動作は、他の 1 回だけのアクセス許可と同様です。

メディアの再選択中の動作

後でアプリが追加の写真や動画にアクセスする必要がある場合は、READ_MEDIA_IMAGES 権限または READ_MEDIA_VIDEO 権限を再度手動でリクエストする必要があります。システムは最初の権限リクエストと同じフローに従い、ユーザーは写真と動画を選択するよう求められます(図 2 を参照)。

アプリが権限に関するベスト プラクティスに従っている場合、この変更によってアプリが影響を受けることはありません。これは特に、アプリで URI アクセスが保持されていることを前提としていない、システム権限の状態を保存していない、または権限の変更後に表示される画像のセットを更新しない場合に特に当てはまります。ただし、アプリのユースケースによっては、この動作が最適でない場合があります。最適なユーザー エクスペリエンスを提供するには、写真選択ツールを実装するか、アプリのギャラリー選択ツールを適応して、READ_MEDIA_VISUAL_USER_SELECTED 権限を使用してこの動作を直接処理できるようにすることをおすすめします。