Reputation: 755
My React Native Android application needs to save a JSON file in the special folder located in the external storage. I am trying to do it using RNFS (https://github.com/itinance/react-native-fs) this way:
const saveData = async () => {
var path = `${RNFS.ExternalStorageDirectoryPath}/MyApp`;
RNFS.mkdir(path);
path += '/data.json';
RNFS.writeFile(path, JSON.stringify(getData()), 'utf8')
.then((success) => {
console.log('Success');
})
.catch((err) => {
console.log(err.message);
});
}
It works well but fails on Android Q device. This error is shown:
Error: Directory could not be created
If I try to write a plain file without creating directory it throws this error:
ENOENT: open failed: ENOENT (No such file or directory), open '/storage/emulated/0/data.json'
However, I've added this permissions to my AndroidManifest.xml
:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
And granted the external storage permissions in the settings. But if I change the RNFS.ExternalStorageDirectoryPath
to RNFS.DocumentDirectoryPath
it works without any errors. But I need to get an access to the external storage. Is there any way to do it?
Upvotes: 5
Views: 28694
Reputation: 21
The following path should results in the file "banana" being in the folder Internal storage > Download on the phone, assuming the above mentioned write permissions are in place:
const path = RNFS.ExternalStorageDirectoryPath/Download/banana;
You must append "/Download" to the path. Files can then be found in "Internal storage > Download".
Note: Initially I was trying to create a file in the top-level RNFS.ExternalStorageDirectoryPath but that failed to show up on the phone for the user. Appending the /Download fixes it and the file "banana" now exists in the folder "Internal storage > Download" on the phone.
Conclusion:
Without /Download it fails to create the file banana anywhere that I can see:
/storage/emulated/0/banana
With /Download it successfully creates the user-accessible file:
/storage/emulated/0/Download/banana
Upvotes: 1
Reputation: 165
UPDATE : USE SCOPED STORAGE
Alternatively, I found this library react-native-file-access which is use Scoped Storage. in my case, I stored the file in Download directory.
First, with RNSF we need to download the file and set the destination at RNFS.TemporaryDirectoryPath
, then copy the downloaded file with react-native-file-access to specific folder (Download in my case).
FileSystem.cpExternal(localFileUrl, `${filenameFormatted}`,'downloads')
maybe you can use this library to store file in your specific directory with some adjustment
USE PERMISSION MANAGE EXTERNAL STORAGE (NOT RECOMMENDED) :
In Android 11 and above, the permission for accessing storage has changed. you have to add additional permission called MANAGE_EXTERNAL_STORAGE, see these post :
https://developer.android.com/about/versions/11/privacy/storage#directory-access https://developer.android.com/training/data-storage/manage-all-files
add this in your AndroidManifest.xml
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
i create simple permisson request based on this post call this native function from react native.
create PermissionFileModule.java in app/src/main/<your_package>
public class PermissionFileModule extends ReactContextBaseJavaModule implements ActivityEventListener {
public PermissionFileModule(@Nullable ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addActivityEventListener(this);
}
@NonNull
@Override
public String getName() {
return "PermissionFile";
}
@ReactMethod
public void checkAndGrantPermission(Callback errorCallback, Callback successCallback) {
try {
if (!checkPermission()) {
requestPermission();
successCallback.invoke(false);
} else {
successCallback.invoke(true);
}
} catch (IllegalViewOperationException e) {
errorCallback.invoke(e.getMessage());
}
}
private boolean checkPermission() {
if (SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
} else {
int result = ContextCompat.checkSelfPermission(getReactApplicationContext(), READ_EXTERNAL_STORAGE);
int result1 = ContextCompat.checkSelfPermission(getReactApplicationContext(), WRITE_EXTERNAL_STORAGE);
return result == PackageManager.PERMISSION_GRANTED && result1 == PackageManager.PERMISSION_GRANTED;
}
}
private void requestPermission() {
if (SDK_INT >= Build.VERSION_CODES.R) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.addCategory("android.intent.category.DEFAULT");
intent.setData(Uri.parse(String.format("package:%s",getReactApplicationContext().getPackageName())));
getCurrentActivity().startActivityForResult(intent, 2296);
} catch (Exception e) {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
getCurrentActivity().startActivityForResult(intent, 2296);
}
} else {
//below android 11
ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{WRITE_EXTERNAL_STORAGE}, 100);
}
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == 2296) {
if (SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
Toast.makeText(getReactApplicationContext(), "Access granted", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getReactApplicationContext(), "Access not granted", Toast.LENGTH_SHORT).show();
}
}
}
}
@Override
public void onNewIntent(Intent intent) {
// do nothing
}
}
create PermissionFilePackage.java in app/src/main/<your_package>
public class PermissionFilePackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new PermissionFileModule(reactContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
in MainApplication.java, add PermissionFilePackage.java as additional package
...
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new PermissionFilePackage());
return packages;
}
...
in your RN Component, call the permission file like this
...
import {NativeModules} from 'react-native';
var PermissionFile = NativeModules.PermissionFile;
...
if (Platform.Version >= 30) {
PermissionFile.checkAndGrantPermission(
(err) => {
DeviceUtils.showAlert(
'Sorry',
'Access not granted',
);
},
(res) => {
if (res) {
checkDirectoryAndDownload(url, name, ext);
}
},
);
} else {
DeviceUtils.grantPermissionSingleAndroid(
PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
(isAllow) => {
if (isAllow) {
checkDirectoryAndDownload(url, name, ext);
} else {
DeviceUtils.showAlert(
'Sorry',
'Access not granted',
);
}
},
);
}
Upvotes: 5
Reputation: 652
With Android version 10 (API level 29) the concept of scoped storage is introduced. According to Docs-
"To give users more control over their files and to limit file clutter, apps that target Android 10 (API level 29) and higher are given scoped access into external storage, or scoped storage, by default. Such apps have access only to the app-specific directory on external storage, as well as specific types of media that the app has created."
For Android version 10, you have the option to opt-out of scoped storage by
adding
android:requestLegacyExternalStorage="true"
in the AndroidManifest.xml like this->
<application ...
android:requestLegacyExternalStorage="true"
...>
....
</application>
This is a temporary solution and will work only for android version 10 and not above it. Also make sure you ask run-time permission for read/write external storage.
Upvotes: 2
Reputation: 755
I've found out that legacy external storage access is required from Android API 29+. So, I've editied my AndroidManifest.xml
(that located in android/app/src/main/
) like this:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.appName">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
...
android:requestLegacyExternalStorage="true"
...
>
</application>
</manifest>
And everything has started to work. Also, I've added a request for granting permissions to the saveData
function:
const saveData = async () => {
try {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
]);
} catch (err) {
console.warn(err);
}
const readGranted = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
const writeGranted = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE);
if(!readGranted || !writeGranted) {
console.log('Read and write permissions have not been granted');
return;
}
var path = `${RNFS.ExternalStorageDirectoryPath}/MyApp`;
RNFS.mkdir(path);
path += '/data.json';
RNFS.writeFile(path, JSON.stringify(getData()), 'utf8')
.then((success) => {
console.log('Success');
})
.catch((err) => {
console.log(err.message);
});
}
Upvotes: 5