12
votes

Background

I have a few apps that make heavy use of SD card for file syncing. The broken external SD card access on Kitkat is still a big problem, but I am trying to resolve this with the new API available on Lollipop for the users which have this.

I successfully request and persist permission to SD card and I can list files in the root Uri returned from the grant permission activity.

See more info on how this is done here: how-to-use-the-new-sd-card-access-api-presented-for-lollipop

The user can then select any folder/subfolder for sync and I persist the folder document Uri as a string in the database.

Problem

Later, potentially after app has been restarted, the syncing of files can initiate. I then try to list the files in a subfolder (remember, I have the right permission granted and the persistence of this works and also grants me access to all children).

I then create a new instance of DocumentFile from the stored string and try to list the files:

  DocumentFile dir = DocumentFile.fromTreeUri(ctx, Uri.parse(storedUri));
  dir.listFiles();

The problem is listFiles always returns the children at the root Uri granted and never the children of the actual Uri I give the DocumentFile.fromTreeUri method.

I have examined the source code of DocumentFile and it seems there is a bug there, specifically I don't see the need to modify the Uri further:

public static DocumentFile fromTreeUri(Context context, Uri treeUri) {
  final int version = Build.VERSION.SDK_INT;
  if (version >= 21) {
    return new TreeDocumentFile(null, context,
      DocumentsContractApi21.prepareTreeUri(treeUri));
  } else {
  return null;
}

If we look at the source of DocumentsContractApi21.prepareTreeUri we see that rebuilds the Uri:

 public static Uri prepareTreeUri(Uri treeUri) {
   return DocumentsContract.buildDocumentUriUsingTree(treeUri,
     DocumentsContract.getTreeDocumentId(treeUri));
 }

And the methods it calls:

 public static Uri buildDocumentUriUsingTree(Uri treeUri, String documentId) {
   return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(treeUri.getAuthority()).appendPath(PATH_TREE)
                 .appendPath(getTreeDocumentId(treeUri)).appendPath(PATH_DOCUMENT)
                 .appendPath(documentId).build();
 }

 public static String getTreeDocumentId(Uri documentUri) {
   final List<String> paths = documentUri.getPathSegments();
   if (paths.size() >= 2 && PATH_TREE.equals(paths.get(0))) {
     return paths.get(1);
   }
   throw new IllegalArgumentException("Invalid URI: " + documentUri);
 }

The found document Id by getTreeDocumentId will always correspond to the root Uri id, no matter what Uri the methods is called with. This makes it impossible to list children of sub folders using the provided framework methods.

Solution

Please fix the fromTreeUri method to not always use the root document Uri id.

Doing the following ugly hack fixes the issue, which I would really prefer not to.

  Class<?> c = Class.forName("android.support.v4.provider.TreeDocumentFile");
  Constructor<?> constructor = c.getDeclaredConstructor(DocumentFile.class, Context.class, Uri.class);
  constructor.setAccessible(true);

  DocumenFile dir = (DocumentFile) constructor.newInstance(null, mCtx, treeUri);
  dir.listFiles();
1
Is there a entry in b.android.com for this case?Morrison Chang
I can't find any entries in the Android bugtracker, but I would be happy to add it there once it has been clarified that it indeed is a bug and not just me doing something wrong.AndersC
I have noticed the same thing and i also find that it makes the new APIs very uncomfortable to use. Did you submit a ticket to b.android.com ?darken
Yes, it "works" exactly how you describe it. I considered it as a feature - of course very odd one.Quark
I''m using the same workaround as you. Did you find any cleaner solution?PerracoLabs

1 Answers

5
votes

The underlying bug seems to be fixed. I also encountered that problem and without any code changes it works now with version 1.0.1 of the documentfile package.

dependencies {
    ...
    implementation 'androidx.documentfile:documentfile:1.0.1'
    ...
}