Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expo-media-library createAssetAsync calls error in Android 11 even with MediaLibrary Permission. #12060

Closed
aeroholic opened this issue Mar 1, 2021 · 23 comments · Fixed by #14518

Comments

@aeroholic
Copy link

aeroholic commented Mar 1, 2021

Summary

Expo-media-library createAssetAsync calls error "Unable to copy file into external storage." in Android 11 even with MediaLibrary Permission.
MediaLibrary does not require MANAGE_EXTERNAL_STORAGE permission.
But always show error message in Android 11. Works fine in others.
Both SDK 40 and SDK 41 give the same result.

Is there a way to replace it with StorageAccessFramework?
Isn't SAF only able to create string or empty files?
I tried to grant access to the DCIM folder using SAF, but createAssetAsync still doesn't work.

Managed or bare workflow? If you have ios/ or android/ directories in your project, the answer is bare!

managed

What platform(s) does this occur on?

Android

SDK Version (managed workflow only)

40.0.0, 41.0.0.

Environment

Expo CLI 4.1.6 environment info:
System:
OS: Windows 10 10.0.19041
Binaries:
Node: 14.15.1 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.10 - C:\Users\Administrator\AppData\Roaming\npm\yarn.CMD
npm: 6.14.9 - C:\Users\Administrator\AppData\Roaming\npm\npm.CMD
IDEs:
Android Studio: Version 4.1.0.0 AI-201.8743.12.41.6953283
npmPackages:
expo: ^40.0.0 => 40.0.1 (And 41.0.0)
react: 16.13.1 => 16.13.1
react-dom: 16.13.1 => 16.13.1
react-native: https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz => 0.63.2
react-native-web: ~0.13.12 => 0.13.18
Expo Workflow: managed

Device info:
Samsung Galaxy S21 Ultra 5G / Android 11 / Internal storage only

Reproducible demo or steps to reproduce from a blank project

import * as MediaLibrary from "expo-media-library";
import * as FileSystem from "expo-file-system";

static copyToLibraryAsync = async (localUri) => {
    // For example: localUri = FileSystem.cacheDirectory + "image.jpg"
    console.log(localUri)
    // file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540username%252Fappname/image.jpg

    const permissions = await MediaLibrary.getPermissionsAsync();
    console.log(permissions); // { canAskAgain: true, expires: "never", granted: true, status: "granted" }

    try {
        await MediaLibrary.createAssetAsync(localUri)
        // await MediaLibrary.saveToLibraryAsync(localUri); // Same error message
    } catch (e) {
        console.log(e) //  [Error: Unable to copy file into external storage.]
    }
}
@aeroholic aeroholic added the needs validation Issue needs to be validated label Mar 1, 2021
@guillaume-g
Copy link

Hi,

I have exactly the same issue ! 👎
I download a file then when I call createAssetAsync I have the following error : Unable to copy file into external storage.

Thanks in advance for help!

The error is located in CreateAsset.java line 97.

@dolphinflow86
Copy link

Same here

@joseocabarcas
Copy link

Any solutions ? I have the same issue, When I call createAssetAsync I have the following error : Unable to copy file into external storage.
SDK 41
Bare Workflow

@aeroholic aeroholic changed the title expo-media-library incorrectly recognizes file address as external storage on android phone that does not have an external storage. Expo-media-library createAssetAsync calls error in Android 11 even with MediaLibrary Permission. May 29, 2021
@Marko-D
Copy link

Marko-D commented Jun 3, 2021

Same here...

Expo-media-library createAssetAsync calls error "Unable to copy file into external storage." in Android 11.

The app that I'm currently working on downloads all kinds of files (pdf, jpg,png,excell,word...) and before the upgrade to expo 41 it was working as expected.

Any solution or hint will be much appreciated.

Thanks

@cruzach cruzach removed the needs validation Issue needs to be validated label Jun 22, 2021
@cruzach
Copy link
Contributor

