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 your Uri values

  • one of the open...() series of methods, such as openFile() or openPipeHelper()

  • query(), to support the OpenableColumns (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 a file-based Intent, then launching the activity that the user chooses with the proper Intent

There may be a simpler pattern for this that I am not thinking of.