Доступ к медиафайлам из общего хранилища

Чтобы обеспечить более обогащенный пользовательский опыт, многие приложения позволяют пользователям вносить и получать доступ к медиа, которые доступны на внешнем томе хранения. Фреймворк предоставляет оптимизированный индекс в медиаколлекциях, называемый хранилищем медиа , который позволяет пользователям легче извлекать и обновлять эти медиафайлы. Даже после удаления вашего приложения эти файлы остаются на устройстве пользователя.

Фотовыборщик

В качестве альтернативы использованию медиа-магазина инструмент выбора фотографий Android предоставляет безопасный встроенный способ для пользователей выбирать медиа-файлы без необходимости предоставлять вашему приложению доступ ко всей их медиа-библиотеке. Это доступно только на поддерживаемых устройствах. Для получения дополнительной информации см. руководство по выбору фотографий .

Медиа-магазин

Для взаимодействия с абстракцией хранилища медиаданных используйте объект ContentResolver , который вы извлекаете из контекста вашего приложения:

Котлин

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

Ява

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 . Ее содержимое зависит от того, использует ли ваше приложение scoped storage , доступное в приложениях, ориентированных на Android 10 или выше.

  • Если включено хранилище scoped, коллекция показывает только фотографии, видео и аудиофайлы, созданные вашим приложением. Большинству разработчиков не нужно использовать MediaStore.Files для просмотра медиафайлов из других приложений, но если у вас есть особые требования, вы можете объявить разрешение READ_EXTERNAL_STORAGE . Однако мы рекомендуем вам использовать API MediaStore для открытия файлов , которые не были созданы вашим приложением.
  • Если выделенное хранилище недоступно или не используется, в коллекции отображаются все типы медиафайлов.

Запросить необходимые разрешения

Перед выполнением операций с медиафайлами убедитесь, что ваше приложение объявило разрешения, необходимые для доступа к этим файлам. Однако будьте осторожны, чтобы не объявлять разрешения, которые вашему приложению не нужны или которые оно не использует.

Разрешения на хранение

Необходимость разрешений для доступа к хранилищу зависит от того, обращается ли приложение только к собственным медиафайлам или к файлам, созданным другими приложениями.

Доступ к вашим собственным медиафайлам

На устройствах под управлением 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 или ниже или если ваше приложение временно отказалось от scoped storage , вам необходимо запросить разрешение 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-подобный оператор выбора, аналогичный показанному в следующем фрагменте кода:

Котлин

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

Ява

// 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() и передайте размер миниатюры, которую вы хотите загрузить, как показано в следующем фрагменте кода:

Котлин

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

Ява

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

Открыть медиа-файл

Конкретная логика, используемая для открытия медиафайла, зависит от того, представлен ли медиаконтент наилучшим образом в виде файлового дескриптора, файлового потока или прямого пути к файлу.

Дескриптор файла

Чтобы открыть медиа-файл с помощью файлового дескриптора, используйте логику, аналогичную показанной в следующем фрагменте кода:

Котлин

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

Ява

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

Поток файлов

Чтобы открыть медиафайл с помощью файлового потока, используйте логику, аналогичную показанной в следующем фрагменте кода:

Котлин

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

Ява

// 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) и выше позволяют использовать API, отличные от API MediaStore , для доступа к медиафайлам из общего хранилища. Вместо этого вы можете получить доступ к медиафайлам напрямую, используя любой из следующих API:

  • API File
  • Собственные библиотеки, такие как fopen()

Если у вас нет разрешений, связанных с хранилищем, вы можете получить доступ к файлам в каталоге вашего приложения, а также к медиафайлам, привязанным к вашему приложению, с помощью API File .

Если ваше приложение пытается получить доступ к файлу с помощью File API и у него нет необходимых разрешений, возникает исключение FileNotFoundException .

Чтобы получить доступ к другим файлам в общем хранилище на устройстве под управлением Android 10 (API уровня 29), мы рекомендуем вам временно отказаться от хранилища с областью действия, установив requestLegacyExternalStorage в значение true в файле манифеста вашего приложения. Чтобы получить доступ к файлам мультимедиа с помощью собственных методов файлов на Android 10, вы также должны запросить разрешение READ_EXTERNAL_STORAGE .

Соображения при доступе к медиаконтенту

При доступе к медиаконтенту помните о соображениях, обсуждаемых в следующих разделах.

Кэшированные данные

Если ваше приложение кэширует URI или данные из хранилища медиа, периодически проверяйте наличие обновлений в хранилище медиа . Эта проверка позволяет кэшированным данным на стороне приложения оставаться синхронизированными с данными поставщика на стороне системы.

Производительность

При последовательном чтении медиафайлов с использованием прямых путей к файлам производительность сопоставима с производительностью API MediaStore .

Однако при выполнении случайных чтений и записей медиафайлов с использованием прямых путей к файлам процесс может быть в два раза медленнее. В таких ситуациях мы рекомендуем использовать API MediaStore .

столбец ДАННЫЕ

При доступе к существующему медиафайлу вы можете использовать значение столбца DATA в своей логике. Это потому, что это значение имеет допустимый путь к файлу. Однако не думайте, что файл всегда доступен. Будьте готовы обрабатывать любые возникающие ошибки ввода-вывода на основе файлов.

С другой стороны, для создания или обновления медиафайла не используйте значение столбца DATA . Вместо этого используйте значения столбцов DISPLAY_NAME и RELATIVE_PATH .

Объемы хранения

Приложения, предназначенные для Android 10 или выше, могут получить доступ к уникальному имени, которое система назначает каждому внешнему тому хранилища. Эта система именования помогает вам эффективно организовывать и индексировать контент, а также дает вам контроль над тем, где хранятся новые медиафайлы.

Особенно полезно иметь в виду следующие тома:

  • Том VOLUME_EXTERNAL обеспечивает просмотр всех общих томов хранения на устройстве. Вы можете читать содержимое этого синтетического тома, но не можете изменять его.
  • Том VOLUME_EXTERNAL_PRIMARY представляет собой основной общий том хранилища на устройстве. Вы можете читать и изменять содержимое этого тома.

Вы можете обнаружить другие тома, вызвав MediaStore.getExternalVolumeNames() :

Котлин

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

Ява

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

Место, где был сделан захват медиа

Некоторые фотографии и видео содержат в своих метаданных информацию о местоположении, которая показывает место, где была сделана фотография или записано видео.

Способ доступа к этой информации о местоположении в вашем приложении зависит от того, нужна ли вам информация о местоположении для фотографии или видео.

Фотографии

Если ваше приложение использует scoped storage , система скрывает информацию о местоположении по умолчанию. Чтобы получить доступ к этой информации, выполните следующие действия:

  1. Запросите разрешение ACCESS_MEDIA_LOCATION в манифесте вашего приложения.
  2. Из объекта MediaStore получите точные байты фотографии, вызвав setRequireOriginal() и передав URI фотографии, как показано в следующем фрагменте кода:

    Котлин

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

    Ява

    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 , как показано в следующем фрагменте кода. Вашему приложению не нужно запрашивать никаких дополнительных разрешений для использования этого класса.

Котлин

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

Ява

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 или выше, включено хранилище scoped , система приписывает приложение каждому медиафайлу, что определяет файлы, к которым ваше приложение может получить доступ, если оно не запросило никаких разрешений на хранение. Каждый файл может быть приписан только одному приложению. Таким образом, если ваше приложение создает медиафайл, который хранится в медиаколлекции фотографий, видео или аудиофайлов, ваше приложение имеет доступ к этому файлу.