cruzach commented Jun 22, 2021

Hi- can someone share a Snack link that we can use to reproduce this error?

@viljark
Copy link

viljark commented Jun 24, 2021

Hi, here is the Snack to reproduce the error on Android 11.
With Samsung A12 (Android 11), the .jpeg file is saved successfully, but .xlsx file throws the error.

https://snack.expo.io/@remato/anxious-churros

Any other ideas how to download .xlsx files to user-accessible location would be appreciated, because this is blocking us from using SDK41.

One way to do this on Android 11 without using media-library is using StorageAccessFramework, but this includes so many steps and is too complicated for users:

  • user has to browse a location
  • there is no way to add any helper text to the folder browsing
  • in Android 11, user can not save to Downloads, but has to create a new folder

@aeroholic
Copy link
Author

aeroholic commented Jun 26, 2021

To save time of people having the same problem as me, this is my code.
I hope this helps someone. Works fine on sdk41 & android 11.

import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library";

static settings = async (newSettings) => {
        return new Promise(async (resolve, reject) => {
            try {
                let settings = (await AsyncStorage.getItem("settings").then((result) => JSON.parse(result))) || {};
                if (newSettings) {
                    settings = Object.assign(settings, newSettings);
                    await AsyncStorage.setItem("settings", JSON.stringify(settings));
                }
                return resolve(settings);
            } catch (e) {
                console.log("Error in settings", e);
                return resolve({});
            }
        });
    };

static getDirectoryPermissions = async (onDirectoryChange) => {
        return new Promise(async (resolve, reject) => {
            try {
                const initial = FileSystem.StorageAccessFramework.getUriForDirectoryInRoot();
                onDirectoryChange({isSelecting: true}) //For handle appStateChange and loading
                const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(initial);
                this.settings({ downloadsFolder: permissions.granted ? permissions.directoryUri : null });
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                onDirectoryChange({downloadsFolder: permissions.granted ? permissions.directoryUri : null, isSelecting: false})
                return resolve(permissions.granted ? permissions.directoryUri : null);
            } catch (e) {
                console.log("Error in getDirectoryPermissions", e);
                onDirectoryChange({downloadsFolder: null})
                return resolve(null);
            }
        });
    };

static downloadFilesAsync = async (files, onDirectoryChange) => {
        // files = [{url: "url", fileName: "new file name" + "extension", mimeType: is_video ? "video/mp4" : "image/jpg"}]
        // onDirectoryChange = () => {cb_something_like_setState()}
        return new Promise(async (resolve, reject) => {
            try {
                const mediaLibraryPermission = await this.getMediaLibraryPermission()
                if (mediaLibraryPermission !== "granted") {
                    return resolve({status: "error"})
                }
                let settings = await this.settings();
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                const androidSDK = Platform.constants.Version
                
                if (Platform.OS === "android" && androidSDK >= 30 && !settings.downloadsFolder) {
                    //Except for Android 11, using the media library works stably
                    settings.downloadsFolder = await this.getDirectoryPermissions(onDirectoryChange)
                }
                const results = await Promise.all(
                    files.map(async (file) => {
                        try {
                        if (file.url.includes("https://")) {
                            // Remote file
                            const { uri, status, headers, md5 } = await FileSystem.downloadAsync(
                                file.url,
                                FileSystem.cacheDirectory + file.name
                            );
                            file.url = uri; //local file(file:///data/~~~/content.jpg)
                            // The document says to exclude the extension, but without the extension, the saved file cannot be viewed in the Gallery app.
                        }
                        if (Platform.OS === "android" && settings.downloadsFolder) {
                            // Creating files using SAF
                            // I think this code should be in the documentation as an example
                            const fileString = await FileSystem.readAsStringAsync(file.url, { encoding: FileSystem.EncodingType.Base64 });
                            const newFile = await FileSystem.StorageAccessFramework.createFileAsync(
                                settings.downloadsFolder,
                                file.name,
                                file.mimeType
                            );
                            await FileSystem.writeAsStringAsync(newFile, fileString, { encoding: FileSystem.EncodingType.Base64 });
                        } else {
                            // Creating files using MediaLibrary
                            const asset = await MediaLibrary.createAssetAsync(file.url);
                        }
                        return Promise.resolve({status: "ok"});
                    } catch (e) {
                        console.log(e)
                        return Promise.resolve({status: "error"});
                    }
                    })
                );
                return resolve({ status: results.every((result) => result.status === "ok") ? "ok" : "error" });
            } catch (e) {
                console.log("Error in downloadFilesAsync", e)
                return resolve({ status: "error" });
            }
        });
    };

