Mengakses file media dari penyimpanan bersama

Untuk memberikan pengalaman pengguna yang lebih kaya, banyak aplikasi memungkinkan pengguna berkontribusi dan mengakses media yang tersedia pada volume penyimpanan eksternal. Framework ini menyediakan indeks yang dioptimalkan ke koleksi media, yang disebut penyimpanan media, yang memungkinkan untuk mengambil dan memperbarui file media ini dengan lebih mudah. Bahkan setelah aplikasi Anda di-uninstal, file ini tetap berada di perangkat pengguna.

Untuk berinteraksi dengan abstraksi penyimpanan media, gunakan objek ContentResolver yang Anda ambil dari konteks aplikasi:

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

Sistem memindai volume penyimpanan eksternal secara otomatis dan menambahkan file media ke koleksi yang terdefinisi dengan baik berikut ini:

  • Gambar, termasuk foto dan screenshot, yang disimpan di direktori DCIM/ dan Pictures/. Sistem akan menambahkan file ini ke tabel MediaStore.Images.
  • Video, yang disimpan di direktori DCIM/, Movies/, dan Pictures/. Sistem akan menambahkan file ini ke tabel MediaStore.Video.
  • File audio, yang disimpan di direktori Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/, dan Ringtones/. Selain itu, sistem akan mengenali playlist audio yang ada dalam direktori Music/ atau Movies/, serta rekaman suara yang ada di direktori Recordings/. Sistem akan menambahkan file ini ke tabel MediaStore.Audio. Direktori rekaman tidak tersedia di Android 11 (API level 30) dan lebih rendah.
  • File yang didownload, yang disimpan di dalam direktori Download/. Pada perangkat yang menjalankan Android 10 (API level 29) dan yang lebih tinggi, file ini disimpan di tabel MediaStore.Downloads. Tabel ini tidak tersedia di Android 9 (API level 28) dan lebih rendah.

Penyimpanan media juga menyertakan koleksi bernama MediaStore.Files. Kontennya bergantung pada apakah aplikasi Anda menggunakan penyimpanan terbatas, tersedia di aplikasi yang menargetkan Android 10 atau yang lebih tinggi:

  • Jika penyimpanan terbatas diaktifkan, koleksi hanya akan menampilkan foto, video, dan file audio yang dibuat oleh aplikasi Anda. Sebagian besar developer tidak perlu menggunakan MediaStore.Files untuk melihat file media dari aplikasi lain, tetapi jika Anda memiliki persyaratan khusus untuk melakukannya, Anda dapat menyatakan izin READ_EXTERNAL_STORAGE. Namun, sebaiknya gunakan MediaStore API untuk membuka file yang belum dibuat oleh aplikasi.
  • Jika penyimpanan terbatas tidak tersedia atau tidak sedang digunakan, koleksi akan menampilkan semua jenis file media.

Meminta izin yang diperlukan

Sebelum menjalankan operasi pada file media, pastikan aplikasi Anda telah mendeklarasikan izin yang diperlukan untuk mengakses file ini. Namun, perlu diingat bahwa aplikasi Anda tidak boleh mendeklarasikan izin yang tidak diperlukan atau digunakan.

Izin penyimpanan

Model izin untuk mengakses file media di aplikasi Anda bergantung pada apakah aplikasi Anda menggunakan penyimpanan terbatas, tersedia di aplikasi yang menargetkan Android 10 atau yang lebih tinggi.

Penyimpanan terbatas diaktifkan

Jika aplikasi Anda menggunakan penyimpanan terbatas, seharusnya aplikasi Anda meminta izin terkait penyimpanan hanya untuk perangkat yang menjalankan Android 9 (API level 28) atau yang lebih rendah. Anda dapat memberlakukan kondisi ini dengan menambahkan atribut android:maxSdkVersion pada pernyataan izin di file manifes aplikasi Anda:

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

Tidak perlu meminta izin terkait penyimpanan untuk perangkat yang menjalankan Android 10 atau yang lebih tinggi. Aplikasi Anda dapat berkontribusi untuk koleksi media yang terdefinisi dengan baik, termasuk koleksi MediaStore.Downloads, tanpa meminta izin terkait penyimpanan. Misalnya, jika Anda mengembangkan aplikasi kamera, Anda tidak perlu meminta izin terkait penyimpanan karena aplikasi Anda memiliki image yang Anda tulis ke penyimpanan media.

