FileNotFoundException в Android Kotlin: чтение файла по URI
Решение FileNotFoundException при чтении выбранного файла в Android на Kotlin. Используйте ContentResolver.openInputStream вместо File, учитывая Scoped Storage Android 10+. Примеры кода, проверка URI, разрешения без манифеста.
Почему в Android-приложении на Kotlin возникает исключение FileNotFoundException при чтении выбранного пользователем файла?
Разрабатываю простое Android-приложение (Basic View Activity). При нажатии на кнопку пользователь выбирает файл. Путь к файлу и его название корректно выводятся в TextView, но при попытке чтения возникает java.io.FileNotFoundException: No such file or directory. Метод File.isFile() возвращает false.
Файл существует и выбран пользователем. Подозреваю проблему с разрешениями, Scoped Storage (Android 10+) или настройками. Как решить эту проблему? Нужны ли дополнительные разрешения в AndroidManifest.xml или специальная обработка URI из Intent?
FileNotFoundException в Android-приложении на Kotlin при чтении выбранного пользователем файла возникает из-за Scoped Storage в Android 10+: intent возвращает content:// URI, а не прямой file:// путь, который нельзя использовать с java.io.File. Вместо создания File по пути применяйте ContentResolver.openInputStream(uri) — это стандартный способ чтения без дополнительных разрешений. File.isFile() всегда вернёт false для таких URI, поскольку они абстрактны и управляются ContentProvider.
Содержание
- Причины FileNotFoundException при чтении файлов Android
- Правильный выбор файла через Intent
- Проверка существования файла по URI
- Чтение содержимого с ContentResolver.openInputStream
- Scoped Storage в Android 10+ и разрешения
- Полный пример кода на Kotlin
- Источники
- Заключение
Причины FileNotFoundException при чтении файлов Android
Выводите путь из intent в TextView, и он выглядит нормально — скажем, content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Ffile.txt. Но при new File(path) или file.isFile() — бац, FileNotFoundException. Почему так?
Всё дело в эволюции Android. До версии 10 разработчики могли запрашивать READ_EXTERNAL_STORAGE и ковыряться везде. Теперь Scoped Storage запрещает прямой доступ к чужим файлам. Когда пользователь выбирает файл через системный пикер (ACTION_OPEN_DOCUMENT или ACTION_GET_CONTENT), система даёт вам URI — не путь, а ссылку на ресурс через ContentProvider. Попытка интерпретировать content:// как файловый путь приводит к ошибке: “No such file or directory”.
А File.isFile()? Он работает только с реальными путями на диске. Для URI это бессмысленно — проверьте в официальной документации: URIs абстрактны, их нельзя “файлизировать” напрямую. В итоге исключение летит, даже если файл на месте.
Хотите подтверждение? Stack Overflow полон таких кейсов — все упираются в тот же барьер.
Правильный выбор файла через Intent
Сначала убедитесь, что intent настроен верно. В вашем Basic View Activity добавьте кнопку с launcher’ом:
private val filePickerLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let { processFile(it) }
}
button.setOnClickListener {
filePickerLauncher.launch("*/*") // Или "text/plain" для текстовых
}
Почему ActivityResultContracts.GetContent()? Это современный API вместо старого startActivityForResult. Он использует ACTION_GET_CONTENT, возвращая content:// URI с правами доступа.
Если хотите persistent доступ (чтобы URI работал после перезапуска), берите ACTION_OPEN_DOCUMENT:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
startActivityForResult(intent, REQUEST_CODE)
Не забудьте в onActivityResult:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
processFile(uri)
}
}
}
FLAG_GRANT_READ_URI_PERMISSION — ключ: он сохраняет права. Без него URI сдохнет через сессию. Документация рекомендует именно так.
Проверка существования файла по URI
Перед чтением всегда проверяйте: существует ли файл по URI? File.exists() не сработает, но есть DocumentFile:
import androidx.documentfile.provider.DocumentFile
val documentFile = DocumentFile.fromSingleUri(context, uri)
if (documentFile?.exists() == true && documentFile.isFile) {
// Файл валиден
} else {
// Обработайте ошибку: файл удалён или недоступен
}
Это работает идеально для content://. Почему? DocumentFile абстрагирует SAF (Storage Access Framework). Stack Overflow подтверждает: альтернативы вроде query() ненадёжны, если диск отмонтирован или файл стёрт.
Коротко: не тратьте время на File — сразу к DocumentFile.
Чтение содержимого с ContentResolver.openInputStream
Вот сердце решения. Забудьте FileInputStream(new File(path)). Используйте:
fun readFileContent(context: Context, uri: Uri): String? {
return try {
context.contentResolver.openInputStream(uri)?.use { input ->
input.bufferedReader().use { reader ->
reader.readText()
}
}
} catch (e: FileNotFoundException) {
Log.e("FileRead", "Файл не найден: ${e.message}")
null
} catch (e: IOException) {
Log.e("FileRead", "Ошибка чтения: ${e.message}")
null
}
}
.openInputStream(uri) возвращает InputStream от ContentProvider. Kotlin’s use {} автоматически закроет ресурсы — чисто и безопасно. Kotlin-пример из SO идеален для строк.
Для бинарки (изображения)? Читаете в ByteArrayOutputStream. Но для текста — readText() рулит.
Если краш? Ловите FileNotFoundException — иногда URI устаревает. Grokking Android показывает полный try-catch-finally.
Scoped Storage в Android 10+ и разрешения
Scoped Storage — главный злодей. С Android 10 (Q) apps не видят весь /storage/emulated/0. Только свои папки или через SAF/MediaStore/FileProvider.
Разрешения? Для выбора файла через пикер — ноль в манифесте. READ_EXTERNAL_STORAGE не нужен: пользователь сам грантит через UI. Но для legacy (Android 9-) добавьте:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
В коде проверяйте ContextCompat.checkSelfPermission. Для Android 13+ — granular media permissions (READ_MEDIA_IMAGES и т.д.), но для произвольных файлов SAF хватит.
Подробно в статье про вызовы Scoped Storage: прямые file:// заблокированы. Holdapp блог советует SAF всегда.
Тестируйте на эмуляторе Android 14 — увидите разницу.
Полный пример кода на Kotlin
Соберём всё в Activity. Добавьте в build.gradle: implementation “androidx.documentfile:documentfile:1.0.1” (если нужно).
class MainActivity : AppCompatActivity() {
private lateinit var textView: TextView
private val filePickerLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let {
val path = it.toString()
textView.text = "URI: $path\nСуществует: ${checkExists(it)}\nСодержимое:\n${readFileContent(it) ?: "Ошибка чтения"}"
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.textView)
findViewById<Button>(R.id.button).setOnClickListener {
filePickerLauncher.launch("text/plain")
}
}
private fun checkExists(uri: Uri): Boolean {
val doc = DocumentFile.fromSingleUri(this, uri)
return doc?.exists() == true && doc.isFile
}
private fun readFileContent(uri: Uri): String? = contentResolver.openInputStream(uri)
?.bufferedReader()
?.use { it.readText() }
}
Запустите — FileNotFoundException уйдёт. URI в TextView покажет content://, но чтение сработает.
Для persistent: добавьте takePersistableUriPermission в launcher.
Источники
- CopyProgramming: Opening a Text File from a URI on Android
- RommanSabbir: Android Fix URI Restrictions
- Medium: Android URIs Explained
- Android Developers: Access documents and other files
- Stack Overflow: ContentResolver.openInputStream
- Stack Overflow: Read InputStream to String in Kotlin
- Stack Overflow: Check URI file exists
- ITNext: Scoped Storage Challenges
- Holdapp: Storage Access Framework
Заключение
FileNotFoundException при чтении выбранного файла в Android Kotlin — типичная засада Scoped Storage: переходите на ContentResolver.openInputStream(uri) и DocumentFile для проверок, без лишних разрешений. С ActivityResultContracts.GetContent() и takePersistableUriPermission код станет надёжным на всех версиях. Протестируйте на реальном устройстве — и забудьте о боли. Если трафик на “чтение файлов android” растёт, это решение сэкономит часы дебаггинга.