Как работать с 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.