react-native-image-crop-picker icon indicating copy to clipboard operation
react-native-image-crop-picker copied to clipboard

Starting May 5th, you must let us know why your app requires broad storage access

Open diegolmello opened this issue 3 years ago β€’ 40 comments

Version

Tell us which versions you are using:

  • react-native-image-crop-picker v0.31.1
  • react-native v0.63.4

Platform

Tell us to which platform this issue is related

  • Android

From Google Play inbox:

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.

https://support.google.com/googleplay/android-developer/answer/10467955

Do we have a workaround for that?

Thanks!

Love react-native-image-crop-picker? Please consider supporting our collective: πŸ‘‰ https://opencollective.com/react-native-image-crop-picker/donate

diegolmello avatar Apr 14 '21 20:04 diegolmello

I got this message too and would like to know if anyone can come up with a solution

phatlaunchdeck avatar Apr 19 '21 05:04 phatlaunchdeck

+1

aj3012 avatar Apr 19 '21 08:04 aj3012

I suspect that some people get that warning after using the suggested fix here https://github.com/ivpusic/react-native-image-crop-picker/issues/1449#issuecomment-724638956, not sure if it affects everyone though?

jacquesdev avatar Apr 19 '21 09:04 jacquesdev

+1

andresribeiro avatar Apr 21 '21 22:04 andresribeiro

I suspect that some people get that warning after using the suggested fix here #1449 (comment), not sure if it affects everyone though?

Yes, indeed. (for me)

Steffi3rd avatar Apr 22 '21 16:04 Steffi3rd

I do research about it and the waring is applicable to case when you are targeting level 30 Android 11 I think. If you target level 29 it should be ok right now, bu in November 2021 it is required to use Api level 30 for existing app if you want to place them in google play store. And also in Api level 30 the flag requestLegacyExternalStorage stop working and you must use scope storage introduce in android 11 , right now this library is not ready for this change. Google play policy timeline https://developer.android.com/distribute/play-policies

proactivebit avatar Apr 23 '21 08:04 proactivebit

Is there some deadline when the lib will be updated?

saulowilson avatar Apr 30 '21 14:04 saulowilson

In case it helps anyone else, I switched to using https://github.com/rnmods/react-native-document-picker for now, it can be used as an Image/Video picker by passing in the correct types, e.g:

DocumentPicker.pickMultiple({
      type: [DocumentPicker.types.images, DocumentPicker.types.video],
    });

And doesn't require the use of requestLegacyExternalStorage to pick images from the gallery/other apps

JDGarner avatar Apr 30 '21 15:04 JDGarner

Have any plan add scope storage for android 11?

thuynt99 avatar Jun 28 '21 09:06 thuynt99

Any solution?

ghasemikasra39 avatar Jun 30 '21 07:06 ghasemikasra39

Any updates on this issue ?

AathikaMSB-Rently avatar Jul 08 '21 04:07 AathikaMSB-Rently

Waiting for solution to this issue.

mkamals avatar Jul 22 '21 17:07 mkamals

Waiting for solution to this issue.

thuynt99 avatar Aug 05 '21 07:08 thuynt99

Any updates? πŸ€” @ivpusic I see that you started working on this on July 19th. Did you make any progress so far?

KochMario avatar Sep 15 '21 18:09 KochMario

would be cool if somebody from the community has some time to finish support for android 11+

ivpusic avatar Sep 16 '21 08:09 ivpusic

With the deadline for existing apps near, are there any updates on the matter? :) @ivpusic could you describe what exactly is left to handle for the Android 11 support to be complete?

Caundy avatar Oct 12 '21 12:10 Caundy

What actually needs to be done, what are possible issues? I tested this library on android 11 devices while targeting SDK 30. My use cases are pretty simple though: taking a picture, upload to backend; take/pick an image, crop it, upload to backend. Had no issues with current state of a library. Am I missing something?

Pyroboomka avatar Oct 12 '21 15:10 Pyroboomka

I think so like @Pyroboomka . They said need to update the app after May 5th for new app. Last week i tried publish dummy app that use library requires storage access include react-native-image-crop-picker and Google approved my dummy app! You can check the app on this link, the app work perfectly fine. I didn't really understand the issue but i still worry about this issue and what happening on November.

fyfirman avatar Oct 12 '21 15:10 fyfirman

I have been able to use the package as well without any issues. I am patching it though to update a number of the dependency versions and to resolve errors/warnings provided by Android Studio. Patch code in txt file below..

react-native-image-crop-picker+0.36.4.patch.txt

mysport12 avatar Oct 12 '21 15:10 mysport12