@Marko-D
Copy link

Marko-D commented Aug 25, 2021

Is there any progress with this? I've updated to expo 42 and the issue is still here.

@FabrizioCozza
Copy link

Hi, I'm having the same issue, I'm leaving the try-catch error stack in case it helps the devs to understand the problem. Notice I change my local ip for {my-ip} in the json.

{
"nativeStackAndroid":[
{
"methodName":"open",
"lineNumber":492,
"file":"IoBridge.java",
"class":"libcore.io.IoBridge"
},
{
"methodName":"",
"lineNumber":236,
"file":"FileOutputStream.java",
"class":"java.io.FileOutputStream"
},
{
"methodName":"",
"lineNumber":186,
"file":"FileOutputStream.java",
"class":"java.io.FileOutputStream"
},
{
"methodName":"safeCopyFile",
"lineNumber":7,
"file":"MediaLibraryUtils.java",
"class":"abi41_0_0.expo.modules.medialibrary.MediaLibraryUtils"
},
{
"methodName":"createAssetFile",
"lineNumber":6,
"file":"CreateAsset.java",
"class":"abi41_0_0.expo.modules.medialibrary.CreateAsset"
},
{
"methodName":"doInBackground",
"lineNumber":4,
"file":"CreateAsset.java",
"class":"abi41_0_0.expo.modules.medialibrary.CreateAsset"
},
{
"methodName":"doInBackground",
"lineNumber":1,
"file":"CreateAsset.java",
"class":"abi41_0_0.expo.modules.medialibrary.CreateAsset"
},
{
"methodName":"call",
"lineNumber":394,
"file":"AsyncTask.java",
"class":"android.os.AsyncTask$3"
},
{
"methodName":"run",
"lineNumber":266,
"file":"FutureTask.java",
"class":"java.util.concurrent.FutureTask"
},
{
"methodName":"runWorker",
"lineNumber":1167,
"file":"ThreadPoolExecutor.java",
"class":"java.util.concurrent.ThreadPoolExecutor"
},
{
"methodName":"run",
"lineNumber":641,
"file":"ThreadPoolExecutor.java",
"class":"java.util.concurrent.ThreadPoolExecutor$Worker"
},
{
"methodName":"run",
"lineNumber":923,
"file":"Thread.java",
"class":"java.lang.Thread"
}
],
"userInfo":null,
"message":"Unable to copy file into external storage.",
"code":"E_IO_EXCEPTION",
"line":2746,
"column":45,
"sourceURL":"http://{my-ip}:19000/node_modules%5Cexpo%5CAppEntry.bundle?platform=android&dev=true&hot=false&minify=false"
}

@VSV6
Copy link

VSV6 commented Sep 22, 2021

Hi, here is the Snack to reproduce the error on Android 11.
With Samsung A12 (Android 11), the .jpeg file is saved successfully, but .xlsx file throws the error.

https://snack.expo.io/@remato/anxious-churros

Any other ideas how to download .xlsx files to user-accessible location would be appreciated, because this is blocking us from using SDK41.

One way to do this on Android 11 without using media-library is using StorageAccessFramework, but this includes so many steps and is too complicated for users:

  • user has to browse a location
  • there is no way to add any helper text to the folder browsing
  • in Android 11, user can not save to Downloads, but has to create a new folder

