react-native-cameraroll / react-native-cameraroll

CameraRoll is a react-native native module that provides access to the local camera roll or photo library.
MIT License
932 stars 437 forks source link

About requestLegacyExternalStorage #304

Closed kperreau closed 1 year ago

kperreau commented 3 years ago

Hello. I just received this message from Play Store.

Starting May 5th, you must let us know why your app requires broad storage access BeReal - Photos & Friends, no filter. BeReal - Photos & Friends, no filter. 14 avr. 2021 19:26

We've detected that your app contains the requestLegacyExternalStorage flag in the manifest file of 1 or more of your app bundles or APKs.

Developers with apps on devices running Android 11+ must use Scoped Storage to give users better access control over their device storage. To release your app on Android 11 or newer after May 5th, you must either:

Update your app to use more privacy friendly best practices, such as the Storage Access Framework or Media Store API
Update your app to declare the All files access (MANAGE_EXTERNAL_STORAGE) permission in the manifest file, and complete the All files access permission declaration in Play Console from May 5th
Remove the All files access permission from your app entirely

For apps targeting Android 11, the requestLegacyExternalStorage flag will be ignored. You must use the All files access permission to retain broad access.

Apps requesting access to the All files access permission without a permitted use will be removed from Google Play, and you won't be able to publish updates.

Any info to solve this? Why not simply use the Storage Access Framework?

Thanks for your answers.

huurray commented 3 years ago

same to me. i'm using android:requestLegacyExternalStorage="true". should i do somthing because of changed policy?

goguda commented 3 years ago

same to me. i'm using android:requestLegacyExternalStorage="true". should i do somthing because of changed policy?

Essentially, we have two options, according to Google:

Update your app to declare the All files access (MANAGE_EXTERNAL_STORAGE) permission in the manifest file, and complete the All files access permission declaration in Play

which will probably be frowned upon from a privacy standpoint by both Google and clients unless absolutely needed, and I can imagine Google will deny requests where they don't deem it necessary to use either (i.e. in the case of only accessing photos and videos).

The other option is that this library needs to be updated in advance before May 5th to allow developers using it time to update our apps to be compatible with Android 11+.

Aka implementing Scoped Storage Support needs to be done in the next 2 weeks if we want this library to remain compatible with Android 11+.

abeyuya commented 3 years ago

Does anyone know if there are any plans to do implementing Scoped Storage Support ?

dvquoc commented 3 years ago

I have the same problem. Is there any solution to fix it?

HugoGresse commented 3 years ago

Based on the VERY short timeline here, I've posted a solution on SO

So it's working well on my side using the MediaStore API (not very intuitive API though) and the react-native-fs package for scanning the file. It can probably be adapted to CameraRoll and I'll be happy to submit a PR if the maintainers are ok with the solution.

dvquoc commented 3 years ago

Based on the VERY short timeline here, I've posted a solution on SO

So it's working well on my side using the MediaStore API (not very intuitive API though) and the react-native-fs package for scanning the file. It can probably be adapted to CameraRoll and I'll be happy to submit a PR if the maintainers are ok with the solution.

How to export this problem. It is very serious!

HugoGresse commented 3 years ago

What do you mean @dvquoc ?

kenMarquez commented 3 years ago

Regarding your solution @HugoGresse, what do you think? If we keep the logic inside the save method, this way the implementers won't have to do anything, just update the library. I based on your proposal and now I am working without requestLegacyExternalStorage flag and it works 🎉


If you are looking for a quick way to fix this while the library owners adjust it, you can do this

  1. Remove from you android manifest.xml file the property: android: requestLegacyExternalStorage = "true"

  2. Make sure you have the following version of react-native-cameraroll in your package.json "@react-native-community/cameraroll": "4.0.4",

  3. Execute: yarn add -d patch-package

  4. In the root directoy from your project at the same herarchy of your node_modules create a new folder called patches and paste this file inside https://drive.google.com/file/d/1tHDpoDSLsP7ntt0fJSEjdaYVKEaG3_Uc/view?usp=sharing

  5. Install your dependencies again to override add the logic to work without the requestLegacyExternalStorage yarn install

  6. After this all should work as normal

