共有ストレージからメディア ファイルにアクセスする

多くのアプリは、充実したユーザー エクスペリエンスを提供するために、外部ストレージ ボリュームで利用可能なメディアにユーザーがアクセスしてデータを書き込む機能を備えています。フレームワークは、「メディアストア」と呼ばれるメディア コレクションに対して、最適化されたインデックスを提供します。これにより、ユーザーはメディア ファイルをより簡単に取得および更新できるようになります。アプリがアンインストールされても、この種のファイルはユーザーのデバイスに残ります。

写真選択ツール

メディアストアの代わりとして、Android の写真選択ツールがあります。このツールにはメディア ファイルを安全に選択できる機能が組み込まれていて、メディア ライブラリ全体へのアクセス権をアプリに付与する必要はありません。この機能は、サポートされているデバイスでのみ使用できます。詳しくは、写真選択ツールのガイドをご覧ください。

メディアストア

メディアストアを抽象化して操作するには、アプリのコンテキストから取得した ContentResolver オブジェクトを使用します。

Kotlin

val projection = arrayOf(media-database-columns-to-retrieve)
val selection = sql-where-clause-with-placeholder-variables
val selectionArgs = values-of-placeholder-variables
val sortOrder = sql-order-by-clause

applicationContext.contentResolver.query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)?.use { cursor ->
    while (cursor.moveToNext()) {
        // Use an ID column from the projection to get
        // a URI representing the media item itself.
    }
}

Java

String[] projection = new String[] {
        media-database-columns-to-retrieve
};
String selection = sql-where-clause-with-placeholder-variables;
String[] selectionArgs = new String[] {
        values-of-placeholder-variables
};
String sortOrder = sql-order-by-clause;

Cursor cursor = getApplicationContext().getContentResolver().query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
);

while (cursor.moveToNext()) {
    // Use an ID column from the projection to get
    // a URI representing the media item itself.
}

システムは、外部ストレージ ボリュームを自動的にスキャンし、メディア ファイルを以下の明確に定義されたコレクションに追加します。

  • 画像(写真とスクリーンショットを含む): DCIM/ ディレクトリと Pictures/ ディレクトリに格納されます。システムは、この種のファイルを MediaStore.Images テーブルに追加します。
  • 動画: DCIM/Movies/Pictures/ ディレクトリに格納されます。システムは、この種のファイルを MediaStore.Video テーブルに追加します。
  • オーディオ ファイル: Alarms/Audiobooks/Music/Notifications/Podcasts/Ringtones/ ディレクトリに格納されます。また、システムは、Music/ ディレクトリまたは Movies/ ディレクトリに格納さたオーディオ プレイリストや、Recordings/ ディレクトリに格納された音声録音を認識します。システムは、この種のファイルを MediaStore.Audio テーブルに追加します。Recordings/ ディレクトリは、Android 11(API レベル 30)以前では使用できません。
  • ダウンロードされたファイル: Download/ ディレクトリに格納されます。Android 10(API レベル 29)以上を実行しているデバイスでは、この種のファイルは MediaStore.Downloads テーブルに格納されます。このテーブルは、Android 9(API レベル 28)以前では使用できません。

メディアストアには、MediaStore.Files というコレクションも含まれています。Android 10 以上をターゲットとするアプリで利用可能な対象範囲別ストレージをアプリが使用しているかどうかによって、コレクションのコンテンツは異なります。

  • 対象範囲別ストレージが有効になっている場合、このコレクションは、アプリが作成した写真、動画、オーディオ ファイルのみを表示します。ほとんどのデベロッパーは MediaStore.Files を使用して他のアプリのメディア ファイルを表示する必要はありませんが、特に必要な場合は、READ_EXTERNAL_STORAGE 権限を宣言できます。Google では、アプリで作成したものではないファイルを開く場合には MediaStore API を使用することをおすすめします。
  • 対象範囲別ストレージが使用できない場合または使用されていない場合、このコレクションはすべてのタイプのメディア ファイルを表示します。

必要な権限をリクエストする

メディア ファイルに対するオペレーションを実行する前に、アプリがメディア ファイルにアクセスするために必要な権限を宣言していることを確認します。ただし、アプリで不要な権限や使用しない権限を宣言しないように注意してください。

ストレージへの権限

アプリがストレージにアクセスする必要があるかどうかは、アプリ自体のメディア ファイルのみにアクセスするか、あるいは他のアプリによって作成されたファイルにもアクセスするかによって異なります。