I tried your code that I'm able to save the file but 2 issues: -

  • App exits automatically once it saves a .xlsx file into the phone storage.
  • .xlsx file is saving in 'DCIM' folder, but I need to save in 'Download' folder.

@durgesh2007
Copy link

Hello @barthap , Any guidance would be appreciated

Facing the same issue on Expo 45.0.0.

"expo-file-system": "~14.0.0",
"expo-media-library": "~14.1.0",
  1. Same code is work fine for image but not for the .xlsx or .pdf or .doc files are giving error as {"error": [Error: Could not create asset.]}
  2. I'm trying to save .xlsx file but MediaLibrary.createAssetAsync(uri); throughs error as "could not create asset".
  3. I'm also tried to MediaLibrary.saveToLibraryAsync(uri); throughs same error as "could not create asset".

Reproduce devices: Oneplus Nord (Android Version 11) & mi Xiaomi 11 lite NE (Android Version 12)
A reproduce issue on snack expo

@barthap
Copy link
Contributor

barthap commented Jun 13, 2022

This is because the Media library is able to save image/video/audio assets so it will fail with other file types.
This is a bit more complicated if you want to save some documents (pdf/csv etc.) to the downloads folder. Actually I worked around it like this:

  if (Platform.OS === 'android') {
    const downloadDir = SAF.getUriForDirectoryInRoot('Download');
    const permission = await SAF.requestDirectoryPermissionsAsync(downloadDir);

    if (!permission.granted) {
      return false;
    }

    const destinationUri = await SAF.createFileAsync(permission.directoryUri, filename, mimeType);
    // TODO: Error in SAF file: Destination '[...] /Download/SelectedDir' directory cannot be created
    // await SAF.copyAsync({ from: uri, to: destinationUri });
    await SAF.writeAsStringAsync(destinationUri, await readAsStringAsync(uri));
    return true;
  } else if (Platform.OS === 'ios') {
    await Sharing.shareAsync(uri, {
      mimeType,
      UTI: uti,
    });
    return true;
  }

However, there is one disadvantage:

there's no way to download a single file and save it to the Downloads folder on Android. With the current API, we open a popup where a user needs to create a subdirectory inside Downloads (because SAF doesn't give permission for Downloads dir itself, for security reasons) and select it for our app to create the file inside that subdir.

Ideally, the ACTION_CREATE_DOCUMENT intent should be used for this purpose, but it's not yet supported in Expo. On iOS, the expo-sharing with correct UTI solves such problems.

@durgesh2007
Copy link

durgesh2007 commented Jun 13, 2022

This is because the Media library is able to save image/video/audio assets so it will fail with other file types. This is a bit more complicated if you want to save some documents (pdf/csv etc.) to the downloads folder. Actually I worked around it like this:

  if (Platform.OS === 'android') {
    const downloadDir = SAF.getUriForDirectoryInRoot('Download');
    const permission = await SAF.requestDirectoryPermissionsAsync(downloadDir);

    if (!permission.granted) {
      return false;
    }

    const destinationUri = await SAF.createFileAsync(permission.directoryUri, filename, mimeType);
    // TODO: Error in SAF file: Destination '[...] /Download/SelectedDir' directory cannot be created
    // await SAF.copyAsync({ from: uri, to: destinationUri });
    await SAF.writeAsStringAsync(destinationUri, await readAsStringAsync(uri));
    return true;
  } else if (Platform.OS === 'ios') {
    await Sharing.shareAsync(uri, {
      mimeType,
      UTI: uti,
    });
    return true;
  }

However, there is one disadvantage:

there's no way to download a single file and save it to the Downloads folder on Android. With the current API, we open a popup where a user needs to create a subdirectory inside Downloads (because SAF doesn't give permission for Downloads dir itself, for security reasons) and select it for our app to create the file inside that subdir.
Ideally, the ACTION_CREATE_DOCUMENT intent should be used for this purpose, but it's not yet supported in Expo. On iOS, the expo-sharing with correct UTI solves such problems.

