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
.
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.