Работа с 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):
- Запуск выбора папки:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_CODE)
- Обработка результата и создание файла:
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 и не всегда проходит проверку.
План миграции:
- Проанализируйте, какие операции требуют произвольного доступа.
- Перенесите медиа-операции в MediaStore.
- Для редактирования/чтения файлов у пользователей — используйте SAF.
- Тестируйте на 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.