Работа с MediaStore API и StorageAccessFramework

О, вы уже прочитали часть 1 этого поста ?! Отлично, тогда продолжим. Если у вас нет, я настоятельно рекомендую прочитать его, прежде чем продолжить. :)

Работа с медиа-контентом

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

Медиа для конкретных приложений

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

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

fun getPhotoDirFromAppStorage(dirName: String): File? {
    val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), dirName)
    if (!file.mkdirs()) {
      Log.e("TEST", "Directory not created")
    }
    return file
  }

При доступе к этим каталогам необходимо использовать константы, предоставляемые API, например DIRECTORY_PICTURES в приведенном выше примере. Если вы не можете найти имя каталога, определенное в API, которое соответствует вашим потребностям, вы можете передать null в getExternalFilesDir(). Передача null возвращает корневой каталог приложения во внешнем хранилище.

Общее хранилище

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

Android предоставляет два API для хранения общих данных и доступа к ним. MediaStore API рекомендуется использовать при работе с мультимедийными файлами (изображениями, аудио, видео). Если, с другой стороны, вам нужно работать с документами и другими файлами, вам следует использовать Storage Access Framework платформы.

MediaStore API

Что такое MediaStore? Согласно документации, MediaStore - это оптимизированный индекс для коллекций мультимедиа, который позволяет легче извлекать и обновлять файлы мультимедиа. Взаимодействие с медиа-магазином осуществляется через объект ContentResolver. Вы можете получить его экземпляр из контекста вашего приложения.

MediaStore работает, определяя коллекции для каждого типа мультимедиа. Система автоматически сканирует объем хранилища и добавляет файлы мультимедиа в соответствующую коллекцию. Коллекции представлены в виде таблиц, к которым вы можете получить доступ, вызвав MediaStore.<media-type>.

Запрос коллекций

Например, для взаимодействия с таблицей изображений вы должны сделать что-то вроде следующего:

val projection = arrayOf(
        //you only want to retrieve _ID and DISPLAY_NAME columns
        MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DISPLAY_NAME)
    context.contentResolver.query(
        uri, projection, null, null, null, null)?.use { cursor ->
      //cache column indices
      val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
      val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
      
        //iterating over all of the found images 
      while (cursor.moveToNext()) {
        val imageId = cursor.getString(idColumn)
        val imageName = cursor.getString(nameColumn)
      }
    }

Примечание. Когда вы работаете с Cursorobjects, не забывайте их закрывать. Я использовал функцию Kotlin use(), чтобы автоматически закрыть ее после выполнения кода внутри блока. Кроме того, не забудьте вызвать метод query() в рабочем потоке.

Вы можете использовать тот же код, если хотите взаимодействовать с видео или аудио файлами. Все, что вам нужно сделать, это изменить MediaStore.Images на MediaStore.Video или MediaStore.Audio соответственно.

В магазине мультимедиа также есть коллекция под названием MediaStore.Files. То, что вы там найдете, зависит от вашей версии Android. Если быть точным, если ваше приложение использует хранилище с ограниченным объемом (доступно на Android 10 и более поздних версиях), в этой коллекции будут отображаться только фотографии, видео и аудиофайлы, созданные вашим приложением. . Когда ограниченное хранилище не используется, в коллекции отображаются все типы файлов мультимедиа.

В более новых версиях Android (только Android 10 и выше) MediaStore API также предоставляет вам MediaStore.Downloads таблицу, в которой вы можете получить доступ к загруженным файлам.

Создание нового файла

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

fun createNewImageFile(): Uri? {
    val resolver = context.contentResolver
    // On API <= 28, use VOLUME_EXTERNAL instead.
    val imageCollection =    MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    val newImageDetails = ContentValues().apply {
      put(MediaStore.Images.Media.DISPLAY_NAME, "New image.jpg")
    }
    //return Uri of newly created file
    return resolver.insert(imageCollection, newImageDetails)
  }

Сначала вы получаете экземпляр класса ContentResolver. После этого вы получите Uri коллекции, в которой вы хотите сохранить новый файл. Чтобы поместить новый файл в коллекцию, вы создаете объект ContentValues и вызываете для него метод put(), передавая пары ключ-значение в качестве аргументов. Последним шагом является вызов метода insert() для ранее полученного экземпляра resolver.
Метод insert() возвращает Uri созданного файла, который можно использовать для изменения файла после создания.

Удаление файла

Я уверен, что вы заметили закономерность в MediaStore API. Это очень похожая процедура с обновлением или удалением. Давайте посмотрим!

Чтобы удалить файл, вы должны использовать код, подобный этому:

fun deleteMediaFile(fileName: String) {
    val fileInfo = getFileInfoFromName(fileName) //this function returns Kotlin Pair<Long, Uri>
    val id = fileInfo.first
    val uri = fileInfo.second
    val selection = "${MediaStore.Images.Media._ID} = ?"
    val selectionArgs = arrayOf(id.toString())
    val resolver = context.contentResolver
    resolver.delete(uri, selection, selectionArgs)
  }