Untuk mengakses file yang telah dibuat oleh aplikasi lain, setiap kondisi berikut ini harus terpenuhi:

Jika aplikasi Anda ingin mengakses file dalam koleksi MediaStore.Downloads yang tidak dibuat oleh aplikasi Anda, Anda harus menggunakan Storage Access Framework. Untuk mempelajari lebih lanjut petunjuk menggunakan framework ini, lihat panduan mengenai petunjuk mengakses dokumen dan file lainnya.

Penyimpanan terbatas tidak tersedia

Jika aplikasi digunakan pada perangkat yang menjalankan Android 9 atau yang lebih rendah, atau jika aplikasi telah memilih untuk sementara waktu tidak menggunakan penyimpanan terbatas, Anda harus meminta Izin READ_EXTERNAL_STORAGE untuk mengakses file media. Jika ingin mengubah file media, Anda juga harus meminta izin WRITE_EXTERNAL_STORAGE.

Izin akses lokasi media

Jika aplikasi menargetkan Android 10 (API level 29) atau yang lebih tinggi agar aplikasi dapat mengambil metadata Exif yang tidak tersunting dari foto, Anda harus mendeklarasikan ACCESS_MEDIA_LOCATION dalam manifes aplikasi, lalu meminta izin ini saat runtime.

Memeriksa pembaruan penyimpanan media

Agar dapat mengakses file media lebih andal, terutama jika aplikasi meng-cache URI atau data dari penyimpanan media, periksa apakah versi penyimpanan media telah berubah atau belum jika dibandingkan dengan terakhir kali Anda menyinkronkan data media. Untuk menjalankan pemeriksaan pembaruan ini, panggil getVersion(). Versi yang ditampilkan adalah string unik yang berubah setiap kali substansi penyimpanan media berubah. Jika versi yang ditampilkan berbeda dengan versi yang terakhir disinkronkan, pindai ulang dan sinkronkan ulang cache media aplikasi.

Selesaikan pemeriksaan ini pada waktu startup proses aplikasi. Anda tidak perlu memeriksa versinya setiap kali membuat kueri penyimpanan media.

Jangan mengasumsikan detail penerapan terkait nomor versi.

Koleksi kueri media

Untuk menemukan media yang memenuhi sekumpulan kondisi tertentu, seperti durasi 5 menit atau lebih, gunakan pernyataan pilihan seperti SQL yang mirip dengan yang ditunjukkan pada cuplikan kode berikut ini:

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));
    }
}

Saat menjalankan kueri semacam itu di aplikasi Anda, perhatikan hal berikut ini:

  • Panggil metode query() di thread pekerja.
  • Simpan indeks kolom dalam cache sehingga tidak perlu memanggil getColumnIndexOrThrow() setiap kali Anda memproses baris dari hasil kueri.
  • Tambahkan ID ke URI konten, seperti yang ditunjukkan dalam cuplikan kode.
  • Perangkat yang menjalankan Android 10 dan yang lebih tinggi memerlukan nama kolom yang ditentukan di MediaStore API. Jika library dependen dalam aplikasi Anda memerlukan nama kolom yang tidak ditentukan dalam API, misalnya "MimeType", gunakan CursorWrapper untuk menerjemahkan nama kolom secara dinamis dalam proses aplikasi Anda.

Memuat thumbnail file

Jika aplikasi Anda menampilkan beberapa file media dan meminta pengguna memilih salah satu file ini, akan lebih efisien untuk memuat versi pratinjau—atau thumbnail—bukan file itu sendiri.

Untuk memuat thumbnail file media tertentu, gunakan loadThumbnail() dan teruskan ukuran thumbnail yang ingin Anda muat, seperti yang ditunjukkan dalam cuplikan kode berikut ini:

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);

Membuka file media

Logika khusus yang Anda gunakan untuk membuka file media bergantung pada apakah konten media sebaiknya ditunjukkan sebagai deskriptor file, aliran file, atau jalur file langsung:

Deskriptor file

Untuk membuka file media menggunakan deskriptor file, gunakan logika yang mirip dengan yang ditunjukkan dalam cuplikan kode berikut ini:

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();
}

Aliran file

