Salta al contenuto principale

Android Scoped Storage nelle app Capacitor

Lo scoped storage di Android nelle app Capacitor: cosa è cambiato da Android 10 a 14, quali permessi funzionano ancora, MediaStore, SAF e directory sicure.

Regole di scoped storage di Android applicate ai file in un'app Capacitor

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 }) oppure Directory.ExternalStorage su "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.pdf da un percorso inserito nelle impostazioni o ricevuto da un server.
  • Richiesta di READ_EXTERNAL_STORAGE per elencare i documenti dell'utente. Su Android 13+ il permesso non ha effetto sui file non multimediali.
  • Usare Filesystem.requestPermissions() e dare per scontato che granted significhi 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:

  1. Il risultato è un URI ad albero, non un percorso. Non puoi passarlo a Filesystem.writeFile. I file interni vengono creati e letti tramite DocumentsContract oppure DocumentFile in code nativo.
  2. L'accesso non sopravvive a un riavvio a meno che la tua app non chiami takePersistableUriPermission sull'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();
}
  1. Alcune cartelle non possono essere concesse. Su Android 11+, il picker rifiuta la radice dello storage interno, la radice della Download cartella e Android/data e Android/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.External su Android, Directory.LibraryNoCloud su 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

  1. Cerca nel tuo code per Directory.Documents, Directory.ExternalStorage, /storage/emulated, e requestLegacyExternalStorage.
  2. Sposta i file di proprietà dell'app in Directory.Data oppure Directory.External. Al primo avvio della nuova versione, copia i vecchi file che puoi ancora leggere, quindi eliminali.
  3. Sostituisci le funzionalità di "salva su dispositivo" con FileSharer.save().
  4. Sostituisci gli input di percorso con un picker.
  5. Aggiungi android:maxSdkVersion alle autorizzazioni di storage legacy e rimuovi READ_MEDIA_* a meno che non ti serva.
  6. 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.

Live updates per le app Capacitor

Quando un bug nel web-layer è live, rilascia la fix tramite Capgo invece di aspettare giorni l'approvazione degli store. Gli utenti ricevono l'update in background, mentre le modifiche native restano nel normale processo di review.

Ultimi articoli dal blog

Capgo ti offre gli insight migliori che ti servono per creare un'app mobile davvero professionale.