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

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

Выбор фотографий

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

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

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

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

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

Фотографии

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

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

Однако если пользователь удалит и переустановит ваше приложение, вам необходимо запросить разрешение 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);

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

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

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

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

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

В частности, getGeneration() более надёжен, чем даты в столбцах типа media, таких как DATE_ADDED и DATE_MODIFIED . Это связано с тем, что значения этих столбцов типа media могут меняться, когда приложение вызывает 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:// . Мы также рекомендуем этот рабочий процесс в качестве наилучшей практики обеспечения безопасности .

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

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

Образцы

Видео