Recommended resources
The freely available resources this lesson will direct you to.
Hello again. In our last session, we installed the Capacitor Filesystem plugin and learned to manage files within your app's private Data directory. This is perfect for internal configuration or cached assets, but many apps need to save files, like documents or images, to a location the user can access.
This lesson focuses on a crucial, platform-specific step for enabling that functionality: implementing runtime permission requests on Android. While writing to your app's internal data directory requires no special approval, accessing shared storage directories like Documents is a different story. Android's security model requires that you explicitly ask the user for permission at the moment you need it. We will cover how to declare these permissions, how to build the request flow in your React code, and the nuances of this system on modern Android versions.
1. The Android Runtime Permission Model
Before Android 6.0 (Marshmallow), users granted all requested permissions at install time. This "all or nothing" approach was inflexible. The modern model requires apps to request "dangerous" permissions—those that access sensitive user data or system features—at runtime. Access to shared external storage falls squarely into this category.
When your app requests a permission, the Android system presents a dialog to the user, who can then allow or deny the request.

Your responsibility as a developer is to trigger this dialog at an appropriate time and gracefully handle either outcome. You should never assume a permission has been granted.
2. Step 1: Declaring Permissions in the Manifest
Even though the permission is requested at runtime, you must first declare your app's intent to use it in the Android manifest file. This file, AndroidManifest.xml, provides essential information about your app to the Android build tools and operating system.
You need to add permissions for reading and writing to external storage.
-
Open your Android project's manifest file, located at
android/app/src/main/AndroidManifest.xml. -
Inside the
<manifest>tag, but outside the<application>tag, add the following two lines:<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
The official Capacitor documentation confirms this requirement.
Filesystem Capacitor Plugin API | Capacitor Documentation
The Android configuration section of the Filesystem docs specifies the necessary manifest entries.
In the "Android" section, review the ">permission tags. Note the documentation's mention that these are required when using Directory.Documents or Directory.ExternalStorage on Android 10 and older. We will discuss the implications for newer versions shortly.
The video tutorial from Simon Grimm also provides a quick visual guide to this step.
The Ionic Image Guide with Capacitor (Capture, Store & Upload)
This video on image handling touches upon the required manifest permissions.
Watch the brief segment from the permissions setup to see exactly where these lines are placed in the AndroidManifest.xml file.
After adding these lines, it's a good practice to sync your project to ensure the native platform is up-to-date.
bunx cap sync android
3. Step 2: Implementing the Permission Request Flow
With the manifest updated, you can now write the code to manage the permission request from your React application. The @capacitor/filesystem plugin provides two essential methods for this: checkPermissions() and requestPermissions().
The best practice is to:
- Check First: Use
checkPermissions()to see the current status (granted,denied, orprompt—meaning the user hasn't been asked yet). - Request if Necessary: If the status is not
granted, callrequestPermissions()to show the system dialog. - Handle the Result: The user's choice will determine whether you can proceed with the file operation.
Let's modify the FileTester component from the previous lesson to save a file to the public Documents directory, incorporating this permission flow.
import React, { useState } from 'react';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { isPlatform } from '@ionic/react';
const FilePermissionsTester: React.FC = () => {
const [permissionStatus, setPermissionStatus] = useState<string>('unknown');
const filePath = 'my-public-document.txt';
const checkAndRequestPermissions = async (): Promise<boolean> => {
// This flow is only required on Android.
if (!isPlatform('android')) {
return true;
}
try {
// Check current permission status
let permStatus = await Filesystem.checkPermissions();
setPermissionStatus(permStatus.publicStorage);
if (permStatus.publicStorage !== 'granted') {
// Request the permission
permStatus = await Filesystem.requestPermissions();
setPermissionStatus(permStatus.publicStorage);
}
return permStatus.publicStorage === 'granted';
} catch (e) {
console.error('Unable to check or request permissions', e);
return false;
}
};
const writePublicFile = async () => {
const hasPermission = await checkAndRequestPermissions();
if (!hasPermission) {
console.error('Permission denied. Cannot write to public storage.');
// Here you would typically show a message to the user.
return;
}
try {
await Filesystem.writeFile({
path: filePath,
data: `This is a public file. Written at ${new Date().toLocaleTimeString()}`,
directory: Directory.Documents,
encoding: Encoding.UTF8,
recursive: true,
});
console.log('Public file written successfully to Documents directory');
} catch (e) {
console.error('Unable to write public file', e);
}
};
return (
<div>
<h2>Android Filesystem Permissions Test</h2>
<button onClick={writePublicFile}>Write to Documents Folder</button>
<p>Current Public Storage Permission: <strong>{permissionStatus}</strong></p>
</div>
);
};
export default FilePermissionsTester;
In this code:
- We use the
isPlatform('android')utility to ensure this logic runs only on Android. iOS handles file permissions differently and doesn't require this explicit runtime flow for theDocumentsdirectory. checkAndRequestPermissionsencapsulates the core logic. It first checks the status and only requests it if not already granted.- The
writePublicFilefunction now acts as a gatekeeper. It calls the permission function and only proceeds if it returnstrue.
You can study the documentation for these two key methods here.
Filesystem Capacitor Plugin API | Capacitor Documentation
The API reference details the permission-handling methods.
Review the definitions for checkPermissions() and requestPermissions(). Notice that they both return a Promise<PermissionStatus>, which is an object containing the status of different permissions. In our case, we're interested in the publicStorage key.
4. Navigating the Complexities of Modern Android
Your extensive experience in development has likely taught you that systems evolve, and what works for one version may not for another. Android storage is a prime example.
Starting with Android 10 and more strictly in Android 11+, Google introduced Scoped Storage. This security feature significantly limits an app's access to external storage. Even with WRITE_EXTERNAL_STORAGE permission, your app can generally only access files that it created within shared directories like Documents. It can no longer freely browse or modify files created by other apps.
Filesystem Capacitor Plugin API | Capacitor Documentation
The documentation for the Directory enum contains critical, version-specific details.
Read the description for the Documents member. Pay close attention to the notes about Android 10 and Android 11+. This explains why, on newer devices, your app is sandboxed even within the "public" documents folder.
This evolving landscape is a common source of bugs. Developers often find that file operations fail on newer Android versions because their code or dependencies are not aligned with the latest platform requirements.
@capacitor/filesystem android 13 storage permission issue - Capacitor - Ionic Forum
This forum thread is a practical case study of this exact problem.
Read the initial problem description where a file download fails on Android 13. Then, see the error message and the community's diagnosis: the code was failing because it didn't include a call to requestPermissions(). This real-world example reinforces the importance of the flow we just implemented.
The key takeaway is that for broad compatibility, you must implement the runtime permission request flow and be mindful of the Scoped Storage limitations on modern Android devices. Always test on a range of Android versions.
Conclusion
You have now equipped your app to handle Android's runtime permissions for filesystem access. This is a non-negotiable step for any app that needs to interact with shared storage and a fundamental concept in modern Android development.
Let's summarize the key points:
- Accessing shared directories like
Documentson Android requires runtime permissions. - You must first declare
READ_EXTERNAL_STORAGEandWRITE_EXTERNAL_STORAGEin yourAndroidManifest.xml. - The standard flow is to use
Filesystem.checkPermissions()before callingFilesystem.requestPermissions(). - Your code must handle both granted and denied permission states gracefully.
- Modern Android versions (10+) enforce Scoped Storage, which further restricts file access even after permission is granted.
In our next lesson, we'll continue exploring plugin integration by tackling biometrics. You will install the @capacitor-community/biometric-auth plugin and learn how to check for hardware and request user authentication, a process that involves its own distinct set of permissions and user interactions.