自分のメディア ファイルにアクセスする

Android 10 以降を実行しているデバイスでは、アプリが所有するメディア ファイル(MediaStore.Downloads コレクションのファイルなど)へのアクセスや変更に使用するストレージ関連の権限は必要ありません。たとえば、カメラアプリを開発する場合、撮影した写真にアクセスするためのストレージ関連の権限をリクエストする必要はありません。メディアストアに書き込む画像をアプリが所有しているからです。

他のアプリのメディア ファイルにアクセスする

他のアプリが作成したメディア ファイルにアクセスするには、適切なストレージ関連の権限を宣言する必要があります。また、ファイルが次のいずれかのメディア コレクションになければなりません。

MediaStore.ImagesMediaStore.Video、または MediaStore.Audio クエリで表示できるファイルであれば、MediaStore.Files クエリでも表示できます。

次のコード スニペットは、適切なストレージ権限の宣言方法を示しています。

<!-- Required only if your app needs to access images or photos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<!-- Required only if your app needs to access videos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- Required only if your app needs to access audio files
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="29" />

以前のデバイスで実行されているアプリには追加の権限が必要

Android 9 以前を搭載しているデバイスでアプリを使用する場合、またはアプリで一時的に対象範囲別ストレージをオプトアウトしている場合は、メディア ファイルにアクセスするための READ_EXTERNAL_STORAGE 権限をリクエストする必要があります。メディア ファイルを変更する場合は、WRITE_EXTERNAL_STORAGE 権限もリクエストする必要があります。

他のアプリのダウンロードを利用するために必要なストレージ アクセス フレームワーク

アプリが作成したものではない MediaStore.Downloads コレクション内のファイルにアクセスする場合は、ストレージ アクセス フレームワークを使用する必要があります。このフレームワークの使用方法については、共有ストレージのドキュメントやファイルにアクセスするをご覧ください。

メディアの位置情報に関する権限

Android 10(API レベル 29)以降をターゲットとするアプリで、写真の無編集の EXIF メタデータを取得する必要がある場合は、アプリのマニフェストで ACCESS_MEDIA_LOCATION 権限を宣言してから、実行時にこの権限をリクエストする必要があります。

メディアストアの更新を確認する

アプリがメディアストアの URI やデータをキャッシュに保存している場合など、確実にメディア ファイルにアクセスするには、メディアデータを最後に同期したときと比較して、メディアストアのバージョンが変更されているかどうかを確認します。更新を確認するには、getVersion() を呼び出します。返されるバージョンは、メディアストアが大幅に変更されるたびに変更される一意の文字列です。返されたバージョンが最後に同期されたバージョンと異なる場合は、アプリのメディア キャッシュを再スキャンして再同期します。

この確認は、アプリプロセスの開始時に行われます。メディアストアにクエリを行うたびにバージョンを確認する必要はありません。

バージョン番号に実装の詳細が関連するとは限りません。

メディア コレクションをクエリする

特定の条件(「再生時間が 5 分以上」など)を満たすメディアを検索するには、次のコード スニペットに示すように、SQL に似た選択ステートメントを使用します。

Kotlin

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
data class Video(val uri: Uri,
    val name: String,
    val duration: Int,
    val size: Int
)
val videoList = mutableListOf<Video>()

val collection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Video.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL
        )
    } else {
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    }

val projection = arrayOf(
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
)

// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
    TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)

// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"

val query = ContentResolver.query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    // Cache column indices.
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    val nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
    val durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
    val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        val id = cursor.getLong(idColumn)
        val name = cursor.getString(nameColumn)
        val duration = cursor.getInt(durationColumn)
        val size = cursor.getInt(sizeColumn)

        val contentUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList += Video(contentUri, name, duration, size)
    }
}

Java

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
class Video {
    private final Uri uri;
    private final String name;
    private final int duration;
    private final int size;

    public Video(Uri uri, String name, int duration, int size) {
        this.uri = uri;
        this.name = name;
        this.duration = duration;
        this.size = size;
    }
}
List<Video> videoList = new ArrayList<Video>();

Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
    collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}

String[] projection = new String[] {
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
};
String selection = MediaStore.Video.Media.DURATION +
        " >= ?";
String[] selectionArgs = new String[] {
    String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
};
String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

try (Cursor cursor = getApplicationContext().getContentResolver().query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    // Cache column indices.
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    int nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME);
    int durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
    int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        long id = cursor.getLong(idColumn);
        String name = cursor.getString(nameColumn);
        int duration = cursor.getInt(durationColumn);
        int size = cursor.getInt(sizeColumn);

        Uri contentUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList.add(new Video(contentUri, name, duration, size));
    }
}

