Как открыть файл и папку через системный пикер SAF

В кратком ответе: вызовите Intent ACTION_OPEN_DOCUMENT (для файла) или ACTION_OPEN_DOCUMENT_TREE (для папки), сразу сохраните persistable permission через takePersistableUriPermission и работайте с Uri через ContentResolver (openInputStream/openOutputStream). Ниже — готовые примеры и практические советы для безопасной и устойчивой работы.

Быстрый старт — выбрать файл или папку

  • Для файлов используйте ACTION_OPEN_DOCUMENT (или ActivityResultContracts.OpenDocument).
  • Для директорий — ACTION_OPEN_DOCUMENT_TREE (или ActivityResultContracts.OpenDocumentTree).
  • Обязательно запрашивайте persistable permission, если хотите сохранить доступ между запусками.

Пример (Kotlin, Activity/Fragment, Activity Result API):

private val openFileLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
    uri?.let {
        contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
        // читать: contentResolver.openInputStream(it)
    }
}

private val openTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
    uri?.let {
        contentResolver.takePersistableUriPermission(
            it,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
        // работать с деревом
    }
}

// Запуск
openFileLauncher.launch(arrayOf("application/pdf")) // или arrayOf("*/*")
openTreeLauncher.launch(null)

Всегда вызывайте takePersistableUriPermission сразу после получения Uri — иначе доступ может быть утерян после перезапуска приложения.

Чтение и запись через ContentResolver

  • Чтение:
fun readFile(uri: Uri) {
    contentResolver.openInputStream(uri)?.use { input ->
        val bytes = input.readBytes()
        // обработка
    }
}
  • Запись (в файл):
contentResolver.openOutputStream(targetUri)?.use { out ->
    out.write("Новый контент".toByteArray())
}
  • Создание файла в выбранной папке (TREE):
fun createFileInTree(treeUri: Uri, fileName: String): Uri? {
    val treeId = DocumentsContract.getTreeDocumentId(treeUri)
    return DocumentsContract.createDocument(contentResolver, treeIdToUri(treeId), "text/plain", fileName)
}

(Уточните правильный uri для createDocument через DocumentsContract.buildDocumentUriUsingTree если нужно.)

Для мониторинга изменений используйте ContentObserver на соответствующем Uri или периодическую ресканку при необходимости.

Продвинутые темы и совместимость

  • Scoped Storage (Android 10+) делает SAF основным способом доступа к медиаконтенту без ALL_FILES_ACCESS.
  • SAF поддерживает внешние SD-карты, но поведение OEM-UI может отличаться — тестируйте на реальных девайсах.
  • Храните URI и права: persistable permissions сохраняются системой; чтобы отозвать — вызовите releasePersistableUriPermission(uri).
  • Ограничение: на устройстве примерно ~1000 persistable permission; не накапливайте ненужные URI.

Не храните тысячи URI — лимит по устройству ~1000. Освобождайте права для неактуальных Uri через releasePersistableUriPermission.

Частые ошибки

  • Permission denied после выбора: забыли вызвать takePersistableUriPermission или использовали неверный флаг.
  • Файлы на SD не видны: приложение не имеет подходящего права через SAF; проверьте, правильно ли выбран каталог и сохранены ли права.
  • Неправильная обработка MIME-типов: используйте arrayOf("/") для общего выбора, конкретный MIME для фильтрации.
  • Ошибка при создании файла в дереве: используйте корректный Uri/DocumentId и проверяйте возвращаемое значение createDocument.

FAQ

  • Нужно ли просить MANAGE_EXTERNAL_STORAGE? Нет. Для большинства сценариев SAF покрывает доступ без запроса ALL_FILES_ACCESS (который ограничен политикой Play).
  • Как сохранить доступ навсегда? Сохраните persistable permission; система хранит её до явного отзыва пользователем или программы.
  • Можно ли открыть несколько файлов сразу? Да — используйте ACTION_OPEN_DOCUMENT с Intent.EXTRA_ALLOW_MULTIPLE или соответствующий контракт.
  • Как протестировать SD-карту? Запустите эмулятор с смонтированной внешней картой и проверьте на разных реальных устройствах (Samsung, Xiaomi и т.д.), так как UI и поведение могут различаться.

Тестируйте на Android 11+ и реальных устройствах, сохраняйте только нужные URI и работайте через ContentResolver — это покрывает большинство сценариев работы с файлами и гарантирует соответствие политике Google Play.