Hello @barthap thanks for the quick response,

I'm trying to save the .xlsx file, its save in the Download/Data folder with a proper name when opening that file gets corrupted.

Reproducable example : snack expo
Code:

const downloadExcel = async () => {
    setError(null);
    const fileUrl =
      'https://filesamples.com/samples/document/xlsx/sample1.xlsx';
    const fileName = 'sample1.xlsx';
    FileSystem.downloadAsync(fileUrl, FileSystem.documentDirectory + fileName)
      .then(({ uri }) => {
        console.log('Finished downloading to ', uri);
        // saveFile(uri);
        // saveF(uri);
        saveData(uri);
      })
      .catch((error) => {
        setError(error);
      });
  };

  const saveData = async (uri: string) => {
    try {
      setLoading(true);
      console.log({ uri });
      if (uri) {
        const mimeType =
          'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
        const filename = 'test.xlsx';
        if (Platform.OS === 'android') {
          const downloadDir =
            FileSystem.StorageAccessFramework.getUriForDirectoryInRoot(
              'Download/Data'
            );
          const permission =
            await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(
              downloadDir
            );
          console.log({ permission });
          if (!permission.granted) {
            return false;
          }
          const destinationUri =
            await FileSystem.StorageAccessFramework.createFileAsync(
              permission.directoryUri,
              filename,
              mimeType
            );
          console.log({ destinationUri });
          // TODO: Error in SAF file: Destination '[...] /Download/SelectedDir' directory cannot be created
          // await StorageAccessFramework.copyAsync({ from: uri, to: destinationUri });
          const contents =
            await FileSystem.StorageAccessFramework.readAsStringAsync(uri);
          console.log({ contents });
          const resp =
            await FileSystem.StorageAccessFramework.writeAsStringAsync(
              destinationUri,
              contents
            );
          console.log({ resp });
          return true;
        } else if (Platform.OS === 'ios') {
          // Need to add dependancy
          // await Sharing.shareAsync(uri, {
          //   mimeType,
          //   // UTI: uti,
          // });
          return true;
        }
      }
    } catch (error) {
      console.log({ error });
    } finally {
      setLoading(false);
    }
  };

@avishka-akva
Copy link

avishka-akva commented Dec 19, 2022

I have the same problem @durgesh2007 did you find any solution

@sejeeth11
Copy link

sejeeth11 commented Dec 27, 2022

 const contents = await FileSystem.readAsStringAsync(uri,{ encoding: FileSystem.EncodingType.Base64 });
 const destinationUri = await FileSystem.StorageAccessFramework.createFileAsync(permission.directoryUri,filename,mimeType);
    await FileSystem.writeAsStringAsync(destinationUri,contents,{encoding: FileSystem.EncodingType.Base64 })

@Abdullah-146
Copy link

To save time of people having the same problem as me, this is my code. I hope this helps someone. Works fine on sdk41 & android 11.

import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library";

static settings = async (newSettings) => {
        return new Promise(async (resolve, reject) => {
            try {
                let settings = (await AsyncStorage.getItem("settings").then((result) => JSON.parse(result))) || {};
                if (newSettings) {
                    settings = Object.assign(settings, newSettings);
                    await AsyncStorage.setItem("settings", JSON.stringify(settings));
                }
                return resolve(settings);
            } catch (e) {
                console.log("Error in settings", e);
                return resolve({});
            }
        });
    };

static getDirectoryPermissions = async (onDirectoryChange) => {
        return new Promise(async (resolve, reject) => {
            try {
                const initial = FileSystem.StorageAccessFramework.getUriForDirectoryInRoot();
                onDirectoryChange({isSelecting: true}) //For handle appStateChange and loading
                const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(initial);
                this.settings({ downloadsFolder: permissions.granted ? permissions.directoryUri : null });
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                onDirectoryChange({downloadsFolder: permissions.granted ? permissions.directoryUri : null, isSelecting: false})
                return resolve(permissions.granted ? permissions.directoryUri : null);
            } catch (e) {
                console.log("Error in getDirectoryPermissions", e);
                onDirectoryChange({downloadsFolder: null})
                return resolve(null);
            }
        });
    };