アプリでこのようなクエリを実行する場合は、次の点に留意してください。

  • ワーカー スレッドで query() メソッドを呼び出します。
  • クエリ結果の行を処理するたびに getColumnIndexOrThrow() を呼び出す必要をなくすため、列インデックスをキャッシュに保存します。
  • 以下の例に示すように、コンテンツ URI の末尾に ID を付加します。
  • Android 10 以降を搭載したデバイスでは、MediaStore API で定義された列名が必要です。アプリ内の依存元ライブラリが想定している列名("MimeType" など)が API で定義されていない場合は、CursorWrapper を使用して、アプリのプロセス内で列名を動的に変換します。

ファイルのサムネイルを読み込む

複数のメディア ファイルを表示するアプリでファイルの選択をユーザーにリクエストする場合は、ファイル自体を読み込むより、ファイルのプレビュー バージョン(サムネイル)を読み込むほうが効率的です。

特定のメディア ファイルのサムネイルを読み込むには、次のコード スニペットに示すように、読み込むサムネイルのサイズを渡して loadThumbnail() を使用します。

Kotlin

// Load thumbnail of a specific media item.
val thumbnail: Bitmap =
        applicationContext.contentResolver.loadThumbnail(
        content-uri, Size(640, 480), null)

Java

// Load thumbnail of a specific media item.
Bitmap thumbnail =
        getApplicationContext().getContentResolver().loadThumbnail(
        content-uri, new Size(640, 480), null);

メディア ファイルを開く

メディア ファイルを開くために使用する具体的なロジックは、メディア コンテンツの表現にファイル記述子、ファイル ストリーム、直接ファイルパスのどれを使用すると最適であるかによって異なります。

ファイル記述子

ファイル記述子を使用してメディア ファイルを開くには、次のコード スニペットに示すようなロジックを使用します。

Kotlin

// Open a specific media item using ParcelFileDescriptor.
val resolver = applicationContext.contentResolver

// "rw" for read-and-write.
// "rwt" for truncating or overwriting existing file contents.
val readOnlyMode = "r"
resolver.openFileDescriptor(content-uri, readOnlyMode).use { pfd ->
    // Perform operations on "pfd".
}

Java

// Open a specific media item using ParcelFileDescriptor.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// "rw" for read-and-write.
// "rwt" for truncating or overwriting existing file contents.
String readOnlyMode = "r";
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(content-uri, readOnlyMode)) {
    // Perform operations on "pfd".
} catch (IOException e) {
    e.printStackTrace();
}

ファイル ストリーム

ファイル ストリームを使用してメディア ファイルを開くには、次のコード スニペットに示すようなロジックを使用します。

Kotlin

// Open a specific media item using InputStream.
val resolver = applicationContext.contentResolver
resolver.openInputStream(content-uri).use { stream ->
    // Perform operations on "stream".
}

Java

// Open a specific media item using InputStream.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();
try (InputStream stream = resolver.openInputStream(content-uri)) {
    // Perform operations on "stream".
}

直接ファイルパス

Android 11(API レベル 30)以降では、サードパーティのメディア ライブラリでアプリをよりスムーズに動作させるために、MediaStore API 以外の API を使用して共有ストレージからメディア ファイルにアクセスできます。代わりに、次のいずれかの API を使用してメディア ファイルに直接アクセスできます。

  • File API
  • ネイティブ ライブラリ(fopen() など)

ストレージ関連の権限がない場合は、File API を使用して、アプリ固有のディレクトリ内のファイルと、アプリに紐付けされたメディア ファイルにアクセスできます。

アプリが File API を使用してファイルにアクセスしようとした場合、必要な権限がなければ、FileNotFoundException が発生します。

Android 10(API レベル 29)を搭載したデバイスで共有ストレージ内の他のファイルにアクセスする場合は、アプリのマニフェスト ファイルで requestLegacyExternalStoragetrue に設定して、対象範囲別ストレージを一時的にオプトアウトすることをおすすめします。Android 10 でネイティブ ファイル メソッドを使用してメディア ファイルにアクセスするには、READ_EXTERNAL_STORAGE 権限もリクエストする必要があります。

メディア コンテンツにアクセスする際の考慮事項

メディア コンテンツにアクセスする際は、以下のセクションで説明する考慮事項に留意してください。

キャッシュ データ

アプリがメディアストアの URI やデータをキャッシュに保存している場合は、定期的にメディアストアのアップデートを確認します。この確認により、アプリ側のキャッシュ データとシステム側のプロバイダ データを同期できます。

パフォーマンス

直接ファイルパスを使用してメディア ファイルの順次読み取りを行うと、MediaStore API と同等のパフォーマンスが得られます。

一方、直接ファイルパスを使用してメディア ファイルのランダム読み取りと書き込みを行う場合は、処理が最大で 2 倍遅くなることがあります。そのような場合は、代わりに MediaStore API を使用することをおすすめします。

DATA 列

既存のメディア ファイルにアクセスする場合は、アプリのロジックで DATA 列の値を使用できます。これは、この値に有効なファイルパスが含まれているためです。ただし、ファイルが常に使用可能であるとは限りません。ファイルベースの I/O エラーが発生した場合の処理を準備してください。

一方、メディア ファイルを作成または更新する場合は、DATA 列の値を使用しないでください。代わりに、DISPLAY_NAME 列と RELATIVE_PATH 列の値を使用します。

ストレージ ボリューム

Android 10 以上をターゲットとするアプリは、システムが外部ストレージ ボリュームごとに割り当てる一意の名前にアクセスできます。この命名システムは、コンテンツを効率的に整理してインデックスを付け、新しいメディア ファイルの保存場所を管理するために役立ちます。

特に、以下のボリュームを覚えておくと便利です。

  • VOLUME_EXTERNAL ボリューム。デバイス上のすべての共有ストレージ ボリュームを表示します。この合成ボリュームのコンテンツを読み取ることはできますが、変更することはできません。
  • VOLUME_EXTERNAL_PRIMARY ボリューム。デバイス上のプライマリ共有ストレージ ボリュームを表します。このボリュームのコンテンツの読み取りと変更を行うことができます。

その他のボリュームを見つけるには、次のように MediaStore.getExternalVolumeNames() を呼び出します。

Kotlin

val volumeNames: Set<String> = MediaStore.getExternalVolumeNames(context)
val firstVolumeName = volumeNames.iterator().next()

Java

Set<String> volumeNames = MediaStore.getExternalVolumeNames(context);
String firstVolumeName = volumeNames.iterator().next();

メディアがキャプチャされた場所

写真や動画によっては、撮影場所や録画場所を示す位置情報がメタデータに含まれていることがあります。

アプリでこの位置情報にアクセスする方法は、写真と動画のどちらで位置情報にアクセスするかによって異なります。

写真

対象範囲別ストレージを使用しているアプリの場合、位置情報はデフォルトで非表示になります。この情報にアクセスするには、次の手順を実施します。

  1. アプリのマニフェストで ACCESS_MEDIA_LOCATION 権限をリクエストします。
  2. MediaStore オブジェクトから、setRequireOriginal() を呼び出して写真の URI を渡すことにより、写真の正確なバイトを取得します。次のコード スニペットをご覧ください。

    Kotlin

    val photoUri: Uri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex)
    )
    
    // Get location data using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    photoUri = MediaStore.setRequireOriginal(photoUri)
    contentResolver.openInputStream(photoUri)?.use { stream ->
        ExifInterface(stream).run {
            // If lat/long is null, fall back to the coordinates (0, 0).
            val latLong = latLong ?: doubleArrayOf(0.0, 0.0)
        }
    }
    

    Java

    Uri photoUri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex));
    
    final double[] latLong;
    
    // Get location data using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    photoUri = MediaStore.setRequireOriginal(photoUri);
    InputStream stream = getContentResolver().openInputStream(photoUri);
    if (stream != null) {
        ExifInterface exifInterface = new ExifInterface(stream);
        double[] returnedLatLong = exifInterface.getLatLong();
    
        // If lat/long is null, fall back to the coordinates (0, 0).
        latLong = returnedLatLong != null ? returnedLatLong : new double[2];
    
        // Don't reuse the stream associated with
        // the instance of "ExifInterface".
        stream.close();
    } else {
        // Failed to load the stream, so return the coordinates (0, 0).
        latLong = new double[2];
    }
    

動画

動画のメタデータ内の位置情報にアクセスするには、次のコード スニペットに示すように、MediaMetadataRetriever クラスを使用します。このクラスを使用するために、アプリで追加の権限をリクエストする必要はありません。

Kotlin

val retriever = MediaMetadataRetriever()
val context = applicationContext

