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

See original GitHub issue

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.]
    }
}

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:7
  • Comments:14 (2 by maintainers)

github_iconTop GitHub Comments

5reactions
aeroholiccommented, Sep 22, 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" });
            }
        });
    };
3reactions
viljarkcommented, 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
Read more comments on GitHub >

github_iconTop Results From Across the Web

Expo: Unable to create and Store PDF into Android 11 in ...
Here is the code, which I am using to generate PDF from html and save that into storage. import * as MediaLibrary from...
Read more >
A brand new website interface for an even better experience!
Expo-media-library createAssetAsync calls error in Android 11 even with MediaLibrary Permission.
Read more >
expo-media-library > Error: Unable to copy file into external ...
Hi, please help me SDK Version: “expo”: “^42.0.3” Platforms(Android/iOS/web/all): android 10 or 11 expo-media-library Repro here: ...
Read more >
Ability to save files on internal storage | Voters - Expo - Canny
I'm getting an error: "This file type is not supported yet" from MediaLibrary.createAssetAsync on ios. ·.
Read more >
Download an Image to Device Media Library
App Permissions. While the two packages we're importing should take care of adding the proper permission flags to your app's configuration files, we'll...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found