static downloadFilesAsync = async (files, onDirectoryChange) => {
        // files = [{url: "url", fileName: "new file name" + "extension", mimeType: is_video ? "video/mp4" : "image/jpg"}]
        // onDirectoryChange = () => {cb_something_like_setState()}
        return new Promise(async (resolve, reject) => {
            try {
                const mediaLibraryPermission = await this.getMediaLibraryPermission()
                if (mediaLibraryPermission !== "granted") {
                    return resolve({status: "error"})
                }
                let settings = await this.settings();
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                const androidSDK = Platform.constants.Version
                
                if (Platform.OS === "android" && androidSDK >= 30 && !settings.downloadsFolder) {
                    //Except for Android 11, using the media library works stably
                    settings.downloadsFolder = await this.getDirectoryPermissions(onDirectoryChange)
                }
                const results = await Promise.all(
                    files.map(async (file) => {
                        try {
                        if (file.url.includes("https://")) {
                            // Remote file
                            const { uri, status, headers, md5 } = await FileSystem.downloadAsync(
                                file.url,
                                FileSystem.cacheDirectory + file.name
                            );
                            file.url = uri; //local file(file:///data/~~~/content.jpg)
                            // The document says to exclude the extension, but without the extension, the saved file cannot be viewed in the Gallery app.
                        }
                        if (Platform.OS === "android" && settings.downloadsFolder) {
                            // Creating files using SAF
                            // I think this code should be in the documentation as an example
                            const fileString = await FileSystem.readAsStringAsync(file.url, { encoding: FileSystem.EncodingType.Base64 });
                            const newFile = await FileSystem.StorageAccessFramework.createFileAsync(
                                settings.downloadsFolder,
                                file.name,
                                file.mimeType
                            );
                            await FileSystem.writeAsStringAsync(newFile, fileString, { encoding: FileSystem.EncodingType.Base64 });
                        } else {
                            // Creating files using MediaLibrary
                            const asset = await MediaLibrary.createAssetAsync(file.url);
                        }
                        return Promise.resolve({status: "ok"});
                    } catch (e) {
                        console.log(e)
                        return Promise.resolve({status: "error"});
                    }
                    })
                );
                return resolve({ status: results.every((result) => result.status === "ok") ? "ok" : "error" });
            } catch (e) {
                console.log("Error in downloadFilesAsync", e)
                return resolve({ status: "error" });
            }
        });
    };

what should be passed in settings

@aeroholic
Copy link
Author

To save time of people having the same problem as me, this is my code. I hope this helps someone. Works fine on sdk41 & android 11.

import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library";

static settings = async (newSettings) => {
        return new Promise(async (resolve, reject) => {
            try {
                let settings = (await AsyncStorage.getItem("settings").then((result) => JSON.parse(result))) || {};
                if (newSettings) {
                    settings = Object.assign(settings, newSettings);
                    await AsyncStorage.setItem("settings", JSON.stringify(settings));
                }
                return resolve(settings);
            } catch (e) {
                console.log("Error in settings", e);
                return resolve({});
            }
        });
    };

static getDirectoryPermissions = async (onDirectoryChange) => {
        return new Promise(async (resolve, reject) => {
            try {
                const initial = FileSystem.StorageAccessFramework.getUriForDirectoryInRoot();
                onDirectoryChange({isSelecting: true}) //For handle appStateChange and loading
                const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(initial);
                this.settings({ downloadsFolder: permissions.granted ? permissions.directoryUri : null });
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                onDirectoryChange({downloadsFolder: permissions.granted ? permissions.directoryUri : null, isSelecting: false})
                return resolve(permissions.granted ? permissions.directoryUri : null);
            } catch (e) {
                console.log("Error in getDirectoryPermissions", e);
                onDirectoryChange({downloadsFolder: null})
                return resolve(null);
            }
        });
    };

static downloadFilesAsync = async (files, onDirectoryChange) => {
        // files = [{url: "url", fileName: "new file name" + "extension", mimeType: is_video ? "video/mp4" : "image/jpg"}]
        // onDirectoryChange = () => {cb_something_like_setState()}
        return new Promise(async (resolve, reject) => {
            try {
                const mediaLibraryPermission = await this.getMediaLibraryPermission()
                if (mediaLibraryPermission !== "granted") {
                    return resolve({status: "error"})
                }
                let settings = await this.settings();
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                const androidSDK = Platform.constants.Version
                
                if (Platform.OS === "android" && androidSDK >= 30 && !settings.downloadsFolder) {
                    //Except for Android 11, using the media library works stably
                    settings.downloadsFolder = await this.getDirectoryPermissions(onDirectoryChange)
                }
                const results = await Promise.all(
                    files.map(async (file) => {
                        try {
                        if (file.url.includes("https://")) {
                            // Remote file
                            const { uri, status, headers, md5 } = await FileSystem.downloadAsync(
                                file.url,
                                FileSystem.cacheDirectory + file.name
                            );
                            file.url = uri; //local file(file:///data/~~~/content.jpg)
                            // The document says to exclude the extension, but without the extension, the saved file cannot be viewed in the Gallery app.
                        }
                        if (Platform.OS === "android" && settings.downloadsFolder) {
                            // Creating files using SAF
                            // I think this code should be in the documentation as an example
                            const fileString = await FileSystem.readAsStringAsync(file.url, { encoding: FileSystem.EncodingType.Base64 });
                            const newFile = await FileSystem.StorageAccessFramework.createFileAsync(
                                settings.downloadsFolder,
                                file.name,
                                file.mimeType
                            );
                            await FileSystem.writeAsStringAsync(newFile, fileString, { encoding: FileSystem.EncodingType.Base64 });
                        } else {
                            // Creating files using MediaLibrary
                            const asset = await MediaLibrary.createAssetAsync(file.url);
                        }
                        return Promise.resolve({status: "ok"});
                    } catch (e) {
                        console.log(e)
                        return Promise.resolve({status: "error"});
                    }
                    })
                );
                return resolve({ status: results.every((result) => result.status === "ok") ? "ok" : "error" });
            } catch (e) {
                console.log("Error in downloadFilesAsync", e)
                return resolve({ status: "error" });
            }
        });
    };

what should be passed in settings

The settings function was created to store the values set by the user while using the app.
In this example, we save the directory the user previously granted permission ( const { directoryUri } = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync() ) to avoid asking for directory permissions every time the user saves a file.

@NycolasSM
Copy link

I had the same problem, after cleaning the folder where the files were being saved, in my case the /DCIM folder, solved the problem

@RobinBobin
Copy link

#12060 (comment)

for me it worked the same

@RobinBobin
Copy link

I used this solution: #12060 (comment)

But I had to clear the contents of my DCIM folder once.

@mirzasetiyono
Copy link

in my case, for android device.
you need to convert uri to local uri

const imgExt = uri.split(".").pop(); await FileSystem.downloadAsync( uri, FileSystem.documentDirectory + "image" + "." + imgExt ).then(async ({uri: localUri}) => { await MediaLibrary.saveToLibraryAsync(localUri); alert("Image saved to gallery"); });

@dranitski
Copy link

dranitski commented Mar 3, 2024

This is my code to save remote images to gallery which works both on IOS and Android in Expo 50, today, 03/03/2024

https://stackoverflow.com/a/78097172/10099510

@DeepLakePR
Copy link

