Serving Files with FileProvider
We cannot assume that any other app has access to the files that we create, given Android’s increasing restrictions on file access. Our app has access to the files, and those on external or removable storage are accessible by the user. Beyond that, though, nothing is guaranteed.
If we want to allow another app to have access to our files, we need to use something like FileProvider
.
Scenarios for FileProvider
If we want to use ACTION_VIEW
or various other Intent
actions, we need to supply a Uri
identifying what the action should be performed upon.
In early versions of Android, you might have written the file to external storage, then used Uri.fromFile()
to get a file://
Uri
that points to the file that you created. However, that has a few problems:
- It has been years since you could assume that every app requested
READ_EXTERNAL_STORAGE
- You are cluttering up the user’s external storage with files that the user may or may not want there
- On Android 7.0,
file://
Uri
values were mostly blocked by the OS, if you try to pass one in anIntent
If you used the Storage Access Framework and wrote your content to a location specified by one of its Uri
values, you can use that Uri
with ACTION_VIEW
. However, this implies that the user has a use for this content independent of both apps (yours and whatever responds to the ACTION_VIEW
Intent
). That may not be the case. For example, your app might package a PDF file to serve as a user manual. Your app has the PDF, and a PDF viewer needs access to it, but the user does not necessarily need (or even want) that PDF to be stored somewhere public.
Those are the scenarios where FileProvider
is useful: for content that your app has, that other apps need, but the user does not need independently of those apps.
Configuring FileProvider
The PdfProvider
sample module in the Sampler
and SamplerJ
projects implements the scenario outlined above:
- We have a PDF packaged with the app as an asset
- We want to allow a PDF viewer to view that PDF, so the user can read it
One way to handle this is to copy the asset to a file, then use FileProvider
to make it available to the PDF viewer.
Metadata XML Resource
We need to teach FileProvider
what files we are willing to provide to other apps. To do that, we need to define an xml
resource, in the same res/xml/
directory where we put our preference screen configuration. res/xml/
is a resource directory that can hold any XML that you want, including arbitrary XML that you create. Some parts of Android, like the preference system and FileProvider
, will want a resource matching their desired XML structure.
In the case of FileProvider
, that consists of a root <paths>
element, followed by one or more child elements indicating where we want to serve from and what we want that location to be called:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="stuff" path="/" />
</paths>
<files-path>
says “serve from getFilesDir()
as a root directory”. That is one of a few possible element names, including <cache-path>
(mapping to getCacheDir()
) and <external-files-path>
(mapping to getExternalFilesDir(null)
).
The path
attribute indicates where underneath the specified root location we want to serve. Here, by using /
, we are saying that anything in getFilesDir()
is fine. If there are files in getFilesDir()
that you do not want to be available via FileProvider
, set up a designated directory under getFilesDir()
for the shareable files, then put that directory name in the path
attribute.
The name
attribute is a unique name for this location. No two locations in your resource can share the same name. This will form part of the Uri
that FileProvider
uses to map to your files.
Manifest Element
FileProvider
is an implementation of ContentProvider
. A ContentProvider
, like an Activity
, needs to be registered in the manifest. Instead of an <activity>
element, it uses a <provider>
element, but otherwise it fills the same basic role: tell Android that this class is an entry point for our app and how it can be used.
So, our manifest has a <provider>
element, pointing to the AndroidX implementation of FileProvider
:
<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>
As with <activity>
, the android:name
attribute on <provider>
points to the class that is the ContentProvider
implementation. Since this one is coming from a library, we need to provide the fully-qualified class name (androidx.core.content.FileProvider
).
android:authorities
is an identifier (or optionally several in a comma-delimited list). This identifier has to be unique on the device; there cannot be two <provider>
elements with the same authority installed at the same time. Fortunately, the Android build system lets us use the ${applicationId}
macro, which expands into our application ID. This app’s application ID is com.commonsware.jetpack.pdfprovider
, so our authority turns into com.commonsware.jetpack.pdfprovider.provider
.
android:exported
says whether or not third-party apps can initiate communications on their own with this ContentProvider
. FileProvider
requires this to be set to false
. The only way another app will be able to work with our content is if we grant it temporary permission, on a case-by-case basis.
android:grantUriPermissions
says whether or not we want to use that “case-by-case basis” approach or not. true
indicates that we do.
Finally, as we saw in the chapter on app widgets, the nested <meta-data>
element is a way that you can add configuration details to the manifest. In this case, FileProvider
expects a <meta-data>
element for android.support.FILE_PROVIDER_PATHS
, pointing to the xml
resource that we saw in the preceding section.
With that resource and this manifest element, our FileProvider
is ready for use.
Employing FileProvider
If you have a file in one of the locations listed in your <paths>
resource, you can call FileProvider.getUriForFile()
to retrieve the corresponding Uri
:
binding.view.setOnClickListener(v -> {
Uri uri = FileProvider.getUriForFile(this, AUTHORITY, state.content);
Intent intent =
new Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, "application/pdf")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
startActivity(intent);
}
catch (ActivityNotFoundException ex) {
Toast.makeText(this, "Sorry, we cannot display that PDF!",
Toast.LENGTH_LONG).show();
}
});
binding.view.setOnClickListener {
val uri = FileProvider.getUriForFile(this, AUTHORITY, state.pdf)
val intent = Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, "application/pdf")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
startActivity(intent);
} catch (ex: ActivityNotFoundException) {
Toast.makeText(
this,
"Sorry, we cannot display that PDF!",
Toast.LENGTH_LONG
).show()
}
}
Here, state.pdf
is a File
object, pointing to a PDF file that we have copied from assets to a file via some code in MainMotor
. this
is MainActivity
. AUTHORITY
is the authority that we used when declaring the FileProvider
in the manifest:
private static final String AUTHORITY =
BuildConfig.APPLICATION_ID + ".provider";
private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider"
The equivalent of the ${applicationId}
macro in the manifest is to refer to BuildConfig.APPLICATION_ID
, so we are assembling the authority string the same way.
Given the Uri
from FileProvider
, we can put it in an ACTION_VIEW
Intent
and pass that to startActivity()
, to try to view the PDF file. However, by default, third-party apps have no rights to view the content identified by that Uri
. By calling addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
on the Intent
, we are telling Android to grant read access (but not write access) to that content.
Overall, the activity has a pair of buttons, one to copy the PDF from the asset to a file in getFilesDir()
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/export"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/btn_export"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:enabled="false"
android:text="@string/btn_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/export" />
<TextView
android:id="@+id/error"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:typeface="monospace"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view" />
</androidx.constraintlayout.widget.ConstraintLayout>
The work to copy the asset to a file is handled by an exportPdf()
function on MainMotor
:
package com.commonsware.jetpack.pdfprovider;
import android.app.Application;
import android.content.res.AssetManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
public class MainMotor extends AndroidViewModel {
private static final String FILENAME = "test.pdf";
private final MutableLiveData<MainViewState> states = new MutableLiveData<>();
private final AssetManager assets;
private final File dest;
private final Executor executor = Executors.newSingleThreadExecutor();
public MainMotor(@NonNull Application application) {
super(application);
assets = application.getAssets();
dest = new File(application.getFilesDir(), FILENAME);
if (dest.exists()) {
states.setValue(new MainViewState(false, dest, null));
}
}
LiveData<MainViewState> getStates() {
return states;
}
void exportPdf() {
states.setValue(new MainViewState(true, null, null));
executor.execute(() -> {
try {
copy(assets.open(FILENAME));
states.postValue(new MainViewState(false, dest, null));
}
catch (IOException e) {
states.postValue(new MainViewState(false, null, e));
}
});
}
private void copy(InputStream in) throws IOException {
FileOutputStream out=new FileOutputStream(dest);
byte[] buf=new byte[8192];
int len;
while ((len=in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.getFD().sync();
out.close();
}
}
package com.commonsware.jetpack.pdfprovider
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
private const val FILENAME = "test.pdf"
class MainMotor(application: Application) : AndroidViewModel(application) {
private val _states = MutableLiveData<MainViewState>()
val states: LiveData<MainViewState> = _states
private val assets = application.assets
private val dest = File(application.filesDir, FILENAME)
init {
if (dest.exists()) {
_states.value = MainViewState.Content(dest)
}
}
fun exportPdf() {
_states.value = MainViewState.Loading
viewModelScope.launch(Dispatchers.IO) {
try {
assets.open(FILENAME).use { pdf ->
FileOutputStream(dest).use { pdf.copyTo(it) }
}
_states.postValue(MainViewState.Content(dest))
} catch (e: IOException) {
_states.postValue(MainViewState.Error(e))
}
}
}
}
MainMotor
does that work on a background thread, since it might take a few moments. It emits MainViewState
objects to report our loading/content/error status:
package com.commonsware.jetpack.pdfprovider;
import java.io.File;
class MainViewState {
final boolean isLoading;
final File content;
final Throwable error;
MainViewState(boolean isLoading, File content, Throwable error) {
this.isLoading = isLoading;
this.content = content;
this.error = error;
}
}
package com.commonsware.jetpack.pdfprovider
import java.io.File
sealed class MainViewState {
object Loading : MainViewState()
class Content(val pdf: File) : MainViewState()
class Error(val throwable: Throwable) : MainViewState()
}
MainActivity
observes the LiveData
of MainViewState
, and when the content is ready, enables the second button. When that button is clicked, we start the PDF viewer activity to view our content.
package com.commonsware.jetpack.pdfprovider;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.commonsware.jetpack.pdfprovider.databinding.ActivityMainBinding;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import androidx.lifecycle.ViewModelProvider;
public class MainActivity extends AppCompatActivity {
private static final String AUTHORITY =
BuildConfig.APPLICATION_ID + ".provider";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBinding binding =
ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
final MainMotor motor = new ViewModelProvider(this).get(MainMotor.class);
motor.getStates().observe(this, state -> {
binding.export.setEnabled(!state.isLoading && state.content == null);
binding.view.setEnabled(!state.isLoading && state.content != null);
if (binding.view.isEnabled()) {
binding.view.setOnClickListener(v -> {
Uri uri = FileProvider.getUriForFile(this, AUTHORITY, state.content);
Intent intent =
new Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, "application/pdf")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
startActivity(intent);
}
catch (ActivityNotFoundException ex) {
Toast.makeText(this, "Sorry, we cannot display that PDF!",
Toast.LENGTH_LONG).show();
}
});
}
if (state.error != null) {
binding.error.setText(state.error.getLocalizedMessage());
}
});
binding.export.setOnClickListener(v -> motor.exportPdf());
}
}
package com.commonsware.jetpack.pdfprovider
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.commonsware.jetpack.pdfprovider.databinding.ActivityMainBinding
private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val motor: MainMotor by viewModels()
motor.states.observe(this) { state ->
when (state) {
MainViewState.Loading -> {
binding.export.isEnabled = false
binding.view.isEnabled = false
}
is MainViewState.Content -> {
binding.export.isEnabled = false
binding.view.isEnabled = true
binding.view.setOnClickListener {
val uri = FileProvider.getUriForFile(this, AUTHORITY, state.pdf)
val intent = Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, "application/pdf")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
startActivity(intent);
} catch (ex: ActivityNotFoundException) {
Toast.makeText(
this,
"Sorry, we cannot display that PDF!",
Toast.LENGTH_LONG
).show()
}
}
}
is MainViewState.Error -> {
binding.export.isEnabled = false
binding.view.isEnabled = false
binding.error.text = state.throwable.localizedMessage
}
}
}
binding.export.setOnClickListener { motor.exportPdf() }
}
}
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.