Untuk membuka file media menggunakan aliran file, gunakan logika yang mirip dengan yang ditunjukkan dalam cuplikan kode berikut ini:

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".
}

Jalur file langsung

Untuk membantu aplikasi berfungsi lebih lancar dengan library media pihak ketiga, Android 11 (API level 30) dan yang lebih tinggi memungkinkan Anda menggunakan API selain MediaStore API untuk mengakses file media dari penyimpanan bersama. Sebagai gantinya, Anda dapat langsung mengakses file media menggunakan salah satu API berikut:

  • File API.
  • Library native, seperti fopen().

Jika tidak memiliki izin terkait penyimpanan, Anda dapat mengakses file di direktori khusus aplikasi, serta file media yang terkait dengan aplikasi, menggunakan File API.

Jika aplikasi Anda mencoba mengakses file menggunakan File API, dan tidak memiliki izin yang diperlukan, FileNotFoundException akan terjadi.

Untuk mengakses file lain dalam penyimpanan bersama di perangkat yang menjalankan Android 10 (API level 29), sebaiknya nonaktifkan penyimpanan terbatas untuk sementara dengan menetapkan requestLegacyExternalStorage ke true dalam file manifes aplikasi Anda. Untuk mengakses file media menggunakan metode file native di Android 10, Anda juga harus meminta izin READ_EXTERNAL_STORAGE.

Pertimbangan saat mengakses konten media

Saat mengakses konten media, perhatikan pertimbangan yang dibahas di bagian berikut ini.

Data dalam cache

Jika aplikasi Anda menyimpan URI atau data ke dalam cache dari penyimpanan media, periksa pembaruan penyimpanan media secara berkala. Pemeriksaan ini memungkinkan data sisi aplikasi dan cache tetap tersinkron dengan data penyedia sisi sistem.

Performa

Saat Anda melakukan operasi baca berurutan pada file media menggunakan jalur file langsung, performanya dapat dibandingkan dengan MediaStore API.

Namun, saat Anda melakukan pembacaan dan penulisan file media secara acak menggunakan jalur file langsung, prosesnya dapat berlangsung hingga dua kali lebih lambat. Dalam situasi ini, sebaiknya gunakan MediaStore API.

Kolom DATA

Saat mengakses file media yang ada, Anda dapat menggunakan nilai kolom DATA dalam logika Anda. Itu karena nilai ini memiliki jalur file yang valid. Namun, jangan berasumsi bahwa file tersebut selalu tersedia. Bersiaplah untuk menangani setiap error I/O berbasis file yang dapat terjadi.

Di sisi lain, untuk membuat atau memperbarui file media, jangan gunakan nilai kolom DATA. Sebaliknya, gunakan nilai DISPLAY_NAME dan kolom RELATIVE_PATH.

Volume penyimpanan

Aplikasi yang menargetkan Android 10 atau yang lebih tinggi dapat mengakses nama unik yang ditetapkan oleh sistem untuk setiap volume penyimpanan eksternal. Sistem penamaan ini membantu Anda mengatur dan mengindeks konten secara efisien, dan memberi Anda kontrol terhadap lokasi file media baru yang disimpan.

Volume berikut sangat berguna untuk diingat:

  • Volume VOLUME_EXTERNAL memberikan tampilan semua volume penyimpanan bersama di perangkat. Anda dapat membaca konten volume sintetis ini, tetapi tidak dapat mengubah kontennya.
  • Volume VOLUME_EXTERNAL_PRIMARY menunjukkan volume penyimpanan bersama utama pada perangkat. Anda dapat membaca dan mengubah konten volume ini.

Anda dapat menemukan volume lain dengan memanggil 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();

Lokasi tempat media diambil

Beberapa foto dan video berisi informasi lokasi dalam metadatanya, yang menunjukkan lokasi pengambilan foto atau perekaman video.

Guna mengakses informasi lokasi ini di aplikasi Anda, gunakan satu API untuk informasi lokasi foto dan API lain untuk informasi lokasi video.

Foto

Jika aplikasi Anda menggunakan penyimpanan terbatas, sistem akan menyembunyikan informasi lokasi secara default. Untuk mengakses informasi ini, selesaikan langkah-langkah berikut:

  1. Minta izin ACCESS_MEDIA_LOCATION dalam manifes aplikasi Anda.
  2. Dari objek MediaStore Anda, dapatkan byte foto yang tepat dengan memanggil setRequireOriginal() dan teruskan URI foto, seperti yang ditunjukkan dalam cuplikan kode berikut:

    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];
    }
    

