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

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

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

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

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

Разрешение на размещение медиафайлов

Если ваше приложение ориентировано на 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)
    }
}

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

Котлин

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

Откройте медиафайл

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

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

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

Котлин

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

Файловый поток

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

Котлин

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

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

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

Если ваше приложение пытается получить доступ к файлу с помощью 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()

Java

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

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

Котлин

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 или выше. Это связано с тем, что на этих устройствах вы можете изменять содержимое тома только в том случае, если он является основным томом, как описано в разделе «Тома хранения» .

Котлин

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

Котлин

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

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

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

Котлин

// 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 в ваш нативный код.

Приведённый ниже фрагмент кода показывает, как передать дескриптор файла медиаобъекта в нативный код вашего приложения:

Котлин

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

Котлин

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

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

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

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

Котлин

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

Котлин

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

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

Котлин

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

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

  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:// . Мы также рекомендуем этот рабочий процесс как передовую практику в области безопасности .

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

Для получения дополнительной информации о том, как хранить и получать доступ к медиафайлам, обратитесь к следующим ресурсам.

Образцы

Видео