@fyfirman You'll need to bump targetSDK to 30 in November to continue updating your app. After doing it, if you currently have requestLegacyExternalStorage in manifest, it stops working on android 11 devices -> so we get some new issues. The ones I've personally found - you can no longer create files on sdcard freely (fixed by using cache directory instead); you can no longer freely rewrite files in downloads directory, if they are not created by your app (requires some renaming or just checking that files exists before writing them to disk). Nothing more came up.

5th May email was send to anyone, who had an requestLegacyExternalStorage in their manifest, made me google like a madman that day. Stumbled on https://www.reddit.com/r/androiddev/comments/mwaqn1/scoped_storage_recap/. Hope someone find it useful.

TL;DR: if stuff works as is, you don't need to worry.

Pyroboomka avatar Oct 12 '21 15:10 Pyroboomka

Since November is only a couple of days ahead I decided it's too risky to keep hoping for an update before then and therefore I've combined react-native-image-picker and react-native-image-crop-picker and implemented some sort of patchy solution with both of them.

Since react-native-image-picker is compatible with targetSdkVersion >= 30 (and therefore Android 11) I use that to access storage and capture images/videos with the camera. For additional cropping of the selected or captured photos I'm then additionally using react-native-image-crop-picker (since react-native-image-picker does not provide that functionality).

In my opinion, this works pretty decently (for the time being), since I've managed to cover the entire workflow I had before. The production release with that update was just accepted and published to Google Play Store.

If anyone is interested in the corresponding code, let me know and I'm happy to share it here.

KochMario avatar Oct 20 '21 19:10 KochMario

@KochMario i would like to see the code. Thx!

kristijantomic avatar Oct 21 '21 12:10 kristijantomic

@KochMario i would like to see the code. Thx!

import React from 'react';
import { ImagePickerResponse, launchCamera, launchImageLibrary } from 'react-native-image-picker';
import ImagePicker, { Image, Options as ImagePickerOptions } from 'react-native-image-crop-picker';
import {
  PermissionsAndroid,
  Platform,
  Pressable,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import { getSystemVersion } from 'react-native-device-info';

const ImagesLimit = 5;
const config: ImagePickerOptionss = {
  // ... your options for react-native-image-crop-picker
  width: undefined,
  height: undefined,
  cropping: true,
  showsSelectedCount: true, // ios only
  compressImageQuality: 0.8, // value from 0 to 1 -> on iOS values larger than 0.8 basically have no effect
  compressImageMaxHeight: 2000,
  compressImageMaxWidth: 2000,
};
type Props = {
  width?: number;
  height?: number;
  onImageSelected: (selected) => void;
  multiple?: boolean | number;
  freeStyleCropEnabled?: boolean;
  useFrontCamera?: boolean; // Whether to default to the front/'selfie' camera when opened.
  compressImageMaxHeight?: number;
  compressImageMaxWidth?: number; 
};

const UploadPhotoModal = ({
  width,
  height,
  freeStyleCropEnabled = false,
  multiple = false,
  useFrontCamera = false,
  compressImageMaxHeight = 2000,
  compressImageMaxWidth = 2000,
  ...props
}: Props) => {
  const onError = (errorCode?, errorMessage?) => {
    console.log('πŸ“ΈπŸ“ΈπŸ“Έ Image Picker Error => ', errorCode, errorMessage);
    // ...
    // (if desired) - handle cropping error, e.g. show toast, modal, etc.
  };

  const cropAndReturnAssets = async assets => {
    const result: Image[] = [];
    if (assets && Array.isArray(assets)) {
      const openProps = {
        ...config,
        freeStyleCropEnabled,
        compressImageMaxHeight,
        compressImageMaxWidth,
      };

      const cropImage = async image => {
        try {
          return await ImagePicker.openCropper({
            path: image.uri as string,
            ...openProps,
          });
        } catch (e: any) {
          console.log('Cropper error', e.message, e.code);
          // if cropping was canceled return null and therefore "remove" this image from selection
          // else return the uncropped image -> might be because of image compatibility issues, e.g. .png sometimes doesn't work
          // with image cropper
          if (e.code === 'E_PICKER_CANCELLED') {
            return null;
          }
          return image;
        }
      };
      let uneditedImages = 0;

      try {
        for (const image of assets) {
          // for some reason the cropper can only handle jpg/jpeg images, therefore we push all other image types
          // without opening the cropper
          if (image.type === 'image/jpeg' || image.type === 'image/jpg') {
            const cropped = await cropImage(image);
            // only push if the image was edited and a valid result returned
            if (cropped) {
              result.push(cropped);
            }
          } else {
            uneditedImages += 1;
            result.push({ ...image, data: image.base64 });
          }
        }
      } catch (e) {
        console.log('Crop error: ', e);
        onError();
      }
      const croppedImages = result.map(image => 'data:image/jpeg;base64,' + image.data);
      if (croppedImages.length === 1) {
        props.onImageSelected(croppedImages[0]);
      } else {
        props.onImageSelected(croppedImages);
      }
      if (uneditedImages > 0) {
        // if desired - show information to the user the X of his selected images couldn't be processed and were therefore
        // added in their original state
        const message =
          uneditedImages === 1
            ? getTranslation('ImagePicker.ImageCouldNotBeEdited')
            : getTranslation('ImagePicker.ImagesCouldNotBeEdited', { amount: uneditedImages });
        // ...
        // logic for showing info message to the user
      }
    }
  };

  const handleResponse = async ({ assets, didCancel, errorCode, errorMessage }: ImagePickerResponse) => {
    if (errorCode || errorMessage) {
      onError(errorCode, errorMessage);
    } else if (didCancel) {
      // ...
      // logic for canceling
    } else {
      await cropAndReturnAssets(assets);
    }
  };

  const openImagePicker = async () => {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
    );

    const multipleValue =
      // ios > 14.0 supports specifying the max amount
      Platform.OS === 'ios' && getSystemVersion() > '14.0' && Number.isInteger(multiple)
        ? (multiple as number)
        : 0;
    if (granted === PermissionsAndroid.RESULTS.GRANTED) {
      launchImageLibrary(
        {
          // ... your options
          mediaType: 'photo',
          quality: 0.9,
          selectionLimit: multiple ? multipleValue : 1,
          includeBase64: true,
        },
        handleResponse,
      );
    } else {
      // handle insufficient permissions, e.g. show modal or info message
    }
  };

  const openCamera = async () => {
    const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
    if (granted === PermissionsAndroid.RESULTS.GRANTED) {
      launchCamera(
        {
          // ... your options
          mediaType: 'photo',
          quality: 0.9,
          cameraType: useFrontCamera ? 'front' : 'back',
          includeBase64: true,
          saveToPhotos: false,
        },
        handleResponse,
      );
    } else {
      // handle insufficient permissions, e.g. show modal or info message
    }
  };

  return (
    <View style={styles.container}>
      <Pressable onPress={openCamera}>
        <Text style={styles.text}>{getTranslation('UploadPhoto.TakePhoto')}</Text>
      </Pressable>
      <Pressable onPress={openImagePicker}>
        <Text style={styles.text}>{getTranslation('UploadPhoto.ChooseFromLibrary')}</Text>
      </Pressable>
    </View>
  );
};

