Bugs coming from unused libraries
2023-10-23
Android's Storage Access Framework
I recently had to write some code that deals with the Android Storage Access Framework. In Android, it's an abstraction that allows to unify how applications deal with documents regardless if they are stored on Google Drive or on the internal device disk.
In order to use it, an app developer typically needs to implement the following flow:
- Start the system UI expressing that your app wants to get access to some document or tree of documents (a folder). That UI will expose the "files browser" and let the user pick what they should.
- Handle the result returned from system UI: it's a URI that identifies the selected document or tree.
- To work with the document:
- Query the system to information about the document (MIME type, display name, creation date, etc)
(this query will be transmitted to the application that backs the document: Google Drive or system files browser).context.contentResolver.query(myDocumentUri, arrayOf(DocumentContract.Document.COLUMN_DISPLAY_NAME), null, null, null)
- Open an input stream to read it.
context.contentResolver.openInputStream(myDocumentUri)
- Query the system to information about the document (MIME type, display name, creation date, etc)
If you ask to get access to a tree, and the user, for example, chooses a folder on their internal storage called "Documents/my-data", you will get a URI that looks like
content://com.android.externalstorage.documents/tree/primary:Documents%2Fmy-data
To read file table.csv
in that directory, you will have a URI that looks like
content://com.android.externalstorage.documents/tree/primary:Documents%2Fmy-data/document/primary:Documents%2Fmy-data%2Ftable.csv
In general, when you ask for permissions to operate on a document tree, the storage access framework deals with the URIs of the following format
content://{PROVIDER}/tree/{GRANTED_TREE_ID}/document/{DOCUMENT_ID}
Whenever you make a query via the ContentResolver
, you must make sure that your query URIs are prefixed with the tree URI returned by the system UI
(where you got the permissions). Otherwise, your query will not reach the target provider being blocked by the system with a permission denial error.
Listing child documents
To query the list of files in a folder we'll need a URI that is structured like:
content://{PROVIDER}/tree/{GRANTED_TREE_ID}/document/{FOLDER_ID}/children
So if we list children of "Documents/my-data/nested", it's
content://com.android.externalstorage.documents/tree/primary:Documents%2Fmy-data/document/primary:Documents%2Fmy-data%2nested/children
JetPack & DocumentFile
Android JetPack also has an extra library
that provides another abstraction DocumentFile
- an interface that resembles the one exposed by java.io.File
with
implementations that are backed both by the device file system and by the Android's Storage Access Framework.
Listing children with this library should be quite easy:
DocumentFile.fromTreeUri(myUriPointingToTheFolder).listFiles() // List of DocumentFile.
However, it appears it didn't work at all for me...
When I construct a DocumentFile
with a URI that looks like
content://{PROVIDER}/tree/{GRANTED_TREE_ID}/document/{FOLDER_ID}
the implementation of DocumentFile
rebuilds the provided URI
and makes a query that looks like
content://{PROVIDER}/tree/{GRANTED_TREE_ID}/document/{GRANTED_TREE_ID}/children
So when the folder you query is a nested one, you get an unexpected result: children of your root instead of the target directory.
You could try to work around it by providing a different URI to DocumentFile
:
content://{PROVIDER}/tree/{FOLDER_ID}/document/{FOLDER_ID}
At least, for the system external storage provider, it would be a valid URI. However, you'll get a permission denial since you query a tree that you don't have explicit permissions for.
The code of DocumentFile
exists for a very long time... It was a part of the now deprecated support library of Android.
But apparently, it was not really used. At least, not to list the files of the nested directories...
It may only work for some system apps that have access to the full storage.
I ended up by not using it too.
The fix is there
Thinking that I could easily fix the problem in the JetPack project, I started browsing the code on their main branch and discovered that the issue is fixed there.
My project was using version 1.0.0
, and the fix is available in 1.0.1
and in the latest published version named 1.1.0-alpha1
...
A bug in the Android issue tracker is also present. Note how it was reported in 2016 and fixed in 2019.
The alpha version is visible on the JetPack website, the 1.0.1
one is not unless you go deeper to the release notes.
Choosing the version marked as alpha
is not the first thing many developers do.
So good luck picking a version that works :).
It's a good motivation to have the mechanism to retract buggy versions.