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:

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:

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.