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 Uri in a DocumentFile, via DocumentFile.fromTreeUri()

  • Call createDirectory() on that DocumentFile to create some sort of sub-tree and give you a DocumentFile pointing to it

  • Call createFile() on the sub-tree DocumentFile for each file that you want to back up, to get a DocumentFile representing the backup of that file

  • Call getUri() on the backup DocumentFile and use that with ContentResolver and openOutputStream() to get an OutputStream that 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 Uri in a DocumentFile, via DocumentFile.fromTreeUri()

  • Call listFiles() to get an array of DocumentFile objects, 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 those DocumentFile objects and use that with ContentResolver and openInputStream() to get an InputStream that 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: