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 aDocumentFile
, viaDocumentFile.fromTreeUri()
-
Call
createDirectory()
on thatDocumentFile
to create some sort of sub-tree and give you aDocumentFile
pointing to it -
Call
createFile()
on the sub-treeDocumentFile
for each file that you want to back up, to get aDocumentFile
representing the backup of that file -
Call
getUri()
on the backupDocumentFile
and use that withContentResolver
andopenOutputStream()
to get anOutputStream
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 aDocumentFile
, viaDocumentFile.fromTreeUri()
-
Call
listFiles()
to get an array ofDocumentFile
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 thoseDocumentFile
objects and use that withContentResolver
andopenInputStream()
to get anInputStream
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:
- 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