export default UploadPhotoModal;

I've truncated some parts and reduced it somewhat so you get an idea without having too much of my custom logic in it. If using that code consider and keep in mind to customize it to your specific needs and purposes (i.e. I don't require video upload currently and for the time being I'm okay with only jpg's being compatible with cropping).

KochMario avatar Oct 22 '21 10:10 KochMario

@KochMario thank you very much. Cheers!

kristijantomic avatar Oct 22 '21 11:10 kristijantomic

@KochMario thanks

andresribeiro avatar Oct 22 '21 12:10 andresribeiro

I simply upgraded the library from 0.35.1 to 0.36.4 and removed requestLegacyExternalStorage from AndroidManifest.xml. Now I can pick and crop an image on Android 11 devices without any error. Is this good enough? I'm considering trying suggestions in this thread but unsure what exactly makes the latest version incomplete for Android 11.

PaperMonster avatar Oct 28 '21 05:10 PaperMonster

@PaperMonster did you test on Android 10 after your changes?

marcshilling avatar Oct 28 '21 11:10 marcshilling

@PaperMonster did you test on Android 10 after your changes?

Yes I did. There was no problem.

Android 10 never had problem before and after the changes in target SDK and library version. On Android 11, cropping fails without requestLegacyExternalStorage=true. Upgrading to 0.36.4 fixed that.

PaperMonster avatar Oct 28 '21 11:10 PaperMonster

@PaperMonster does that mean the issue is fixed on 0.36.4 ? Can the maintainer of this library shed some light? Did someone work on fixing this? Thanks πŸ™

SudoPlz avatar Nov 12 '21 13:11 SudoPlz

~Ok to answer my question, this library is still using the deprecated WRITE_EXTERNAL_STORAGE permission and thus won't work on any app that uses Android API 30 and above (which since we're on November, is required to upload a binary to the store).~

https://github.com/ivpusic/react-native-image-crop-picker/blob/master/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java#L200

@ivpusic can you please confirm? ~Also are there any plans of fixing this properly (which as far as I know, would be to support the Data Storage api)~

UPDATE: I was wrong, this library no longer asks for that permission.

Thanks

SudoPlz avatar Nov 12 '21 13:11 SudoPlz