// Find the videos that are stored on a device by querying the video collection.
val query = ContentResolver.query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    while (cursor.moveToNext()) {
        val id = cursor.getLong(idColumn)
        val videoUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )
        extractVideoLocationInfo(videoUri)
    }
}

private fun extractVideoLocationInfo(videoUri: Uri) {
    try {
        retriever.setDataSource(context, videoUri)
    } catch (e: RuntimeException) {
        Log.e(APP_TAG, "Cannot retrieve video file", e)
    }
    // Metadata uses a standardized format.
    val locationMetadata: String? =
            retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
}

Java

MediaMetadataRetriever retriever = new MediaMetadataRetriever();
Context context = getApplicationContext();

// Find the videos that are stored on a device by querying the video collection.
try (Cursor cursor = context.getContentResolver().query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    while (cursor.moveToNext()) {
        long id = cursor.getLong(idColumn);
        Uri videoUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
        extractVideoLocationInfo(videoUri);
    }
}

private void extractVideoLocationInfo(Uri videoUri) {
    try {
        retriever.setDataSource(context, videoUri);
    } catch (RuntimeException e) {
        Log.e(APP_TAG, "Cannot retrieve video file", e);
    }
    // Metadata uses a standardized format.
    String locationMetadata = retriever.extractMetadata(
            MediaMetadataRetriever.METADATA_KEY_LOCATION);
}

共有

一部のアプリでは、ユーザーはメディア ファイルを互いに共有できます。たとえば、ソーシャル メディア アプリでは、写真や動画を友だちと共有できます。

メディア ファイルを共有するには、コンテンツ プロバイダの作成ガイドで推奨されているように、content:// URI を使用します。

アプリとメディア ファイルの紐付け

Android 10 以降をターゲットとするアプリで対象範囲別ストレージが有効になっている場合、システムはアプリを各メディア ファイルに紐付けます。これにより、ストレージ権限をリクエストしていない場合にアプリがアクセスできるファイルが決まります。各ファイルは 1 つのアプリにのみ紐付けることができます。したがって、写真、動画、オーディオ ファイルのいずれかのメディア コレクションに格納されるメディア ファイルをアプリが作成した場合、アプリはそのファイルにアクセスできます。

ただし、ユーザーがアプリをアンインストールして再インストールした場合、アンインストール前にアプリが作成したファイルにアクセスするには、READ_EXTERNAL_STORAGE をリクエストする必要があります。この権限のリクエストが必要になるのは、ファイルは新たにインストールされたバージョンではなく、以前インストールされたバージョンに紐付けられていると見なされるためです。

1 つのアイテムを追加する

既存のコレクションにメディア アイテムを追加するには、次のようなコードを使用します。このコード スニペットは、Android 10 以降を搭載したデバイスで VOLUME_EXTERNAL_PRIMARY ボリュームにアクセスします。そのため、これらのデバイスでは、ストレージ ボリュームのセクションで説明したように、プライマリ ボリュームの場合にのみボリュームのコンテンツを変更できます。

Kotlin

// Add a specific media item.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

// Publish a new song.
val newSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Song.mp3")
}

// Keep a handle to the new song's URI in case you need to modify it
// later.
val myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails)

Java

// Add a specific media item.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

// Publish a new song.
ContentValues newSongDetails = new ContentValues();
newSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Song.mp3");

// Keep a handle to the new song's URI in case you need to modify it
// later.
Uri myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails);

メディア ファイルの保留中ステータスを切り替える

メディア ファイルへの書き込みなど、時間がかかるオペレーションを行うアプリでは、ファイルを処理している間、排他的アクセスを行うと便利です。Android 10 以上を実行しているデバイスでは、アプリで IS_PENDING フラグの値を 1 に設定することにより、この排他的アクセス権を取得できます。IS_PENDING の値を 0 に戻すまで、他のアプリはファイルを表示できなくなります。

次のコード スニペットは、前のコード スニペットを基にしています。このスニペットは、MediaStore.Audio コレクションに対応するディレクトリに長い曲を保存するときに IS_PENDING フラグを使用する方法を示しています。

Kotlin

// Add a media item that other apps don't see until the item is
// fully written to the media store.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

val songDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Workout Playlist.mp3")
    put(MediaStore.Audio.Media.IS_PENDING, 1)
}

val songContentUri = resolver.insert(audioCollection, songDetails)

// "w" for write.
resolver.openFileDescriptor(songContentUri, "w", null).use { pfd ->
    // Write data into the pending audio file.
}

