Scoped Storage — это модель доступа к файловой системе, введённая в Android 10 (API 29), которая ограничивает произвольный доступ приложений к общему хранилищу устройства. По данным Google Android Developer Documentation (2024), Scoped Storage заменяет старую модель разрешений READ_EXTERNAL_STORAGE и WRITE_EXTERNAL_STORAGE, предоставляя приложениям доступ только к их собственным файлам в изолированной песочнице и к общим коллекциям через MediaStore. Эта модель повышает конфиденциальность пользовательских данных и предотвращает несанкционированный сбор информации приложениями.
Главное
Scoped Storage — это архитектурное изменение в Android 10 (API 29), которое фундаментально меняет способ доступа приложений к файловой системе устройства. Вместо полного доступа к общему хранилищу приложение получает доступ только к своей песочнице и к определённым коллекциям медиафайлов через MediaStore.
До Scoped Storage любое приложение с разрешением READ_EXTERNAL_STORAGE могло читать все файлы в общем хранилище — фотографии, документы, загрузки, данные других приложений. Это создавало риски конфиденциальности: недобросовестные приложения могли собирать информацию о пользователе без его ведома.
По данным Google I/O 2019, более 65% пользователей Android считают конфиденциальность данных критическим фактором при выборе приложения. Scoped Storage — прямой ответ на этот запрос: каждое приложение работает в изолированной среде и получает доступ к чужим данным только при явном согласии пользователя.
Важно: Scoped Storage в Android 10 был опциональным (приложение могло отключить через requestLegacyExternalStorage). Начиная с Android 11 (API 30), Scoped Storage стал обязательным для всех приложений, независимо от target SDK.
Google ввёл Scoped Storage для решения трёх фундаментальных проблем старой модели доступа к файлам: конфиденциальность, управление разрешениями и очистка остаточных файлов.
В старой модели приложение с READ_EXTERNAL_STORAGE могло сканировать всё общее хранилище и собирать метаданные о файлах пользователя — географические координаты фотографий, имена документов, структуру директорий. Scoped Storage устраняет эту возможность: даже имея разрешение, приложение видит только файлы, созданные им самим, и явно выбранные пользователем через SAF.
До Scoped Storage приложение могло оставлять файлы в общем хранилище после удаления. Со временем накапливались мусорные директории. Scoped Storage решает это: все файлы в песочнице приложения удаляются вместе с приложением, а файлы через MediaStore имеют владельца и могут быть очищены системой.
| Аспект | Старая модель (Legacy) | Scoped Storage |
|---|---|---|
| Доступ к общему хранилищу | Полный (с разрешением) | Только своя песочница |
| Медиафайлы | Прямой file path | Через MediaStore URI |
| Чужие файлы | Доступны всегда | Только через SAF |
| Очистка при удалении | Файлы остаются | Песочница удаляется |
| Контроль пользователя | Минимальный | Явное согласие на каждый файл |
Scoped Storage — это часть общей стратегии Google по усилению конфиденциальности в Android, которая также включает разрешения "только во время использования", индикаторы доступа к камере и микрофону, и Privacy Dashboard.
Каждое приложение в Android получает собственную приватную директорию, доступную через Context.getFilesDir() и context.getCacheDir(). Для доступа к внешней (shared) приватной директории используется Context.getExternalFilesDir().
В Scoped Storage приложение имеет полный доступ к своей внешней приватной директории без каких-либо разрешений. Это основное место для хранения файлов, которые не должны быть доступны другим приложениям или которые приложение создаёт для собственного использования.
import android.os.Environment
import java.io.File
import java.io.IOException
class FileManager {
fun saveToAppStorage(context: Context, fileName: String, data: ByteArray) {
val appDir = context.getExternalFilesDir(null)
?: return
val file = File(appDir, fileName)
file.writeBytes(data)
}
fun readFromAppStorage(context: Context, fileName: String): ByteArray? {
val appDir = context.getExternalFilesDir(null)
?: return null
val file = File(appDir, fileName)
return file.takeIf { it.exists() }?.readBytes()
}
}
Путь к внешней приватной директории: /storage/emulated/0/Android/data/{packageName}/files/. Начиная с Android 11 прямой file path недоступен — только через API. Это ещё одно усиление безопасности Scoped Storage.
MediaStore — это основной API для доступа к общим медиафайлам (изображения, видео, аудио) в Scoped Storage. Приложение работает не с file paths, а с content URIs, предоставляемыми MediaStore provider.
MediaStore разделён на три основные коллекции: Images, Video и Audio. Каждая коллекция поддерживает CRUD-операции через ContentResolver. Для вставки нового файла в коллекцию Images используется MediaStore.Images.Media, для запроса существующих — query с соответствующим URI.
import android.content.ContentValues
import android.provider.MediaStore
import android.os.Environment
import java.io.OutputStream
fun saveImageToGallery(context: Context, bitmap: Bitmap, title: String) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/MyApp"
)
}
val uri = context.contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: return
context.contentResolver.openOutputStream(uri)?.use { output: OutputStream ->
bitmap.compress(
Bitmap.CompressFormat.JPEG,
95,
output
)
}
}
Важно: MediaStore не поддерживает произвольные file paths — только content URIs. Попытка получить file path из URI через MediaStore.Files.getContentUri() не даст прямой путь к файлу на Android 11+. Вместо этого используй ContentResolver.openInputStream() и openOutputStream() для работы с содержимым.
Storage Access Framework (SAF) — это API для доступа к произвольным файлам и директориям за пределами песочницы приложения. SAF предоставляет пользователю интерфейс выбора файла или директории, после чего приложение получает content URI с временным доступом.
SAF используется для сценариев, которые не покрываются MediaStore: работа с произвольными документами (PDF, ZIP, APK), доступ к директориям на SD-карте, импорт и экспорт файлов из других приложений. Пользователь явно выбирает файл через системный файловый пикер — это гарантирует его согласие на доступ.
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
const val REQUEST_CODE_PICK_DIR = 1001
fun pickDirectory(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
activity.startActivityForResult(intent, REQUEST_CODE_PICK_DIR)
}
fun handlePickResult(requestCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_DIR && data != null) {
val treeUri: Uri = data.data ?: return
// Take persistent URI permission
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(treeUri, flags)
}
}
SAF даёт приложению доступ к выбранной пользователем директории до перезагрузки устройства (при использовании takePersistableUriPermission). Это единственный способ получить доступ к произвольным файлам в общем хранилище на Android 11+.
Миграция существующего Android-приложения на Scoped Storage требует изменений в нескольких ключевых областях. Google рекомендует поэтапный подход с тестированием на Android 11+.
Замени все прямые File операции в общем хранилище на ContentResolver.openInputStream / openOutputStream. Для файлов в песочнице приложения (getExternalFilesDir) file paths продолжают работать.
Удали READ_EXTERNAL_STORAGE и WRITE_EXTERNAL_STORAGE, если они не нужны для специфических сценариев (например, управление файлами — для этого есть специальное разрешение MANAGE_EXTERNAL_STORAGE).
| Сценарий | Старый подход | Scoped Storage подход |
|---|---|---|
| Сохранение фото | File(path).writeBytes() | MediaStore + ContentResolver |
| Чтение PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| Свои файлы | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| Кэш | File(cacheDir).writeBytes() | context.cacheDir (без изменений) |
После миграции протестируй приложение на Android 11+ (API 30), включив в build.gradle targetSdk = 30 или выше. Убедись, что все операции с файлами в общем хранилище работают через MediaStore или SAF, а не через прямые file paths.
import android.os.Build
fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
fun compatReadFile(context: Context, uri: Uri): ByteArray? {
return if (isScopedStorage()) {
context.contentResolver
.openInputStream(uri)?.readBytes()
} else {
File(uri.path ?: return null)
.takeIf { it.exists() }
?.readBytes()
}
}
По данным Google Play Console (2024), более 78% активных Android-устройств работают на Android 10+ с Scoped Storage. Миграция — обязательный шаг для публикации обновлений в Google Play: новые приложения должны target API 31+, обновления — API 30+.
Часто задаваемые вопросы
Нет, начиная с Android 11 (API 30) Scoped Storage обязателен для всех приложений. Флаг requestLegacyExternalStorage, доступный в Android 10, не работает на API 30+. Единственный способ получить широкий доступ к файловой системе — разрешение MANAGE_EXTERNAL_STORAGE, но оно предназначено только для файловых менеджеров и антивирусов.
MANAGE_EXTERNAL_STORAGE — специальное разрешение для приложений, которым нужен полный доступ к файловой системе (файловые менеджеры, backup-инструменты). Запрос разрешения открывает системный экран с предупреждением для пользователя. При публикации в Google Play требуется заполнить форму Declaration о необходимости этого разрешения.
В Scoped Storage на Android 11+ прямой file path из content URI получить нельзя. Вместо этого используй ContentResolver.openInputStream() для чтения и openOutputStream() для записи. Если тебе нужен file path для совместимости со сторонней библиотекой — создай копию файла в getCacheDir() и работай с копией.
FileProvider продолжает работать без изменений — он используется для предоставления доступа к файлам из песочницы приложения другим приложениям через content URIs. Scoped Storage не влияет на FileProvider, так как FileProvider работает на уровне content URIs, а не прямых file paths.
Проверь Build.VERSION.SDK_INT: если >= Build.VERSION_CODES.Q (29), то устройство поддерживает Scoped Storage. Однако на Android 10 Scoped Storage может быть отключён через requestLegacyExternalStorage. На Android 11+ проверь, установлено ли флаг-манифест requestLegacyExternalStorage — если нет, Scoped Storage активен.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также