Scoped Storage — este un model de acces la sistemul de fișiere introdus în Android 10 (API 29) care limitează accesul arbitrar al aplicațiilor la stocarea partajată a dispozitivului. Conform Google Android Developer Documentation (2024), Scoped Storage înlocuiește vechiul model de permisiuni READ_EXTERNAL_STORAGE și WRITE_EXTERNAL_STORAGE, oferind aplicațiilor acces doar la propriile fișiere într-un sandbox izolat și la colecții comune prin MediaStore. Acest model sporește confidențialitatea datelor utilizatorilor și previne colectarea neautorizată de informații de către aplicații.
Puncte principale
Scoped Storage — este o modificare arhitecturală în Android 10 (API 29) care schimbă fundamental modul de acces al aplicațiilor la sistemul de fișiere al dispozitivului. În loc de acces complet la stocarea partajată, aplicația primește acces doar la propriul sandbox și la anumite colecții de fișiere media prin MediaStore.
Înainte de Scoped Storage, orice aplicație cu permisiunea READ_EXTERNAL_STORAGE putea citi toate fișierele din stocarea partajată — fotografii, documente, descărcări, datele altor aplicații. Acest lucru crea riscuri de confidențialitate: aplicațiile necinstite puteau colecta informații despre utilizator fără știrea acestuia.
Conform Google I/O 2019, peste 65% dintre utilizatorii Android consideră confidențialitatea datelor un factor critic în alegerea unei aplicații. Scoped Storage este un răspuns direct la această cerință: fiecare aplicație funcționează într-un mediu izolat și obține acces la datele altora doar cu consimțământul explicit al utilizatorului.
Important: Scoped Storage în Android 10 a fost opțional (aplicația îl putea dezactiva prin requestLegacyExternalStorage). Începând cu Android 11 (API 30), Scoped Storage a devenit obligatoriu pentru toate aplicațiile, indiferent de target SDK.
Google a introdus Scoped Storage pentru a rezolva trei probleme fundamentale ale vechiului model de acces la fișiere: confidențialitatea, gestionarea permisiunilor și curățarea fișierelor reziduale.
În modelul vechi, o aplicație cu READ_EXTERNAL_STORAGE putea scana întreaga stocare partajată și colecta metadate despre fișierele utilizatorului — coordonatele geografice ale fotografiilor, numele documentelor, structura directoarelor. Scoped Storage elimină această posibilitate: chiar și având permisiunea, aplicația vede doar fișierele create de ea însăși și pe cele selectate explicit de utilizator prin SAF.
Înainte de Scoped Storage, aplicația putea lăsa fișiere în stocarea partajată după dezinstalare. În timp, se acumulau directoare gunoi. Scoped Storage rezolvă aceasta: toate fișierele din sandbox-ul aplicației sunt șterse odată cu aplicația, iar fișierele prin MediaStore au un proprietar și pot fi curățate de sistem.
| Aspect | Model vechi (Legacy) | Scoped Storage |
|---|---|---|
| Acces la stocarea partajată | Complet (cu permisiune) | Doar propriul sandbox |
| Fișiere media | Cale directă de fișier | Prin URI MediaStore |
| Fișiere altora | Întotdeauna disponibile | Doar prin SAF |
| Curățare la dezinstalare | Fișierele rămân | Sandbox-ul este șters |
| Controlul utilizatorului | Minim | Consimțământ explicit pentru fiecare fișier |
Scoped Storage face parte din strategia generală Google de consolidare a confidențialității în Android, care include și permisiunile „doar în timpul utilizării”, indicatoarele de acces la cameră și microfon și Privacy Dashboard.
Fiecare aplicație în Android primește propriul director privat, accesibil prin Context.getFilesDir() și context.getCacheDir(). Pentru accesul la directorul privat extern (partajat) se utilizează Context.getExternalFilesDir().
În Scoped Storage, aplicația are acces complet la propriul director privat extern fără nicio permisiune. Acesta este locul principal pentru stocarea fișierelor care nu ar trebui să fie accesibile altor aplicații sau pe care aplicația le creează pentru propria utilizare.
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()
}
}
Calea către directorul privat extern: /storage/emulated/0/Android/data/{packageName}/files/. Începând cu Android 11, calea directă de fișier nu este disponibilă — doar prin API. Aceasta este o altă consolidare a securității Scoped Storage.
MediaStore — este API-ul principal pentru accesul la fișierele media partajate (imagini, video, audio) în Scoped Storage. Aplicația nu lucrează cu căi de fișiere, ci cu content URIs furnizate de MediaStore provider.
MediaStore este împărțit în trei colecții principale: Images, Video și Audio. Fiecare colecție suportă operații CRUD prin ContentResolver. Pentru inserarea unui fișier nou în colecția Images se utilizează MediaStore.Images.Media, pentru interogarea celor existente — query cu URI-ul corespunzător.
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
)
}
}
Important: MediaStore nu suportă căi de fișiere arbitrare — doar content URIs. Încercarea de a obține calea fișierului din URI prin MediaStore.Files.getContentUri() nu va da o cale directă către fișier pe Android 11+. În schimb, utilizează ContentResolver.openInputStream() și openOutputStream() pentru lucrul cu conținutul.
Storage Access Framework (SAF) — este API-ul pentru accesul la fișiere și directoare arbitrare în afara sandbox-ului aplicației. SAF oferă utilizatorului o interfață de selecție a fișierului sau directorului, după care aplicația primește un content URI cu acces temporar.
SAF este utilizat pentru scenarii care nu sunt acoperite de MediaStore: lucrul cu documente arbitrare (PDF, ZIP, APK), accesul la directoare de pe cardul SD, importul și exportul de fișiere din alte aplicații. Utilizatorul selectează explicit fișierul prin selectorul de fișiere al sistemului — aceasta garantează consimțământul său pentru acces.
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
// Obține permisiunea persistentă URI
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(treeUri, flags)
}
}
SAF oferă aplicației acces la directorul selectat de utilizator până la repornirea dispozitivului (când se utilizează takePersistableUriPermission). Aceasta este singura modalitate de a obține acces la fișiere arbitrare în stocarea partajată pe Android 11+.
Migrarea aplicației Android existente la Scoped Storage necesită modificări în câteva domenii cheie. Google recomandă o abordare etapizată cu testare pe Android 11+.
Înlocuiește toate operațiile directe cu File în stocarea partajată cu ContentResolver.openInputStream / openOutputStream. Pentru fișierele din sandbox-ul aplicației (getExternalFilesDir), căile de fișiere continuă să funcționeze.
Elimină READ_EXTERNAL_STORAGE și WRITE_EXTERNAL_STORAGE dacă nu sunt necesare pentru scenarii specifice (de exemplu, gestionarea fișierelor — pentru aceasta există permisiunea specială MANAGE_EXTERNAL_STORAGE).
| Scenariu | Abordare veche | Abordare Scoped Storage |
|---|---|---|
| Salvarea unei fotografii | File(path).writeBytes() | MediaStore + ContentResolver |
| Citirea unui PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| Fișiere proprii | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| Cache | File(cacheDir).writeBytes() | context.cacheDir (fără schimbări) |
După migrare, testează aplicația pe Android 11+ (API 30) cu targetSdk = 30 sau mai mare în build.gradle. Asigură-te că toate operațiile cu fișiere în stocarea partajată funcționează prin MediaStore sau SAF, nu prin căi directe de fișiere.
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()
}
}
Conform Google Play Console (2024), peste 78% dintre dispozitivele Android active rulează pe Android 10+ cu Scoped Storage. Migrarea este un pas obligatoriu pentru publicarea actualizărilor în Google Play: aplicațiile noi trebuie să targeteze API 31+, actualizările — API 30+.
Întrebări frecvente
Nu, începând cu Android 11 (API 30), Scoped Storage este obligatoriu pentru toate aplicațiile. Flag-ul requestLegacyExternalStorage, disponibil în Android 10, nu funcționează pe API 30+. Singura modalitate de a obține acces larg la sistemul de fișiere este permisiunea MANAGE_EXTERNAL_STORAGE, dar aceasta este destinată doar managerilor de fișiere și antivirusurilor.
MANAGE_EXTERNAL_STORAGE — o permisiune specială pentru aplicațiile care necesită acces complet la sistemul de fișiere (manageri de fișiere, instrumente de backup). Solicitarea permisiunii deschide un ecran de sistem cu o avertizare pentru utilizator. La publicarea în Google Play, este necesară completarea formularului Declaration despre necesitatea acestei permisiuni.
În Scoped Storage pe Android 11+, nu se poate obține o cale directă de fișier dintr-un content URI. În schimb, utilizează ContentResolver.openInputStream() pentru citire și openOutputStream() pentru scriere. Dacă ai nevoie de calea fișierului pentru compatibilitate cu o bibliotecă terță — creează o copie a fișierului în getCacheDir() și lucrează cu copia.
FileProvider continuă să funcționeze fără modificări — este utilizat pentru a oferi acces la fișierele din sandbox-ul aplicației altor aplicații prin content URIs. Scoped Storage nu afectează FileProvider, deoarece FileProvider funcționează la nivel de content URIs, nu de căi directe de fișiere.
Verifică Build.VERSION.SDK_INT: dacă >= Build.VERSION_CODES.Q (29), dispozitivul suportă Scoped Storage. Totuși, pe Android 10, Scoped Storage poate fi dezactivat prin requestLegacyExternalStorage. Pe Android 11+, verifică dacă flag-ul requestLegacyExternalStorage este setat în manifest — dacă nu, Scoped Storage este activ.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și