// Now that you're finished, release the "pending" status and let other apps
// play the audio track.
songDetails.clear()
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(songContentUri, songDetails, null, null)

Java

// Add a media item that other apps don't see until the item is
// fully written to the media store.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

ContentValues songDetails = new ContentValues();
songDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Workout Playlist.mp3");
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 1);

Uri songContentUri = resolver
        .insert(audioCollection, songDetails);

// "w" for write.
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(songContentUri, "w", null)) {
    // Write data into the pending audio file.
}

// Now that you're finished, release the "pending" status and let other apps
// play the audio track.
songDetails.clear();
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0);
resolver.update(songContentUri, songDetails, null, null);

ファイルの場所に関するヒントを提供する

Android 10 を搭載したデバイスにアプリがメディアを保存する際、デフォルトでは、メディアはタイプに基づいて整理されます。たとえば、デフォルトでは新しい画像ファイルは Environment.DIRECTORY_PICTURES ディレクトリに配置されます。これは MediaStore.Images コレクションに対応しています。

ファイルを保存するべき特定の場所(Pictures/MyVacationPictures というフォトアルバムなど)をアプリが認識している場合は、MediaColumns.RELATIVE_PATH を設定して、新しく書き込まれたファイルを保存する場所のヒントをシステムに提供できます。

アイテムを更新する

アプリが所有するメディア ファイルを更新するには、次のようなコードを使用します。

Kotlin

// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver

// When performing a single item update, prefer using the ID.
val selection = "${MediaStore.Audio.Media._ID} = ?"

// By using selection + args you protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())

// Update an existing song.
val updatedSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Favorite Song.mp3")
}

// Use the individual song's URI to represent the collection that's
// updated.
val numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs)

Java

// Updates an existing media item.
long mediaId = // MediaStore.Audio.Media._ID of item to update.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// When performing a single item update, prefer using the ID.
String selection = MediaStore.Audio.Media._ID + " = ?";

// By using selection + args you protect against improper escaping of
// values. Here, "song" is an in-memory object that caches the song's
// information.
String[] selectionArgs = new String[] { getId().toString() };

// Update an existing song.
ContentValues updatedSongDetails = new ContentValues();
updatedSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Favorite Song.mp3");

// Use the individual song's URI to represent the collection that's
// updated.
int numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs);

対象範囲別ストレージが使用できない場合または有効になっていない場合、上記のコード スニペットで示したプロセスは、アプリが所有していないファイルでも機能します。

ネイティブ コードでの更新

ネイティブ ライブラリを使用してメディア ファイルを書き込む必要がある場合は、Java ベースまたは Kotlin ベースのコードから、ファイルに関連付けられたファイル記述子をネイティブ コードに渡します。

次のコード スニペットは、メディア オブジェクトのファイル記述子をアプリのネイティブ コードに渡す方法を示しています。

Kotlin

val contentUri: Uri = ContentUris.withAppendedId(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        cursor.getLong(BaseColumns._ID))
val fileOpenMode = "r"
val parcelFd = resolver.openFileDescriptor(contentUri, fileOpenMode)
val fd = parcelFd?.detachFd()
// Pass the integer value "fd" into your native code. Remember to call
// close(2) on the file descriptor when you're done using it.

Java

Uri contentUri = ContentUris.withAppendedId(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        cursor.getLong(Integer.parseInt(BaseColumns._ID)));
String fileOpenMode = "r";
ParcelFileDescriptor parcelFd =
        resolver.openFileDescriptor(contentUri, fileOpenMode);
if (parcelFd != null) {
    int fd = parcelFd.detachFd();
    // Pass the integer value "fd" into your native code. Remember to call
    // close(2) on the file descriptor when you're done using it.
}

他のアプリのメディア ファイルを更新する

対象範囲別ストレージを使用しているアプリでは、通常、別のアプリがメディアストアに書き込んだメディア ファイルを更新することはできません。

ファイルの変更についてユーザーの同意を得ることはできます。そのためには、まず、プラットフォームがスローする RecoverableSecurityException をキャッチします。次に、該当アイテムへの書き込みアクセス権をアプリに付与するようユーザーにリクエストします。次のコード スニペットをご覧ください。

Kotlin

