Search code examples
androiduriandroid-contentproviderandroid-permissions

Android - unable to use same URI twice when picking an image


I am using a photo crop library (version 2.7 here: https://github.com/ArthurHub/Android-Image-Cropper) to pick and crop an image using gallery apps (like samsung gallery, google photos, files etc). The library works by launching the picker intent with Intent.ACTION_GET_CONTENT and after the user chooses an image, the Uri is passed to the cropping view whcich opens the bitmap. In some situations I need to reset the cropping, by automatically using the Uri from the photo last chosen by the user. So, after the user chooses the image, I remember the Uri, then use the same Uri to start the cropping step (that decodes the bitmap).

This works of for Samsung Gallery, but I have an issue with Google Photos, which throws a SecurityException when the uri is re-opened. So the image pick with google photos works correctly on first time the user chooses the image only. When I try to pass the same Uri (when I reset the crop), I get the exception below (this also happenes with "Files" - I tested on emulator and motorola one):

java.lang.RuntimeException: Failed to load sampled bitmap: content://com.google.android.apps.photos.contentprovider/0/1/mediakey%3A%2Flocal%253Ace62f6ca-6054-486b-bbb1-b6145ac50272/ORIGINAL/NONE/27402515
    Permission Denial: opening provider com.google.android.apps.photos.contentprovider.impl.MediaContentProvider from ProcessRecord{82bb7a9 12497:app.myapp/u0a188} (pid=12497, uid=10188) that is not exported from UID 10093
        at com.theartofdev.edmodo.cropper.BitmapUtils.decodeSampledBitmap(BitmapUtils.java:130)
        at com.theartofdev.edmodo.cropper.BitmapLoadingWorkerTask.doInBackground(BitmapLoadingWorkerTask.java:73)
        at com.theartofdev.edmodo.cropper.BitmapLoadingWorkerTask.doInBackground(BitmapLoadingWorkerTask.java:24)
        at android.os.AsyncTask$2.call(AsyncTask.java:333)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)
     Caused by: java.lang.SecurityException: Permission Denial: opening provider com.google.android.apps.photos.contentprovider.impl.MediaContentProvider from ProcessRecord{82bb7a9 12497:app.myapp/u0a188} (pid=12497, uid=10188) that is not exported from UID 10093
        at android.os.Parcel.createException(Parcel.java:1950)
        at android.os.Parcel.readException(Parcel.java:1918)
        at android.os.Parcel.readException(Parcel.java:1868)
        at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:4118)

The code used for showing the user with which app to choose photo is:

List<Intent> allIntents = new ArrayList<>();
PackageManager packageManager = this.getPackageManager();

boolean includeCamera = useCamera;
// collect all camera intents if Camera permission is available
if (!CropImage.isExplicitCameraPermissionRequired(this) && includeCamera) {
    allIntents.addAll(CropImage.getCameraIntents(this, packageManager));
}

boolean includeDocuments = false;
if (useGallery) {
    List<Intent> galleryIntents =
            CropImage.getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT, includeDocuments);
    if (galleryIntents.size() == 0) {
        // if no intents found for get-content try pick intent action (Huawei P9).
        galleryIntents = CropImage.getGalleryIntents(packageManager, Intent.ACTION_PICK, includeDocuments);
    }
    allIntents.addAll(galleryIntents);
}

Intent target;
if (allIntents.isEmpty()) {
    target = new Intent();
} else {
    target = allIntents.get(allIntents.size() - 1);
    allIntents.remove(allIntents.size() - 1);
}

// Create a chooser from the main  intent
Intent chooserIntent = Intent.createChooser(target, "Choose");

// Add all other intents
chooserIntent.putExtra(
        Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()]));

this.startActivityForResult(
        chooserIntent
        , CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE);

And the method CropImage.getGalleryIntents:

  public static List<Intent> getGalleryIntents(
      @NonNull PackageManager packageManager, String action, boolean includeDocuments) {
    List<Intent> intents = new ArrayList<>();
    Intent galleryIntent =
        action == Intent.ACTION_GET_CONTENT
            ? new Intent(action)
            : new Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    galleryIntent.setType("image/*");
    List<ResolveInfo> listGallery = packageManager.queryIntentActivities(galleryIntent, 0);
    for (ResolveInfo res : listGallery) {
      Intent intent = new Intent(galleryIntent);
      intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
      intent.setPackage(res.activityInfo.packageName);
      intents.add(intent);
    }

    // remove documents intent
    if (!includeDocuments) {
      for (Intent intent : intents) {
        if (intent
            .getComponent()
            .getClassName()
            .equals("com.android.documentsui.DocumentsActivity")) {
          intents.remove(intent);
          break;
        }
      }
    }
    return intents;
  }

I there a correct solution or a workaround for this? Thank you.


Solution

  • Use ACTION_OPEN_DOCUMENT for your galleryIntent.

    But beware that even ACTION_OPEN_DOCUMENT will still be invalidated after a few hours.

    The only way to have an unlimited access to an image is to save it to your app's own storage and access it there for later use.