Aller au contenu principal

Le Scoped Storage d'Android dans les applications Capacitor

Le scoped storage Android dans les apps Capacitor : ce qui a changé d’Android 10 à 14, permissions encore valides, MediaStore, SAF et répertoires sûrs.

Règles du scoped storage Android appliquées aux fichiers d'une application Capacitor

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 }) ou Directory.ExternalStorage vers « 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_STORAGE pour 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 que granted signifie 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 :

  1. 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 via DocumentsContract ou DocumentFile en code natif.
  2. L'accès ne survit pas à un redémarrage sauf si votre app appelle takePersistableUriPermission sur 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();
}
  1. Certains dossiers ne peuvent pas être accordés. Sur Android 11+, le sélecteur refuse la racine du stockage interne, la racine du Download dossier, et Android/data et Android/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.External sur Android, Directory.LibraryNoCloud sur 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

  1. Recherchez dans votre code : Directory.Documents, Directory.ExternalStorage, /storage/emulated, et requestLegacyExternalStorage.
  2. Déplacer les fichiers de l’app vers Directory.Data ou Directory.External. Lors du premier lancement de la nouvelle version, copiez tous les anciens fichiers que vous pouvez encore lire, puis supprimez-les.
  3. Replace “save to device” features with FileSharer.save().
  4. Remplacez les champs de chemin par un sélecteur.
  5. Ajoutez android:maxSdkVersion aux permissions de stockage héritées et supprimez READ_MEDIA_* à moins d'en avoir besoin.
  6. 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.

Live updates pour les apps Capacitor

Lorsqu'un bug de la couche web est en ligne, déployez le correctif via Capgo au lieu d'attendre des jours l'approbation des stores. Les utilisateurs reçoivent la mise à jour en arrière-plan, tandis que les changements natifs suivent le circuit de validation habituel.

Les derniers articles du blog

Capgo vous offre les meilleures informations pour créer une application mobile vraiment professionnelle.