// Apply a grayscale filter to the image at the given content URI.
try {
    // "w" for write.
    contentResolver.openFileDescriptor(image-content-uri, "w")?.use {
        setGrayscaleFilter(it)
    }
} catch (securityException: SecurityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val recoverableSecurityException = securityException as?
            RecoverableSecurityException ?:
            throw RuntimeException(securityException.message, securityException)

        val intentSender =
            recoverableSecurityException.userAction.actionIntent.intentSender
        intentSender?.let {
            startIntentSenderForResult(intentSender, image-request-code,
                    null, 0, 0, 0, null)
        }
    } else {
        throw RuntimeException(securityException.message, securityException)
    }
}

Java

try {
    // "w" for write.
    ParcelFileDescriptor imageFd = getContentResolver()
            .openFileDescriptor(image-content-uri, "w");
    setGrayscaleFilter(imageFd);
} catch (SecurityException securityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        RecoverableSecurityException recoverableSecurityException;
        if (securityException instanceof RecoverableSecurityException) {
            recoverableSecurityException =
                    (RecoverableSecurityException)securityException;
        } else {
            throw new RuntimeException(
                    securityException.getMessage(), securityException);
        }
        IntentSender intentSender =recoverableSecurityException.getUserAction()
                .getActionIntent().getIntentSender();
        startIntentSenderForResult(intentSender, image-request-code,
                null, 0, 0, 0, null);
    } else {
        throw new RuntimeException(
                securityException.getMessage(), securityException);
    }
}

アプリが作成したものではないメディア ファイルを変更する必要が生じるたびに、このプロセスを行います。

または、Android 11 以降で動作するアプリの場合、ユーザーがメディア ファイルのグループへの書き込みアクセス権をアプリに付与できるようにすることが可能です。メディア ファイルのグループを管理する方法についてのセクションの説明にあるように、createWriteRequest() メソッドを使用します。

アプリの別のユースケースに対象範囲別ストレージを適用できない場合は、機能リクエストを提出し、対象範囲別ストレージを一時的にオプトアウトしてください。

アイテムを削除する

アプリにとって不要になったアイテムをメディアストアから削除するには、次のコード スニペットに示すようなロジックを使用します。

Kotlin

// Remove a specific media item.
val resolver = applicationContext.contentResolver

// URI of the image to remove.
val imageUri = "..."

// WHERE clause.
val selection = "..."
val selectionArgs = "..."

// Perform the actual removal.
val numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs)

Java

// Remove a specific media item.
ContentResolver resolver = getApplicationContext()
        getContentResolver();

// URI of the image to remove.
Uri imageUri = "...";

// WHERE clause.
String selection = "...";
String[] selectionArgs = "...";

// Perform the actual removal.
int numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs);

対象範囲別ストレージが使用できない場合または有効になっていない場合は、上記のコード スニペットを使用して、他のアプリが所有するファイルを削除できます。一方、対象範囲別ストレージが有効になっている場合は、メディア アイテムを更新するセクションで説明しているように、アプリが削除するファイルごとに RecoverableSecurityException をキャッチする必要があります。

Android 11 以降で動作するアプリの場合、削除するメディア ファイルのグループをユーザーが選択できるようにすることが可能です。メディア ファイルのグループを管理する方法についてのセクションの説明にあるように、createTrashRequest() メソッドまたは createDeleteRequest() メソッドを使用します。

アプリの別のユースケースに対象範囲別ストレージを適用できない場合は、機能リクエストを提出し、対象範囲別ストレージを一時的にオプトアウトしてください。

メディア ファイルの更新を検出する

以前のある時点と比較して、アプリが追加または変更したメディア ファイルがあるストレージ ボリュームを、アプリで特定する必要が生じる場合があります。このような変更を確実に検出するには、対象となるストレージ ボリュームを getGeneration() に渡します。メディアストアのバージョンが変わらない限り、このメソッドの戻り値は時間の経過とともに単調に増加します。

具体的には、DATE_ADDEDDATE_MODIFIED などのメディア列の日付よりも getGeneration() の方が堅牢です。これは、アプリが setLastModified() を呼び出したときや、ユーザーがシステム クロックを変更したときに、メディア列の値が変更される可能性があるためです。

メディア ファイルのグループを管理する

Android 11 以降では、メディア ファイルのグループを選択してもらい、そのメディア ファイルを 1 回の操作で更新することができます。こうすることでデバイス間の一貫性が向上し、ユーザーがメディア コレクションを管理しやすくなります。

この「一括更新」機能を実現するメソッドには、次のようなものがあります。

