Create a custom document provider

If you're developing an app that provides storage services for files (such as
a cloud save service), you can make your files available through the
Storage Access Framework (SAF) by writing a custom document provider.
This page describes how to create a custom document provider.

Manifest

To implement a custom document provider, add the following to your application's
manifest:

A target of API level 19 or higher.

A <provider> element that declares your custom storage
provider.

The attribute android:name set to the name of your
DocumentsProvider subclass,
which is its class name, including package name:

com.example.android.storageprovider.MyCloudProvider.

The attribute android:authority attribute,
which is your package name (in this example,
com.example.android.storageprovider)
plus the type of content provider
(documents).

The attribute android:exported set to "true".
You must export your provider so that other apps can see it.

The attribute android:grantUriPermissions set to
"true". This setting allows the system to grant other apps access
to content in your provider. For a discussion of how to persist a grant for
a particular document, see Persist permissions.

The MANAGE_DOCUMENTS permission. By default a provider is available
to everyone. Adding this permission restricts your provider to the system.
This restriction is important for security.

An intent filter that includes the
android.content.action.DOCUMENTS_PROVIDER action, so that your provider
appears in the picker when the system searches for providers.

Supporting devices running Android 4.3 and lower

The
ACTION_OPEN_DOCUMENT intent is only available
on devices running Android 4.4 and higher.
If you want your application to support ACTION_GET_CONTENT
to accommodate devices that are running Android 4.3 and lower, you should
disable the ACTION_GET_CONTENT intent filter in
your manifest for devices running Android 4.4 or higher. A
document provider and ACTION_GET_CONTENT should be considered
mutually exclusive. If you support both of them simultaneously, your app
appears twice in the system picker UI, offering two different ways of accessing
your stored data. This is confusing for users.

Here is the recommended way of disabling the
ACTION_GET_CONTENT intent filter for devices
running Android version 4.4 or higher:

Contracts

Usually when you write a custom content provider, one of the tasks is
implementing contract classes, as described in the
Content providers developers guide. A contract class is a public final class
that contains constant definitions for the URIs, column names, MIME types, and
other metadata that pertain to the provider. The SAF
provides these contract classes for you, so you don't need to write your
own:

Define a root

In the following snippet, the projection parameter represents the
specific fields the caller wants to get back. The snippet creates a new cursor
and adds one row to it—one root, a top level directory, like
Downloads or Images. Most providers only have one root. You might have more than one,
for example, in the case of multiple user accounts. In that case, just add a
second row to the cursor.

Kotlin

override fun queryRoots(projection: Array<out String>?): Cursor {
// Use a MatrixCursor to build a cursor
// with either the requested fields, or the default
// projection if "projection" is null.
val result = MatrixCursor(resolveRootProjection(projection))
// If user is not logged in, return an empty root cursor. This removes our
// provider from the list entirely.
if (!isUserLoggedIn()) {
return result
}
// It's possible to have multiple roots (e.g. for multiple accounts in the
// same app) -- just add multiple cursor rows.
result.newRow().apply {
add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)
// You can provide an optional summary, which helps distinguish roots
// with the same title. You can also use this field for displaying an
// user account name.
add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.root_summary))
// FLAG_SUPPORTS_CREATE means at least one directory under the root supports
// creating documents. FLAG_SUPPORTS_RECENTS means your application's most
// recently used documents will show up in the "Recents" category.
// FLAG_SUPPORTS_SEARCH allows users to search all documents the application
// shares.
add(
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
)
// COLUMN_TITLE is the root title (e.g. Gallery, Drive).
add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))
// This document id cannot change after it's shared.
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))
// The child MIME types are used to filter the roots and only present to the
// user those roots that contain the desired type somewhere in their file hierarchy.
add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir))
add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher)
}
return result
}

