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 dalam koleksi media, yang disebut penyimpanan media dan memungkinkan pengguna mengambil dan memperbarui file media ini dengan lebih mudah. Bahkan setelah aplikasi Anda di-uninstal, file ini tetap berada di perangkat pengguna.
Pemilih foto
Sebagai alternatif menggunakan penyimpanan media, alat pemilih foto Android menyediakan cara bawaan yang aman bagi pengguna untuk memilih file media, tanpa perlu memberi aplikasi Anda akses ke seluruh koleksi media mereka. Fitur ini hanya tersedia di perangkat yang didukung. Untuk informasi selengkapnya, lihat panduan pemilih foto.
Penyimpanan media
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:
- Gambar, termasuk foto dan screenshot, yang disimpan di direktori
DCIM/
danPictures/
. Sistem akan menambahkan file ini ke tabelMediaStore.Images
. - Video, yang disimpan di direktori
DCIM/
,Movies/
, danPictures/
. Sistem akan menambahkan file ini ke tabelMediaStore.Video
. - File audio, yang disimpan di direktori
Alarms/
,Audiobooks/
,Music/
,Notifications/
,Podcasts/
, danRingtones/
. Selain itu, sistem akan mengenali playlist audio yang ada dalam direktoriMusic/
atauMovies/
, serta rekaman suara yang ada di direktoriRecordings/
. Sistem akan menambahkan file ini ke tabelMediaStore.Audio
. DirektoriRecordings/
tidak tersedia di Android 11 (level API 30) dan yang lebih rendah. - File yang didownload, yang disimpan di dalam direktori
Download/
. Pada perangkat yang menjalankan Android 10 (level API 29) dan yang lebih tinggi, file ini disimpan di tabelMediaStore.Downloads
. Tabel ini tidak tersedia di Android 9 (level API 28) dan versi yang 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 izinREAD_EXTERNAL_STORAGE
. Namun, sebaiknya Anda menggunakanMediaStore
API untuk membuka file yang belum dibuat oleh aplikasi Anda. - 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, berhati-hatilah, jangan mendeklarasikan izin yang tidak diperlukan atau digunakan aplikasi Anda.
Izin penyimpanan
Apakah aplikasi Anda memerlukan izin untuk mengakses penyimpanan bergantung pada apakah aplikasi tersebut hanya mengakses file medianya sendiri atau file yang dibuat oleh aplikasi lain.
Mengakses file media Anda sendiri
Pada perangkat yang menjalankan Android 10 atau yang lebih baru, Anda tidak memerlukan
izin terkait penyimpanan untuk mengakses dan mengubah file media yang
dimiliki aplikasi Anda, termasuk file dalam koleksi
MediaStore.Downloads
. Misalnya, jika mengembangkan aplikasi kamera, Anda tidak perlu
meminta izin terkait penyimpanan untuk mengakses foto yang diperlukan, karena aplikasi
Anda memiliki gambar yang Anda tulis ke penyimpanan media.
Mengakses file media aplikasi lain
Untuk mengakses file media yang dibuat oleh aplikasi lain, Anda harus mendeklarasikan izin terkait penyimpanan yang sesuai, dan file harus berada di salah satu koleksi media berikut:
Selama dapat dilihat dari kueri MediaStore.Images
,
MediaStore.Video
, atau MediaStore.Audio
, file juga dapat dilihat menggunakan
kueri MediaStore.Files
.
Cuplikan kode berikut menunjukkan cara mendeklarasikan izin penyimpanan yang sesuai:
<!-- 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" />
Diperlukan izin tambahan untuk aplikasi yang berjalan di perangkat lama
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
.
Diperlukan Storage Access Framework untuk mengakses hasil download aplikasi lain
Jika aplikasi Anda ingin mengakses file dalam koleksi MediaStore.Downloads
yang tidak dibuat oleh aplikasi Anda, Anda harus menggunakan Storage Access Framework. Untuk mempelajari
cara menggunakan framework ini lebih lanjut, lihat Mengakses dokumen dan file lainnya dari
penyimpanan bersama.
Izin akses lokasi media
Jika aplikasi menargetkan Android 10 (level API 29) atau yang lebih tinggi dan harus
mengambil metadata EXIF yang tidak tersunting dari foto, Anda harus mendeklarasikan
izin
ACCESS_MEDIA_LOCATION
di manifes aplikasi, lalu minta izin ini saat runtime.
Memeriksa pembaruan penyimpanan media
Agar dapat mengakses file media dengan lebih andal, terutama jika aplikasi meng-cache URI atau
data dari penyimpanan media, periksa apakah versi penyimpanan media telah berubah 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.
Membuat kueri koleksi 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 contoh ini.
- 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"
, gunakanCursorWrapper
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—file, 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 (level API 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 (level
API 29), sebaiknya pilih untuk sementara waktu tidak menggunakan penyimpanan
terbatas 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.
Cara Anda mengakses informasi lokasi ini di aplikasi Anda bergantung pada apakah Anda perlu mengakses informasi lokasi untuk foto atau video.
Foto
Jika aplikasi Anda menggunakan penyimpanan terbatas, sistem akan menyembunyikan informasi lokasi secara default. Untuk mengakses informasi ini, selesaikan langkah-langkah berikut:
- Minta izin
ACCESS_MEDIA_LOCATION
dalam manifes aplikasi Anda. Dari objek
MediaStore
Anda, dapatkan byte foto yang tepat dengan memanggilsetRequireOriginal()
dan meneruskan 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 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); }
Berbagi
Beberapa aplikasi memungkinkan pengguna saling berbagi file media. Misalnya, aplikasi media sosial memungkinkan pengguna berbagi foto dan video dengan 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, gunakan 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") } // 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);
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
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 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);
Memberikan petunjuk untuk lokasi file
Saat aplikasi Anda menyimpan media di perangkat yang menjalankan Android 10, media tersebut
diatur berdasarkan jenisnya secara default. Misalnya, secara default file gambar
baru ditempatkan di
direktori
Environment.DIRECTORY_PICTURES
, yang sesuai dengan
koleksi MediaStore.Images
.
Jika aplikasi Anda mengetahui lokasi spesifik tempat file dapat disimpan, seperti
album bernama Pictures/MyVacationPictures
, Anda dapat menyetel
MediaColumns.RELATIVE_PATH
untuk memberikan petunjuk kepada sistem tempat menyimpan file yang baru ditulis.
Memperbarui item
Untuk memperbarui file media yang dimiliki aplikasi Anda, gunakan 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 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);
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.
Namun, Anda dapat memperoleh izin pengguna untuk mengubah file, dengan menangkap
RecoverableSecurityException
yang ditampilkan 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 { // "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); } }
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. Gunakan 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 pilih untuk sementara waktu tidak menggunakan penyimpanan terbatas.
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 tentang 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. Gunakan 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 pilih untuk sementara waktu tidak menggunakan penyimpanan terbatas.
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 (level API 31) atau yang lebih tinggi, Anda dapat meminta agar pengguna memberi aplikasi Anda 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:
- Ubah file menggunakan
createWriteRequest()
. - Pindahkan file ke dan dari sampah menggunakan
createTrashRequest()
. - Hapus file menggunakan
createDeleteRequest()
.
Caranya, selesaikan langkah-langkah berikut:
Nyatakan izin
MANAGE_MEDIA
baru dan izinREAD_EXTERNAL_STORAGE
di file manifes aplikasi Anda.Untuk memanggil
createWriteRequest()
tanpa menampilkan dialog konfirmasi, nyatakan juga izinACCESS_MEDIA_LOCATION
.Di aplikasi, tampilkan UI kepada pengguna untuk menjelaskan alasan mengapa pengguna ingin memberikan akses pengelolaan media ke aplikasi Anda.
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.
Kasus penggunaan 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 untuk menyimpan
dan mengakses dokumen serta file
lainnya.
Berbagi file di aplikasi pendamping
Jika Anda menyediakan serangkaian aplikasi pendamping, seperti aplikasi pesan dan
aplikasi profil, siapkan fitur 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
- MediaStore tersedia di GitHub.