Granting Permissions on a Uri in an Intent Extra

With the ban on file Uri values coming into force with Android 7.0, more developers will be using things like FileProvider and content Uri values to get content from their app to other apps. This means that developers also need to arrange to grant permissions to that content. For example, FileProvider refuses to be exported, so you cannot simply open up your FileProvider to all third parties.

Typically, we get the Uri to the third-party app via an Intent, often for use with startActivity() or startActivityForResult(). Calling addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) and/or addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) is supposed to grant rights to the component that eventually handles the request and its Intent.

But, what Uri values do these flags affect?

One affected Uri is the “data” facet of the Intent. This is the Uri that you supply via the Intent constructor, via setData(), or via setDataAndType(). So, if you have code like this:

Intent i=
    new Intent(Intent.ACTION_VIEW,
               FileProvider.getUriForFile(this, AUTHORITY, f));

i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(i);

then whatever activity handles this ACTION_VIEW request will be able to read the content identified by the Uri that we get from FileProvider.

Most of the time, the Uri that we hand to third-party apps comes in the form of that “data” facet. But, sometimes, the Uri is delivered via an extra, such as EXTRA_STREAM on ACTION_SEND, or EXTRA_OUTPUT on ACTION_IMAGE_CAPTURE.

Starting with Android 5.0, `addFlags()` affects `Uri` values passed via extras.

However, prior to that, addFlags() did not affect such Uri values… or so I thought.

Ian Lake pointed out that as of API Level 16, EXTRA_STREAM on ACTION_SEND is affected by addFlags(). It turns out that this also works for ACTION_SEND_MULITPLE, and even if these Intents are wrapped in an ACTION_CHOOSER Intent, used to display the activity chooser. UPDATE 2016-09-01: And, as of Android 5.0, it also handles EXTRA_OUTPUT on ACTION_IMAGE_CAPTURE, ACTION_IMAGE_CAPTURE_SECURE, and ACTION_VIDEO_CAPTURE.

The way this works is sneaky.

API Level 16 added setClipData() on Intent. This is another way of passing data from one app to another in an Intent, alongside the extras, action string, and so on. However, setClipData() has an additional property: addFlags() affects the ClipData and whatever is inside of it.

What Jelly Bean then did was add some hacks into Intent such that for ACTION_SEND and ACTION_SEND_MULTIPLE, the Uri values in EXTRA_STREAM are also placed into a ClipData, which then is attached to the Intent via setClipData(). While addFlags() does not affect the extras themselves (until Lollipop, anyway), it does affect the ClipData. And this works even if the app processing the request totally ignores the ClipData and just uses the extras directly.

This is really useful, even for scenarios beyond ACTION_SEND and EXTRA_STREAM. In the absence of setClipData(), the only way you have to let third-party apps have access to the content is to manually grant permissions to every third-party app that might handle your request. This is tedious and insecure.

For example, without setClipData(), for EXTRA_OUTPUT on ACTION_IMAGE_CAPTURE, you have to go through code like this:

i.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);

if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP) {
  i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
else {
  List<ResolveInfo> resInfoList=
    getPackageManager()
      .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);

  for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    grantUriPermission(packageName, outputUri,
      Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  }
}

(where i is the Intent and outputUri is the Uri to the content)

But setClipData() allows us to avoid that on API Level 16+ devices:

i.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);

if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP) {
  i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
else if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN) {
  ClipData clip=
    ClipData.newUri(getContentResolver(), "A photo", outputUri);

  i.setClipData(clip);
  i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
else {
  List<ResolveInfo> resInfoList=
    getPackageManager()
      .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);

  for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    grantUriPermission(packageName, outputUri,
      Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  }
}

And, if your minSdkVersion is 16 or higher, you can skip the nasty grant-all-sorts-of-apps-the-permission code entirely.

This too does not handle everything, such as a Uri passed via AIDL-defined methods on a remote service. But, addFlags(), plus the setClipData() hack for Android 4.1-4.4, you can fairly easily grant temporary rights to your content to third-party apps, even when that content is backed by your own ContentProvider.

UPDATE 2016-09-01: Many thanks to cketti for pointing out that Android 5.0 didn’t really fix this either.