Работа с файлами в 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, мы можем открыть файл из такого хранилища, отредактировать его, и после сохранения внесённые изменения синхронизируются с облаком. Также некоторые облачные хранилища интегрированы в Storage Access Framework, позволяя открывать файлы из них через системный файловый менеджер.
Протестировав получившийся текстовый редактор, мы обнаружили, что следующие облачные хранилища позволяют открывать свои файлы во внешних приложениях:
- Google Drive
- Dropbox
- pCloud
- Яндекс Диск
Из них записывать изменения в файл позволяют лишь:
- Google Drive
- Dropbox
Таким образом, работая с URI контента мы можем легко писать приложения как для просмотра файлов, так и для их редактирования.