In the terminal you will see something like this image

HugoGresse commented 3 years ago

LGTM, will have done a PR rather than a patch though... possible improvements: move most of the patches to the native side:

  1. take the fileName from the path directly on Android, not on React Native,
  2. remove RNFS dependencies to use MediaScannerConnection directly
  3. profit
kenMarquez commented 3 years ago

Great recommendations @HugoGresse, i updated the patch https://drive.google.com/file/d/1tHDpoDSLsP7ntt0fJSEjdaYVKEaG3_Uc/view?usp=sharing

  1. Good catch i remove the fileName from the param and now its taken from the path
  2. I remove the react-native-fs dependecy and i added the MediaScannerConnection directly from the native side

Hopefully the owner says if he agrees we can work on a PR thank you very much @HugoGresse

proactivebit commented 3 years ago

@kenMarquez i use your patch and solution. I deleted requestLegacyStorage and was able to save Photo to the Pictures directory on my Phone. But there is a problem with getting this Photo using uri returned from save method. The Photo not appear "Image source={{uri:path}} " . I think that that is a second problem, how to retrieve Photo using scope storage?

anatolysamoilenko commented 3 years ago

@HugoGresse's solution work only for images( What about videos?

HugoGresse commented 3 years ago

@HugoGresse's solution work only for images( What about videos?

You need to look into MediaStore API for video but it shouldn't be much different, maybe just replace MediaStore.Images. by MediaStore.Videos or something like this, and the same for the EXTERNAL_CONTENT_URI

kenMarquez commented 3 years ago

Hello @kursonix , how are you calling the save method? , did you called with await syntax? something like this:

const image = await CameraRoll.save(imagePath, {
  album: ALBUM_NAME,
});
console.log('image', image);
proactivebit commented 3 years ago

@kenMarquez hi yes I use it like this. With requestLegacy flag it works

` public savePhoto = async (imagePath: string): Promise<string | undefined> => { if (Platform.OS === 'android' && !(await hasAndroidPermission())) { return; } const newPath = await CameraRoll.save(imagePath, { album: 'puzzleTracker', }); return getOrginalPath(newPath); };

async function getOrginalPath(rawPath: string): Promise { let correctURI = rawPath; if(Platform.OS === 'android'){ const stat = await RNFS.stat(rawPath); correctURI = ${Platform.OS === 'android' ? 'file://' : ''}${ stat.originalFilepath }; } return correctURI; }`

getOrginalPath right now as I see is unnecessary.

But stil i'm unable to retrieve the photo

HugoGresse commented 3 years ago

@bartolkaruza @Naturalclar would you mind taking a look at the proposed solution to fix this and let us know if you are somewhat ok so we can start the PR?

Naturalclar commented 3 years ago

@HugoGresse thanks for taking up the issue, the proposed solution sounds fine to me, would you be able to start the PR?

HugoGresse commented 3 years ago

I'll work on the PR tomorrow. In the meantime, I needed tochange the method to get the file path from the final MediaStore URI for some reason... here is the latest one I used:

    // From https://stackoverflow.com/a/64359655/1377145
    public static String getNameFromContentUri(Context context, Uri contentUri){
        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(contentUri, null, null, null, null);
        cursor.moveToFirst();
        String document_id = cursor.getString(0);
        document_id = document_id.substring(document_id.lastIndexOf(":") + 1);
        cursor.close();

        cursor = contentResolver.query(
            android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            null, MediaStore.Images.Media._ID + " = ? ", new String[]{document_id}, null);
        cursor.moveToFirst();
        String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
        cursor.close();
        return path;
    }
billnbell commented 3 years ago

When using the new patch, I get images 90 degree angle now.

billnbell commented 3 years ago

