Step #2: Adding FileProvider

For the purposes of a “share” app bar item, we want the experience to be fairly seamless: the user clicks the item, then is prompted with options for sharing it. The flow should not be: the user clicks the item, goes through some UI to choose where to save it, then is prompted with options for sharing it. In other words, we should not be using ACTION_CREATE_DOCUMENT as we are in the “save” scenario.

We can save the report to a file fairly easily. However, on modern versions of Android, we cannot share a file with other apps. However, Jetpack offers FileProvider, which is way for us to serve files to other apps. All we need to do is add it to our manifest and configure it.

Open the app/src/main/AndroidManifest.xml file and add this XML element to the manifest, below the two existing <activity> elements:

<provider
  android:name="androidx.core.content.FileProvider"
  android:authorities="${applicationId}.provider"
  android:exported="false"
  android:grantUriPermissions="true">
</provider>

FileProvider is a ContentProvider, which is an Android component that… provides content. Just as an <activity> element identifies an Activity in our app, a <provider> element identifies a ContentProvider in our app. In this case, instead of it being one that we wrote, we are going to use FileProvider. As a result, our android:name attribute has to be the fully-qualified class name to FileProvider (androidx.core.content.FileProvider).

The android:authorities attribute indicates what name we wish to use for our provider. This name needs to be unique, and it fills a similar role as does a domain name in Web development. Here, we use ${applicationId}.provider. The ${applicationId} part is a “manifest placeholder” — a macro that will be expanded when our app is compiled and turned into our app’s application ID. If you click on the “Merged Manifest” sub-tab, you will see the results of this expansion:

Merged Manifest, Showing Expanded applicationId Placeholder
Merged Manifest, Showing Expanded applicationId Placeholder

By using ${applicationId}, we are helping to ensure that our authority value is unique, as the applicationId value itself is guaranteed to be unique on the device.

The android:exported="false" value indicates that our provider is not to be exported, meaning that by default, other apps have no ability to access our provider’s content. This may seem silly, as the point of having this provider is to get our report to other apps. However, FileProvider does not support android:exported="true". Instead, we use android:grantUriPermissions="true" to indicate that we will grant rights to other apps on a case-by-case basis at runtime.

We need to tell our FileProvider what files it should serve. To do this, we need to create an XML file with instructions, a bit reminiscent of how you configure a Web server to say what directories it should serve.

To that end, right-click over res/ and choose “New” > “Android Resource Directory” from the context menu. This brings up the “New Resource Directory” dialog:

New Resource Directory Dialog
New Resource Directory Dialog

In the “Resource type” drop-down, choose “xml”. Leave everything else alone, and click “OK” to create a res/xml/ directory in our project. This directory is good for holding arbitrary XML files — so long as they are well-formed XML, the build tools do not care about the exact contents of those files.

Then, right-click over the new res/xml/ directory and choose “New” > “XML resource file” from the context menu. Fill in provider_paths.xml as the name, fill in paths for the “Root element”, then click “OK” to create this file.

This should give you a file like this:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">

</paths>

Replace that with:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
  <cache-path name="shared" path="shared" />
</paths>

The <paths> list all of the locations that we want FileProvider to serve. In our case, there is only one child element, so we will only serve from this one place.

The child element name — cache-path — says that we start with the filesystem location that represents the “cache” portion of our app’s internal storage. This is the location identified by the getCacheDir() method on Context, and we will use that method to save our report to a file. The path attribute further constrains FileProvider to only serve files from the shared/ directory inside of getCacheDir(). The name attribute indicates that the Uri values that FileProvider uses to identify its content should have a shared path segment that maps to this filesystem location.

Then, to teach FileProvider about this XML, modify the manifest entry to have a child <meta-data> element:

    <provider
      android:name="androidx.core.content.FileProvider"
      android:authorities="${applicationId}.provider"
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
    </provider>

In a manifest, <meta-data> refers to additional information that the component can use to configure its operation, or that users of that component can use to know what that component is supposed to do. In this case, we are using it to configure the FileProvider. The FileProvider class knows to look up its android.support.FILE_PROVIDER_PATHS metadata entry and read the <paths> out of the associated XML resource. This way, we do not need to subclass FileProvider and override methods to teach it what files to serve — it can handle that on its own via this metadata.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.