Во-первых, вам нужно получить id и uri файла, который вы хотите удалить. Это то, что делает функция getFileInfoFromName(). Подсказка, это моя вспомогательная функция, она не является частью официального API. Когда у вас есть необходимая информация, вы можете получить экземпляр ContentResolver, как и раньше, и вызвать его метод delete(). Этот метод принимает три аргумента:

  • Uri файла
  • Предложение WHERE без фактических аргументов, называется selection - укажите, какие строки будут удалены.
  • Массив аргументов для параметра selection - укажите id для элемента, который вы хотите удалить.

Обновление файла

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

Платформа доступа к хранилищу

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

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

Открытие файла

Открыть файл с помощью этого фреймворка довольно просто. Вот один пример, чтобы продемонстрировать это:

private fun openFile() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
      addCategory(Intent.CATEGORY_OPENABLE)
      type = "*/*"
    }
    startActivityForResult(intent, OPEN_DOCUMENT)
  }

Этот код позволяет пользователям выбирать любой файл из системного приложения для выбора файлов. Давайте разберем его построчно.
Сначала вы создаете намерение с помощью ACTION_OPEN_DOCUMENT действия. Затем вы устанавливаете тип MIME, который указывает, какие типы файлов поддерживает ваше приложение. В приведенном выше коде я использовал */*, что означает, что я хочу показать все файлы. Например, если вы хотите показывать только изображения, используйте images/*, а для файлов PDF application/pdf. Помимо типа, вы должны добавить категорию для файлов. В данном случае я выбрал Intent.CATEGORY_OPENABLE. Это покажет только файлы, которые можно открыть с помощью ContentResolver.openFileDescriptor() метода.
Когда вы подготовили свое намерение, вызовите startActivityForResult() и передайте намерение с уникальным кодом запроса.

Попробуйте запустить приложение и проверьте, что происходит. Вы должны увидеть экран, похожий на этот:

Создание нового файла

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

private fun createFile(name: String) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
      addCategory(Intent.CATEGORY_OPENABLE)
      type = "application/pdf"
      putExtra(Intent.EXTRA_TITLE, name)
    }
    startActivityForResult(intent, CREATE_DOCUMENT)
  }

Как и в случае с открытием, вам нужно добавить категорию.
В данном случае это Intent.CATEGORY_OPENABLE. Затем установите тип MIME для файла, который вы хотите создать. Если вы хотите добавить заголовок к файлу, вы можете сделать это с помощью Intent.EXTRA_TITLE intent extra. Здесь следует отметить, что это действие не может перезаписать существующий файл. Если вы укажете то же имя, система добавит номер в конце имени файла.

Попробуйте запустить код. Вы должны увидеть что-то вроде этого:

Предоставление доступа к каталогу

Если по какой-то причине вашему приложению требуется доступ к содержимому каталога, вы можете использовать действие ACTION_OPEN_DOCUMENT_TREE intent. Используя это, пользователь может предоставить вашему приложению доступ ко всему дереву каталогов. После этого ваше приложение сможет получить доступ к любому файлу в каталоге и его подкаталогах.
Стоит отметить, что ваше приложение не имеет доступа к файлам других приложений за пределами каталога, выбранного пользователем.

Получение результата обратно

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

Я объясню это более подробно позже, но прежде чем вносить какие-либо изменения в файл, вы должны проверить значение DocumentsContract.Document.COLUMN_FLAGS. Он указывает, какие операции с данным файлом поддерживает провайдер.

Вот код, который вы можете использовать для получения Uri:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == OPEN_DOCUMENT && resultCode == Activity.RESULT_OK) {
      data?.data?.let { uri ->
        //use this uri to make supported modifications to the file
      }
    }
  }

Под капотом

Storage Access Framework работает с поставщиками контента под капотом. Основные части фреймворка:

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

Каждое приложение, которое хочет внести свои файлы в пользовательский интерфейс средства выбора системы, должно реализовать своего собственного поставщика документов.
Теперь вернемся к утверждению из предыдущей главы, где я сказал, что вам нужно проверить DocumentsContract.Document.COLUMN_FLAGS, прежде чем пытаться сделать любые модификации файла. Когда приложения реализуют поставщика, они могут устанавливать различные возможности для каждого документа с помощью вышеупомянутых флагов. Например, провайдер может установить Document.FLAG_SUPPORTS_REMOVE, чтобы указать, что вы можете удалить этот документ.

Заключение

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

Начать работать с файловым хранилищем на Android может быть очень сложно. При любом управлении файлами следует учитывать несколько вещей. Я призываю вас взять эти примеры и написать их самостоятельно (а не просто копировать и вставлять их). Также запустите приложение и посмотрите, что происходит. Как только вы попробуете, станет намного понятнее, как это работает.

Надеюсь, этот пост был вам полезен! Если у вас есть какие-либо вопросы или предложения по улучшению, оставьте комментарий ниже.

Спасибо за чтение!

Лука Кордич - разработчик Android в компании COBE в Осиеке. В своей повседневной разработке он в основном использует Kotlin, но также возможен вариант с Java. Когда он не пишет приложения для Android, ему нравится узнавать новое из мира информатики. Ему очень нравится спорт. В свободное время он играет в футбол, но также любит бег, скалолазание и баскетбол. Когда он не работает с программным обеспечением или не занимается спортом, он любит играть в видеоигры.

Ранее опубликовано на cobeisfresh.com.

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