Amphyx
Amphyx

Reputation: 755

Write files to the Android external storage using React Native FS

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

Answers (4)

Will Thrive
Will Thrive

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

Iqbal Pratomo Santoso
Iqbal Pratomo Santoso

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

mohit arora
mohit arora

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

Amphyx
Amphyx

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

Related Questions