queryRoots 首先调用 queryDocument,而不是 queryChildDocuments

queryRoots calls queryDocument first, not queryChildDocuments

我正在为 Dropbox 的 SAF 包装器编写一个包装器,因为每个人(包括 Google)都懒得实施这个 "very rich"(即:糟糕)API。我扎根于选择器,但我认为 queryChildren 应该首先被调用。但是,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
}

错误:

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)

pathROOT_DOCUMENT_ID,我希望先去 queryChildDocuments

我在这里错过了什么?

实现 DocumentsProvider 的文档……有限。特别是,没有记录调用顺序的保证。因此,确实应该实施 DocumentsProvider 以尽可能少地假设这些调用的顺序。

例如,我不会假定 queryRoots() 首先被调用。如果此过程的第一个 DocumentsProvider 碰巧是存储访问框架 UI,它可能会是第一个。但是,鉴于客户端可以(小心地)持久化文档或文档树 Uri,如果第一件事恰好是使用持久化 Uri.

并且,在您的具体情况下,我不会假设 queryChildDocuments() 发生在 queryDocument() 之前或之后。

我也写过一个SAF DropBox实现,一开始我也有点困惑。

来自documentation

注意以下几点:

  • 每个文档提供者报告一个或多个 "roots" 正在启动 指向探索文档树。每个根都有一个独特的 COLUMN_ROOT_ID,它指向一个文件(一个目录) 表示该根目录下的内容。根是动态的 旨在支持多个帐户、瞬态 USB 等用例 存储设备,或用户 login/log out.
  • 每个根目录下都有一个文档。那个文件指向1到N 文档,每个文档又可以指向1到N个文档。
  • 每个存储后端通过 用唯一的 COLUMN_DOCUMENT_ID 引用它们。文件 ID 必须 是独一无二的,一旦发布就不会改变,因为它们用于 跨设备重新启动的持久 URI 授权。
  • 文档可以是可打开的文件(具有特定的 MIME 类型), 或包含附加文件的目录(带有 MIME_TYPE_DIR MIME 类型)。
  • 每个文档都可以有不同的功能,如 COLUMN_FLAGS。例如,FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE,以及 FLAG_SUPPORTS_THUMBNAIL。相同 COLUMN_DOCUMENT_ID 可以包含在多个目录中。

第二个项目符号是关键项目符号。在来自 queryRoots() 的 return 之后,对于您传回的每个根,SAF 都会调用 queryDocument()。这实际上是为了创建出现在列表中的 "root file folder" 文档。我所做的是在 queryDocument() 中检查传入的 documentId 是否与我在 queryRoots() 调用中赋予 DocumentsContract.Root.COLUMN_ROOT_ID 的唯一值匹配。如果是,那么您知道此 queryDocument() 调用需要 return 表示该根目录的文件夹。否则,我在其他任何地方都使用来自 DropBox 的路径作为我的 documentId,因此我在通过 DbxClientV2 的调用中使用该 documentID 值。

这是一些示例代码 - 请注意,在我的例子中,我创建了一个 AbstractStorageProvider class,我的所有各种提供程序(Dropbox、Instagram 等)都从中扩展。基础 class 处理接收来自 SAF 的调用,它会做一些内务处理(比如创建游标),然后调用实现 classes 中的方法来根据特定服务的需要填充游标:

基地Class

public Cursor queryRoots(final String[] projection) {
    Timber.d( "Lifecycle: queryRoots called");

    // If they are not paid up, they do not get to use any of these implementations
    if (!InTouchUtils.isLoginPaidSubscription()) {
        return null;
    }

    // Create a cursor with either the requested fields, or the default projection if "projection" is null.
    final MatrixCursor cursor = new MatrixCursor(projection != null ? projection : getDefaultRootProjection());

    // Classes that extend this one must implement this method
    addRowsToQueryRootsCursor(cursor);

    return cursor;
}

来自 DropboxProvider addRowsToQueryRootsCursor:

