Sharing files through Intents (part 2): fixing the permissions before Lollipop

Since I wrote my piece about how to use FileProvider to enhance security and add compatibility with Nougat, I discovered some issues happening in older versions and now it’s time to fix them.

If you’ve updated your app to drop the old file:// Uri and you’ve been trying to adopt FileProvider instead, you might have encountered some crashes on devices running on KitKat or lower versions of Android. I wouldn’t be too surprised if you could even find a device with Lollipop affected by this issue.

So, what’s going on? Why is it happening? As usual let’s start from the stacktrace:

02-16 05:41:07.477 8063-8063/com.android.camera E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.camera, PID: 8063
java.lang.IllegalStateException: Could not execute method of the activity
at android.view.View$1.onClick(View.java:3823)
at android.view.View.performClick(View.java:4438)
at android.view.View$PerformClick.run(View.java:18422)
at android.os.Handler.handleCallback(Handler.java:733)
Caused by: java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{9d5abad8 8063:com.android.camera/u0a31} (pid=8063, uid=10031) that is not exported from uid 10057
at android.os.Parcel.readException(Parcel.java:1465)
at android.os.Parcel.readException(Parcel.java:1419)
at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:2848)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:4415)
at android.app.ContextImpl$ApplicationContentResolver.acquireUnstableProvider(ContextImpl.java:2207)
at android.content.ContentResolver.acquireUnstableProvider(ContentResolver.java:1425)
at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:906)
at android.content.ContentResolver.openOutputStream(ContentResolver.java:669)
at android.content.ContentResolver.openOutputStream(ContentResolver.java:645)
at com.android.camera.Camera.doAttach(Camera.java:1385)
at com.android.camera.Camera.onReviewDoneClicked(Camera.java:1362)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)

I’ve highlighted the important part which makes it quite clear that this is about a permission issue. Weird, why do we have the right permissions on Lollipop and higher but not on KitKat and lower?

At this point you might try to simply add permission with the line:

intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION|FLAG_GRANT_WRITE_URI_PERMISSION);

But not even adding that line is going to fix the issue. You might be tempted to set android:exported="true" in the provider declaration in the AndroidManifest.xml but the documentation clearly states that the provider does not need to be public.

The bad solution

If you need to pass the Uri to a specific app you trust, you might just grant the permission manually by calling:

context.grantUriPermission(String toPackage, Uri uri, int flags);

Where toPackage is the application Id of the receiver app and flags is the type of permission you want to set. You could then revoke those permissions by calling:

context.revokeUriPermission(Uri uri, int modeFlags);

This solution raises another problem: we don’t know which app is going to receive our Intent(it will be up to the user to pick its favourite) and granting permission to all of them capable of receiving the Intent wouldn’t definitely be an elegant solution.

Going deeper

Taking a look at the Intent documentation reveals that those flags only affect the ClipData and the data field (the one you set via setData(Uri data)), not the Intent extras. Here’s what it says about FLAG_GRANT_READ_URI_PERMISSION:

If set, the recipient of this Intent will be granted permission to perform read operations on the URI in the Intent’s data and any URIs specified in its ClipData. When applying to an Intent’s ClipData, all URIs as well as recursive traversals through data or other ClipData in Intent items will be granted; only the grant flags of the top-level Intent are used.

If you Google it and try to find any possible information about this issue you might find some comments by Ian Lake on StackOverflow: since Android 4.1 Jelly Bean (API Level 16) there’s a new hidden method called migrateExtraStreamToClipData in the Intent class which is called to migrate some specific extras (depending on your Intent action) to ClipData and grants permissions to it. For example if you use ACTION_SEND your URI will be granted the FLAG_GRANT_READ_URI_PERMISSION without having you worried to do so.

In our case we are using MediaStore.ACTION_IMAGE_CAPTURE as Intent action. If we take a look at the code of the migrateExtraStreamToClipData method in Lollipop we can see that it migrates the MediaStore.EXTRA_OUTPUT Uri to ClipData and it grants both read and write permissions on it. The same isn’t happening in KitKat which is the reason of the permission issue.

How can we fix it in previous versions?

The fix is fairly simple in our case: since migrateExtraStreamToClipData won’t do it for us, we will put our Uri as ClipData and manually grant permissions to it. So, to fix this code:

We just add an additional if statement:

We are basically checking if the version is lower or equal to Lollipop and if it is we create a new ClipData object from the Uri by calling the static method ClipData.newRawUri(String label, Uri uri). That’s exactly what migrateExtraStreamToClipData does for us in newer versions.

This solution works on Jelly Bean and higher, because there’s no setClipData method in Ice Cream Sandwich. If you really need to support API Level 15 or lower you might want to generate the old good file:// Uri.

Why are we including Lollipop?

The handling of a MediaStore.ACTION_IMAGE_CAPTURE intent wasn’t there yet in the L Preview but was added before the public release (here’s the actual commit). My idea (though I might be wrong on this matter) is that there is a small number of devices out there which doesn’t include this change even if they are running a public release of Android 5.0 Lollipop. So, since calling it manually won’t hurt us we may want to add it to avoid any crash.

Wrapping up

Depending on your Intent action, you might have to manually grant the required permissions to a file before sharing it through an Intent with FileProvider. Remember that Intent permission flags are applied only to the Uri passed as setData(Uri uri) and to its DataClip object set with setDataClip(DataClip data).

Even if the receiver app is accessing the Uri via the extra and not the ClipData, everything should be fine since the permission are applied to the Uri, not to the field itself. There are some exceptions to the rule where the Uri receives some permissions automatically thanks to migrateExtraStreamToClipData:

  • Intent.ACTION_SEND: since API level 16 the EXTRA_STREAM Uri is granted the FLAG_GRANT_READ_URI_PERMISSION.
  • Intent.ACTION_SEND_MULTIPLE: since API level 16 all the EXTRA_STREAM Uris are granted FLAG_GRANT_READ_URI_PERMISSION.
  • MediaStore.ACTION_IMAGE_CAPTURE, MediaStore.ACTION_IMAGE_CAPTURE_SECURE and MediaStore.ACTION_VIDEO_CAPTURE: since API level 21 the MediaStore.EXTRA_OUTPUT Uri is granted both FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION.

I’ve also updated my GitHub repo with the fixes to make it work on API Level 17 (previously minSdk was set to 21).

Thanks to the CommonsBlog for this piece, which helped me getting a proper understanding of the issue and to everyone else providing feedbacks on the previous article.