Scoped Storage è un modello di accesso al file system introdotto in Android 10 (API 29) che limita l'accesso arbitrario delle app all'archiviazione condivisa del dispositivo. Secondo Google Android Developer Documentation (2024), Scoped Storage sostituisce il vecchio modello di autorizzazioni READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE, concedendo alle app l'accesso solo ai propri file in una sandbox isolata e alle raccolte condivise tramite MediaStore. Questo modello migliora la privacy dei dati dell'utente e previene la raccolta non autorizzata di informazioni da parte delle app.
Punti chiave
Scoped Storage è un cambiamento architetturale in Android 10 (API 29) che trasforma fondamentalmente il modo in cui le app accedono al file system del dispositivo. Invece dell'accesso completo all'archiviazione condivisa, l'app ottiene accesso solo alla propria sandbox e a raccolte multimediali specifiche tramite MediaStore.
Prima di Scoped Storage, qualsiasi app con autorizzazione READ_EXTERNAL_STORAGE poteva leggere tutti i file nell'archiviazione condivisa — foto, documenti, download, dati di altre app. Questo creava rischi per la privacy: app inaffidabili potevano raccogliere informazioni sull'utente a sua insaputa.
Secondo Google I/O 2019, oltre il 65% degli utenti Android considera la privacy dei dati un fattore critico nella scelta di un'app. Scoped Storage è una risposta diretta a questa richiesta: ogni app opera in un ambiente isolato e ottiene accesso ai dati di altre app solo con il consenso esplicito dell'utente.
Importante: Scoped Storage in Android 10 era opzionale (l'app poteva disattivarlo tramite requestLegacyExternalStorage). A partire da Android 11 (API 30), Scoped Storage è diventato obbligatorio per tutte le app, indipendentemente dal target SDK.
Google ha introdotto Scoped Storage per risolvere tre problemi fondamentali del vecchio modello di accesso ai file: privacy, gestione delle autorizzazioni e pulizia dei file residui.
Nel vecchio modello, un'app con READ_EXTERNAL_STORAGE poteva scansionare l'intera archiviazione condivisa e raccogliere metadati dei file dell'utente — coordinate geografiche delle foto, nomi dei documenti, struttura delle directory. Scoped Storage elimina questa possibilità: anche con l'autorizzazione, l'app vede solo i file che ha creato lei stessa e i file selezionati esplicitamente dall'utente tramite SAF.
Prima di Scoped Storage, le app potevano lasciare file nell'archiviazione condivisa dopo la disinstallazione. Con il tempo, si accumulavano directory spazzatura. Scoped Storage risolve questo problema: tutti i file nella sandbox dell'app vengono eliminati insieme all'app, e i file tramite MediaStore hanno un proprietario e possono essere puliti dal sistema.
| Aspetto | Vecchio modello (Legacy) | Scoped Storage |
|---|---|---|
| Accesso all'archiviazione condivisa | Completo (con autorizzazione) | Solo la propria sandbox |
| File multimediali | Percorso file diretto | Tramite URI MediaStore |
| File di altre app | Sempre accessibili | Solo tramite SAF |
| Pulizia alla disinstallazione | I file rimangono | La sandbox viene rimossa |
| Controllo dell'utente | Minimo | Consenso esplicito per file |
Scoped Storage fa parte della strategia complessiva di privacy di Google in Android, che include anche autorizzazioni "solo durante l'uso", indicatori di accesso a fotocamera e microfono, e il Privacy Dashboard.
Ogni app Android riceve la propria directory privata, accessibile tramite Context.getFilesDir() e context.getCacheDir(). Per accedere alla directory privata esterna (condivisa), si usa Context.getExternalFilesDir().
In Scoped Storage, un'app ha accesso completo alla sua directory privata esterna senza alcuna autorizzazione. Questo è il luogo principale per archiviare file che non devono essere accessibili ad altre app o che l'app crea per uso proprio.
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()
}
}
Percorso della directory privata esterna: /storage/emulated/0/Android/data/{packageName}/files/. A partire da Android 11, il percorso file diretto non è disponibile — solo tramite API. Questo è un ulteriore miglioramento della sicurezza di Scoped Storage.
MediaStore è l'API principale per accedere ai file multimediali condivisi (immagini, video, audio) in Scoped Storage. L'app non lavora con percorsi file, ma con content URI forniti dal provider MediaStore.
MediaStore è diviso in tre raccolte principali: Images, Video e Audio. Ogni raccolta supporta operazioni CRUD tramite ContentResolver. Per inserire un nuovo file nella raccolta Images, si usa MediaStore.Images.Media; per interrogare i file esistenti — query con l'URI corrispondente.
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
)
}
}
Importante: MediaStore non supporta percorsi file arbitrari — solo content URI. Tentare di ottenere un percorso file da un URI tramite MediaStore.Files.getContentUri() non darà un percorso diretto al file su Android 11+. Invece, usa ContentResolver.openInputStream() e openOutputStream() per lavorare con il contenuto.
Storage Access Framework (SAF) è un'API per accedere a file e directory arbitrari al di fuori della sandbox dell'app. SAF fornisce all'utente un'interfaccia di selezione di file o directory, dopo di che l'app riceve un content URI con accesso temporaneo.
SAF viene utilizzato per scenari non coperti da MediaStore: lavorare con documenti arbitrari (PDF, ZIP, APK), accedere a directory sulla scheda SD, importare ed esportare file da altre app. L'utente seleziona esplicitamente un file tramite il selettore di file di sistema — questo garantisce il suo consenso all'accesso.
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 concede all'app l'accesso alla directory selezionata dall'utente fino al riavvio del dispositivo (quando si usa takePersistableUriPermission). Questo è l'unico modo per ottenere accesso a file arbitrari nell'archiviazione condivisa su Android 11+.
Migrare un'app Android esistente a Scoped Storage richiede modifiche in diverse aree chiave. Google raccomanda un approccio graduale con test su Android 11+.
Sostituisci tutte le operazioni dirette con File nell'archiviazione condivisa con ContentResolver.openInputStream / openOutputStream. Per i file nella sandbox dell'app (getExternalFilesDir), i percorsi file continuano a funzionare.
Rimuovi READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE se non sono necessari per scenari specifici (ad esempio, gestione file — per questo esiste l'autorizzazione speciale MANAGE_EXTERNAL_STORAGE).
| Scenario | Vecchio approccio | Approccio Scoped Storage |
|---|---|---|
| Salvare una foto | File(path).writeBytes() | MediaStore + ContentResolver |
| Leggere PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| File propri | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| Cache | File(cacheDir).writeBytes() | context.cacheDir (invariato) |
Dopo la migrazione, testa l'app su Android 11+ (API 30), impostando targetSdk = 30 o superiore in build.gradle. Assicurati che tutte le operazioni sui file nell'archiviazione condivisa funzionino tramite MediaStore o SAF, non tramite percorsi file diretti.
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()
}
}
Secondo Google Play Console (2024), oltre il 78% dei dispositivi Android attivi esegue Android 10+ con Scoped Storage. La migrazione è un passo obbligatorio per pubblicare aggiornamenti su Google Play: le nuove app devono avere target API 31+, gli aggiornamenti — API 30+.
Domande frequenti
No, a partire da Android 11 (API 30) Scoped Storage è obbligatorio per tutte le app. Il flag requestLegacyExternalStorage, disponibile in Android 10, non funziona su API 30+. L'unico modo per ottenere accesso esteso al file system è l'autorizzazione MANAGE_EXTERNAL_STORAGE, ma è destinata solo a file manager e antivirus.
MANAGE_EXTERNAL_STORAGE è un'autorizzazione speciale per app che necessitano di accesso completo al file system (file manager, strumenti di backup). La richiesta di autorizzazione apre una schermata di sistema con un avviso per l'utente. Quando pubblichi su Google Play, devi compilare un modulo di Dichiarazione sulla necessità di questa autorizzazione.
In Scoped Storage su Android 11+, non è possibile ottenere un percorso file diretto da un content URI. Invece, usa ContentResolver.openInputStream() per leggere e openOutputStream() per scrivere. Se hai bisogno di un percorso file per compatibilità con una libreria di terze parti, crea una copia del file in getCacheDir() e lavora con la copia.
FileProvider continua a funzionare senza modifiche — viene utilizzato per fornire accesso ai file dalla sandbox dell'app ad altre app tramite content URI. Scoped Storage non influisce su FileProvider, poiché FileProvider opera a livello di content URI, non di percorsi file diretti.
Controlla Build.VERSION.SDK_INT: se >= Build.VERSION_CODES.Q (29), il dispositivo supporta Scoped Storage. Tuttavia, su Android 10, Scoped Storage può essere disabilitato tramite requestLegacyExternalStorage. Su Android 11+, verifica se il flag del manifest requestLegacyExternalStorage è impostato — in caso contrario, Scoped Storage è attivo.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche