Tectes
Sharing files via FileProvider
Why
Android Nougat (SDK 24) enforces the StrictMode
API policy that prohibits exposing file://
URIs outside your app.
As a result, sharing a file://
URI outside of your application will throw an FileUriExposedException.
How
If your app supports Android 7.0 and newer versions, make sure to generate a content://
URI instead and share it with the appropriate permissions. FileProvider is a custom class extending from ContentProvider that facilitates this process.
There are 3 main steps to follow in order to share a content://
URI:
Step 1: Define locations for files that you intend to share
FileProvider can only generate URI for files that are stored in directories specified by your application. This is done via an XML file with <paths>
root element and one to multiple <path>
children elements depending on the type of file you intend to share and the storage area where they’ll be in.
<paths>
<files-path name="my_files" path="files" />
<files-path name="my_other_files" path="other_files" />
<cache-path name="my_cache" path="cache" />
<external-path name="my_external" path="external" />
<external-files-path name="my_external_files" path="external_files" />
<external-cache-path name="my_external_cache" path="external_cache" />
<external-media-path name="my_external_media" path="external_media" />
</paths>
There are 6 type of paths:
<files-path>
are files stored in thefiles/
directory of your application<cache-path>
are files stored in thecache/
directory of your application<external-path>
are files stored in the external storage area<external-files-path>
are files stored in the external storage area dedicated to the app<external-cache-path>
are files stored in the external cache storage area dedicated to the app<external-media-path>
are files stored in external media area dedicated to the app
Each element has two attributes:
path
is the path of the directory the files will be stored inname
is a virtual name for the path (this is a security measure to prevent exposing the files’ paths).
If you are sharing files from multiple locations, you’ll have to define as many path elements:
<paths>
<files-path name="documents" path="files/documents" />
<files-path name="forms" path="files/forms" />
</paths>
Step 2: Define a custom file provider for your application
Your custom provider must extend from FileProvider
and its constructor takes a resourceId
which correspond to the XML file you’ve created in the previous step
class CustomFileProvider: FileProvider(R.xml.file_paths)
Add a <provider>
tag to your manifest (as a child of <application>
)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application ...>
<provider
android:name=".CustomFileProvider"
android:authorities="${applicationId}.customfileprovider"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>
Make sure that:
- Your provider tag has the attribute
android:name
set to your custom provider - You’re defining the
android:authorities
attribute. It can be one or multiple authorities separated by a semi-colon. - The attribute
grantUriPermissions
is set totrue
. This allows the content provider to temporary grant permission to the data (note that if you set this tofalse
you can control Uri permission at a more granular level) - You must add the
<meta-data>
tag as a child to<provider>
and set- its
android:name
attribute toandroid.support.FILE_PROVIDER_PATHS
- its
android:resource
attribute to the your path XML file.
- its
Step 3: Share the file
Sharing the file is quite simple once the previous configurations are done:
- Create a reference to your File
- Generate a
content://
URI from the file using FileProvider - Share your
content://
URI using thecreateChooser
Intent
Here’s a code sample for creating and sharing a file
enum class PathType {
FILES,
CACHE,
EXTERNAL,
EXTERNAL_FILES,
EXTERNAL_CACHE,
}
fun createDummyFile(context: Context, pathType: PathType = PathType.FILES): File {
val timestamp = System.currentTimeMillis()
val path = when (pathType) {
PathType.FILES -> File(context.filesDir, "files")
PathType.CACHE -> File(context.cacheDir, "cache")
PathType.EXTERNAL -> File(Environment.getExternalStorageDirectory(), "external")
PathType.EXTERNAL_FILES -> File(context.getExternalFilesDir(null), "external_files")
PathType.EXTERNAL_CACHE -> File(context.externalCacheDir, "external_cache")
}
try {
path.mkdirs()
} catch (_: SecurityException) {}
val file = File(path, "$timestamp.txt")
file.printWriter().use { out ->
out.println("hello world")
}
return file
}
fun shareFile(file: File, context: Context) {
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.customfileprovider", file)
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
setDataAndType(uri, "text/html")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val shareIntent = Intent.createChooser(intent, "Share file")
try {
context.startActivity(shareIntent)
} catch (e: ActivityNotFoundException) {
Log.w("MainActivity", "failed to initiate `createChooser`", e)
}
}
The pitfalls
Here’s a list of things that are easy to miss when working with FileProvider:
- In your manifest, make sure the
<provider>
tag is a child of the<application>
tag. Also don’t forget the<meta-data>
tag. - Make sure that your
<meta-data>
tag has the propertiesandroid:name
andandroid.resource
. Specifically, do not replaceandroid.resource
withandroid.value
- The provider authority must match the authority injected in your
FileProvider.getUriForFile()
. - According to Google, you can use
FileProvider
directly instead of creating your custom provider, however “this is not reliable and will causes crashes on some devices” - If you’re defining file paths with sub-folders, do not forget to create these folders as needed when creating and storing files. This can be done easily using File.mkdirs().
- Creating a file to share is done differently from one file path to another. Here’s the mapping:
<files-path /> : File(Context.filesDir, "")
<cache-path /> : File(Context.cacheDir, "")
<external-path /> : File(Environment.getExternalStorageDirectory(), "")
<external-files-path /> : File(Context.getExternalFilesDir(null), "")
<external-cache-path /> : File(Context.externalCacheDir, "")