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:

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.