Java

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
// Use a MatrixCursor to build a cursor
// with either the requested fields, or the default
// projection if "projection" is null.
final MatrixCursor result =
new MatrixCursor(resolveRootProjection(projection));
// If user is not logged in, return an empty root cursor. This removes our
// provider from the list entirely.
if (!isUserLoggedIn()) {
return result;
}
// It's possible to have multiple roots (e.g. for multiple accounts in the
// same app) -- just add multiple cursor rows.
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, ROOT);
// You can provide an optional summary, which helps distinguish roots
// with the same title. You can also use this field for displaying an
// user account name.
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));
// FLAG_SUPPORTS_CREATE means at least one directory under the root supports
// creating documents. FLAG_SUPPORTS_RECENTS means your application's most
// recently used documents will show up in the "Recents" category.
// FLAG_SUPPORTS_SEARCH allows users to search all documents the application
// shares.
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
Root.FLAG_SUPPORTS_RECENTS |
Root.FLAG_SUPPORTS_SEARCH);
// COLUMN_TITLE is the root title (e.g. Gallery, Drive).
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));
// This document id cannot change after it's shared.
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));
// The child MIME types are used to filter the roots and only present to the
// user those roots that contain the desired type somewhere in their file hierarchy.
row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir));
row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace());
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
return result;
}

If your document provider connects to a dynamic set of roots—for example, to a USB
device that might be disconnected or an account that the user can sign out from—you
can update the document UI to stay in sync with those changes using the
ContentResolver.notifyChange() method, as shown in the following code snippet.

List documents in the provider

This method gets called when the user chooses your root in the picker UI.
The method retrieves the children of the document ID specified by
COLUMN_DOCUMENT_ID.
The system then calls this method any time the user selects a
subdirectory within your documents provider.

This snippet makes a new cursor with the requested columns, then adds
information about every immediate child in the parent directory to the cursor.
A child can be an image, another directory—any file:

Kotlin

override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor {
// This example implementation walks a
// local file structure to find the most recently
// modified files. Other implementations might
// include making a network call to query a
// server.
// Create a cursor with the requested projection, or the default projection.
val result = MatrixCursor(resolveDocumentProjection(projection))
val parent: File = getFileForDocId(rootId)
// Create a queue to store the most recent documents,
// which orders by last modified.
val lastModifiedFiles = PriorityQueue(
5,
Comparator<File> { i, j ->
Long.compare(i.lastModified(), j.lastModified())
}
)
// Iterate through all files and directories
// in the file structure under the root. If
// the file is more recent than the least
// recently modified, add it to the queue,
// limiting the number of results.
val pending : MutableList<File> = mutableListOf()
// Start by adding the parent to the list of files to be processed
pending.add(parent)
// Do while we still have unexamined files
while (pending.isNotEmpty()) {
// Take a file from the list of unprocessed files
val file: File = pending.removeAt(0)
if (file.isDirectory) {
// If it's a directory, add all its children to the unprocessed list
pending += file.listFiles()
} else {
// If it's a file, add it to the ordered queue.
lastModifiedFiles.add(file)
}
}
// Add the most recent files to the cursor,
// not exceeding the max number of results.
for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) {
val file: File = lastModifiedFiles.remove()
includeFile(result, null, file)
}
return result
}

Java

@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
throws FileNotFoundException {
// This example implementation walks a
// local file structure to find the most recently
// modified files. Other implementations might
// include making a network call to query a
// server.
// Create a cursor with the requested projection, or the default projection.
final MatrixCursor result =
new MatrixCursor(resolveDocumentProjection(projection));
final File parent = getFileForDocId(rootId);
// Create a queue to store the most recent documents,
// which orders by last modified.
PriorityQueue lastModifiedFiles =
new PriorityQueue(5, new Comparator() {
public int compare(File i, File j) {
return Long.compare(i.lastModified(), j.lastModified());
}
});
// Iterate through all files and directories
// in the file structure under the root. If
// the file is more recent than the least
// recently modified, add it to the queue,
// limiting the number of results.
final LinkedList pending = new LinkedList();
// Start by adding the parent to the list of files to be processed
pending.add(parent);
// Do while we still have unexamined files
while (!pending.isEmpty()) {
// Take a file from the list of unprocessed files
final File file = pending.removeFirst();
if (file.isDirectory()) {
// If it's a directory, add all its children to the unprocessed list
Collections.addAll(pending, file.listFiles());
} else {
// If it's a file, add it to the ordered queue.
lastModifiedFiles.add(file);
}
}
// Add the most recent files to the cursor,
// not exceeding the max number of results.
for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) {
final File file = lastModifiedFiles.remove();
includeFile(result, null, file);
}
return result;
}

