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 |
|---|---|---|
| Достъп до общото хранилище | Пълен (с разрешение) | Само собствена пясъчна кутия |
| Медийни файлове | Директен път на файл | Чрез MediaStore URI |
| Чужди файлове | Винаги достъпни | Само чрез SAF |
| Почистване при премахване | Файловете остават | Пясъчната кутия се изтрива |
| Контрол на потребителя | Минимален | Изрично съгласие за всеки файл |
Scoped Storage е част от общата стратегия на Google за укрепване на поверителността в Android, която включва също разрешения “само по време на използване”, индикатори за достъп до камера и микрофон и Privacy Dashboard.
Всяко приложение в Android получава собствена частна директория, достъпна чрез Context.getFilesDir() и context.getCacheDir(). За достъп до външната (обща) частна директория се използва 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, директният път на файл не е достъпен — само чрез API. Това е още едно укрепване на сигурността на Scoped Storage.
MediaStore — е основният API за достъп до общи медийни файлове (изображения, видео, аудио) в Scoped Storage. Приложението работи не с пътища на файлове, а с 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 не поддържа произволни пътища на файлове — само content URIs. Опитът за получаване на път на файл от 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
// Вземете постоянно URI разрешение
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) пътищата на файлове продължават да работят.
Премахнете 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), като зададете targetSdk = 30 или по-високо в build.gradle. Уверете се, че всички операции с файлове в общото хранилище работят чрез MediaStore или SAF, а не чрез директни пътища на файлове.
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: новите приложения трябва да таргетират 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+ не може да се получи директен път на файл от content URI. Вместо това използвайте ContentResolver.openInputStream() за четене и openOutputStream() за запис. Ако ви е необходим път на файл за съвместимост с библиотека на трета страна — създайте копие на файла в getCacheDir() и работете с копието.
FileProvider продължава да работи без промени — използва се за предоставяне на достъп до файлове от пясъчната кутия на приложението на други приложения чрез content URIs. Scoped Storage не влияе на FileProvider, тъй като FileProvider работи на ниво content URIs, а не на директни пътища на файлове.
Проверете 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също