Однако если пользователь удаляет и переустанавливает ваше приложение, вы должны запросить READ_EXTERNAL_STORAGE для доступа к файлам, которые изначально создало ваше приложение. Этот запрос на разрешение необходим, поскольку система считает, что файл относится к ранее установленной версии приложения, а не к новой.

При запросе разрешений на фото и видео приложением, ориентированным на SDK 36 или выше на устройствах под управлением Android 16 или выше, пользователи, которые решили ограничить доступ к выбранным медиа, увидят любые фотографии, принадлежащие приложению, предварительно выбранные в средстве выбора фотографий. Пользователи могут отменить выбор любого из этих предварительно выбранных элементов, что отменит доступ приложения к этим фотографиям и видео.

Добавить элемент

Чтобы добавить элемент мультимедиа в существующую коллекцию, используйте код, аналогичный следующему. Этот фрагмент кода обращается к тому VOLUME_EXTERNAL_PRIMARY на устройствах под управлением Android 10 или выше. Это связано с тем, что на этих устройствах вы можете изменять содержимое тома, только если это основной том, как описано в разделе «Тома хранения» .

Котлин

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

Ява

// 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 :

Котлин

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

Ява

// 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 , чтобы предоставить системе подсказку о том, где следует хранить вновь записанные файлы.

Обновить элемент

Чтобы обновить медиафайл, принадлежащий вашему приложению, используйте код, аналогичный следующему:

Котлин

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

Ява

// 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 в собственный код.

В следующем фрагменте кода показано, как передать дескриптор файла медиа-объекта в собственный код вашего приложения:

Котлин

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.

Ява

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 , которое бросает платформа. Затем вы можете запросить у пользователя предоставить вашему приложению доступ на запись к этому конкретному элементу, как показано в следующем фрагменте кода:

Котлин

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

Ява

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() , как описано в разделе об управлении группами медиафайлов .

Если у вашего приложения есть другой вариант использования, который не охвачен ограниченным хранилищем, отправьте запрос на функцию и временно откажитесь от ограниченного хранилища .

Удалить элемент

Чтобы удалить элемент, который больше не нужен вашему приложению в медиамагазине, используйте логику, аналогичную той, что показана в следующем фрагменте кода:

Котлин

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

Ява

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

Если хранилище scoped недоступно или не включено, вы можете использовать предыдущий фрагмент кода для удаления файлов, которыми владеют другие приложения. Однако, если хранилище scoped включено, вам необходимо перехватить RecoverableSecurityException для каждого файла, который ваше приложение хочет удалить, как описано в разделе об обновлении элементов мультимедиа .

Если ваше приложение работает на Android 11 или выше, вы можете позволить пользователям выбирать группу медиафайлов для удаления. Используйте метод createTrashRequest() или метод createDeleteRequest() , как описано в разделе об управлении группами медиафайлов .

Если у вашего приложения есть другой вариант использования, который не охвачен ограниченным хранилищем, отправьте запрос на функцию и временно откажитесь от ограниченного хранилища .

Обнаружение обновлений медиафайлов

Вашему приложению может потребоваться определить объемы хранилища, содержащие медиафайлы, которые приложения добавили или изменили, по сравнению с предыдущим моментом времени. Чтобы наиболее надежно обнаружить эти изменения, передайте интересующий объем хранилища в getGeneration() . Пока версия хранилища медиа не меняется, возвращаемое значение этого метода монотонно увеличивается с течением времени.

В частности, getGeneration() более надежен, чем даты в столбцах медиа, таких как DATE_ADDED и DATE_MODIFIED . Это потому, что значения этих столбцов медиа могут меняться, когда приложение вызывает setLastModified() или когда пользователь изменяет системные часы.

Управление группами медиафайлов

На Android 11 и выше вы можете попросить пользователя выбрать группу медиафайлов, а затем обновить эти медиафайлы за одну операцию. Эти методы обеспечивают лучшую согласованность между устройствами, и эти методы упрощают для пользователей управление их медиаколлекциями.