Video

Untuk mengakses informasi lokasi dalam metadata video, gunakan class MediaMetadataRetriever, seperti ditunjukkan dalam cuplikan kode berikut. Aplikasi tidak perlu meminta izin tambahan untuk menggunakan class ini.

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 should use 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 should use a standardized format.
    String locationMetadata = retriever.extractMetadata(
            MediaMetadataRetriever.METADATA_KEY_LOCATION);
}

Berbagi

Beberapa aplikasi memungkinkan pengguna saling berbagi file media. Misalnya, aplikasi media sosial memberi pengguna kemampuan untuk membagikan foto dan video kepada teman.

Untuk berbagi file media, gunakan URI content://, seperti yang disarankan dalam panduan untuk membuat penyedia konten.

Atribusi aplikasi dari file media

Saat penyimpanan terbatas diaktifkan untuk aplikasi yang menargetkan Android 10 atau yang lebih tinggi, sistem menghubungkan aplikasi ke setiap file media, yang menentukan file yang dapat diakses oleh aplikasi Anda saat aplikasi Anda belum meminta izin penyimpanan apa pun. Setiap file hanya dapat dihubungkan dengan satu aplikasi. Oleh karena itu, jika aplikasi Anda membuat file media yang disimpan dalam koleksi media file foto, video, atau audio, aplikasi Anda memiliki akses ke file tersebut.

Namun, jika pengguna meng-uninstal dan menginstal ulang aplikasi Anda, Anda harus meminta READ_EXTERNAL_STORAGE untuk mengakses file yang awalnya dibuat oleh aplikasi Anda. Permintaan izin ini diperlukan karena sistem menganggap file tersebut terkait dengan versi aplikasi yang diinstal sebelumnya, bukan versi yang baru diinstal.

Menambahkan item

Untuk menambahkan item media ke koleksi yang ada, panggil kode yang mirip berikut ini. Cuplikan kode ini mengakses volume VOLUME_EXTERNAL_PRIMARY di perangkat yang menjalankan Android 10 atau yang lebih tinggi. Ini karena, pada perangkat ini, Anda hanya dapat mengubah konten volume jika itu adalah volume utama, seperti yang dijelaskan di bagian volume penyimpanan.

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")
}

// Keeps a handle to the new song's URI in case we 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");

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

Beralih status tertunda untuk file media

Jika aplikasi Anda menjalankan operasi yang perlu waktu lebih banyak, seperti menulis ke file media, ada gunanya untuk memiliki akses eksklusif ke file saat sedang diproses. Pada perangkat yang menjalankan Android 10 atau yang lebih tinggi, aplikasi Anda bisa mendapatkan akses eksklusif ini dengan menyetel nilai flag IS_PENDING ke 1. Hanya aplikasi Anda yang dapat menampilkan file sampai aplikasi Anda mengubah nilai IS_PENDING kembali ke 0.

Cuplikan kode berikut ini dibuat berdasarkan cuplikan kode sebelumnya. Cuplikan berikut ini menunjukkan cara menggunakan flag IS_PENDING saat menyimpan lagu panjang di dalam direktori yang sesuai dengan koleksi MediaStore.Audio:

Kotlin

// Add a media item that other apps shouldn'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)

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

// Now that we're finished, release the "pending" status, and allow other apps
// to 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 shouldn'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);

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

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

Memberikan petunjuk untuk lokasi file

Saat aplikasi Anda menyimpan media di perangkat yang menjalankan Android 10, media akan diatur berdasarkan jenisnya secara default. Misalnya, file gambar baru ditempatkan secara default di dalam direktori Environment.DIRECTORY_PICTURES, yang sesuai dengan koleksi MediaStore.Images.

Jika aplikasi Anda mengetahui lokasi spesifik tempat file seharusnya disimpan, seperti album bernama Pictures/MyVacationPictures, Anda dapat menyetel MediaColumns.RELATIVE_PATH untuk memberikan petunjuk kepada sistem tempat penyimpanan file yang baru ditulis.

Memperbarui item

Untuk memperbarui file media yang dimiliki aplikasi Anda, jalankan kode yang mirip dengan hal berikut ini:

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 we 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 we 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);

