2

I am having trouble with retrieving the Uri from an intent in Android N. As far as I know on Android 24 and above to get an external Uri you need FileProvider declared in Manifest. That's all done and it works with the camera, but when I try to get an image from the gallery I get an error in onActivityResult data.getData();

These are a few samples of my code:

public void getPictureFromGallery(){
    picUriCar = null;
    try {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        startActivityForResult(intent, PIC_SELECT);
    } catch (ActivityNotFoundException e) {
        Toast.makeText(MainActivity.this, getResources().getString(R.string.text_error_no_gallery_app),
                Toast.LENGTH_SHORT).show();
    }
}

And onActivityResult:

else if(requestCode == PIC_SELECT){
     picUriCar = data.getData();
     if (picUriCar != null){
         performCrop();
     }
}

As far as I know data.getData() returns a Uri and this works ok on Marshmallow, but on a Nougat phone i get this error:

java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=5, result=-1, data=Intent { dat=content://com.android.externalstorage.documents/document/4996-1EFF:DCIM/100ANDRO/DSC_0004.JPG flg=0x1 }} to activity {com.company.example/com.company.example.MainActivity}: java.lang.SecurityException: Uid 10246 does not have permission to uri 0 @ content://com.android.externalstorage.documents/document/4996-1EFF%3ADCIM%2F100ANDRO%2FDSC_0004.JPG at android.app.ActivityThread.deliverResults(ActivityThread.java:4267) at android.app.ActivityThread.handleSendResult(ActivityThread.java:4310) at android.app.ActivityThread.-wrap20(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1628) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loop(Looper.java:203) at android.app.ActivityThread.main(ActivityThread.java:6361) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924) Caused by: java.lang.SecurityException: Uid 10246 does not have permission to uri 0 @ content://com.android.externalstorage.documents/document/4996-1EFF%3ADCIM%2F100ANDRO%2FDSC_0004.JPG at android.os.Parcel.readException(Parcel.java:1683) at android.os.Parcel.readException(Parcel.java:1636) at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:3213) at android.app.Instrumentation.execStartActivity(Instrumentation.java:1525) at android.app.Activity.startActivityForResult(Activity.java:4235) at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:767) at android.app.Activity.startActivityForResult(Activity.java:4194) at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:754) at com.company.example.MainActivity.performCrop(MainActivity.java:1654) at com.company.example.MainActivity.onActivityResult(MainActivity.java:1534) at android.app.Activity.dispatchActivityResult(Activity.java:6928) at android.app.ActivityThread.deliverResults(ActivityThread.java:4263) at android.app.ActivityThread.handleSendResult(ActivityThread.java:4310)  at android.app.ActivityThread.-wrap20(ActivityThread.java)  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1628)  at android.os.Handler.dispatchMessage(Handler.java:110)  at android.os.Looper.loop(Looper.java:203)  at android.app.ActivityThread.main(ActivityThread.java:6361)  at java.lang.reflect.Method.invoke(Native Method)  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)

My question is: How do I pass data.getData() uri to picUriCar without any errors?

Said
  • 689
  • 6
  • 20

2 Answers2

2

Use this code to create new intent to choose image:

Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
        intent.setType(image/*);
        startActivityForResult(Intent.createChooser(intent, ""), Constants.SELECT_PICTURE);

Use this code in onActivityResult:

if (resultCode == Activity.RESULT_OK) {
        if (requestCode == Constants.SELECT_PICTURE) {
            Uri selectedImageUri = data.getData();
            try {
                if (isNewGooglePhotosUri(selectedImageUri)) {
                    resultFile = getPhotoFile(selectedImageUri);
                } else {
                    resultFile = getFilePathForGallery(selectedImageUri);
                }
                if (resultFile == null) {
                    //error
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
                //error
                return;
            }
        }
    }
}

Also here is some usefull function that i use in my code:

private File getFilePathForGallery(Uri contentUri) {
        String path = null;
        String[] proj = {MediaStore.Images.Media.DATA};
        Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
        if (cursor.moveToFirst()) {
            int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            path = cursor.getString(column_index);
        }
        cursor.close();
        return new File(path);
    }

  public static boolean isNewGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority());
    }

private File getPhotoFile(Uri selectedImageUri) {
        try {
            InputStream is = mActivityInstance.getContentResolver().openInputStream(selectedImageUri);
            if (is != null) {
                Bitmap pictureBitmap = BitmapFactory.decodeStream(is);
                ByteArrayOutputStream bytes = new ByteArrayOutputStream();
                pictureBitmap.compress(Bitmap.CompressFormat.JPEG, 80, bytes);
                File output = new File(FileManager.getImageCacheDir(mActivityInstance), System.currentTimeMillis() + ".jpg");
                output.createNewFile();
                FileOutputStream fo = new FileOutputStream(output);
                fo.write(bytes.toByteArray());
                fo.close();
                return output;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

Functions from FileManager class:

private static File getCacheDir(Context context) {
        File cacheDir = context.getExternalFilesDir(null);
        if (cacheDir != null) {
            if (!cacheDir.exists())
                cacheDir.mkdirs();
        } else {
            cacheDir = context.getCacheDir();
        }
        return cacheDir;
    }

     public static File getImageCacheDir(Context context) {
        File imageCacheDir = new File(getCacheDir(context), "cache_folder");
        if (!imageCacheDir.exists())
            imageCacheDir.mkdirs();
        return imageCacheDir;
    }

Also you need to create new xml file in your xml folder:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="external_files"
        path="." />
</paths>

and then add new provider to manifest file:

 <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/your_xml_file" />
        </provider>
Onix
  • 662
  • 3
  • 10
  • Thanks for the answer. But I can't import FileManager. Is FileManager from a specific library or something? – Теодор Митев Nov 14 '18 at 14:15
  • 1
    @ТеодорМитев i added functions from FileManager class to answer – Onix Nov 14 '18 at 14:34
  • Well, I implemented the code, no syntax errors, but it still doesn't seem to work. `picUriCar = FileProvider.getUriForFile(this, authority, resultFile);` This throws an exception that says: `Java.lang.IllegalArgumentException: Failed to find configured root that contains /mnt/media_rw/4996-1EFF/DCIM/100ANDRO/DSC_0004.JPG 2018-11-14 16:47:44.151 24744-24744/com.company.example W/example : type=1400 audit(0.0:8811): avc: denied { getattr } for path="/mnt/media_rw" dev="tmpfs" ino=8056 scontext=u:r:untrusted_app:s0:c5` – Теодор Митев Nov 14 '18 at 14:53
  • @ТеодорМитев check provider in manifest. is it implemented properly. also i dont use FileProvider.getUriForFile in my code? where you get this? – Onix Nov 15 '18 at 07:02
  • @ТеодорМитев or provide stacktrace and code where it crashes – Onix Nov 15 '18 at 07:11
  • Thank you for your support, Onix. I already found the answer to my question and posted it. The solution was much simpler than I thought, but everything works now. I need a Uri, not a file from the gallery, which I pass on to the crop app later. I was using fileProvider for both Gallery and Camera Uris, as Android 24 and above requires it to share uris across intents. Turns out there is no need to call fileProvider on the Gallery Uri. Still have no idea why it works now, but it does. Thanks again. – Теодор Митев Nov 15 '18 at 16:24
0

I did a few checks and as always the solution was simpler that I expected. This is my performCrop function. I have two options when clicking on a View. Either get the image from the gallery or capture it with the camera. I remembered that the gallery option problem occurred only after I fixed the camera problem. So the issue might be somewhere within the crop code. So this is what I did:

public void performCrop(boolean cameraShot){
            try {
                Intent cropIntent = new Intent("com.android.camera.action.CROP");
                //indicate image type and Uri
                //cropIntent.setDataAndType(picUri, "image");
                cropIntent.setDataAndType(picUriCar, "image/*");
                //set crop properties
                cropIntent.putExtra("crop", "true");
                //indicate aspect of desired crop
                cropIntent.putExtra("aspectX", 2);
                cropIntent.putExtra("aspectY", 1);
                //indicate output X and Y
                cropIntent.putExtra("outputX", 1024);
                cropIntent.putExtra("outputY", 512);
                cropIntent.putExtra("scale", true);
                cropIntent.putExtra("return-data", false);

                Uri uri;
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !cameraShot) {
                    uri = Uri.fromFile(croppedFileCar);
                }else{
                    String authority = "com.company.example.provider";
                    uri = FileProvider.getUriForFile(
                            MainActivity.this,
                            authority,
                            croppedFileCar);
                    cropIntent.setClipData(ClipData.newRawUri( "", uri ) );
                    cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                }

Please notice - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !cameraShot)

So basically the logic behind it is that if the image is not provided by the camera (which is determined in onActivityResult) or API is below 24, execute the line:

uri = Uri.fromFile(croppedFileCar);

Otherwise use the FileProvider. Before I did these changes the app crashed when picking an image from the gallery. I have no idea why it works now, but if someone knows the answer, I would be happy to hear it.