Мобильная разработка

FileNotFoundException в Android Kotlin: чтение файла по URI

Решение FileNotFoundException при чтении выбранного файла в Android на Kotlin. Используйте ContentResolver.openInputStream вместо File, учитывая Scoped Storage Android 10+. Примеры кода, проверка URI, разрешения без манифеста.

1 ответ 1 просмотр

Почему в 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 в 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’ом:

kotlin
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:

kotlin
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
 addCategory(Intent.CATEGORY_OPENABLE)
 type = "*/*"
}
startActivityForResult(intent, REQUEST_CODE)

Не забудьте в onActivityResult:

kotlin
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:

kotlin
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)). Используйте:

kotlin
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-) добавьте:

xml
<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” (если нужно).

kotlin
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.


Источники

  1. CopyProgramming: Opening a Text File from a URI on Android
  2. RommanSabbir: Android Fix URI Restrictions
  3. Medium: Android URIs Explained
  4. Android Developers: Access documents and other files
  5. Stack Overflow: ContentResolver.openInputStream
  6. Stack Overflow: Read InputStream to String in Kotlin
  7. Stack Overflow: Check URI file exists
  8. ITNext: Scoped Storage Challenges
  9. Holdapp: Storage Access Framework

Заключение

FileNotFoundException при чтении выбранного файла в Android Kotlin — типичная засада Scoped Storage: переходите на ContentResolver.openInputStream(uri) и DocumentFile для проверок, без лишних разрешений. С ActivityResultContracts.GetContent() и takePersistableUriPermission код станет надёжным на всех версиях. Протестируйте на реальном устройстве — и забудьте о боли. Если трафик на “чтение файлов android” растёт, это решение сэкономит часы дебаггинга.

Авторы
Проверено модерацией
Модерация