Безопасный обмен файлами в Android без FileUriExposedException
Нельзя передавать file:// URI внешним приложениям — это вызывает FileUriExposedException на Android 7+; решение — отдавать content:// URI через FileProvider и добавлять Intent.FLAG_GRANT_READ_URI_PERMISSION / FLAG_GRANT_WRITE_URI_PERMISSION. Ниже — минимальная конфигурация, код и быстрые проверки.
Настройка FileProvider (манифест и file_paths.xml)
- В манифесте внутри
добавьте провайдера (AndroidX):
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.myapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
- Создайте res/xml/file_paths.xml — укажите только те каталоги, которые действительно хотите разрешить:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="internal_files" path="." />
<cache-path name="internal_cache" path="." />
<external-files-path name="external_files" path="." />
<external-cache-path name="external_cache" path="." />
</paths>
Не добавляйте root-path без крайней надобности — он открывает корневую файловую систему устройства.
Генерация URI и отправка Intent — практические примеры
- Получение content:// URI для File
Kotlin:
val file = File(context.filesDir, "reports/report1.pdf")
val uri = FileProvider.getUriForFile(context, "com.example.myapp.fileprovider", file)
Java:
File file = new File(context.getFilesDir(), "reports/report1.pdf");
Uri uri = FileProvider.getUriForFile(context, "com.example.myapp.fileprovider", file);
- ACTION_SEND (share) — обязательно дать флаг чтения:
val send = Intent(Intent.ACTION_SEND).apply {
type = "application/pdf"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(send, "Share file"))
- ACTION_VIEW:
val view = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "image/*")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(view)
- Камера — EXTRA_OUTPUT (запись в ваш файл):
val photoFile = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "shot.jpg")
val photoUri = FileProvider.getUriForFile(context, AUTHORITY, photoFile)
val takePic = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivityForResult(takePic, REQ_CAMERA)
getUriForFile бросит IllegalArgumentException, если файл не попадает под объявления в file_paths.xml — проверяйте соответствие путей.
Гарантирование доступа: grantUriPermission и ClipData
Когда Intent может быть обработан несколькими приложениями (chooser) — явно выдайте права всем потенциальным получателям:
val resList = context.packageManager.queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY)
for (res in resList) {
val pkg = res.activityInfo.packageName
context.grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
Не забудьте revokeUriPermission при необходимости. В современных API ClipData с установленными флагами часто автоматически распространяет права, но явный grant повышает совместимость.
Сравнение основных тегов file_paths.xml
| Тег | Что даёт | Когда использовать |
|---|---|---|
| files-path | getFilesDir() | Внутренние приватные файлы |
| cache-path | getCacheDir() | Временные файлы |
| external-files-path | getExternalFilesDir() | Пользовательские файлы, видимые извне (но приватны для приложения) |
| external-cache-path | getExternalCacheDir() | Кэш на внешнем носителе |
Частые ошибки
- Несовпадение authority в манифесте и в getUriForFile() → IllegalArgumentException или отказ в доступе.
- file_paths.xml отсутствует в res/xml/ или имя meta-data не совпадает.
- Забыт FLAG_GRANT_READ_URI_PERMISSION / WRITE → SecurityException у приёмника.
- Использование Uri.fromFile() → FileUriExposedException на API ≥ 24.
- Слишком широкий root-path → случайный доступ к нежелательным файлам.
При отладке смотрите logcat на предмет FileUriExposedException, IllegalArgumentException и SecurityException — это быстро указывает на проблему.
FAQ
-
Нужно ли android:exported="true" для провайдера?
Нет — лучше оставить false и выдавать доступ через grantUriPermission/флаги Intent. -
Можно ли давать постоянный доступ к URI?
Для FileProvider постоянные persisted permissions обычно недоступны; для долгого доступа смотрите DocumentsProvider и CONTENT_RESOLVER.takePersistableUriPermission. -
Где хранить файлы при Scoped Storage (Android 10+)?
Используйте getFilesDir(), getCacheDir(), getExternalFilesDir() и соответствующие теги в file_paths.xml.
Если хотите, подготовлю готовый file_paths.xml под ваш кейс или утилиту getShareIntent(File) на Kotlin.