Методы, обеспечивающие эту функциональность «пакетного обновления», включают в себя следующее:

createWriteRequest()
Попросите пользователя предоставить вашему приложению доступ на запись к указанной группе медиафайлов.
createFavoriteRequest()
Запросите, чтобы пользователь отметил указанные медиафайлы как некоторые из его «любимых» медиафайлов на устройстве. Любое приложение, имеющее доступ на чтение к этому файлу, может видеть, что пользователь отметил файл как «избранный».
createTrashRequest()

Запросить у пользователя поместить указанные медиафайлы в корзину устройства. Элементы в корзине удаляются навсегда по истечении определенного системой периода времени.

createDeleteRequest()

Попросите пользователя немедленно и навсегда удалить указанные медиафайлы, не помещая их предварительно в корзину.

После вызова любого из этих методов система создает объект PendingIntent . После того, как ваше приложение вызывает это намерение, пользователи видят диалоговое окно, запрашивающее их согласие на обновление или удаление указанных медиафайлов вашим приложением.

Например, вот как структурировать вызов createWriteRequest() :

Котлин

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)

Ява

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

Оцените ответ пользователя. Если пользователь дал согласие, продолжайте операцию с медиа. В противном случае объясните пользователю, почему вашему приложению необходимо разрешение:

Котлин

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

Ява

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

Этот же общий шаблон можно использовать с createFavoriteRequest() , createTrashRequest() и createDeleteRequest() .

Разрешение на управление медиа

Пользователи могут доверять определенному приложению управление медиа, например, частое редактирование медиафайлов. Если ваше приложение предназначено для Android 11 или более поздней версии и не является приложением галереи по умолчанию на устройстве, вы должны показывать пользователю диалоговое окно подтверждения каждый раз, когда ваше приложение пытается изменить или удалить файл.

Если ваше приложение ориентировано на Android 12 (уровень API 31) или выше, вы можете запросить у пользователей предоставление вашему приложению доступа к специальному разрешению управления мультимедиа . Это разрешение позволяет вашему приложению выполнять каждое из следующих действий без необходимости запрашивать у пользователя каждую файловую операцию:

Для этого выполните следующие шаги:

  1. Объявите разрешения MANAGE_MEDIA и READ_EXTERNAL_STORAGE в файле манифеста вашего приложения.

    Чтобы вызвать createWriteRequest() без отображения диалогового окна подтверждения, также объявите разрешение ACCESS_MEDIA_LOCATION .

  2. В своем приложении покажите пользователю пользовательский интерфейс, чтобы объяснить, почему ему может потребоваться предоставить доступ к управлению мультимедиа в вашем приложении.

  3. Вызовите действие намерения ACTION_REQUEST_MANAGE_MEDIA . Это перенаправит пользователей на экран приложений управления мультимедиа в системных настройках. Отсюда пользователи могут предоставить специальный доступ к приложению.

Варианты использования, требующие альтернативы хранилищу медиафайлов

Если ваше приложение в первую очередь выполняет одну из следующих ролей, рассмотрите альтернативу API MediaStore .

Работа с другими типами файлов

Если ваше приложение работает с документами и файлами, которые не содержат исключительно медиаконтент, например, с файлами, использующими расширение EPUB или PDF, используйте действие намерения ACTION_OPEN_DOCUMENT , как описано в руководстве по хранению и доступу к документам и другим файлам .

Обмен файлами в сопутствующих приложениях

В случаях, когда вы предоставляете набор сопутствующих приложений, таких как приложение для обмена сообщениями и приложение для профиля, настройте общий доступ к файлам с использованием URI content:// . Мы также рекомендуем этот рабочий процесс в качестве лучшей практики безопасности .

Дополнительные ресурсы

Дополнительную информацию о хранении и доступе к медиафайлам можно найти в следующих ресурсах.

Образцы

Видео