Jika penyimpanan terbatas tidak tersedia atau tidak diaktifkan, proses yang ditunjukkan dalam kode cuplikan sebelumnya juga berfungsi untuk file yang tidak dimiliki oleh aplikasi Anda.

Memperbarui kode native

Jika Anda harus menulis file media menggunakan library native, teruskan deskriptor file yang terkait dengan file tersebut dari kode berbasis Java atau Kotlin ke kode native.

Cuplikan kode berikut ini menunjukkan cara meneruskan deskriptor file objek media ke kode native aplikasi:

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

Memperbarui file media aplikasi lain

Jika aplikasi Anda menggunakan penyimpanan terbatas, biasanya aplikasi Anda tidak dapat memperbarui file media yang dikontribusikan ke penyimpanan media oleh aplikasi yang berbeda.

Anda masih bisa mendapatkan izin pengguna untuk mengubah file, tetapi, dengan menangkap RecoverableSecurityException yang dimunculkan oleh platform. Selanjutnya, Anda dapat meminta agar pengguna memberi aplikasi Anda akses tulis ke item spesifik, seperti yang ditunjukkan dalam cuplikan kode berikut ini:

Kotlin

// Apply a grayscale filter to the image at the given content URI.
try {
    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 {
    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);
    }
}

Selesaikan proses ini setiap kali aplikasi Anda perlu mengubah file media yang tidak dibuatnya.

Atau, jika aplikasi Anda berjalan di Android 11 atau yang lebih tinggi, Anda dapat mengizinkan pengguna memberikan akses tulis ke grup file media kepada aplikasi Anda. Panggil metode createWriteRequest(), seperti yang dijelaskan di bagian cara mengelola grup file media.

Jika aplikasi Anda memiliki kasus penggunaan lain yang tidak tercakup oleh penyimpanan terbatas, ajukan permintaan fitur dan memilih untuk sementara waktu tidak menggunakan penyimpanan.

Menghapus item

Untuk menghapus item yang tidak lagi diperlukan aplikasi Anda dalam penyimpanan media, gunakan logika yang mirip dengan yang ditunjukkan dalam cuplikan kode berikut:

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);

Jika penyimpanan terbatas tidak tersedia atau tidak diaktifkan, Anda dapat menggunakan cuplikan kode sebelumnya untuk menghapus file yang dimiliki oleh aplikasi lain. Namun, jika penyimpanan terbatas diaktifkan, Anda perlu menangkap RecoverableSecurityException untuk setiap file yang ingin dihapus oleh aplikasi Anda, seperti yang dideskripsikan di bagian mengenai memperbarui item media.

Jika aplikasi berjalan di Android 11 atau yang lebih tinggi, Anda dapat mengizinkan pengguna untuk memilih grup file media yang akan dihapus. Panggil metode createTrashRequest() atau metode createDeleteRequest(), seperti yang dijelaskan di bagian cara mengelola grup file media.

Jika aplikasi Anda memiliki kasus penggunaan lain yang tidak tercakup oleh penyimpanan terbatas, ajukan permintaan fitur dan memilih untuk sementara waktu tidak menggunakan penyimpanan.

Mendeteksi pembaruan pada file media

Aplikasi Anda mungkin perlu mengidentifikasi volume penyimpanan yang berisi file media yang telah ditambah atau diubah oleh aplikasi, dibandingkan dengan waktu sebelumnya. Untuk mendeteksi perubahan ini dengan sangat cermat, teruskan volume penyimpanan yang diinginkan ke getGeneration(). Selama versi penyimpanan media tidak berubah, nilai hasil dari metode ini akan meningkat secara monoton dari waktu ke waktu.

Terlebih lagi getGeneration() lebih kuat daripada tanggal di kolom media, seperti DATE_ADDED dan DATE_MODIFIED. Hal ini dikarenakan nilai kolom media tersebut dapat berubah saat aplikasi memanggil setLastModified() atau saat pengguna mengubah jam sistem.

Mengelola grup file media

Di Android 11 dan yang lebih tinggi, Anda dapat meminta pengguna memilih sekumpulan file media, lalu memperbarui file media tersebut dalam sekali operasi. Metode ini menawarkan konsistensi yang lebih baik di seluruh perangkat, dan juga memudahkan pengguna untuk mengelola koleksi medianya.

