Безопасный обмен файлами в 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)

  1. В манифесте внутри добавьте провайдера (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>
  1. Создайте 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 — практические примеры

  1. Получение 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);
  1. 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"))
  1. ACTION_VIEW:
val view = Intent(Intent.ACTION_VIEW).apply {
    setDataAndType(uri, "image/*")
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(view)
  1. Камера — 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-pathgetFilesDir()Внутренние приватные файлы
cache-pathgetCacheDir()Временные файлы
external-files-pathgetExternalFilesDir()Пользовательские файлы, видимые извне (но приватны для приложения)
external-cache-pathgetExternalCacheDir()Кэш на внешнем носителе

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

  • Несовпадение 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.