ContentProvider / FileProvider

FileProvider

In the previous series of articles we looked at DownloadManager and saw that DownloadManager actually handles the sharing of downloaded content with other apps. But what if we actually need to do this and we’re not using DownloadManager? A common case for such things would be if we either want to share content with other apps or, as in the example in the previous series, the content was in a format that our app didn’t support so we wanted to defer the displaying of that content to another app on the device which did support the content type – in the example this was PDF. In this article we’ll explore why this can be problematic and look at FileProvider which enables us to quite simply do exactly what we need to.

Let’s first consider why sharing files can be problematic. The most obvious place for us to store content is in the app’s private storage area (using Context#openFileOutput()), or possibly the internal cache (using Context#getCacheDir()). The one aspect that both of these locations share is that they are only accessible to our app. While this may be good for security, it poses a problem if we wish to defer the displaying of the content to another app, such as with our PDF example. The obvious way to overcome this would be to use external storage using, for example but not limited to, Environment#getExternalStorageDirectory() or Environment#getDownloadCacheDirectory().

This poses two potential problems. Firstly the content will be publicly accessible to all apps and, depending on the nature of our app, we may want to limit which other apps can actually access the data. The second problem is rather more subtle. In order for our app to read and write to external storage we’ll need the READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE. If we then share content which we’re stored on external storage using a file://... URI then the app which is given responsibility to display that content must also have the READ_EXTERNAL_STORAGE permission otherwise it will not be able to actually read the content. We should not make assumptions about what permissions other apps may hold – this can olnly cause problems.

It is for this reason that simply sharing content via file://... URIs is not a good idea.

So what’s the alternative? The standard mechanism supported by Android is to define a ContentProvider which can expose content using a content://... URI, and the app sharing the content (i.e. our app) retains the responsibility for how and where the content is stored. Anyone who has ever implemented a ContentProvider will be aware that they can require a bit of work to implement. However, there is a simpler way: FileProvider.

FileProvider is in the v4 support library (so there’s really no excuse not to use it!) and enables us to create a ContentProvider which will share files with very little effort.

Let’s look at the differences between a file://... URI and a content://... one. The file://... URI representing the content in the sample app accompanying this post is file:///data/user/0/com.stylingandroid.fileprovider/cache/pdf-sample.pdf and the content://... URI is content://com.stylingandroid.fileprovider/my_files/pdf-sample.pdf. The file://... URI desribes an absolute position within the file system of the device, whereas the content://... URI identifies the package name of the app (com.stylingandroid.fileprovider) along with some identification of the content itself which is relevant only to the app itself.

The MainActivity from the sample app needs to share some content which has been stored in the internal cache directory. I won’t bother listing the full source here and providing a full explanation as it is fairly straightforward. We’ll focus on how we can share that content.

The first thing that we need to do is declare the FileProvider in our Manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.stylingandroid.fileprovider">

  <application
    android:allowBackup="false"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:ignore="GoogleAppIndexingWarning">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <provider
      android:name="android.support.v4.content.FileProvider"
      android:authorities="com.stylingandroid.fileprovider"
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/paths" />
    </provider>

  </application>

</manifest>

The authorities attribute is what gets registered with the OS in order for it to identify this app as being the ContentProvider for that specific content. When we looked at the differences between the two types of URI this clearly identified our app, so it is usual to use the package name of the app here.

It is important for security reasons that we don’t export the provider hence android:exported="false". Although this may seem counter intuitive, we don’t want other apps directly accessing this ContentProvider – only via the OS which can access the provider without it being exported.

Uri permissions are a useful security feature. Essentially we can share permission to access the content along with the content URI which we create and only the app selected by the OS or user will be able to access that content. We’ll look at this a little more in due course.

Finally we have some meta-data which defines the actual content which will be shared, and this is defined in paths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <cache-path
    name="my_files"
    path="/"/>
</paths>

The <cache-path> element indicates that the content is actually stored within the internal cache for the app. If we had stored the content somewhere else then we would need to use either <files-path> for internal app storage; <external-path> for public external storage; <external-files-path> for external app storage; or <external-cache-path> for external app cache storage.

The name is a logical directory name (which enables multiple directories to be shared) and this is visible in the content://... URI that we looked at earlier.

The path attribute indicates a sub-directory within the internal cache directory. In our case we’re not using one, so this is a ‘/’.

So the cache-path and path identify the location of the physical directory within which the content we wish to share is stored, and the name is a logical mapping which is used in the content URI.

Now that we have this defined we can actually share content:

    void share() {
        if (cacheFileDoesNotExist()) {
            createCacheFile();
        }
        Uri uri = FileProvider.getUriForFile(this, getPackageName(), cacheFile);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(uri);
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(intent);
    }

The first three lines simply create the content in the cache directory if it is not already there.

FileProvider#getUriForFile() is what does most of the work. We pass it a File object representing the content to be shared and it will create a content URI based upon the provider paths which we defined. In this case we share a file named ‘pdf-sample.pdf’ and the content URI that is generated is content://com.stylingandroid.fileprovider/my_files/pdf-sample.pdf.

Next we create a new Intent with ACTION_VIEW and set this URI as the Intent data. Next we set the flag FLAG_GRANT_READ_URI_PERMISSION on the Intent and this will automatically grant read permission to whichever app gains responsibility to display the content.

Finally we call startActivity() and this defers control to the OS to select an appropriate app to display the content. If the user has multiple apps capable of displaying PDF files, then the user may be prompted to select on, if (s)he hasn’t already specified a default PDF viewer.

When the app requests the content, FileProvider actually implements the ContentProvider responsible for serving the content, so there’s nothing further that our app needs to do:

That’s actually pretty impressive for a little bit of XML and 5 lines of Java code.

Although the reasoning behind why you need to use FileProvider isn’t always immediately obvious, FileProvider is really easy to implement and provides an elegant and simple solution to sharing content.

The source code for this article is available here.

© 2016, Mark Allison. All rights reserved.

Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.