createWriteRequest()
メディア ファイルの指定のグループへの書き込みアクセス権をユーザーがアプリに付与するためのリクエスト。
createFavoriteRequest()
ユーザーが指定のメディア ファイルをデバイス上の「お気に入り」のメディアの一部としてマークするためのリクエスト。このファイルに読み取りアクセス権を持つアプリは、ユーザーがそのファイルを「お気に入り」としてマークしたことを確認できます。
createTrashRequest()

ユーザーが指定のメディア ファイルをデバイスのゴミ箱に入れるためのリクエスト。ゴミ箱内のアイテムは、システムが規定する期間後に完全に削除されます。

createDeleteRequest()

前もってゴミ箱に入れずに、ユーザーが指定したメディア ファイルを完全に削除するためのリクエスト。

これらのメソッドのいずれかを呼び出すと、システムは PendingIntent オブジェクトを作成します。アプリがこのインテントを呼び出すと、指定のメディア ファイルをアプリが更新または削除する同意を求めるダイアログがユーザーに表示されます。

たとえば、createWriteRequest() の呼び出しは次のように作成します。

Kotlin

val urisToModify = /* A collection of content URIs to modify. */
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
        urisToModify)

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
    null, 0, 0, 0)

Java

List<Uri> urisToModify = /* A collection of content URIs to modify. */
PendingIntent editPendingIntent = MediaStore.createWriteRequest(contentResolver,
                  urisToModify);

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.getIntentSender(),
    EDIT_REQUEST_CODE, null, 0, 0, 0);

ユーザーのレスポンスを評価します。ユーザーが同意した場合は、メディア オペレーションに進みます。それ以外の場合は、アプリが権限を必要とする理由をユーザーに説明します。

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int,
                 data: Intent?) {
    ...
    when (requestCode) {
        EDIT_REQUEST_CODE ->
            if (resultCode == Activity.RESULT_OK) {
                /* Edit request granted; proceed. */
            } else {
                /* Edit request not granted; explain to the user. */
            }
    }
}

Java

@Override
protected void onActivityResult(int requestCode, int resultCode,
                   @Nullable Intent data) {
    ...
    if (requestCode == EDIT_REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            /* Edit request granted; proceed. */
        } else {
            /* Edit request not granted; explain to the user. */
        }
    }
}

createFavoriteRequest()createTrashRequest()createDeleteRequest() でも、この一般的なパターンを使用できます。

メディア管理の権限

ユーザーは、メディア ファイルを頻繁に編集するなど、特定のアプリを信頼してメディア管理を行う場合があります。Android 11 以降をターゲットとし、デバイスのデフォルト ギャラリー アプリではないアプリの場合、アプリがファイルを変更または削除しようとするたびに、確認ダイアログを表示する必要があります。

Android 12(API レベル 31)以降をターゲットとするアプリの場合、メディア管理の特別な権限へのアクセスをアプリに許可するよう、ユーザーにリクエストできます。この権限によってアプリは、ファイル操作のたびにユーザーに確認を求めることなく、以下のことを行えます。

手順は次のとおりです。

  1. アプリのマニフェスト ファイルで、MANAGE_MEDIA 権限と READ_EXTERNAL_STORAGE 権限を宣言します。

    確認ダイアログを表示せずに createWriteRequest() を呼び出すために、ACCESS_MEDIA_LOCATION 権限も宣言します。

  2. アプリで UI を表示して、アプリにメディア管理アクセス権を付与する理由を説明します。

  3. ACTION_REQUEST_MANAGE_MEDIA インテントのアクションを呼び出します。これにより、ユーザーはシステム設定の [メディア管理アプリ] 画面に移動します。ここから、ユーザーは特別なアプリアクセス権を付与できます。

メディアストアの代替手段が必要なユースケース

アプリが主に次のいずれかの役割を果たす場合は、MediaStore API の代替手段を検討してください。

他の種類のファイルを処理する

メディア コンテンツ以外のデータも含むドキュメントとファイル(EPUB 拡張子や PDF 拡張子を持つファイルなど)を処理するアプリでは、ドキュメントとその他のファイルを保存、アクセスする方法についてのガイドで説明されている ACTION_OPEN_DOCUMENT インテント アクションを使用します。

コンパニオン アプリでファイル共有を行う

メッセージ アプリやプロフィール アプリなどのコンパニオン アプリのスイートを提供する場合は、content:// URI を使用してファイル共有をセットアップします。このワークフローは、セキュリティに関するおすすめの方法でもあります。

参考情報

メディアの保存およびアクセス方法については、以下のリソースをご覧ください。

サンプル

動画