Also on Samsung Galaxy A21 running Android 10, we get a list of images, but cannot show them on the device.

HugoGresse commented 3 years ago

First draft: https://github.com/react-native-cameraroll/react-native-cameraroll/pull/307

lucaspang commented 3 years ago

@HugoGresse Hello, I have applied your changes and removed requestLegacyExternalStorage. But I can no longer get photos in download folder in Android 10, is it save to keep requestLegacyExternalStorage as Android 11 will ignore it? Thank you.

proactivebit commented 3 years ago

@lucaspang Hi. Right now the option with requestLegacyExternalStorage will work if you target api level 29(Android 10). For exising app in app store it is still valid until November. After that to update app you must use api level 30. For new apps in app store you must target api level 30. Can someone confirm that is true? I will not have problem after 5 May if i still use api level 29 with this flag

HugoGresse commented 3 years ago

@lucaspang Hi. Right now the option with requestLegacyExternalStorage will work if you target api level 29(Android 10). For exising app in app store it is still valid until November. After that to update app you must use api level 30. For new apps in app store you must target api level 30. Can someone confirm that is true? I will not have problem after 5 May if i still use api level 29 with this flag

If that's the case, this gave us some time to finish the PR. In any case, I'll test in production the first solution I posted in SO starting next week on 10k users and keep you posted.

pabloearg commented 3 years ago

@lucaspang Hi. Right now the option with requestLegacyExternalStorage will work if you target api level 29(Android 10). For exising app in app store it is still valid until November. After that to update app you must use api level 30. For new apps in app store you must target api level 30. Can someone confirm that is true? I will not have problem after 5 May if i still use api level 29 with this flag

I understood the same as you, but the google warning in the console does not specify anything about whether it is for 30 or 29, it would be great if someone can confirm this with some documentation

proactivebit commented 3 years ago

@HugoGresse @pabloearg there is a thread about it on reddit https://www.reddit.com/r/androiddev/comments/mwaqn1/scoped_storage_recap/ and the answer from google tem member "This Google Play policy refers specifically to apps that target API level 30 and need the MANAGE_EXTERNAL_STORAGE permission (All Files Access). If you don’t use or plan to use this permission, this policy shouldn’t affect you. If you are currently targeting API level 29 and want to use this permission when you update to target API level 30, you will need to comply with this policy."

dokkis commented 3 years ago

can somebody look also at my research posted in here and do some double checks? https://github.com/status-im/status-react/issues/12063

pabloearg commented 3 years ago

I just contact google support and this was their answer

Screen Shot 2021-04-29 at 17 14 15 Screen Shot 2021-04-29 at 17 19 00

so as long as you point to api 29, nothing should happen

MadCoderme commented 3 years ago

So we can wait for complete update in this library until November with Api 29, right?

ext {
        buildToolsVersion = "29.0.2"
        minSdkVersion = 19
        compileSdkVersion = 29
        targetSdkVersion = 29
    }
kunalyelne commented 3 years ago

@kenMarquez @kursonix I used the patch but the retrieved images are not visible, I tried

<Image 
source={{
    uri: x.node.image.uri, //uri
  }}>

it works when legacy storage flag is true.

Any idea why is this issue happening?

kenMarquez commented 3 years ago

@kunalyelne Are you still with the problem?

Could you validate if the path you are receiving is starting with the file:// format ?

kunalyelne commented 3 years ago

@kenMarquez Yeah still facing the problem. Also, I did check the uri and it is correct. It begins with file:///. Do we have to access the file using Media Store APIs on Native side or something?

kenMarquez commented 3 years ago

A better attempt might be to test directly from the pull request made for @HugoGresse and don't forget remove the patch file

yarn add https://github.com/HugoGresse/react-native-cameraroll.git#304-scopedStorage

iamvucms commented 3 years ago

@kenMarquez @kursonix I used the patch but the retrieved images are not visible, I tried

