How to Publish Files Via a content Uri
As noted on Monday, it appears that
[the file
scheme is being banned, by and large]((https://commonsware.com/blog/2016/03/14/psa-file-scheme-ban-n-developer-preview.html).
Yesterday, I covered how to consume content from a Uri
,
particularly a Uri
using the content
scheme, which is our primary
way of sharing content between apps going forward.
Today, let’s look at the other side: how to publish “files” via a content
Uri
.
Option #1: FileProvider
Google will steer you in the direction of FileProvider
, from the
support-v4
library. This provides a canned implementation of a
ContentProvider
that can be used to serve files from internal
storage and external storage. You configure it with an XML resource,
indicating what directories (or individual files) can be served,
add the <provider>
element to your manifest, and you are (in theory)
set.
Note that there is a long-standing documentation bug that, at this rate, will never get fixed.
Option #2: FileProvider
with LegacyCompatCursorWrapper
The other problem with FileProvider
is that it works properly… which
conflicts with various apps that assume that content
Uri
values come
from MediaStore
and try to query()
for things that FileProvider
does not support.
To help with this, I put together
a LegacyCompatCursorWrapper
that supplies the “missing” values, to try to improve compatibility with
broken clients.
To use it with FileProvider
, you create a tiny subclass of FileProvider
that wraps the query()
result in a LegacyCompatCursorWrapper
, then
use your subclass in your <provider>
element:
public class LegacyCompatFileProvider extends FileProvider {
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return(new LegacyCompatCursorWrapper(super.query(uri, projection, selection, selectionArgs, sortOrder)));
}
}
(from this sample project, from this book that you really ought to read, if I do say so myself :-)
Option #3: StreamProvider
FileProvider
, though, only works with files in a few locations. Among
other things, it does not support things like assets or raw resources.
I wrote a StreamProvider
that handles those cases.
Option #4: Your Own Custom ContentProvider
If your needs are more esoteric — for example, you need to serve
streams coming from a BLOB
column in a SQLite database — you
can roll your own streaming ContentProvider
. You create a custom
subclass of ContentProvider
that handles:
-
getType()
to return the MIME type for the content pointed to by one of yourUri
values -
one of the
open...()
series of methods, such asopenFile()
oropenPipeHelper()
-
query()
, to support theOpenableColumns
(returning the size and “display name” of the content)
Compatibility becomes a problem if the stream is not backed by a local
file on the filesystem, though. Any open...()
method that winds up
using a pipe-based ParcelFileDescriptor
seems to result in a stream
that is not seekable, and various clients will expect to be able to
seek forwards and backwards in the stream (e.g., media players). I am
not aware of a workaround for this for content
Uri
values —
if somebody knows of one, please publicize it!
Option #5: MediaStore
I see some developers on Stack Overflow using methods like insertImage()
on MediaStore.Images.Media
as a way of getting a Uri
to an image.
These methods are ill-documented, and I have never used them, so I do not
know whether they are a great idea, a terrible idea, or something in
between.
More Fictionally-Asked Questions
Here’s the FAQ list for this particular topic.
Do I Export These Providers?
Ideally, no. In fact, for FileProvider
and StreamProvider
, you cannot
export them — you will crash on startup.
An exported ContentProvider
can be used by any app on the device, subject
to any android:readPermission
and/or android:writePermission
attributes
that you have on the <provider>
. That’s not usually what you want here.
Instead, you want to be able to grant individual apps permissions for
individual Uri
values at the point when they are needed, and that’s it.
How Do I Grant Permissions, Then?
If the Uri
is going in an Intent
(e.g., ACTION_VIEW
), you can
add FLAG_GRANT_READ_URI_PERMISSION
and/or FLAG_GRANT_WRITE_URI_PERMISSION
to the Intent
. This signals that you are willing to have whatever
component handles the Intent
have read and/or write access to the content.
This also requires you to configure android:grantUriPermissions
on the <provider>
element. A common pattern, particularly for providers
that are only serving files, is to simply set android:grantUriPermissions
to be true
.
On Android 4.4+, you can optionally add FLAG_GRANT_PERSISTABLE_URI_PERMISSION
,
indicating that you are willing for your permission grants to be
persistable, so the client will have continued access to the content,
even after the client’s process gets terminated.
What If I Have a Uri
, But I Do Not Have an Intent
?
Um, things start to get messy.
The only solution that I know of off the top of my head is to call
grantUriPermission()
on Context
. This requires the Uri
, the
flags,… and the application ID of the other app. Knowing what
the application ID is of the other app may be quite a challenge.
Should I Use a Custom Permission?
Probably not. If the only clients are other apps of yours, then a
custom permission might work, particularly if set to signature
as the
protection level.
Conversely, if you are trying to work with arbitrary third-party apps (e.g., the user’s chosen PDF viewer), a custom permission will not work, as the other app will not hold that permission.
Can I Support Both file
and content
?
In principle, through Android 6.0, yes.
In practice, this may be a challenge. For example, you cannot specify alternative
Uri
values on an Intent
. So, if you are trying to view a PDF, your
options are:
-
Craft the
content
Uri
and go with that -
Craft the
file
Uri
and go with that -
Show your own activity chooser, using various tricks to get it to display options from both a
content
- and afile
-basedIntent
, then launching the activity that the user chooses with the properIntent
There may be a simpler pattern for this that I am not thinking of.