Search code examples
androidstorage-access-frameworkdocuments-provider

queryRoots calls queryDocument first, not queryChildDocuments


I'm writing a wrapper for SAF wrapper for Dropbox since everyone (including Google) is too lazy to implement this "very rich" (ie: awful) API. I've got my root in the picker, but I thought queryChildren should be called first. However, queryChildren is never called and it goes straight toqueryDocument`.

override fun queryRoots(projection: Array<out String>?): Cursor {
    // TODO: Likely need to be more strict about projection (ie: map to supported)
    val result = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)

    val row = result.newRow()
    row.add(DocumentsContract.Root.COLUMN_ROOT_ID, "com.anthonymandra.cloudprovider.dropbox")
    row.add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_dropbox_gray)
    row.add(DocumentsContract.Root.COLUMN_TITLE, "Dropbox")
    row.add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE)   // TODO:
    row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID)
    return result
}

override fun queryChildDocuments(
    parentDocumentId: String?,
    projection: Array<out String>?,
    sortOrder: String?
): Cursor {
    // TODO: Likely need to be more strict about projection (ie: map to supported)
    val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
    val dropboxPath = if (parentDocumentId == ROOT_DOCUMENT_ID) "" else parentDocumentId

    try {
        val client = DropboxClientFactory.client

        var childFolders = client.files().listFolder(dropboxPath)
        while (true) {
            for (metadata in childFolders.entries) {
                addDocumentRow(result, metadata)
            }

            if (!childFolders.hasMore) {
                break
            }

            childFolders = client.files().listFolderContinue(childFolders.cursor)
        }
    } catch(e: IllegalStateException) { // Test if we can attempt auth thru the provider
        context?.let {
            Auth.startOAuth2Authentication(it, appKey)   // TODO: appKey
        }
    }
    return result
}

override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
    // TODO: Likely need to be more strict about projection (ie: map to supported)
    val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)

    try {
        val client = DropboxClientFactory.client
        val metadata = client.files().getMetadata(documentId)
        addDocumentRow(result, metadata)
    } catch(e: IllegalStateException) { // Test if we can attempt auth thru the provider
        context?.let {
            Auth.startOAuth2Authentication(it, appKey)   // TODO: appKey
        }
    }
    return result
}

Error:

java.lang.IllegalArgumentException: String 'path' does not match pattern
    at com.dropbox.core.v2.files.GetMetadataArg.<init>(GetMetadataArg.java:58)
    at com.dropbox.core.v2.files.GetMetadataArg.<init>(GetMetadataArg.java:80)
    at com.dropbox.core.v2.files.DbxUserFilesRequests.getMetadata(DbxUserFilesRequests.java:1285)
    at com.anthonymandra.cloudprovider.dropbox.DropboxProvider.queryDocument(DropboxProvider.kt:98)
    at android.provider.DocumentsProvider.query(DocumentsProvider.java:797)
    at android.content.ContentProvider$Transport.query(ContentProvider.java:240)
    at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:102)
    at android.os.Binder.execTransact(Binder.java:731)

path is ROOT_DOCUMENT_ID which I'm expecting to go to queryChildDocuments first.

What am I missing here?


Solution

  • The documentation for implementing a DocumentsProvider is... limited. In particular, there is no documented guarantee of the order of calls. As such, a DocumentsProvider really should be implemented to make as few assumptions as possible about the order of those calls.

    For example, I would not assume that queryRoots() is called first. It probably will be first, if the first use of the DocumentsProvider for this process happens to be the Storage Access Framework UI. However, given that clients can (with care) persist a document or document tree Uri, you might wind up being called with something else first in your process, if the first thing happens to be a client using a persisted Uri.

    And, in your specific case, I would not assume that queryChildDocuments() occurs before or after queryDocument().