Как работать с Uri, content:// и SAF в Android
В двух словах: net.Uri — это универсальный идентификатор ресурса в Android; content:// — схема доступа через ContentProvider и MediaStore; а SAF (Storage Access Framework) нужен для безопасного, постоянного доступа к папкам и файлам через системный picker. Ниже — краткие правила и рабочие примеры, которые можно сразу вставить в проект.
Что такое net.Uri и схема content://
Uri в Android — не просто путь к файлу, а структура: схема://authority/path?query. Для работы с внешними файлами чаще всего встречаются:
- content:// — доступ через ContentResolver и провайдеры (MediaStore, Downloads, DocumentsProvider);
- file:// — устаревший прямой путь (вызывает FileUriExposedException на Android 7+).
Пример создания Uri:
val uri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload")
Основные правила:
- Не пытайтесь получить реальный путь на диск из content:// — он может отсутствовать или быть недоступен.
- Для чтения используйте ContentResolver.openInputStream(uri), для записи — openOutputStream(uri).
Storage Access Framework (SAF): запрос папки и persistent permissions
SAF позволяет пользователю выбрать папку/файл и дать приложению права, которые можно сохранить навсегда (persistable).
Запуск picker для папки:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_CODE_OPEN_TREE)
Обработка результата и сохранение прав:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_OPEN_TREE && resultCode == Activity.RESULT_OK) {
val treeUri = data?.data ?: return
contentResolver.takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
val doc = DocumentFile.fromTreeUri(this, treeUri)
// дальше можно искать файлы: doc.findFile("example.txt")
}
}
Без вызова takePersistableUriPermission доступ по Uri будет действовать только пока жив процесс — после перезапуска вы потеряете права.
Практические советы и примеры кода
Чтение файла через SAF:
fun readFileFromSaf(context: Context, treeUri: Uri, fileName: String): String? {
val doc = DocumentFile.fromTreeUri(context, treeUri)?.findFile(fileName) ?: return null
return context.contentResolver.openInputStream(doc.uri)?.bufferedReader()?.use { it.readText() }
}
Чтение через ContentResolver (MediaStore/Downloads):
val cursor = contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), selection, null, null)
Не полагайтесь на OpenableColumns.DATA — на многих Android это пусто. Используйте потоки.
Советы:
- Предпочитайте SAF для работы с пользовательскими папками и документами.
- Для медиатеки используйте MediaStore и content://.
- Запросы READ_EXTERNAL_STORAGE нужны только при прямом доступе через устаревшие API; для SAF они не требуются.
- Всегда ловите SecurityException и проверяйте, что uri != null.
Частые ошибки
- Попытка получить файл-системный путь из content:// — часто невозможна.
- Не вызван takePersistableUriPermission — права теряются после перезапуска.
- Использование file:// приводит к FileUriExposedException на Android 7+.
- Ожидание, что OpenableColumns.DATA всегда вернёт путь — нет гарантии.
FAQ
- Нужно ли манипулировать реальными путями к файлам?
- Обычно нет. Работайте через InputStream/OutputStream и DocumentFile/API провайдера.
- Когда использовать SAF, а когда MediaStore?
- SAF — для документов и пользовательских папок. MediaStore — для медиаконтента (фото, видео, аудио).
- Как сохранить доступ навсегда?
- Через contentResolver.takePersistableUriPermission(...) и хранение Uri (например, в SharedPreferences).
Используйте описанные подходы — это сделает работу с файлами надёжной и совместимой с Android 10+ и Scoped Storage.