androidsaf

Why is DocumentsContract.getTreeDocumentId(uri) giving me the doc ID for a parent of the Uri argument?


The app sends the user to SAF picker with ACTION_OPEN_DOCUMENT_TREE:

void openStoragePicker() {
    String messageTitle = "Choose directory app to use";
    Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(Intent.createChooser(intent, messageTitle), Dry.REQUEST_CHOOSE_APP_DIR);
}

In onActivityResult, we take persistable permission and store a String of the Uri:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    switch (resultCode) {
        case Activity.RESULT_OK:
            if (requestCode == Dry.REQUEST_CHOOSE_APP_DIR) {
                if (resultData == null) {
                    Log.d(Dry.TAG, "result data null");
                } else {
                    if (resultData.getData() != null) {
                        Uri uri = resultData.getData();
                        Storage.releasePersistedPermissions(this);
                        getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                        Storage.setSharedPrefString(uri.toString(), Storage.SHARED_PREF_APP_DIR_URI, this);
                        dbw.clearAlreadyPlayed();
                    }
                }
            }
            break;
        case Activity.RESULT_CANCELED:
            //
            break;
    }
}

We recreate the tree Uri when we need it:

static Uri getTheDir(Context context) {
    String result = Storage.getSharedPrefString(SHARED_PREF_APP_DIR_URI, context);
    if (result == DEFAULT_SHARED_PREF_STRING) {
        return null;
    }
    Uri dirUriParsed = Uri.parse(Uri.decode(result));
    Log.d(Dry.TAG, "the dir uri parsed: " + dirUriParsed.toPath());
    return dirUriParsed;
}

We want to list the files, and we can, using the pattern shown here.

static ArrayList<String> getFiles(Context context) {
    ArrayList<String> fileStrings = new ArrayList<>();
    Uri rootUri = getTheDir(context);
    if (rootUri == null) {
        return fileStrings;
    }

    long startTime = System.currentTimeMillis();

    ContentResolver contentResolver = context.getContentResolver();
    String theDocToReturnChildrenFor = DocumentsContract.getTreeDocumentId(rootUri);
    Log.d(Dry.TAG, "theDocToReturnChildrenFor: " + theDocToReturnChildrenFor);
    Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, theDocToReturnChildrenFor);
    List<Uri> dirNodes = new LinkedList<>();
    dirNodes.add(childrenUri);
    while(!dirNodes.isEmpty()) {
        childrenUri = dirNodes.remove(0);
        Cursor c = contentResolver.query(childrenUri, new String[]{DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE}, null, null, null);
        try {
            while (c.moveToNext()) {
                final String docId = c.getString(0);
                final String name = c.getString(1);
                final String mime = c.getString(2);
                if (isDirectory(mime)) {
                    if (Arrays.asList(SUBDIRECTORIES_TO_OMIT).contains(name)) {
                        continue;
                    }
                    final Uri newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId);
                    dirNodes.add(newNode);
                } else {
                    for (String ext: SUPPORTED_FILE_EXTENSIONS) {
                        if (name.endsWith(ext)) {
                            fileStrings.add(docId);
                            break;
                        }
                    }
                }
            }
        } finally {
            closeQuietly(c);
        }
    }

    Log.d(Dry.TAG, "fileStrings length: " + fileStrings.size() + "time spent building song list: " + ((System.currentTimeMillis() - startTime) / 1000.0) + "s");
    return fileStrings;
}

But, this only works as expected when the directory happens to be a top-level directory within the storage volume. If the directory that the user chose is not a direct child of the volume root, then, when we try DocumentsContract.getTreeDocumentId(rootUri), it returns not the document ID for that URI, but rather the document ID for its highest parent before the volume root!

The log call that prints the reconstructed Uri gives this output:

the dir uri parsed: /tree/primary:a test dir/a child test dir/3rd level dir

But the other log call that prints the doc ID prints this:

theDocToReturnChildrenFor: primary:a test dir

Am I doing it wong? Is this an Android bug? I noticed this question describes the exact same behavior from this method. That issue was solvable by following the established recursive listing pattern, but, that user says:

It is almost like getTreeDocumentId(rootUri) is returning what getRootId(rootUri) should be returning.

The docs for this method are not helpful, they are brief and have a typo, leaving the meaning unclear. DocumentsContract.getTreeDocumentId docs.

Target SDK of the app is 30. The device Android version is also api 30 (Android 11).

If someone could help me to get the correct doc ID for the user-selected directory, I would appreciate it.


Solution

  • Uri dirUriParsed = Uri.parse(Uri.decode(result))

    Try:

      Uri dirUriParsed = Uri.parse(result)