Чтобы сделать работу с приложением удобнее, многие разработчики позволяют пользователям добавлять медиафайлы и получать доступ к ним на внешнем накопителе. Фреймворк предоставляет оптимизированный индекс для коллекций медиафайлов, называемый хранилищем медиафайлов, который позволяет пользователям извлекать и обновлять эти медиафайлы. Эти файлы остаются на устройстве пользователя даже после удаления приложения.
Выбор фотографий
В качестве альтернативы хранилищу мультимедиа можно использовать инструмент выбора фотографий 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. Однако мы рекомендуем использовать APIMediaStore, чтобы открывать файлы, которые не были созданы вашим приложением. - Если области хранения данных недоступны или не используются, в коллекции показываются все типы медиафайлов.
Запросите необходимые разрешения
Прежде чем выполнять операции с медиафайлами, убедитесь, что в приложении объявлены разрешения, необходимые для доступа к этим файлам. Однако не следует указывать разрешения, которые не требуются для работы приложения или не используются в нем.
Разрешения на доступ к хранилищу
Нужно ли вашему приложению разрешение на доступ к хранилищу, зависит от того, обращается ли оно только к собственным медиафайлам или к файлам, созданным другими приложениями.
Доступ к собственным медиафайлам
На устройствах с Android 10 и более поздними версиями ОС для доступа к медиафайлам, принадлежащим вашему приложению, и их изменения не требуются разрешения, связанные с хранилищем. Это касается и файлов в коллекции MediaStore.Downloads. Например, если вы разрабатываете приложение для камеры, вам не нужно запрашивать разрешения, связанные с хранилищем, чтобы получить доступ к сделанным фотографиям, поскольку ваше приложение владеет изображениями, которые вы записываете в хранилище мультимедиа.
Доступ к медиафайлам других приложений
Чтобы получить доступ к медиафайлам, созданным другими приложениями, необходимо объявить соответствующие разрешения, связанные с хранилищем, а файлы должны находиться в одной из следующих медиаколлекций:
Если файл можно просмотреть с помощью запросов MediaStore.Images, MediaStore.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.
Для доступа к скачанным файлам других приложений требуется Storage Access Framework
Если приложению требуется доступ к файлу в коллекции MediaStore.Downloads, который не был создан этим приложением, необходимо использовать Storage Access Framework. Подробнее о том, как использовать этот фреймворк, см. Доступ к документам и другим файлам из общего хранилища.
Доступ к данным о местоположении медиафайлов
Если ваше приложение предназначено для 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 контента, как показано в примере ниже.
- Для устройств с Android 10 и более поздними версиями ОС требуются названия столбцов, определенные в API
MediaStore. Если зависимая библиотека в вашем приложении ожидает название столбца, которое не определено в API, например"MimeType", используйте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:
FileAPI- Нативные библиотеки, например
fopen().
Если у вас нет разрешений, связанных с хранилищем, вы можете получить доступ к файлам в каталоге приложения, а также к медиафайлам, связанным с вашим приложением, с помощью API File.
Если приложение попытается получить доступ к файлу с помощью API File, не имея необходимых разрешений, возникнет ошибка FileNotFoundException.
Чтобы получить доступ к другим файлам в общем хранилище на устройстве с Android 10 (уровень API 29), рекомендуем временно отключить области хранения данных, задав для атрибута requestLegacyExternalStorage значение true в файле манифеста приложения. Чтобы получить доступ к медиафайлам с помощью нативных методов в Android 10, необходимо также запросить разрешение READ_EXTERNAL_STORAGE.
Что нужно учитывать при доступе к медиаконтенту
При доступе к медиаконтенту учитывайте рекомендации, приведенные в следующих разделах.
Данные кеша
Если ваше приложение кеширует URI или данные из хранилища мультимедиа, периодически проверяйте наличие обновлений в хранилище. Это позволяет синхронизировать кешированные данные на стороне приложения с данными поставщика на стороне системы.
Эффективность
При последовательном чтении медиафайлов с использованием прямых путей к ним производительность сопоставима с производительностью API MediaStore.
Однако при произвольном чтении и записи медиафайлов с использованием прямых путей к файлам процесс может быть вдвое медленнее. В таких случаях мы рекомендуем использовать MediaStore API.
Столбец DATA
При доступе к существующему медиафайлу вы можете использовать в своей логике значение столбца DATA. Это связано с тем, что у этого значения есть действительный путь к файлу. Однако не следует полагать, что файл доступен всегда. Будьте готовы к обработке любых ошибок ввода-вывода, связанных с файлами.
Чтобы создать или обновить медиафайл, не используйте значение столбца 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();
Место, где был снят медиафайл.
Некоторые фотографии и видео содержат в метаданных информацию о месте съемки.
Способ доступа к этим данным о местоположении в вашем приложении зависит от того, нужно ли вам получить доступ к данным о местоположении для фотографии или для видео.
Фотографии.
Если в приложении используются области хранения данных, система по умолчанию скрывает геоданные. Чтобы получить доступ к этой информации, выполните следующие действия:
- Запросите разрешение
ACCESS_MEDIA_LOCATIONв манифесте приложения. Из объекта
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); }
Совместный доступ
Некоторые приложения позволяют пользователям обмениваться медиафайлами. Например, приложения социальных сетей позволяют пользователям делиться фотографиями и видео с друзьями.
Чтобы поделиться медиафайлами, используйте URI content://, как рекомендуется в руководстве по созданию поставщика контента.
Атрибуция приложений для медиафайлов
Если для приложения, предназначенного для Android 10 или более поздней версии, включены области хранения данных, система связывает приложение с каждым медиафайлом. Это определяет, к каким файлам приложение может получить доступ, не запрашивая разрешения на доступ к хранилищу. Каждый файл может быть связан только с одним приложением. Поэтому, если ваше приложение создает медиафайл, который хранится в коллекции фотографий, видео или аудиофайлов, у него есть доступ к этому файлу.
Однако если пользователь удалит и заново установит приложение, вам потребуется запросить разрешение READ_EXTERNAL_STORAGE, чтобы получить доступ к файлам, которые были созданы в приложении ранее. Запрос разрешения необходим, поскольку система считает, что файл относится к ранее установленной версии приложения, а не к новой.
Когда приложение, предназначенное для SDK 36 или более поздней версии, запрашивает разрешения на доступ к фото и видео на устройствах с Android 16 или более поздней версии, пользователи, которые решат ограничить доступ к выбранным медиафайлам, увидят все фотографии, принадлежащие приложению, предварительно выбранными в инструменте выбора фотографий. Пользователи могут отменить выбор любого из этих предварительно выбранных элементов, что отзовет доступ приложения к этим фотографиям и видео.
Добавить элемент
Чтобы добавить мультимедийный объект в существующую коллекцию, используйте код, похожий на следующий. Этот фрагмент кода обращается к хранилищу VOLUME_EXTERNAL_PRIMARY на устройствах с Android 10 или более поздней версией. Это связано с тем, что на таких устройствах можно изменять только содержимое основного хранилища, как описано в разделе Хранилища.
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, файл будет доступен только вашему приложению.
Следующий фрагмент кода основан на предыдущем. В этом фрагменте кода показано, как использовать флаг IS_PENDING при сохранении длинной песни в каталоге, соответствующем коллекции 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);
Показать подсказку о местоположении файла
Если приложение сохраняет медиаконтент на устройстве с 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().
Пока версия хранилища мультимедиа не меняется, возвращаемое значение этого метода монотонно увеличивается со временем.
В частности, getGeneration() надежнее, чем даты в столбцах медиа, таких как DATE_ADDED и DATE_MODIFIED.
Это связано с тем, что значения в столбцах медиафайлов могут меняться, когда приложение вызывает setLastModified() или когда пользователь меняет системное время.
Управление группами медиафайлов
В Android 11 и более поздних версиях вы можете попросить пользователя выбрать группу медиафайлов, а затем обновить их за одну операцию. Эти методы обеспечивают лучшую согласованность между устройствами и упрощают пользователям управление своими медиаколлекциями.
К методам, обеспечивающим пакетное обновление, относятся следующие:
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) или более поздней версии, вы можете запросить у пользователей специальное разрешение на управление медиафайлами. Это разрешение позволяет приложению выполнять следующие действия без запроса на каждую операцию с файлом:
- Изменять файлы с помощью
createWriteRequest(). - Перемещайте файлы в корзину и из нее с помощью клавиши
createTrashRequest(). - Удалите файлы с помощью
createDeleteRequest().
Для этого выполните следующие действия:
Объявите разрешения
MANAGE_MEDIAиREAD_EXTERNAL_STORAGEв файле манифеста приложения.Чтобы вызывать
createWriteRequest()без показа диалогового окна подтверждения, также объявите разрешениеACCESS_MEDIA_LOCATION.Покажите в приложении интерфейс, объясняющий, почему пользователю может потребоваться предоставить доступ к управлению медиафайлами.
Вызовите действие intent
ACTION_REQUEST_MANAGE_MEDIA. Пользователь перейдет на экран Приложения для управления медиаконтентом в системных настройках. Здесь пользователи могут предоставить специальный доступ к приложению.
Случаи, когда требуется альтернатива хранилищу мультимедиа
Если ваше приложение в основном выполняет одну из следующих ролей, рассмотрите альтернативу API MediaStore.
Работа с файлами других типов
Если ваше приложение работает с документами и файлами, которые содержат не только медиаконтент, например с файлами, использующими расширение имени файла EPUB или PDF, используйте действие интента ACTION_OPEN_DOCUMENT, как описано в руководстве по хранению документов и других файлов и доступу к ним.
Обмен файлами в сопутствующих приложениях
Если вы предлагаете набор сопутствующих приложений, например мессенджер и приложение для профиля, настройте обмен файлами с помощью URI content://. Мы также рекомендуем этот рабочий процесс в качестве рекомендации по безопасности.
Дополнительные ресурсы
Дополнительную информацию о хранении медиафайлов и доступе к ним можно найти в следующих ресурсах:
Образцы
- MediaStore (доступно на GitHub)