<Image 
source={{
    uri: x.node.image.uri, //uri
  }}>

it works when legacy storage flag is true.

Any idea why is this issue happening?

Same, any update ?

kunalyelne commented 3 years ago

A better attempt might be to test directly from the pull request made for @HugoGresse and don't forget remove the patch file

yarn add https://github.com/HugoGresse/react-native-cameraroll.git#304-scopedStorage

Nope, problem still persist. @kenMarquez

HugoGresse commented 3 years ago

Following my PR, I've released PlantNet app to 5% of the users for a few days resulting in no issue reported on this subject after 7 days (~500k users / day). I've increased the release to 50%, will update you next week.

iamvucms commented 3 years ago

Are you keeping requestLegacyExternalStorage on android 10 ? In my side, images from camera roll aren't able to show in FastImage & Image component if use your PR and remove requestLegacyExternalStorage on android 10

HugoGresse commented 3 years ago

I'm using the PR for android 10 and +, and fallback to cameraroll current state for below. (not keeping requestLegacyExternalStorage which was only needed for android 10) To be fair, I'm not using the exacte PR as I've released the app before making the pr here, so it's using thoses exacte methods only for Q+:

            if (isAndroidQAndAbove) {
                // Google ask that the requestLegacyExternalStorage is no longer used when targeting android 11, and use
                // the scoped storage or the new global permission, see https://gitlab.inria.fr/floristic/pn-mobile-test/-/issues/417
                // Solution here, custom module which use the MediaStore API and copy the file to the DCIM folders.
                const segments = path.split('/')
                const fileName = segments[segments.length - 1]

                const fileUriPath = await Module.moveToMediaStore(path.replace('file://', ''), fileName)
                if (!fileUriPath) {
                    return null
                }
                await RNFS.scanFile(fileUriPath)
                if (fileUriPath.startsWith('file:///')) {
                    return fileUriPath
                }
                return `file://${fileUriPath}`
            }
    @ReactMethod
    public void moveToMediaStore(String filePath, String fileName, Promise promise) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            promise.resolve(null);
            return;
        }
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/PlantNet");
        values.put(MediaStore.MediaColumns.IS_PENDING, 1);

        ContentResolver resolver = getReactApplicationContext().getContentResolver();
        Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

        try {
            OutputStream fos = resolver.openOutputStream(imageUri);
            copy(new File(filePath), fos);
            values.clear();
            values.put(MediaStore.Images.Media.IS_PENDING, 0);
            resolver.update(imageUri, values, null, null);
            promise.resolve(getNameFromContentUri(getReactApplicationContext(), imageUri));
        } catch (Exception e) {
            e.printStackTrace();
            promise.reject(e);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    public static void copy(File src, OutputStream out) throws IOException {
        try (InputStream in = new FileInputStream(src)) {
            FileUtils.copy(in, out);
        }
    }

    // From https://stackoverflow.com/a/64359655/1377145
    public static String getNameFromContentUri(Context context, Uri contentUri){
        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(contentUri, null, null, null, null);
        cursor.moveToFirst();
        String document_id = cursor.getString(0);
        document_id = document_id.substring(document_id.lastIndexOf(":") + 1);
        cursor.close();

        cursor = contentResolver.query(
            android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            null, MediaStore.Images.Media._ID + " = ? ", new String[]{document_id}, null);
        cursor.moveToFirst();
        String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
        cursor.close();
        return path;
    }
ShubhankSG commented 3 years ago

Can verify app update has been approved with target sdk 29 and with the flag requestLegacyExternalStorage in place. Will be waiting for the next release that solves the scoped storage on sdk 30

LuongTruong commented 3 years ago

Hello guys, is this issue still be considering?

har2008preet commented 3 years ago

Hi, We are getting this error: https://stackoverflow.com/questions/68207334/getting-error-while-running-react-native-run-andorid-in-react-native and the solution to that is to upgrade to version 30.

MadCoderme commented 3 years ago

Any Update on this topic?

According to @HugoGresse 's proposed PR, is it working with both getPhotos() and save() function? I will be updating my production app soon.

HugoGresse commented 3 years ago

Following changes made in my last comment, PlantNet app is doing great, already have 4M users in July with no complaint for this part. I cannot guaranty this is without any risk though.

Wtrapp commented 3 years ago

I'm using the PR for android 10 and +, and fallback to cameraroll current state for below. (not keeping requestLegacyExternalStorage which was only needed for android 10) To be fair, I'm not using the exacte PR as I've released the app before making the pr here, so it's using thoses exacte methods only for Q+:

            if (isAndroidQAndAbove) {
                // Google ask that the requestLegacyExternalStorage is no longer used when targeting android 11, and use
                // the scoped storage or the new global permission, see https://gitlab.inria.fr/floristic/pn-mobile-test/-/issues/417
                // Solution here, custom module which use the MediaStore API and copy the file to the DCIM folders.
                const segments = path.split('/')
                const fileName = segments[segments.length - 1]

                const fileUriPath = await Module.moveToMediaStore(path.replace('file://', ''), fileName)
                if (!fileUriPath) {
                    return null
                }
                await RNFS.scanFile(fileUriPath)
                if (fileUriPath.startsWith('file:///')) {
                    return fileUriPath
                }
                return `file://${fileUriPath}`
            }
    @ReactMethod
    public void moveToMediaStore(String filePath, String fileName, Promise promise) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            promise.resolve(null);
            return;
        }
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/PlantNet");
        values.put(MediaStore.MediaColumns.IS_PENDING, 1);

        ContentResolver resolver = getReactApplicationContext().getContentResolver();
        Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

        try {
            OutputStream fos = resolver.openOutputStream(imageUri);
            copy(new File(filePath), fos);
            values.clear();
            values.put(MediaStore.Images.Media.IS_PENDING, 0);
            resolver.update(imageUri, values, null, null);
            promise.resolve(getNameFromContentUri(getReactApplicationContext(), imageUri));
        } catch (Exception e) {
            e.printStackTrace();
            promise.reject(e);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    public static void copy(File src, OutputStream out) throws IOException {
        try (InputStream in = new FileInputStream(src)) {
            FileUtils.copy(in, out);
        }
    }

    // From https://stackoverflow.com/a/64359655/1377145
    public static String getNameFromContentUri(Context context, Uri contentUri){
        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(contentUri, null, null, null, null);
        cursor.moveToFirst();
        String document_id = cursor.getString(0);
        document_id = document_id.substring(document_id.lastIndexOf(":") + 1);
        cursor.close();

        cursor = contentResolver.query(
            android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            null, MediaStore.Images.Media._ID + " = ? ", new String[]{document_id}, null);
        cursor.moveToFirst();
        String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
        cursor.close();
        return path;
    }

@HugoGresse Can you provide more detail on where to add this code? Mind sharing the full code? Ie - how do you get isAndroidQAndAbove? I'm a javascript dev :)

MadCoderme commented 3 years ago

It could be better if this repo was updated officially. So, we could stay updated as well as use the PR.

November is near

tomgransden commented 3 years ago

Do we have any confirmation from the maintainers/contributors that someone is working to get this library updated for both reading images and saving images?

bartolkaruza commented 2 years ago

@mikbry @Naturalclar @doranteseduardo

bartolkaruza commented 2 years ago

@MarcoScabbiolo

MarcoScabbiolo commented 2 years ago

I'll try to get it ready for Android 11 if I can find the time

marcshilling commented 2 years ago

I've set targetSdkVersion = 30 and interestingly CameraRoll.save() seems to be working for me on an Android 11 device with no changes. However, on an Android 10 device it only seems to work with android:requestLegacyExternalStorage="true" still specified in my manifest. If I remove that, I get a permission denied error. I guess no harm leaving that. Is this expected? I'm not using getPhotos(), so I can't speak to the impact there.