Scoped Storage Stories: DocumentsContract

Android 10 is greatly restricting access to external storage via filesystem APIs. Instead, we need to use other APIs to work with content. This is the fifth post in a series where we will explore how to work with those alternatives, starting with the Storage Access Framework (SAF).


DocumentFile is easy to use and gives us an API that resembles java.io.File for basic operations. Couple that with ContentResolver to be able to get streams on Uri-identified content, and we seem to have a reasonable API to use.

However, that API does not always perform that well.

An oft-cited complaint about the Storage Access Framework — particularly as the long-term alternative to filesystem operations — is speed. Figures of 100x slowdowns are cited. Some of that is hyperbole, but there are definite performance considerations when using DocumentFile.

DocumentFile and its support classes use DocumentsContract to work with SAF-supplied Uri values. DocumentsContract provides a ContentResolver-centric API for obtaining information about a document. That is because the implementation of SAF is based on ContentProvider, specifically the DocumentsProvider subclass. As a result, the underlying protocol is based on Cursor objects, which is a workable but clunky approach. However, it is the only option available to us.

Worse, multiple IPC hops seem to be made as part of using DocumentsContract:

  • You use DocumentsContract and ContentResolver, such as using DocumentsContract.buildChildDocumentsUriUsingTree() and query() to find the child documents for a tree Uri

  • The Uri returned by DocumentsContract.buildChildDocumentsUriUsingTree() is one pointing to a system-supplied ContentProvider, and we make one IPC hop to query() that provider

  • However, that provider serves as a proxy, and it in turn makes a similar query() on the DocumentsProvider supporting this document, resulting in a second IPC hop

That adds overhead, overhead that is unavoidable.

However, DocumentFile does not always use the DocumentsContract API very efficiently. For example, here is its findFiles() implementation (from version 1.0.1):

@Nullable
public DocumentFile findFile(@NonNull String displayName) {
    for (DocumentFile doc : listFiles()) {
        if (displayName.equals(doc.getName())) {
            return doc;
        }
    }
    return null;
}

This seems innocuous. However, listFiles() is an abstract method, as DocumentFile supports both regular files and SAF-backed document trees. listFiles(), for a SAF Uri, retrieves all child documents from the provider (from version 1.0.1):

@Override
public DocumentFile[] listFiles() {
    final ContentResolver resolver = mContext.getContentResolver();
    final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(mUri,
            DocumentsContract.getDocumentId(mUri));
    final ArrayList<Uri> results = new ArrayList<>();

    Cursor c = null;
    try {
        c = resolver.query(childrenUri, new String[] {
                DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null);
        while (c.moveToNext()) {
            final String documentId = c.getString(0);
            final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri,
                    documentId);
            results.add(documentUri);
        }
    } catch (Exception e) {
        Log.w(TAG, "Failed query: " + e);
    } finally {
        closeQuietly(c);
    }

    final Uri[] result = results.toArray(new Uri[results.size()]);
    final DocumentFile[] resultFiles = new DocumentFile[result.length];
    for (int i = 0; i < result.length; i++) {
        resultFiles[i] = new TreeDocumentFile(this, mContext, result[i]);
    }
    return resultFiles;
}

For cases where we need all of the children, that is not an unreasonable implementation. However, findFiles() needs only one child, not all of them. It would be more efficient to query() for children whose DISPLAY_NAME matches the desired value… but that is not how DocumentFile is implemented at the present time. Perhaps it will be improved in the future, but for now, we have an inefficient implementation.

So, use DocumentFile, but if you run into performance issues, consider working directly with DocumentsContract, using the implementation of DocumentFile as a guide. DocumentsContract is not a particularly developer-friendly API, but it is “bare metal” in terms of SAF, so you will not get better performance.


Other posts in this series include:


Stuck on an Android problem? Subscribers have access to live office hours chats with Mark Murphy, to help you work through your challenges!