You can get the complete code for the snippet above by downloading the
StorageProvider
code sample.

Support document creation

You can allow client apps to create files within your document provider.
If a client app sends an ACTION_CREATE_DOCUMENT
intent, your document provider can allow that client app to create
new documents within the document provider.

Your document provider also needs to implement the
createDocument() method. When a user selects a directory within your
document provider to save a new file, the document provider receives a call to
createDocument(). Inside the implementation of the
createDocument() method, you return a new
COLUMN_DOCUMENT_ID for the
file. The client app can then use that ID to get a handle for the file
and, ultimately, call
openDocument() to write to the new file.

The following code snippet demonstrates how to create a new file within
a document provider.

You can get the complete code for the snippet above by downloading the
StorageProvider
code sample.

Support document management features

In addition to opening, creating, and viewing files, your document provider
can also allow client apps the ability to rename, copy, move, and delete
files. To add document management functionality to
your document provider, add a flag to the document's
COLUMN_FLAGS column
to indicate the supported functionality. You also need to implement
the corresponding method of the DocumentsProvider
class.

The following table provides the
COLUMN_FLAGS flag
and DocumentsProvider method that a documents
provider needs to implement to expose specific features.

Support virtual files and alternate file formats

Virtual files,
a feature introduced in Android 7.0 (API level 24), allows document providers
to provide viewing access to files that do not have a
direct bytecode representation. To enable other apps to view virtual files,
your document provider needs to produce an alternative openable file
representation for the virtual files.

For example, imagine a document provider contains a file
format that other apps cannot directly open, essentially a virtual file.
When a client app sends an ACTION_VIEW intent
without the CATEGORY_OPENABLE category,
then users can select these virtual files within the document provider
for viewing. The document provider then returns the virtual file
in a different, but openable, file format like an image.
The client app can then open the virtual file for the user to view.

To declare that a document in the provider is virtual, you need to add the
FLAG_VIRTUAL_DOCUMENT
flag to the file returned by the
queryDocument()
method. This flag alerts client apps that the file does not have a direct
bytecode representation and cannot be directly opened.

If you declare that a file in your document provider is virtual,
it is strongly recommended that you make it available in another
MIME type like an image or a PDF. The document provider
declares the alternate MIME types that it
supports for viewing a virtual file by overriding the
getDocumentStreamTypes()
method. When client apps call the
getStreamTypes(android.net.Uri, java.lang.String)
method, the system calls the
getDocumentStreamTypes()
method of the document provider. The
getDocumentStreamTypes()
method then returns an array of alternate MIME types that the
document provider supports for the file.

After the client determines
that the document provider can produce the document in a viewable file
format, the client app calls the
openTypedAssetFileDescriptor()
method, which internally calls the document provider's
openTypedDocument()
method. The document provider returns the file to the client app in
the requested file format.

Security

Suppose your document provider is a password-protected cloud storage service
and you want to make sure that users are logged in before you start sharing their files.
What should your app do if the user is not logged in? The solution is to return
zero roots in your implementation of queryRoots(). That is, an empty root cursor:

The other step is to call getContentResolver().notifyChange().
Remember the DocumentsContract? We’re using it to make
this URI. The following snippet tells the system to query the roots of your
document provider whenever the user's login status changes. If the user is not
logged in, a call to queryRoots() returns an
empty cursor, as shown above. This ensures that a provider's documents are only
available if the user is logged into the provider.