Работа с external storage и Scoped Storage на Android

В двух словах: с Android 10+ прямой доступ к общему external storage ограничен — используйте Storage Access Framework (SAF) для произвольных папок и MediaStore для фото/видео/аудио; legacy-режим временный, мигрируйте заранее.

Оглавление {{TOC_AUTOMATIC}}

Что такое external storage и как Scoped Storage изменил доступ

External storage — это публичные разделы устройства (DCIM, Downloads, Movies и т. п.), видимые в файловых менеджерах и на ПК. Раньше приложения могли читать/писать напрямую в эти папки, теперь Scoped Storage (начиная с Android 10, обязателен для targetSdkVersion ≥ 30) ограничивает область видимости: приложение видит свои файлы и те, к которым пользователь явно дал доступ. Это повышает приватность, но требует новых API.

Ключевые отличия:

  • Internal storage — приватна для приложения.
  • External public — индексируемая пользователем область, теперь доступна через SAF или MediaStore.
  • requestLegacyExternalStorage работает ограниченно и не даёт долгосрочного решения.

Как получить доступ: SAF (Storage Access Framework)

Когда нужно работать с произвольной папкой (Downloads, внешняя SD, USB), используйте SAF — пользователь выбирает папку, приложение получает Uri и постоянные права.

Пример последовательности (Kotlin):

  1. Запуск выбора папки:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_CODE)
  1. Обработка результата и создание файла:
val uri = data?.data ?: return
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val tree = DocumentFile.fromTreeUri(context, uri)
val file = tree?.createFile("text/plain", "file.txt")
contentResolver.openOutputStream(file!!.uri)?.use { it.write("Данные".toByteArray()) }

Советы:

  • Сохраняйте Uri (например, в EncryptedSharedPreferences) и используйте takePersistableUriPermission.
  • Для больших файлов используйте ParcelFileDescriptor через contentResolver.openFileDescriptor(uri, "w").

SAF требуется для записи в чужие папки. Попытка писать по пути /storage/emulated/0/Download без SAF запрещена.

Работа с медиа: MediaStore

Для фотографий, видео и музыки используйте MediaStore — он индексирует медиа и даёт доступ без полного filesystem-привилегирования.

Создание медиафайла:

val values = ContentValues().apply {
  put(MediaStore.MediaColumns.DISPLAY_NAME, "photo.jpg")
  put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
  put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp")
}
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
contentResolver.openOutputStream(uri!!)?.use { it.write(bytes) }

Чтение/поиск — через ContentResolver.query() с нужными проекциями (DISPLAY_NAME, SIZE, RELATIVE_PATH и т.д.).

Особенности:

  • RELATIVE_PATH доступен с API 29+.
  • MediaStore управляет видимостью для галерей и медиа-сканеров.

Миграция legacy-приложений и MANAGE_EXTERNAL_STORAGE

Если у вас старое приложение:

  • Во временном варианте можно включить в манифест android:requestLegacyExternalStorage="true" (эффект ограничен и не применяется для новых установок с targetSdkVersion ≥ 30).
  • MANAGE_EXTERNAL_STORAGE даёт полный доступ (All files access) но требует обоснования при публикации в Google Play и не всегда проходит проверку.

План миграции:

  1. Проанализируйте, какие операции требуют произвольного доступа.
  2. Перенесите медиа-операции в MediaStore.
  3. Для редактирования/чтения файлов у пользователей — используйте SAF.
  4. Тестируйте на API 29–34, проверяйте поведение при обновлении targetSdkVersion.

Для Android 14+ проверяйте StorageManager.isExternalStorageLegacy() и готовьте приложение к тому, что legacy режим будет полностью недоступен.

Таблица: когда какой способ применять

СценарийМетодОграничения
Произвольная папка (Downloads, SD)SAF (ACTION_OPEN_DOCUMENT_TREE)Нужно взаимодействие с пользователем
Фото/видео/аудиоMediaStoreТолько медиа-типы, удобна для галерей
Старое поведение полного доступаrequestLegacy / MANAGE_EXTERNAL_STORAGEУстаревает / требует разрешений и review

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

  • Ожидание доступа по файловому пути (/storage/emulated/0/...) — не работает на Scoped Storage.
  • Отсутствие takePersistableUriPermission — права не сохранятся после перезапуска.
  • Использование MANAGE_EXTERNAL_STORAGE без обоснования — отклонение в магазине.
  • Запись в MediaStore без корректных MIME или RELATIVE_PATH — файлы могут не отображаться.

FAQ

  • Нужен ли SAF для чтения файлов пользователя? — Да, если это не медиа и вы хотите доступ к произвольной папке.
  • Можно ли восстановить работу старого кода? — Частично: requestLegacy временно помогает, но лучше мигрировать.
  • Как тестировать на SD-карте? — AVD Manager → Advanced → укажите SD Card или тестируйте на реальном устройстве с картой.

Используйте SAF и MediaStore как стандартные инструменты работы с external storage — это гарантия совместимости и соответствия требованиям приватности Android.