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

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

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

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

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

Если вашему приложению требуется доступ к файлу в коллекции 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:

  • File API
  • Собственные библиотеки, такие как 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 для доступа к файлам, которые изначально создали ваше приложение. Этот запрос разрешения необходим, поскольку система считает, что файл относится к ранее установленной версии приложения, а не к новой установленной версии.

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

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

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

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

Образцы

Видео