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.





Previously, I covered:




The basics of using the Storage Access Framework
Getting durable access to the selected content
Working with DocumentFile for individual documents


In my next post in the series, I will point out the limitations of DocumentFile for
batch operations, and will point out alternative approaches that will improve
performance.

 •  0 comments  •  flag
Share on Twitter
Published on November 09, 2019 07:13
No comments have been added yet.