To save time of people having the same problem as me, this is my code. I hope this helps someone. Works fine on sdk41 & android 11.

import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library";

static settings = async (newSettings) => {
        return new Promise(async (resolve, reject) => {
            try {
                let settings = (await AsyncStorage.getItem("settings").then((result) => JSON.parse(result))) || {};
                if (newSettings) {
                    settings = Object.assign(settings, newSettings);
                    await AsyncStorage.setItem("settings", JSON.stringify(settings));
                }
                return resolve(settings);
            } catch (e) {
                console.log("Error in settings", e);
                return resolve({});
            }
        });
    };

static getDirectoryPermissions = async (onDirectoryChange) => {
        return new Promise(async (resolve, reject) => {
            try {
                const initial = FileSystem.StorageAccessFramework.getUriForDirectoryInRoot();
                onDirectoryChange({isSelecting: true}) //For handle appStateChange and loading
                const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(initial);
                this.settings({ downloadsFolder: permissions.granted ? permissions.directoryUri : null });
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                onDirectoryChange({downloadsFolder: permissions.granted ? permissions.directoryUri : null, isSelecting: false})
                return resolve(permissions.granted ? permissions.directoryUri : null);
            } catch (e) {
                console.log("Error in getDirectoryPermissions", e);
                onDirectoryChange({downloadsFolder: null})
                return resolve(null);
            }
        });
    };

static downloadFilesAsync = async (files, onDirectoryChange) => {
        // files = [{url: "url", fileName: "new file name" + "extension", mimeType: is_video ? "video/mp4" : "image/jpg"}]
        // onDirectoryChange = () => {cb_something_like_setState()}
        return new Promise(async (resolve, reject) => {
            try {
                const mediaLibraryPermission = await this.getMediaLibraryPermission()
                if (mediaLibraryPermission !== "granted") {
                    return resolve({status: "error"})
                }
                let settings = await this.settings();
                // Unfortunately, StorageAccessFramework has no way to read a previously specified folder without popping up a selector.
                // Save the address to avoid asking for the download folder every time
                const androidSDK = Platform.constants.Version
                
                if (Platform.OS === "android" && androidSDK >= 30 && !settings.downloadsFolder) {
                    //Except for Android 11, using the media library works stably
                    settings.downloadsFolder = await this.getDirectoryPermissions(onDirectoryChange)
                }
                const results = await Promise.all(
                    files.map(async (file) => {
                        try {
                        if (file.url.includes("https://")) {
                            // Remote file
                            const { uri, status, headers, md5 } = await FileSystem.downloadAsync(
                                file.url,
                                FileSystem.cacheDirectory + file.name
                            );
                            file.url = uri; //local file(file:///data/~~~/content.jpg)
                            // The document says to exclude the extension, but without the extension, the saved file cannot be viewed in the Gallery app.
                        }
                        if (Platform.OS === "android" && settings.downloadsFolder) {
                            // Creating files using SAF
                            // I think this code should be in the documentation as an example
                            const fileString = await FileSystem.readAsStringAsync(file.url, { encoding: FileSystem.EncodingType.Base64 });
                            const newFile = await FileSystem.StorageAccessFramework.createFileAsync(
                                settings.downloadsFolder,
                                file.name,
                                file.mimeType
                            );
                            await FileSystem.writeAsStringAsync(newFile, fileString, { encoding: FileSystem.EncodingType.Base64 });
                        } else {
                            // Creating files using MediaLibrary
                            const asset = await MediaLibrary.createAssetAsync(file.url);
                        }
                        return Promise.resolve({status: "ok"});
                    } catch (e) {
                        console.log(e)
                        return Promise.resolve({status: "error"});
                    }
                    })
                );
                return resolve({ status: results.every((result) => result.status === "ok") ? "ok" : "error" });
            } catch (e) {
                console.log("Error in downloadFilesAsync", e)
                return resolve({ status: "error" });
            }
        });
    };

Thank you soo much, this worked for me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.