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
andContentResolver
, such as usingDocumentsContract.buildChildDocumentsUriUsingTree()
andquery()
to find the child documents for a treeUri
-
The
Uri
returned byDocumentsContract.buildChildDocumentsUriUsingTree()
is one pointing to a system-suppliedContentProvider
, and we make one IPC hop toquery()
that provider -
However, that provider serves as a proxy, and it in turn makes a similar
query()
on theDocumentsProvider
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.
The entire series of “Scoped Storage Stories” posts includes posts on:
- The basics of using the Storage Access Framework
- Getting durable access to the selected content
- Working with
DocumentFile
for individual documents - Working with document trees
- Working with
DocumentsContract
- Problems with the SAF API
- A specific problem with
listFiles()
onDocumentFile
- Storing content using
MediaStore
- Reading content from the
MediaStore
- Modifying
MediaStore
content from other apps - Limitations of
MediaStore.Downloads
- The undocumented
Documents
option - More on
RecoverableSecurityException
- How to modify more metadata in
MediaStore