Scoped Storage Stories: Trees
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 fourth post in a series where we will explore how to work with those alternatives, starting with the Storage Access Framework (SAF).
Working with individual pieces of content via ACTION_OPEN_DOCUMENT or
ACTION_CREATE_DOCUMENT is not that difficult and is not that different than
working with files.
However, suppose you need to work with several related pieces of content. For example, you want to offer a manual backup mechanism, and rather than backing up several files to a single ZIP, you want to offer a backup to a user-chosen directory.
In principle, you could still use ACTION_CREATE_DOCUMENT for this, asking
the user for the location of each piece of content that you wish to create as
part of the backup. This is annoying for the user, as they have to go through
the ACTION_CREATE_DOCUMENT UI N times for your N pieces of content. And if
the user screws something up — such as choosing different directories instead
of the same directory for all of the content — your app may run into problems
in consuming that content later on.
What would be better is if the user could create a directory for you, then grant you access to that entire directory. You could then put your content into that directory as you see fit.
Unfortunately, that is not completely supported. What is supported is for
the user to choose some “directory”, via ACTION_OPEN_DOCUMENT_TREE, on Android 5.1+.
Your app can then create its own “sub-directory” in the user-chosen location, then
put your content there.
Here, I have “directory” and “sub-directory” in quotes, because technically that
is not what you are working with. You are working with document trees, reflecting
the name ACTION_OPEN_DOCUMENT_TREE. Whether or not a given document tree
reflects some directory on some filesystem on some machine is up to the implementers
of the user-selected document provider. In practice, it is likely to be a filesystem
directory on the device, as few cloud storage providers seem to support ACTION_OPEN_DOCUMENT_TREE.
At the outset, you use ACTION_OPEN_DOCUMENT_TREE similarly to how you use
ACTION_OPEN_DOCUMENT. You create an Intent with ACTION_OPEN_DOCUMENT_TREE
as the action, then pass that Intent to startActivityForResult():
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), REQUEST_TREE)
Then, in onActivityResult(), you can get a Uri that represents the tree:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
data?.data?.let { useTheTree(it) }
}
}
private fun useTheTree(root: Uri) {
TODO("something useful, but on a background thread please")
}
From there, what you do will vary based on scenario.
Suppose you want to save a backup of several files, as suggested earlier. You could:
-
Wrap that
Uriin aDocumentFile, viaDocumentFile.fromTreeUri() -
Call
createDirectory()on thatDocumentFileto create some sort of sub-tree and give you aDocumentFilepointing to it -
Call
createFile()on the sub-treeDocumentFilefor each file that you want to back up, to get aDocumentFilerepresenting the backup of that file -
Call
getUri()on the backupDocumentFileand use that withContentResolverandopenOutputStream()to get anOutputStreamthat you can use to create the backup itself
Suppose instead that you want to restore from the backup. If your instructions
are for the user to choose the sub-tree that you created above, you could then
do this with the Uri from ACTION_OPEN_DOCUMENT_TREE:
-
Wrap that
Uriin aDocumentFile, viaDocumentFile.fromTreeUri() -
Call
listFiles()to get an array ofDocumentFileobjects, representing the contents of that tree -
Examine those to see if they look like your backed-up content, showing some error to the user if it looks like they chose the wrong place
-
Use
getUri()for each of thoseDocumentFileobjects and use that withContentResolverandopenInputStream()to get anInputStreamthat you can use to restore the content from the backup
There are many other patterns that you could follow — these are just
for illustration purposes. The key is that you use DocumentFile much like you
would use File for creating or iterating over the contents of a directory.
While the details are different, the general approach is the same as with classic
Java file I/O.
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
DocumentFilefor 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
MediaStorecontent from other apps - Limitations of
MediaStore.Downloads - The undocumented
Documentsoption - More on
RecoverableSecurityException - How to modify more metadata in
MediaStore

