Работа с файлами в Android — одна из фундаментальных вещей, с которыми сталкиваются разработчики в большинстве приложений. Многие приложения используют взаимодействие с файловой системой в качестве своего основного функционала. Это может быть галерея изображений пользователя, читалка электронных книг, приложение для редактирования текстовых файлов и многое-многое другое. По этой причине важно понимать, в каком виде с ними работать и как.
В Android одним из основополагающих компонентов является URI (Uniform Resourse Identifier). URI по сути является строкой, которая представляет собой уникальный идентификатор ресурса в системе. У URI существует так называемая «схема», которая сообщает тип ресурса.
В большинстве случаев используется 4 схемы:
- URI ресурсов (android.resource://)
- URI файлов (file://)
- URI контента (content://)
- URI данных (data:[mime-type];base64,…)
В зависимости от того, какую схему имеет URI, различается формат идентификатора и способы работы с ним. Для работы с файловой системой основными являются file:// и content://.
Когда одно приложение взаимодействует с другим и отправляет ему данные внутри Intent, то оно передаёт не сам файл, а его идентификатор, которым и является URI.
В старых версиях Android работа с файлами не составляла труда: приложение объявляло в разрешениях, что у него есть полный доступ к файловой системе и получало URI со схемой file://. Однако со временем это сильно изменилось и вот почему.
Схема file:// по сути содержит в себе прямой физический путь к файлу в системе. Это означает, что любое приложение могло узнать и получить доступ к закрытым файлам другого приложения.
Со временем Google сделала (и продолжает делать) систему более безопасной. Теперь нельзя просто взять и получить доступ к файловой системе: пользователь должен собственноручно дать разрешение приложению. Также Storage Access Framework ограничил доступность файлов, разделив файлы на доступные приложению и недоступные. Кроме того, вместо file:// схемы теперь практически единственным способом передачи файлов стал content://.
Схема content:// пришла на замену file:// как более безопасный способ работы с файловой системой. URI файлов в настоящее время хоть и не помечен как deprecated, но его использование в Android сильно ограничено. Мы можем работать с этой схемой в рамках папки приложения, т.к. приложение имеет к своей папке полный доступ и нет смысла скрывать её содержимое. Попытки же передать такой URI наружу будут приводить к исключению File
URI контента генерирует специальный идентификатор с помощью провайдера, благодаря чему настоящий путь к файлу знает только система. Также для URI определяются права доступа, определяющие, какое приложение может изменять содержимое URI, а какое — нет. Это накладывает дополнительные ограничения: теперь не получится перезаписать файл, если на то не было дано специального разрешения.
В этой статье мы разберём, как можно в приложении получить URI из внешнего источника и работать с ним. Для примера возьмём наше приложение для редактирования текстовых файлов.
Сначала нам нужно сообщить системе, что наше приложение может принимать определённые Intent. Для этого в AndroidManifest.xml добавим к активности следующие интент-фильтры.
<activity
android:name=".activity.MainActivity"
...
>
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="*" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="text/*" />
</intent-filter>
</activity>
android.intent.action.VIEW, android.intent.action.EDIT и android.intent.action.SEND это стандартные интенты, используемые для передачи контента из одного приложения в другое. Таким образом мы указываем, что наше приложение может принимать Intent, содержащий текстовый файл, и будет предлагаться пользователю в списке приложений.
Далее нам нужно написать метод, который принимает входящий интент. Переопределим у активити метод onNewIntent(Intent) и добавим его туда.
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
val action = intent.action ?: return
val isNotViewIntent = action != Intent.ACTION_VIEW
&& action != Intent.ACTION_EDIT
&& action != Intent.ACTION_SEND
if (isNotViewIntent) return
val uri = when (intent.action) {
Intent.ACTION_SEND -> {
if (isSdkVanillaIceCream) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
}
}
else -> intent.data
}
if (uri == null) return
openFileFromExternal(uri)
intent.setAction(null)
}
Здесь мы проверяем, что Intent имеет нужный нам action и извлекаем из него URI. Важно отметить, что в случае с Intent.ACTION_SEND его нужно извлекать из бандла по ключу Intent.EXTRA_STREAM, в то время как в остальных случаях URI находится в поле data.
Изначально, при получении URI со схемой file:// прочитать файл можно было с помощью простого преобразования в File.
File(uri.toString())
Однако в случае со схемой content:// мы можем лишь получить InputStream с содержанием файла. Чтобы прочитать такой URI нам нужно передать в ContentResolver полученный URI, в результате чего он вернёт InputStream для работы.
private fun openFileFromExternal(uri: Uri) {
lifecycleScope.launch {
val text = withContext(Dispatchers.IO) {
uri.extractText(Charsets.UTF_8.name())
}
// Отображаем полученный текст на UI
...
}
}
fun Uri.extractText(encoding: String): String =
try {
contentResolver.openInputStream(this)?.use { inputStream ->
inputStream.extractText(encoding)
} ?: ""
} catch (e: SecurityException) {
if (isSdkQ) {
try {
contentResolver.openFileDescriptor(this, "r")?.use { parcelFileDescriptor ->
FileInputStream(parcelFileDescriptor.fileDescriptor).use { inputStream ->
inputStream.extractText(encoding)
}
} ?: ""
} catch (e: Exception) {
""
}
} else {
""
}
}
private fun InputStream.extractText(encoding: String): String =
InputStreamReader(this, encoding).use { reader ->
StringBuilder().apply {
val buffer = CharArray(16 * 1024)
while (true) {
val readCount = reader.read(buffer, 0, buffer.size)
if (readCount < 0) break
appendRange(buffer, 0, readCount)
}
}.toString()
}
Как было сказано ранее, ContentResolver.openInputStream() возвращает для работы InputStream, связанный с заданным URI. Если всё успешно, то мы записываем содержимое InputStream в StringBuilder.
Может произойти так, что у приложения нет прав на чтение этого URI. В таком случае дополнительно проверяем SecurityException и пробуем в качестве альтернативы открыть URI через ContentResolver.openFileDescriptor().
Стоит также учитывать, что URI не всегда может указывать на реально существующий файл. В этом случае при попытке открыть его мы получим FileNotFoundException.
В результате чтения URI мы получаем содержимое текстового файла и выводим его на экран.
Допустим, мы изменили текст и хотим его сохранить в тот же файл. В этом случае нам понадобится запросить у ContentResolver OutputStream и записывать содержимое уже в него.
Добавим метод для сохранения URI.
private fun saveText(uri: Uri) {
lifecycleScope.launch {
val text = captureCurrentText()
val isSaved = withContext(Dispatchers.IO) {
saveTextToOutputStream(uri, text)
}
// Обрабатываем результат сохранения
...
}
}
private fun saveTextToOutputStream(uri: Uri, text: String): Boolean {
return try {
contentResolver.openOutputStream(uri, "wt")?.bufferedWriter(Charsets.UTF_8)?.use { writer ->
writer.write(text)
true
} ?: false
} catch (_: SecurityException) {
try {
if (isSdkQ) {
contentResolver.openFileDescriptor(uri, "w")?.use { parcelFileDescriptor ->
FileOutputStream(parcelFileDescriptor.fileDescriptor).bufferedWriter(Charsets.UTF_8).use { writer ->
writer.write(text)
true
}
} ?: false
} else {
false
}
} catch (_: Exception) {
false
}
} catch (_: Exception) {
false
}
}
В случае успеха мы в требуемый файл внесутся новые изменения и он будет сохранён в системе. Однако важно учитывать, что далеко не всегда URI, поставляемые другими приложениями, дают разрешение на запись. Чаще всегда внешние файлы доступны только для чтения. Если прав на запись нет, то мы получим SecurityException в данном случае сохранение в текущий файл будет невозможно. В качестве альтернативы можно создать новый файл и сохранить в него новые изменения.
Данный способ работает практически с любым приложением, имеющим возможность делиться файлами с другими приложениями. Это может быть как системный Storage Access Framework, так и различные файловые менеджеры и облачные хранилища.
Остановимся подробнее на облачных хранилищах. Некоторые приложения предоставляют права не только на чтение URI, но и на запись. Имея права на запись URI, мы можем открыть файл из такого хранилища, отредактировать его, и после сохранения внесённые изменения синхронизируются с облаком. Однако в большинстве случаев для запись файла в облако требуется подключать дополнительные API для каждого из требуемых хранилищ.
Также некоторые облачные хранилища интегрированы в Storage Access Framework, позволяя открывать файлы из них через системный файловый менеджер.
Мы протестировали работу самых популярных облачных хранилищ с нашим текстовым редактором и сделали небольшие выводы по каждому из них.
Google Drive
Google Drive является одним из самых популярных приложений для хранения файлов. Он предоставляет бесплатно 15 ГБ для хранения файлов, а также имеет интеграцию с другими сервисами Google и Android в том числе.
URI, полученный от Google Drive, помимо прав на чтение имеет также права на запись, благодаря чему получилось в приложении отредактировать файл, сохранить его и Google Drive автоматически синхронизировал его с облаком.
Yandex Disk
Yandex Disk представляет собой хорошую альтернативу Google Drive для работы с файлами. Он также имеет интеграцию с другими сервисами Яндекса. Бесплатно предоставляет только 5 ГБ для хранения, чего хватит лишь для небольших файлов.
Не позволяет записывать изменения через полученный URI, для взаимодействия требуется использование специального SDK.
Dropbox
Dropbox — один из самых популярных облачных хранилищ и хорошая альтернатива другим крупным. Единственным недостатком является небольшой объём бесплатного пространства, всего 2 ГБ.
Отправляемый Dropbox URI имеет права и на чтение, и на запись.
Koofr
Koofr является облачным хранилищем, ориентированным на страны ЕС. Предоставляет 10 ГБ для бесплатного пользования, а также возможность подключения других поставщиков облачных хранилищ.
URI имеет права и на чтение, и на запись, благодаря чему можно редактировать и сохранять файл на ходу.
MEGA
MEGA позиционируется как облачное хранилище с шифрованием файлов, которое также предоставляет возможность создавать зашифрованные чаты и совершать звонки. Для бесплатного пользования MEGA даёт 25 ГБ, что весьма ощутимо в сравнении с другими альтернативами.
Не позволяет записывать изменения через полученный URI.
pCloud
pCloud это защищённое облачное хранилище с шифрованием данных. Бесплатно пользователям доступно 10 ГБ для хранения.
Не позволяет записывать изменения через полученный URI.
Icedrive
Icedrive, хоть и является менее популярным, тоже предоставляет удобный способ для хранения файлов в облаке. Бесплатно доступно 10 ГБ пространства.
По какой-то причине, Icedrive не смог открыть тестовый файл или передать его в наше приложение.
Internxt
Internxt позиционирует себя как облачное хранилище, нацеленное на конфиденциальность пользователей. Однако бесплатно даёт всего 1 ГБ места, что крайне мало.
Аналогично Icedrive, приложению не удалось передать текстовый файл для открытия.
Sync
Sync — ещё одно облачное хранилище с шифрованием файлов. Для бесплатного пользования предлагает 5 ГБ пространства.
Не позволяет записывать изменения через полученный URI.
Заключение
Возможность приложениям взаимодействовать между собой и понимание того, как приложение обрабатывает данные от другого, это одни из важных инструментов для построения целостной экосистемы. Таким образом, работая с URI контента мы можем легко писать приложения как для просмотра файлов, так и для их редактирования.



