Android Scoped Storage begrenzt, was eine Capacitor-App außerhalb ihrer eigenen Ordner lesen und schreiben darf. Seit Android 10, und vollständig seit Android 11, kann eine App ihre privaten Verzeichnisse frei nutzen, Dateien über MediaStore zu gemeinsamen Sammlungen hinzufügen und für alles andere muss der Nutzer über das Storage Access Framework auswählen. Dieser Guide erklärt, was sich pro Android-Version geändert hat, welche Capacitor Directory Werte weiterhin funktionieren und wie man Dateien auf Capacitor 8 auf unterstützte Weise speichert, importiert und exportiert.
Was sich geändert hat, Version für Version
| Android | API | Wichtige Änderung für Capacitor-Apps |
|---|---|---|
| 10 | 29 | Scoped Storage eingeführt. Apps können opt-outen mit requestLegacyExternalStorage="true" solange sie auf API 29 targeten |
| 11 | 30 | Scoped Storage ist Pflicht für Apps, die 30+ targeten. requestLegacyExternalStorage ignoriert. WRITE_EXTERNAL_STORAGE gewährt keine Berechtigungen mehr. MANAGE_EXTERNAL_STORAGE eingeführt. Tree Picker kann Download-Root oder Volume-Root nicht auswählen |
| 12 | 31-32 | Keine größeren Speicheränderungen |
| 13 | 33 | READ_EXTERNAL_STORAGE wird ersetzt durch READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO. Keine Berechtigung gewährt Zugriff auf die Dokumente anderer Apps |
| 14 | 34 | Teilweiser Fotozugriff mit READ_MEDIA_VISUAL_USER_SELECTED |
| 15 und 16 | 35-36 | Kein neues Speichermodell, Play-Richtlinie zu Foto- und Videoberechtigungen wird durchgesetzt |
Capacitor 8 targetet API 36, sodass deine App auf modernen Geräten immer mit vollem Scoped Storage läuft. Es gibt kein Flag, um das alte Verhalten wiederherzustellen.
Was in älteren Capacitor code kaputtging
Code, geschrieben für Capacitor 2 oder 3 oder von Cordova portiert, schlägt auf neuen Geräten oft still fehl:
Filesystem.writeFile({ directory: Directory.Documents })oderDirectory.ExternalStorageum „auf dem Telefon zu speichern“. Ab Android 11+ zeigen diese Pfade auf den Shared Storage; Schreibvorgänge schlagen fehl oder die Datei kann nach einer Neuinstallation nicht mehr gelesen werden.- Das Lesen beliebiger Pfade wie
/storage/emulated/0/Download/file.pdfvon einem in den Einstellungen eingegebenen oder von einem Server empfangenen Pfad. - Anfordern
READ_EXTERNAL_STORAGEum die Dokumente des Anzeigens zu listen. Auf Android 13+ hat die Berechtigung für Nicht-Medien-Dateien keine Wirkung. - Verwendung
Filesystem.requestPermissions()und unter der Annahmegrantedbedeutet, dass die App überall schreiben kann. - Dateipfade aus einer
content://URI, das von einem Picker zurückgegeben wird.
Was ohne Berechtigung weiterhin funktioniert
Die eigenen Verzeichnisse der App sind von Scoped Storage nicht betroffen und benötigen keine Berechtigung:
Capacitor Directory |
Android-Pfad | Für den Benutzer sichtbar | Bei Deinstallation entfernt |
|---|---|---|---|
Data, Library, LibraryNoCloud |
/data/data/<package>/files |
Nein | Ja |
Cache |
/data/data/<package>/cache |
Nein | Ja |
External |
/storage/emulated/0/Android/data/<package>/files |
Nur per USB oder über einige Dateimanager | Ja |
ExternalCache |
/storage/emulated/0/Android/data/<package>/cache |
Dasselbe | Ja |
Documents |
Freigegeben Documents Ordner |
Ja | Nein |
ExternalStorage |
Root des gemeinsamen Speichers | Ja | Nein |
Die ersten vier Zeilen sind sicher. Documents und ExternalStorage sind die Legacy-Zeilen: Seit Android 11 kann deine App dort nur Dateien sehen, die sie selbst erstellt hat, und verliert nach einer Neuinstallation sogar den Zugriff auf diese. Behandle sie für neue code als nicht unterstützt.
External ist nützlich für große Dateien (Offline-Videos, Karten), da er auf einer größeren Partition liegen kann und Dateien dort nicht zu Auto Backup gehören. Seit Android 11 können andere Apps nicht lesen Android/data, daher ist es in der Praxis immer noch privat.
Dateien speichern, die der Benutzer finden kann: MediaStore
Wenn der Nutzer auf „Herunterladen“ bei einer Rechnung tippt, erwartet er sie im Downloads-Ordner. Ab Android 10 können Apps Dateien in die geteilten Downloads, Pictures, Movies, Music und Documents Sammlungen über MediaStore einfügen, ohne dass Berechtigungen erforderlich sind.
@capgo/capacitor-file-sharer kapselt dies 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
Auf iOS öffnet derselbe Aufruf das Share-Menu, wo der Nutzer „In Dateien speichern“ auswählt, sodass ein einziger code-Pfad beide Plattformen abdeckt.
On Android 9 and lower, writing to shared storage still needs WRITE_EXTERNAL_STORAGE. Deklariere sie nur für diese Versionen:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Capacitor 8 unterstützt Android 7 (API 24) und neuer, also behalte diese Zeile bei, wenn du noch auf Android-7- bis 9-Geräte auslieferst.
Fotos und Videos
Nutze für Medien die Plattform-Workflows, statt Dateien selbst zu schreiben. @capacitor/camera schreibt in die Galerie mit saveToGallery: true, und benötigt nur Legacy-Speicherberechtigungen auf älteren Android-Versionen. Siehe unseren Leitfaden zu Fotos aufnehmen und bearbeiten in Capacitor.
Benutzerdateien lesen: Picker statt Berechtigungen
Scoped Storage erwartet, dass der Benutzer Dateien auswählt. Der Android-Picker gibt eine content:// URI zurück, die Ihre App lesen kann, obwohl sie keine Speicherberechtigung hat.
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,
});
Für Fotos und Videos @capgo/capacitor-file-pickers pickMedia und @capacitor/cameras chooseFromGallery verwenden Sie den Android Photo Picker auf Android 13+, der keinerlei Berechtigung benötigt.
READ_MEDIA_*-Berechtigungen vermeiden
Die Richtlinie von Google Play zu Foto- und Videoberechtigungen erlaubt nur READ_MEDIA_IMAGES und READ_MEDIA_VIDEO für Apps, deren Hauptzweck einen breiten Zugriff auf die Galerie erfordert, wie z. B. Galerie-Apps oder Fotoeditoren. Alle anderen müssen den Picker verwenden und dies in der Play Console deklarieren. Falls ein Plugin, das du verwendest, diese Berechtigungen deklariert, entferne sie aus dem gemergten Manifest:
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
Füge xmlns:tools="http://schemas.android.com/tools" zum <manifest> zuerst taggen.
Ordnerzugriff mit dem Storage Access Framework
Einige Apps benötigen dauerhaften Zugriff auf einen Ordner: eine Notizen-App, die mit einem Benutzerordner synchronisiert, oder ein Export-Ordner für eine Field-App. Das Storage Access Framework (SAF) ermöglicht es dem Nutzer, einen Ordner mit ACTION_OPEN_DOCUMENT_TREE:
const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI
Drei Dinge, die man wissen sollte:
- Das Ergebnis ist eine Tree-URI, kein Pfad. Du kannst sie nicht an
Filesystem.writeFileübergeben. Dateien darin werden überDocumentsContractoderDocumentFilein nativem code erstellt und gelesen. - Zugriff bleibt nach Neustart nicht erhalten es sei denn, deine App ruft
takePersistableUriPermissionauf der URI auf. Füge eine kleine native Methode hinzu, falls du das benötigst:
@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();
}
- Für einige Ordner kann keine Berechtigung erteilt werden. Ab Android 11+ verweigert der Picker den Root des internen Speichers, den Root des
DownloadOrdners undAndroid/dataundAndroid/obb. Ask users to create or pick a subfolder.
Persistierte Berechtigungen sind pro App in ihrer Anzahl begrenzt, und Nutzer können sie in den Systemeinstellungen oder durch das Löschen der App-Daten widerrufen. Prüfe den Zugriff mit getPersistedUriPermissions() beim Start und frage erneut, wenn er fehlt.
Zugriff auf alle Dateien: MANAGE_EXTERNAL_STORAGE
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Diese Berechtigung ermöglicht das alte Verhalten „alles lesen und schreiben“, ist aber kein Runtime-Prompt. Der Nutzer muss sie auf einer eigenen Einstellungsseite aktivieren, und Google Play prüft sie manuell. Zulässige Use Cases sind Dateimanager, Backup und Restore, Antivirus, Dokumentenmanagement, Dateisuche auf dem Gerät und einige weitere, bei denen breiter Zugriff die Kernfunktion ist. Eine Notiz-App, ein PDF-Viewer, der Dateien aus Downloads öffnet, oder eine App, die „nur Exporte speichern muss“, wird abgelehnt.
Wenn du außerhalb von Play verteilst (Enterprise-MDM, sideloadete Kiosk-Geräte), ist es eine Option. Andernfalls: Designe um MediaStore und SAF herum.
Wie andere Plugins passen
| Plugin | Auswirkungen von Scoped Storage |
|---|---|
@capacitor/filesystem |
App-Verzeichnisse sind unproblematisch. Documents und ExternalStorage sind unter Android 11+ veraltet. requestPermissions() betrifft nur diese Verzeichnisse auf altem Android |
@capacitor/camera |
Nutzt Systemkamera und Photo Picker, keine Speicherberechtigung außer saveToGallery auf Android 9 und älter |
@capacitor/share |
Teilt über einen FileProvider. Nur Pfade, die in res/xml/file_paths.xml können geteilt werden |
@capgo/capacitor-file-sharer |
Nutzt sein eigenes FileProvider und MediaStore für save(), keine Konfiguration erforderlich |
@capgo/capacitor-downloader |
Downloads in den externen App-Speicher, dann verwenden save() zum Exportieren |
@capgo/capacitor-file-picker |
Picker-basiert, keine Berechtigung auf Android 13+ |
@capgo/capacitor-photo-library |
Sein Manifest fügt hinzu READ_MEDIA_* for full library reading, subject to the Play policy above. pickMedia() verwendet den System-Picker und benötigt keine Berechtigung, also wenn das alles ist, was du nutzt, entferne READ_MEDIA_* mit tools:node="remove" |
Welches Verzeichnis verwenden
- App-Daten, Einstellungen, Datenbanken:
Directory.Data - Temporäre Dateien und geteilte Exporte:
Directory.Cache - Große, erneut herunterladbare Medien:
Directory.Externalauf AndroidDirectory.LibraryNoCloudunter iOS - Eine Datei, die der Nutzer in Downloads oder Bilder haben will: MediaStore über
FileSharer.save() - Eine Datei, die der Nutzer in die App bringt: Picker, dann kopieren nach
Directory.Data - Ein Ordner, in den die App weiterhin schreiben soll: SAF-Tree-URI mit persistiertem Grant
Für die vollständige Lese- und Schreib-API siehe die Capacitor-Dateiverwaltung.
Bestehende App migrieren
- Suche in deinem code nach
Directory.Documents,Directory.ExternalStorage,/storage/emulated, undrequestLegacyExternalStorage. - App-eigene Dateien verschieben nach
Directory.DataoderDirectory.External. Kopiere beim ersten Start der neuen Version alle alten Dateien, die du noch lesen kannst, und lösche sie danach. - Ersetze „Speichern auf Gerät“-Funktionen durch
FileSharer.save(). - Ersetze Pfad-Eingaben durch einen Picker.
- Füge
android:maxSdkVersionzu Legacy-Speicherberechtigungen und entferneREAD_MEDIA_*es sei denn, du brauchst es. - Teste auf Geräten oder Emulatoren mit Android 10, 11, 13 und 15 oder 16. Das Verhalten unterscheidet sich genau an diesen Grenzen.
preserveLegacyExternalStorage="true" im <application> der Tag behält den Legacy-Zugriff für Apps, die von einer Version aktualisiert wurden, die requestLegacyExternalStorage, bis die App neu installiert wird. Er hilft nur bei einem einmaligen Migrationsschritt, verlass dich also nicht darauf.
Fehlerbehebung
EACCES (Permission denied) Schreiben in /storage/emulated/0/...: Du schreibst direkt in den gemeinsamen Speicher. Verwende MediaStore oder ein App-Verzeichnis.
Datei gespeichert mit Directory.Documents ist nach der Neuinstallation verschwunden: Die App hat das Eigentum daran verloren. Verwende MediaStore für Benutzerdateien.
FileNotFoundException eine content:// URI später: Die temporäre Berechtigung ist abgelaufen. Kopiere die Datei beim Auswählen oder sichere die Berechtigung für Tree-URIs.
Play-Console-Warnung zu Foto- und Video-Berechtigungen: Ein Plugin hat READ_MEDIA_IMAGES oder READ_MEDIA_VIDEO. Entferne sie mit tools:node="remove" , falls du sie nicht benötigst.
requestPermissions() returns granted but writes still fail: Auf Android 11+ wird die Legacy-Speicherberechtigung erteilt, ist für den gemeinsamen Speicher aber bedeutungslos.
Native und Web-Komponenten synchron halten
Speicherregeln sind eine native Konfiguration, daher erfordern Änderungen am Manifest oder an Plugins einen Store-Release, was Capgo Build können in der Cloud erstellt werden. Die Dateilogik rund um sie (Pfade, Ordnernamen, Export-Flows) ist JavaScript, das du mit Capgo Live Updates.