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 theFLAG_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 bothFLAG_GRANT_READ_URI_PERMISSION
andFLAG_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.