Le scoped storage Android limite ce qu'une application Capacitor peut lire et écrire en dehors de ses propres dossiers. Depuis Android 10, et de façon complète depuis Android 11, une application peut librement utiliser ses répertoires privés, ajouter des fichiers aux collections partagées via MediaStore, et doit faire choisir à l'utilisateur tout le reste via le Storage Access Framework. Ce guide explique ce qui a changé selon la version d'Android, quels Capacitor Directory valeurs fonctionnent encore, et comment enregistrer, importer et exporter des fichiers selon la méthode prise en charge sur Capacitor 8.
Ce qui a changé, version par version
| Android | API | Changement important pour les apps Capacitor |
|---|---|---|
| 10 | 29 | Introduction du Scoped storage. Les apps peuvent le désactiver avec requestLegacyExternalStorage="true" tout en ciblant API 29 |
| 11 | 30 | Le scoped storage est imposé pour les apps ciblant l'API 30+. requestLegacyExternalStorage ignoré. WRITE_EXTERNAL_STORAGE n'accorde plus rien. MANAGE_EXTERNAL_STORAGE introduced. Tree picker cannot select Download root or volume root |
| 12 | 31-32 | Aucun changement majeur de stockage |
| 13 | 33 | READ_EXTERNAL_STORAGE est remplacée par READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO. Aucune autorisation n’accorde l’accès aux documents d’autres applications |
| 14 | 34 | Accès partiel aux photos avec READ_MEDIA_VISUAL_USER_SELECTED |
| 15 et 16 | 35-36 | Pas de nouveau modèle de stockage, politique Play sur les autorisations photo et vidéo appliquée |
Capacitor 8 cible API 36, votre application fonctionne donc toujours avec le scoped storage complet sur les appareils modernes. Aucun flag ne permet de revenir à l’ancien comportement.
Ce qui a cassé dans les anciennes Capacitor code
Code écrit pour Capacitor 2 ou 3, ou porté depuis Cordova, échoue souvent silencieusement sur les nouveaux appareils :
Filesystem.writeFile({ directory: Directory.Documents })ouDirectory.ExternalStoragevers « enregistrer sur le téléphone ». Sur Android 11+, ces chemins pointent vers le stockage partagé, et les écritures échouent ou le fichier ne peut pas être relu après réinstallation.- Lire des chemins arbitraires tels que
/storage/emulated/0/Download/file.pdfà partir d’un chemin saisi dans un paramètre ou reçu d’un serveur. - Demander
READ_EXTERNAL_STORAGEpour lister les documents de l’utilisateur. Sur Android 13+, l’autorisation n’a aucun effet pour les fichiers non multimédias. - Utiliser
Filesystem.requestPermissions()et supposer quegrantedsignifie que l’app peut écrire n’importe où. - Construire des chemins de fichiers à partir d’une
content://URI renvoyée par un sélecteur.
Ce qui fonctionne toujours sans aucune permission
Les répertoires propres à votre application ne sont pas affectés par le scoped storage et n’exigent aucune permission :
Capacitor Directory |
Chemin Android | Visible par l'utilisateur | Supprimé lors de la désinstallation |
|---|---|---|---|
Data, Library, LibraryNoCloud |
/data/data/<package>/files |
Non | Oui |
Cache |
/data/data/<package>/cache |
Non | Oui |
External |
/storage/emulated/0/Android/data/<package>/files |
Only via USB or some file managers | Oui |
ExternalCache |
/storage/emulated/0/Android/data/<package>/cache |
Idem | Oui |
Documents |
Partagé Documents dossier |
Oui | Non |
ExternalStorage |
Racine du stockage partagé | Oui | Non |
Les quatre premières lignes sont sûres. Documents et ExternalStorage sont les lignes historiques : depuis Android 11, votre application ne peut y voir que les fichiers qu'elle a elle-même créés, et après une réinstallation, elle perd l'accès même à ceux-ci. Considérez-les comme non prises en charge pour les nouveaux code.
External est utile pour les fichiers volumineux (vidéos hors ligne, cartes) car il peut se trouver sur une partition plus grande, et les fichiers qui s'y trouvent ne font pas partie d'Auto Backup. Depuis Android 11, les autres applications ne peuvent pas lire Android/data, il reste donc privé en pratique.
Saving files the user can find: MediaStore
Quand l'utilisateur appuie sur « Télécharger » pour une facture, il s'attend à la retrouver dans le dossier Téléchargements. Sur Android 10+, les applications peuvent insérer des fichiers dans les Downloads, Pictures, Movies, Music et Documents collections partagées via MediaStore, sans aucune permission.
@capgo/capacitor-file-sharer enveloppe ceci dans 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
Sur iOS, le même appel ouvre la feuille de partage, où l'utilisateur choisit « Enregistrer dans Fichiers » : un seul chemin code couvre donc les deux plateformes.
On Android 9 and lower, writing to shared storage still needs WRITE_EXTERNAL_STORAGE. Ne le déclarez que pour ces versions :
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Capacitor 8 prend en charge Android 7 (API 24) et versions supérieures ; si vous déployez encore sur des appareils Android 7 à 9, conservez cette ligne.
Photos et vidéos
Pour les médias, privilégiez les parcours natifs plutôt que d'écrire les fichiers vous-même. @capacitor/camera écrit dans la galerie avec saveToGallery: true, et n'a besoin des permissions de stockage legacy que sur les anciennes versions d'Android. Consultez notre guide pour prendre et modifier des photos dans Capacitor.
Lire les fichiers de l'utilisateur : des pickers plutôt que des permissions
Le scoped storage suppose que l'utilisateur choisisse lui-même les fichiers. Le picker Android renvoie un content:// URI que votre application peut lire, même sans permission de stockage.
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,
});
Pour les photos et vidéos, @capgo/capacitor-file-pickerde pickMedia et @capacitor/camerade chooseFromGallery utilisez le sélecteur de photos Android sur Android 13+, qui ne nécessite aucune permission.
Éviter les permissions READ_MEDIA_*
La politique de permissions photos et vidéos de Google Play n'autorise que READ_MEDIA_IMAGES et READ_MEDIA_VIDEO pour les applications dont le cœur de métier nécessite un accès large à la galerie, comme les applications de galerie ou les éditeurs photo. Tous les autres doivent utiliser le sélecteur et le déclarer dans Play Console. Si un plugin que vous utilisez déclare ces permissions, supprimez-les du manifeste fusionné :
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
Ajoutez xmlns:tools="http://schemas.android.com/tools" aux <manifest> tag en premier.
Accès aux dossiers via le Storage Access Framework
Certaines applications ont besoin d'un accès continu à un dossier : une application de notes synchronisée vers un dossier utilisateur, un dossier d'export pour une application de terrain. Le Storage Access Framework (SAF) permet à l'utilisateur d'accorder un dossier avec ACTION_OPEN_DOCUMENT_TREE:
const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI
Trois choses à savoir :
- Le résultat est une URI de type tree, pas un chemin. Vous ne pouvez pas la transmettre à
Filesystem.writeFile. Les fichiers qu'il contient sont créés et lus viaDocumentsContractouDocumentFileen code natif. - L'accès ne survit pas à un redémarrage sauf si votre app appelle
takePersistableUriPermissionsur l'URI. Ajoutez une petite méthode native si nécessaire :
@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();
}
- Certains dossiers ne peuvent pas être accordés. Sur Android 11+, le sélecteur refuse la racine du stockage interne, la racine du
Downloaddossier, etAndroid/dataetAndroid/obb. Ask users to create or pick a subfolder.
Les autorisations persistantes sont limitées en nombre par application, et l’utilisateur peut les révoquer dans les paramètres système ou en effaçant les données de l’application. Vérifiez l’accès avec getPersistedUriPermissions() au démarrage et redemandez si l’accès a été retiré.
Accès à tous les fichiers : MANAGE_EXTERNAL_STORAGE
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Cette permission offre l’ancien comportement « lire et écrire tout », mais elle ne déclenche pas de prompt au moment de l’exécution. L’utilisateur doit l’activer sur un écran de paramètres dédié, et Google Play l’examine manuellement. Les cas d’usage acceptés sont les gestionnaires de fichiers, la sauvegarde et la restauration, l’antivirus, la gestion de documents, la recherche de fichiers sur l’appareil, et quelques autres où l’accès large est la fonctionnalité principale. Une application de prise de notes, un lecteur PDF qui ouvre des fichiers depuis Téléchargements, ou une application qui « a juste besoin d’enregistrer des exports » sera rejetée.
Si vous distribuez en dehors de Play (MDM d’entreprise, appareils kiosques en sideload), c’est une option. Sinon, concevez autour de MediaStore et de SAF.
Comment les autres plugins s’intègrent
| Plugin | Impact du scoped storage |
|---|---|
@capacitor/filesystem |
Aucun problème avec les répertoires de l'app. Documents et ExternalStorage sont obsolètes sur Android 11+ requestPermissions() ne concerne que ces répertoires sur les anciens Android |
@capacitor/camera |
Utilise l'appareil photo système et le Photo Picker, sans permission de stockage sauf saveToGallery sur Android 9 et versions antérieures |
@capacitor/share |
Partage via un FileProvider. Seuls les chemins listés dans res/xml/file_paths.xml peuvent être partagés |
@capgo/capacitor-file-sharer |
Utilise son propre FileProvider et MediaStore pour save(), aucune config requise |
@capgo/capacitor-downloader |
Télécharge dans le stockage externe de l’app, puis utilisez save() pour exporter |
@capgo/capacitor-file-picker |
Basé sur le picker, aucune permission sur Android 13+ |
@capgo/capacitor-photo-library |
Son manifest ajoute READ_MEDIA_* for full library reading, subject to the Play policy above. pickMedia() utilise le picker système et ne nécessite aucune permission ; si c'est tout ce que vous utilisez, supprimez READ_MEDIA_* par tools:node="remove" |
Quel répertoire utiliser
- Données de l'app, paramètres, bases de données :
Directory.Data - Fichiers temporaires et exports de partage :
Directory.Cache - Médias volumineux re-téléchargeables :
Directory.Externalsur Android,Directory.LibraryNoCloudsur iOS - Un fichier que l'utilisateur veut dans Downloads ou Pictures : MediaStore via
FileSharer.save() - Un fichier que l'utilisateur importe dans l'app : picker, puis copie dans
Directory.Data - Un dossier dans lequel l'utilisateur veut que l'app continue d'écrire : URI tree SAF avec autorisation persistée
Pour la lecture et l'écriture complètes de API, voir le guide de gestion des fichiers Capacitor.
Migrer une application existante
- Recherchez dans votre code :
Directory.Documents,Directory.ExternalStorage,/storage/emulated, etrequestLegacyExternalStorage. - Déplacer les fichiers de l’app vers
Directory.DataouDirectory.External. Lors du premier lancement de la nouvelle version, copiez tous les anciens fichiers que vous pouvez encore lire, puis supprimez-les. - Replace “save to device” features with
FileSharer.save(). - Remplacez les champs de chemin par un sélecteur.
- Ajoutez
android:maxSdkVersionaux permissions de stockage héritées et supprimezREAD_MEDIA_*à moins d'en avoir besoin. - Testez sur des appareils ou émulateurs Android 10, 11, 13 et 15 ou 16. Le comportement diffère précisément à ces seuils.
preserveLegacyExternalStorage="true" dans l' <application> le tag conserve l'accès hérité pour les apps mises à jour depuis une version qui utilisait requestLegacyExternalStorage, jusqu'à la réinstallation de l'app. Cela n'aide que lors d'une étape de migration ponctuelle, donc ne vous y fiez pas.
Troubleshooting
EACCES (Permission denied) écriture dans /storage/emulated/0/...: vous écrivez directement dans le stockage partagé. Utilisez MediaStore ou un répertoire d'application.
Fichier enregistré avec Directory.Documents disparaît après réinstallation: l'application en a perdu la propriété. Utilisez MediaStore pour les fichiers utilisateur.
FileNotFoundException lecture d'un content:// URI plus tard: l'autorisation temporaire a expiré. Copiez le fichier lors de la sélection, ou persistez l'autorisation pour les URI d'arborescence.
Avertissement Play Console concernant les permissions photo et vidéo: un plugin a ajouté READ_MEDIA_IMAGES ou READ_MEDIA_VIDEO. Supprimez-les avec tools:node="remove" si vous n'en avez pas besoin.
requestPermissions() renvoie granted mais les écritures échouent toujours: sur Android 11+, l'ancienne permission de stockage est accordée mais sans effet pour le stockage partagé.
Gardez les parties natives et web synchronisées
Les règles de stockage sont de la configuration native : toute modification du manifest ou des plugins nécessite une release sur les stores, ce qui Build Capgo peut être fait dans le cloud. La logique de fichiers autour (chemins, noms de dossiers, flux d'export) est du JavaScript que vous pouvez mettre à jour avec les live updates Capgo.