Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issues traversing through directory hierarchy with Android Storage Access Framework / DocumentProvider using MTP

UPDATE: My initial question may be misleading so I want to rephrase it: I want to traverse through the hierarchy tree from an MTP connected device through Android's Storage Access Framework. I can't seem to achieve this because I get a SecurityException stating that a subnode is not a descendant of its parent node. Is there a way to workaround this issue? Or is this a known issue? Thanks.

I'm writing an Android application that attempts to traverse and access documents through the hierarchy tree using Android's Storage Access Framework (SAF) via the MtpDocumentsProvider. I am more or less following the code example described in https://github.com/googlesamples/android-DirectorySelection on how to launch the SAF Picker from my app, select the MTP data source, and then, in onActivityResult, use the returned Uri to traverse through the hierarchy. Unfortunately, this doesn't seem to work because as soon as I access a sub-folder and try to traverse that, I always get a SecurityException stating that document xx is not a descendant of yy

So my question is, using the MtpDocumentProvider, how can I successfully traverse through the hierarchy tree from my app and avoid this exception?

To be specific, in my app, first, I call the following method to launch the SAF Picker:

private void launchStoragePicker() {
    Intent browseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    browseIntent.addFlags(
        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
            | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
            | Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    );
    startActivityForResult(browseIntent, REQUEST_CODE_OPEN_DIRECTORY);
}

The Android SAF picker then launches, and I see my connected device recognized as the MTP data source. I select said data source and I get the Uri from my onActivityResult:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == Activity.RESULT_OK) {
        traverseDirectoryEntries(data.getData()); // getData() returns the root uri node
    }
}

Then, using the returned Uri, I call DocumentsContract.buildChildDocumentsUriUsingTree to get a Uri which I can then use to query and access the tree hierarchy:

void traverseDirectoryEntries(Uri rootUri) {
    ContentResolver contentResolver = getActivity().getContentResolver();
    Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, DocumentsContract.getTreeDocumentId(rootUri));

    // Keep track of our directory hierarchy
    List<Uri> dirNodes = new LinkedList<>();
    dirNodes.add(childrenUri);

    while(!dirNodes.isEmpty()) {
        childrenUri = dirNodes.remove(0); // get the item from top
        Log.d(TAG, "node uri: ", childrenUri);
        Cursor c = contentResolver.query(childrenUri, new String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, 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);
                Log.d(TAG, "docId: " + id + ", name: " + name + ", mime: " + mime);
                if(isDirectory(mime)) {
                    final Uri newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId);
                    dirNodes.add(newNode);
                }
            }
        } finally {
            closeQuietly(c);
        }
    }
}

// Util method to check if the mime type is a directory
private static boolean isDirectory(String mimeType) {
    return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
}

// Util method to close a closeable
private static void closeQuietly(Closeable closeable) {
    if (closeable != null) {
        try {
            closeable.close();
        } catch (RuntimeException re) {
            throw re;
        } catch (Exception ignore) {
            // ignore exception
        }
    }
}

The first iteration on the outer while loop succeeds: the call to query returned a valid Cursor for me to traverse. The problem is the second iteration: when I try to query for the Uri, which happens to be a subnode of rootUri, I get a SecurityException stating the document xx is not a descendent of yy.

D/MyApp(19241): node uri: content://com.android.mtp.documents/tree/2/document/2/children D/MyApp(19241): docId: 4, name: DCIM, mime: vnd.android.document/directory D/MyApp(19241): node uri: content://com.android.mtp.documents/tree/2/document/4/children E/DatabaseUtils(20944): Writing exception to parcel E/DatabaseUtils(20944): java.lang.SecurityException: Document 4 is not a descendant of 2

Can anyone provide some insight as to what I'm doing wrong? If I use a different data source provider, for example, one that is from external storage (i.e. an SD Card attached via a standard USB OTG reader), everything works fine.

Additional information: I'm running this on a Nexus 6P, Android 7.1.1, and my app minSdkVersion is 19.

like image 776
spring.ace Avatar asked Dec 12 '16 07:12

spring.ace


2 Answers

I am using your code to go in sub folders with adding lines:

Uri childrenUri;
try {
    //for childs and sub child dirs
    childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri));
} catch (Exception e) {
    // for parent dir
    childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
}
like image 80
Foobnix Avatar answered Oct 23 '22 02:10

Foobnix


The flags added before start activity for result do nothing.

I do not understand that you use DocumentsContract. The user picks a directory. From the uri you get you can construct a DocumentFile for that directory.

After that use DocumentFile::listFiles() on that instance to get a list of subdirectories and files.

like image 37
greenapps Avatar answered Oct 23 '22 02:10

greenapps