7

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.

spring.ace
  • 111
  • 8
  • `and I see my connected device recognized as the MTP data source.`. Can you elaborate on that? MTP? Did you connect a device on your Android device? How? What kind of device? – greenapps Dec 12 '16 at 11:03
  • `if(isDirectory(mime)) `. You did not post code for that function. Nor did you explain it. – greenapps Dec 12 '16 at 13:10
  • You have Uri rootUri and Uri uri. But the latter is not explained. – greenapps Dec 12 '16 at 13:11
  • `closeQuietly(childCursor);` childCursor? – greenapps Dec 12 '16 at 13:12
  • @greenapps hi, yes I connected a camera device on my Android phone using a USB-C to USB-C cable. Android detected the device successfully and determined I can access the contents via MTP. the `isDirectory` and `closeQuietly` methods are just helper methods. I added the code in my edited post. `rootUri` is the `Uri` returned from `onActivityResult`. I made a copy/paste mistake, which I have fixed. – spring.ace Dec 12 '16 at 18:55
  • `and determined I can access the contents via MTP`. Can you elaborate on that? What does the user see by which he knows that the connection is MTP? Or the programmer? I do not undestand that you use that mtp provrider code from your link as your android app is not a provider. The camera will be the provider. I have never seen a camera which communicates using mtp. That is all new to me. – greenapps Dec 12 '16 at 19:02
  • `and its trying to communicate via MTP`. How do you know? I asked that before! What is the make/type of the camera? I asked that before too. – greenapps Dec 12 '16 at 22:31
  • @greenapps, My phone (Nexus 6P, Android 7.1.1) detected when I connected my camera device directly to it that USB is attached (a notification appeared). I'm assuming it would behave the same as connecting an SD card to my phone using an SD Card Reader and treat it like external storage, which I KNOW works. So in code I simply used the Android Storage Access Framework and launched the picker using the `ACTION_OPEN_DOCUMENT_TREE` intent and I see the camera connected device in the picker as a result. I'm not doing anything special. – spring.ace Dec 12 '16 at 22:38
  • @greenapps, YES! I'm telling you my phone is autodetecting that the device is connected!. And I"m using an action camera, a go pro camera to be exact. – spring.ace Dec 12 '16 at 22:39
  • Please try your code on an Android 6 or lower version device. – greenapps Dec 14 '16 at 08:55
  • Hello! Still here? I could now try your code with a camera attached on Android device. An uri like content://com.android.mtp.documents/tree/147 and your code did its job. – greenapps Mar 31 '18 at 10:35
  • When I try to use buildChildDocumentsUriUsingTree(uri, docId) recursively, it returns a Uri with the parent tree, instead of the uri tree. How do I fix that? – John Glen Oct 18 '20 at 01:27

3 Answers3

4

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));
}
Foobnix
  • 679
  • 5
  • 7
1

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.

greenapps
  • 10,799
  • 2
  • 13
  • 19
  • I want to traverse the tree hierarchy starting from `rootUri`. I call the `DocumentsContract` method to build the children uri so I can recursively traverse the directory under that. So imagine I have a root node A, and has child directories B and C. And B has files called B1, B2, B3. Ultimately I want to access B1, B2, B3 (they can be image files), but the problem is I can't traverse to the children of B because as soon as I ask for the child nodes of B, it gives me the `SecurityException`. – spring.ace Dec 12 '16 at 19:00
  • You do not have to explain again what you want and what you do. I already understood that and tried your code which worked for a sd card. All your code was new for me so interesting. But what i like to know is why you do not use the code i suggested. You are not even reacting on my proposal. With my code you can traverse all too. Otherwise i had not posted it. – greenapps Dec 12 '16 at 19:04
  • Because I want direct access to the `Uri`, I don't want to simply list them. If I want to open the contents of said directories, for example, the image files, and open it via a stream, I should be able to do it. But yes, let me give your approach a shot and let you know what happens. I'm simply just responding to the comments to make sure my intentions were clear. – spring.ace Dec 12 '16 at 19:26
  • As said you can browse and list all with DocumentFile. And use getContentResolver(). openInputStream(docfile.getUri()) to read the contents of files. Thst is the normal way for the consumer. Meanwhile you have not explained why you would use code for a provider for a consumer. – greenapps Dec 12 '16 at 20:03
  • I tried using `DocumentFile#listFiles()` and it didn't work either. I get the same behavior. Whenever I try to access the child `Uri`, for example when opening it in a stream using `getContentResolver().openInputStream`, or even calling `DocumentFile#isDirectory()`, it throws the same `SecurityException`. The `Uri` generated is the same, whether I use `DocumentFile` or through the provider. Again, this is only if the data source is an MTP data source. Works fine if its from external storage (i.e. SD Card) – spring.ace Dec 12 '16 at 22:20
  • Again: how do you know that it is an mtp data source? Why do i have to ask thst again and again? Realise that i never saw something with mtp so explain what the user sees please. – greenapps Dec 12 '16 at 22:36
  • I don't know how else to explain it to you. I'm telling you when I launch the SAF picker it shows it as an MTP Data Source. I'm not doing anything special! I looked at the Android framework code and I see there's an internal `MtpDocumentProvider` so I'm assuming the framework knows to launch that item when I launch the `ACTION_OPEN_DOCUMENT_TREE` intent. I don't know how else to explain to you unless I show you a picture of it. – spring.ace Dec 12 '16 at 22:44
  • I'm not sure where the disconnect is. The point of the Android Storage Access Framework is to abstract the data source from the user regardless of where the source is coming from. In the app code, there is no MTP code because I don't need it; I rely on the framework to give me a `Uri` pointing to the source and I don't care if it came from an MTP source, external storage source, cloud source, whatever. I interface with a `ContentProvider`. That's the point of the SAF. The only "knowledge" that I have that I know I'm using an MTP Data Source is because the picker said so in the name, MTP! – spring.ace Dec 12 '16 at 22:49
  • `the SAF picker it shows it as an MTP Data Source`. Ok. Now i know. That is what the user sees. – greenapps Dec 12 '16 at 23:11
0

After different attempts, I'm not sure there is a way around this SecurityException for an MTP Data Source (unless someone can refute me on this). Looking at the DocumentProvider.java source code and the stack trace, it appears that the call to DocumentProvider#isChildDocument inside DocumentProvider#enforceTree may not have been properly overriden in the MtpDocumentProvider implementation in the Android framework. Default implementation always returns false. #sadface

spring.ace
  • 111
  • 8
  • Maybe. But those functions are not used if you only use DocumentFile as i do. So where would the exception come from then? – greenapps Dec 13 '16 at 07:59
  • The issue is when you query the `Uri` for the item in question. To access the contents of the `DocumentFile`, you still need to get its `Uri` through `DocumentFile#getUri`, or internally `DocumentFile` uses this `Uri` reference to do what it needs to accomplish (for example, the call to `DocumentFile#isDirectory` throws the same exception). So my guess is, `DocumentFile` queries this Uri through a content resolver, which in turn calls the concrete implentation of `queryChildren` internally, which eventually calls `enforceTree`, which then throws the Exception. That's my guess. – spring.ace Dec 13 '16 at 08:47
  • Sadly i have no go pro to test. Moreover is handling mtp implemented for other versioons then Android 7? – greenapps Dec 13 '16 at 10:55
  • `may not have been properly overriden in the MtpDocumentProvider implementation in the Android framework`. Sorry but i cannot imagine that this code is used. The provider is on your go pro camera and how would you know how its implemented there? – greenapps Dec 14 '16 at 08:57