Lo scoped storage di Android limita ciò che un'app Capacitor può leggere e scrivere al di fuori delle sue cartelle. Da Android 10, e in modo completo da Android 11, un'app può usare liberamente le sue directory private, aggiungere file a collezioni condivise tramite MediaStore e richiede all'utente di selezionare qualsiasi altra cosa tramite lo Storage Access Framework. Questa guida spiega cosa è cambiato per versione di Android, quali Capacitor Directory valori funzionano ancora e come salvare, importare ed esportare file nel modo supportato su Capacitor 8.
Cosa è cambiato, versione per versione
| Android | API | Cambiamento rilevante per le app Capacitor |
|---|---|---|
| 10 | 29 | Introduzione dello scoped storage. Le app possono fare opt-out con requestLegacyExternalStorage="true" puntando a API 29 |
| 11 | 30 | Scoped storage imposto per le app con target 30+. requestLegacyExternalStorage ignorato. WRITE_EXTERNAL_STORAGE non concede più nulla. MANAGE_EXTERNAL_STORAGE introdotto. L'albero di selezione non può scegliere la radice di Download o la radice del volume |
| 12 | 31-32 | Nessuna modifica importante allo storage |
| 13 | 33 | READ_EXTERNAL_STORAGE sostituita da READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO. Nessun permesso consente l'accesso ai documenti di altre app |
| 14 | 34 | Accesso parziale alle foto con READ_MEDIA_VISUAL_USER_SELECTED |
| 15 e 16 | 35-36 | Nessun nuovo modello di storage, Play applica la policy sui permessi per foto e video |
Capacitor 8 punta a API 36, quindi la tua app funziona sempre con scoped storage completo sui dispositivi moderni. Non esiste un flag per ripristinare il comportamento precedente.
Cosa si è rotto nei vecchi Capacitor code
Code scritto per Capacitor 2 o 3, o portato da Cordova, spesso fallisce in silenzio sui nuovi dispositivi:
Filesystem.writeFile({ directory: Directory.Documents })oppureDirectory.ExternalStoragesu "salva sul telefono". Su Android 11+ questi percorsi puntano allo storage condiviso e le scritture falliscono o il file non può essere letto dopo la reinstallazione.- Lettura di percorsi arbitrari come
/storage/emulated/0/Download/file.pdfda un percorso inserito nelle impostazioni o ricevuto da un server. - Richiesta di
READ_EXTERNAL_STORAGEper elencare i documenti dell'utente. Su Android 13+ il permesso non ha effetto sui file non multimediali. - Usare
Filesystem.requestPermissions()e dare per scontato chegrantedsignifichi che l'app può scrivere ovunque. - Costruzione di percorsi file da un
content://URI restituito da un picker.
Cosa funziona ancora senza alcun permesso
Le directory della tua app non sono interessate da scoped storage e non richiedono permessi:
Capacitor Directory |
Percorso Android | Visibile all'utente | Rimosso alla disinstallazione |
|---|---|---|---|
Data, Library, LibraryNoCloud |
/data/data/<package>/files |
No | Sì |
Cache |
/data/data/<package>/cache |
No | Sì |
External |
/storage/emulated/0/Android/data/<package>/files |
Solo tramite USB o alcuni file manager | Sì |
ExternalCache |
/storage/emulated/0/Android/data/<package>/cache |
Uguale | Sì |
Documents |
Condivisi Documents cartella |
Sì | No |
ExternalStorage |
Root dello storage condiviso | Sì | No |
Le prime quattro righe sono sicure. Documents e ExternalStorage sono le righe legacy: da Android 11 in poi la tua app può vedere solo i file lì che ha creato lei stessa, e dopo una reinstallazione perde l'accesso anche a quelli. Trattale come non supportate per i nuovi code.
External è utile per file di grandi dimensioni (video offline, mappe) perché può risiedere su una partizione più grande, e i file lì non fanno parte di Auto Backup. Da Android 11, altre app non possono leggere Android/data, quindi è comunque privato nella pratica.
Salvare file che l'utente può trovare: MediaStore
Quando l'utente tocca “Download” su una fattura, si aspetta di trovarla nella cartella Download. Su Android 10+, le app possono inserire file nelle raccolte condivise Downloads, Pictures, Movies, Music e Documents tramite MediaStore senza alcun permesso.
@capgo/capacitor-file-sharer incapsula tutto questo in save():
bun add @capgo/capacitor-file-sharer
bunx cap sync
import { FileSharer } from '@capgo/capacitor-file-sharer';
import { Filesystem, Directory } from '@capacitor/filesystem';
const { uri } = await Filesystem.getUri({ path: 'exports/invoice-1042.pdf', directory: Directory.Data });
const result = await FileSharer.save({
filename: 'invoice-1042.pdf',
path: uri,
contentType: 'application/pdf',
android: {
saveDirectory: 'downloads', // 'downloads' | 'pictures' | 'movies' | 'music' | 'documents'
relativePath: 'MyApp/Invoices', // subfolder, Android 10+
},
});
console.log(result.uri); // content:// URI of the saved file
Su iOS la stessa chiamata apre la share sheet, dove l'utente sceglie “Salva in Files”, così un unico percorso code copre entrambe le piattaforme.
Su Android 9 e versioni precedenti, la scrittura nello storage condiviso richiede ancora WRITE_EXTERNAL_STORAGE. Dichiaralo solo per quelle versioni:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Capacitor 8 supporta Android 7 (API 24) e versioni successive, quindi se pubblichi ancora su dispositivi Android 7-9, mantieni quella riga.
Foto e video
Per i media, preferisci i flussi della piattaforma a scrivere i file manualmente. @capacitor/camera scrive nella galleria con saveToGallery: true, e richiede permessi di storage legacy solo sulle vecchie versioni di Android. Consulta la nostra guida a scattare e modificare foto in Capacitor.
Lettura dei file utente: picker al posto dei permessi
Lo scoped storage prevede che l'utente selezioni i file. Il picker di Android restituisce un content:// URI che la tua app può leggere anche senza permessi di storage.
import { CapgoFilePicker } from '@capgo/capacitor-file-picker';
import { Filesystem, Directory } from '@capacitor/filesystem';
const { files } = await CapgoFilePicker.pickFiles({ types: ['application/pdf'], limit: 1 });
const picked = files[0];
// Copy into app storage right away: picker grants can expire
await Filesystem.copy({
from: picked.path!,
to: `imports/${picked.name}`,
toDirectory: Directory.Data,
});
Per foto e video, @capgo/capacitor-file-picker, il cui pickMedia e @capacitor/camera, il cui chooseFromGallery usa il Photo Picker di Android su Android 13+, che non richiede alcun permesso.
Evita i permessi READ_MEDIA_*
La policy sui permessi per foto e video di Google Play consente solo READ_MEDIA_IMAGES e READ_MEDIA_VIDEO per le app il cui scopo principale richiede un ampio accesso alla galleria, come le app di galleria o gli editor di foto. Tutti gli altri devono usare il picker e dichiararlo in Play Console. Se un plugin che usi dichiara queste autorizzazioni, rimuovile dal manifest unito:
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
Aggiungi xmlns:tools="http://schemas.android.com/tools" al <manifest> tag prima.
Accesso alle cartelle con lo Storage Access Framework
Alcune app hanno bisogno di un accesso continuo a una cartella: un'app di note che sincronizza in una cartella utente, una cartella di esportazione per un'app da campo. Lo Storage Access Framework (SAF) consente all'utente di concedere una cartella con ACTION_OPEN_DOCUMENT_TREE:
const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI
Tre cose da sapere:
- Il risultato è un URI ad albero, non un percorso. Non puoi passarlo a
Filesystem.writeFile. I file interni vengono creati e letti tramiteDocumentsContractoppureDocumentFilein code nativo. - L'accesso non sopravvive a un riavvio a meno che la tua app non chiami
takePersistableUriPermissionsull'URI. Aggiungi un piccolo metodo nativo se ti serve:
@PluginMethod
public void persist(PluginCall call) {
Uri uri = Uri.parse(call.getString("uri"));
getContext().getContentResolver().takePersistableUriPermission(
uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
call.resolve();
}
- Alcune cartelle non possono essere concesse. Su Android 11+, il picker rifiuta la radice dello storage interno, la radice della
Downloadcartella eAndroid/dataeAndroid/obb. Chiedi agli utenti di creare o selezionare una sottocartella.
Le concessioni persistite sono limitate in numero per app e l'utente può revocarle nelle impostazioni di sistema o cancellando i dati dell'app. Verifica l'accesso con getPersistedUriPermissions() all'avvio e richiedi di nuovo quando scompare.
Accesso a tutti i file: MANAGE_EXTERNAL_STORAGE
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Questo permesso offre il vecchio comportamento di "lettura e scrittura di tutto", ma non è un prompt runtime. L'utente deve attivarlo in una schermata impostazioni dedicata e Google Play lo revisiona manualmente. I casi d'uso accettati sono file manager, backup e ripristino, antivirus, gestione documenti, ricerca file on-device e pochi altri in cui l'accesso ampio è la funzione principale. Un'app di note, un visualizzatore PDF che apre file da Downloads o un'app che "deve solo salvare esportazioni" verrà rifiutata.
Se distribuisci al di fuori di Play (MDM aziendale, dispositivi kiosk sideloaded), è un'opzione. Altrimenti, progetta attorno a MediaStore e SAF.
Come si integrano gli altri plugin
| Plugin | Impatto dello scoped storage |
|---|---|
@capacitor/filesystem |
Va bene con le directory dell'app. Documents e ExternalStorage sono legacy su Android 11+. requestPermissions() rileva solo per quelle directory su Android vecchi |
@capacitor/camera |
Usa la fotocamera di sistema e Photo Picker, nessuna autorizzazione di storage tranne saveToGallery su Android 9 e versioni precedenti |
@capacitor/share |
Condivide tramite un FileProvider. Solo i percorsi elencati in res/xml/file_paths.xml può essere condiviso |
@capgo/capacitor-file-sharer |
Usa il proprio FileProvider e MediaStore per save(), nessuna configurazione necessaria |
@capgo/capacitor-downloader |
Scarica nello storage esterno dell'app, poi usa save() per esportare |
@capgo/capacitor-file-picker |
Basato su picker, nessuna autorizzazione su Android 13+ |
@capgo/capacitor-photo-library |
Il suo manifest aggiunge READ_MEDIA_* per la lettura completa della libreria, soggetto alla policy di Play sopra. pickMedia() usa il picker di sistema e non richiede autorizzazioni, quindi se usi solo quello, rimuovi READ_MEDIA_* con tools:node="remove" |
Quale directory usare
- Dati app, impostazioni, database:
Directory.Data - File temporanei ed esportazioni condivise:
Directory.Cache - Media di grandi dimensioni scaricabili:
Directory.Externalsu Android,Directory.LibraryNoCloudsu iOS - Un file che l'utente vuole in Download o Immagini: MediaStore tramite
FileSharer.save() - Un file che l'utente porta nell'app: picker, poi copia in
Directory.Data - Una cartella in cui l'utente vuole che l'app continui a scrivere: URI tree SAF con autorizzazione persistente
Per la lettura e la scrittura complete di API, consulta il guida alla gestione dei file Capacitor.
Migrazione di un'app esistente
- Cerca nel tuo code per
Directory.Documents,Directory.ExternalStorage,/storage/emulated, erequestLegacyExternalStorage. - Sposta i file di proprietà dell'app in
Directory.DataoppureDirectory.External. Al primo avvio della nuova versione, copia i vecchi file che puoi ancora leggere, quindi eliminali. - Sostituisci le funzionalità di "salva su dispositivo" con
FileSharer.save(). - Sostituisci gli input di percorso con un picker.
- Aggiungi
android:maxSdkVersionalle autorizzazioni di storage legacy e rimuoviREAD_MEDIA_*a meno che non ti serva. - Testa su dispositivi o emulatori Android 10, 11, 13 e 15 o 16. Il comportamento cambia esattamente a quei confini.
preserveLegacyExternalStorage="true" nel <application> il tag mantiene l'accesso legacy per le app aggiornate da una versione che usava requestLegacyExternalStorage, finché l'app non viene reinstallata. È utile solo per un passaggio di migrazione una tantum, quindi non contarci.
Risoluzione problemi
EACCES (Permission denied) scrittura su /storage/emulated/0/...: stai scrivendo direttamente nello storage condiviso. Usa MediaStore o una directory dell'app.
File salvato con Directory.Documents scompare dopo la reinstallazione: l'app ne ha perso la proprietà. Usa MediaStore per i file utente.
FileNotFoundException lettura di un content:// URI in seguito: l'autorizzazione temporanea è scaduta. Copia il file quando viene selezionato, oppure rendi persistente l'autorizzazione per gli URI tree.
Avviso di Play Console sulle autorizzazioni per foto e video: un plugin ha aggiunto READ_MEDIA_IMAGES oppure READ_MEDIA_VIDEO. Rimuovili con tools:node="remove" se non ti servono.
requestPermissions() restituisce granted ma le scritture falliscono comunque: su Android 11+ il permesso di storage legacy è concesso ma privo di significato per lo storage condiviso.
Mantieni sincronizzate le parti native e web
Le regole di storage sono configurazione nativa, quindi le modifiche al manifest o ai plugin richiedono un rilascio sullo store, che Build Capgo può generare nel cloud. La logica file attorno a questi elementi (path, nomi delle cartelle, flussi di export) è JavaScript che puoi aggiornare con Live updates di Capgo.