protected void addRowsToQueryRootsCursor(MatrixCursor cursor) {
    // See if we need to init
    long l = System.currentTimeMillis();
    if ( !InTouchUtils.initDropboxClient()) {
        return;
    }
    Timber.d( "Time to test initialization of DropboxClient: %dms.", (System.currentTimeMillis() - l));
    l = System.currentTimeMillis();
    try {
        SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(Objects.requireNonNull(getContext()).getApplicationContext());
        String displayname = sharedPrefs.getString(getContext().getString(R.string.pref_dropbox_displayname_token_key),
                getContext().getResources().getString(R.string.pref_dropbox_displayname_token_default));

        batchSize = Long.valueOf(Objects.requireNonNull(sharedPrefs.getString(getContext().getString(R.string.pref_dropbox_query_limit_key),
                getContext().getResources().getString(R.string.pref_dropbox_query_limit_key_default))));

        final MatrixCursor.RowBuilder row = cursor.newRow();

        row.add(DocumentsContract.Root.COLUMN_ROOT_ID, <YOUR_UNIQUE_ROOTS_KEY_HERE>);
        row.add(DocumentsContract.Root.COLUMN_TITLE,
                String.format(getContext().getString(R.string.dropbox_root_title),getContext().getString(R.string.app_name)));
        row.add(DocumentsContract.Root.COLUMN_SUMMARY,displayname+
                getContext().getResources().getString(R.string.dropbox_root_summary));
        row.add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_RECENTS | DocumentsContract.Root.FLAG_SUPPORTS_SEARCH);
        row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID,<YOUR_UNIQUE_ROOT_FOLDER_ID_HERE>);
        row.add(DocumentsContract.Root.COLUMN_ICON, R.drawable.intouch_for_dropbox);
    } catch (Exception e) {
        Timber.d( "Called addRowsToQueryRootsCursor got exception, message was: %s", e.getMessage());
    }
    Timber.d( "Time to queryRoots(): %dms.", (System.currentTimeMillis() - l));
}

然后是基础中的queryDocument()方法class:

@Override
public Cursor queryDocument(final String documentId, final String[] projection) {
    Timber.d( "Lifecycle: queryDocument called for: %s", documentId);

    // Create a cursor with either the requested fields, or the default projection if "projection" is null.
    // Return a cursor with a getExtras() method, to avoid the immutable ArrayMap problem.
    final MatrixCursor cursor = new MatrixCursor(projection != null ? projection : getDefaultDocumentProjection()){
        Bundle cursorExtras = new Bundle();
        @Override
        public Bundle getExtras() {
            return cursorExtras;

        }
    };
    addRowToQueryDocumentCursor(cursor, documentId);
    return cursor;
}

以及 DropboxProvider 中的 addRowToQueryDocumentCursor():

protected void addRowToQueryDocumentCursor(MatrixCursor cursor,
                                           String documentId) {

    try {
        SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(Objects.requireNonNull(getContext()).getApplicationContext());
        String displayname = sharedPrefs.getString(getContext().getString(R.string.pref_dropbox_displayname_token_key),
                getContext().getString(R.string.pref_dropbox_displayname_token_default));
        if ( !InTouchUtils.initDropboxClient()) {
            return;
        }

        if ( documentId.equals(<YOUR_UNIQUE_ROOTS_ID_HERE>)) {
            // root Dir
            Timber.d( "addRowToQueryDocumentCursor called for the root");
            final MatrixCursor.RowBuilder row = cursor.newRow();
            row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, <YOUR_UNIQUE_FOLDER_ID_HERE>);
            row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                    String.format(getContext().getString(R.string.dropbox_root_title),
                                  getContext().getString(R.string.app_name)));
            row.add(DocumentsContract.Document.COLUMN_SUMMARY,displayname+
                    getContext().getString(R.string.dropbox_root_summary));
            row.add(DocumentsContract.Document.COLUMN_ICON, R.drawable.folder_icon_dropbox);
            row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR);
            row.add(DocumentsContract.Document.COLUMN_FLAGS, 0);
            row.add(DocumentsContract.Document.COLUMN_SIZE, null);
            row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, null);
            return;
        }
        Timber.d( "addRowToQueryDocumentCursor called for documentId: %s", documentId);
        DbxClientV2 mDbxClient = DropboxClientFactory.getClient();
        Metadata metadata = mDbxClient.files().getMetadata(documentId);

        if ( metadata instanceof FolderMetadata) {
            Timber.d( "Document was a folder");
            includeFolder(cursor, (FolderMetadata)metadata);
        } else {
            Timber.d( "Document was a file");
            includeFile(cursor, (FileMetadata) metadata);
        }
    } catch (Exception e ) {
        Timber.d( "Called addRowToQueryDocumentCursor got exception, message was: %s documentId was: %s.", e.getMessage(), documentId);
    }
}