Metode yang menyediakan fungsi "kumpulan update" ini mencakup:

createWriteRequest()
Meminta pengguna memberi aplikasi Anda akses tulis ke grup file media tertentu.
createFavoriteRequest()
Meminta pengguna untuk menandai file media tertentu sebagai beberapa media “favorit” mereka di perangkat. Semua aplikasi yang memiliki akses baca ke file ini dapat mengetahui bahwa pengguna telah menandai file sebagai “favorit”.
createTrashRequest()

Meminta pengguna untuk memindahkan file media tertentu ke tempat sampah perangkat. Item di tempat sampah akan dihapus secara permanen setelah jangka waktu yang ditentukan sistem.

createDeleteRequest()

Meminta pengguna untuk langsung menghapus file media tertentu secara permanen, tanpa memindahkannya ke tempat sampah terlebih dahulu.

Setelah memanggil salah satu metode ini, sistem akan mem-build objek PendingIntent. Setelah aplikasi Anda memanggil intent ini, pengguna akan melihat dialog yang meminta izin mereka agar aplikasi dapat memperbarui atau menghapus file media yang telah ditentukan.

Sebagai contoh, berikut cara membuat struktur panggilan ke 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);

Evaluasi respons pengguna. Jika pengguna memberikan izin, lanjutkan dengan operasi media. Jika tidak, jelaskan kepada pengguna alasan aplikasi Anda memerlukan izin:

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. */
        }
    }
}

Anda dapat menggunakan pola umum yang sama ini dengan createFavoriteRequest(), createTrashRequest(), dan createDeleteRequest().

Izin pengelolaan media

Pengguna mungkin akan memercayai aplikasi tertentu untuk menjalankan pengelolaan media, seperti sering mengedit file media. Jika aplikasi Anda menargetkan Android 11 atau yang lebih tinggi dan bukan aplikasi galeri default perangkat, Anda harus menampilkan dialog konfirmasi kepada pengguna setiap kali aplikasi mencoba untuk mengubah atau menghapus file.

Jika aplikasi menargetkan Android 12 (API level 31) atau yang lebih tinggi, Anda dapat meminta agar pengguna memberi aplikasi Anda dengan akses ke izin khusus Pengelolaan media. Dengan izin ini, aplikasi Anda dapat melakukan setiap hal berikut tanpa harus meminta izin kepada pengguna setiap kali melakukan operasi file:

Caranya, selesaikan langkah-langkah berikut:

  1. Nyatakan izin MANAGE_MEDIA baru dan izin READ_EXTERNAL_STORAGE di file manifes aplikasi Anda.

    Untuk memanggil createWriteRequest() tanpa menampilkan dialog konfirmasi, nyatakan juga izin ACCESS_MEDIA_LOCATION.

  2. Di aplikasi, tampilkan UI kepada pengguna untuk menjelaskan alasan mengapa pengguna ingin memberikan akses pengelolaan media ke aplikasi Anda.

  3. Panggil tindakan intent ACTION_REQUEST_MANAGE_MEDIA. Tindakan ini akan mengarahkan pengguna ke layar Aplikasi pengelolaan media di setelan sistem. Dari sini, pengguna dapat memberikan akses aplikasi khusus.

Menggunakan kasus yang memerlukan alternatif penyimpanan media

Jika sebagian besar aplikasi Anda menjalankan salah satu peran berikut ini, pertimbangkan alternatif untuk MediaStore API.

Bekerja dengan jenis file lain

Jika aplikasi Anda menangani dokumen dan file yang tidak berisi konten media secara eksklusif, seperti file yang menggunakan ekstensi file EPUB atau PDF, gunakan tindakan intent ACTION_OPEN_DOCUMENT, seperti yang dideskripsikan dalam panduan mengenai petunjuk menyimpan dan mengakses dokumen serta file lainnya.

Berbagi file di aplikasi pendamping

Apabila Anda menyediakan serangkaian aplikasi pendamping—seperti aplikasi pesan dan aplikasi profil—siapkan berbagi file menggunakan URI content://. Kami juga merekomendasikan alur kerja ini sebagai praktik keamanan terbaik.

Referensi lainnya

Untuk informasi lebih lanjut mengenai petunjuk menyimpan dan mengakses media, lihat referensi berikut ini.

Contoh

Video