QueryRoots сначала вызывает queryDocument, а не queryChildDocuments

Я пишу обертку для SAF-оболочки для Dropbox, так как всем (включая Google) лень реализовывать этот «очень богатый» (т.е. ужасный) 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)

path - это ROOT_DOCUMENT_ID, к которому я ожидаю пойти queryChildDocuments в первую очередь.

Что мне здесь не хватает?

Почему вы ожидаете, что queryChildDocuments() будет либо до, либо после queryDocument()? IOW, почему вы делаете предположения о порядке этих вызовов?

CommonsWare 07.04.2019 16:33

Когда вы впервые нажимаете на корень, я ожидаю, что первым вызовом будет queryChildDocuments. Я думал, что концепция заключалась в том, что корень предполагался как папка. Я все еще учусь здесь в движении, но я ожидал, что queryDocument сработает, как только будет нажато что-то, чего не было DocumentsContract.Document.MIME_TYPE_DIR.

Anthony 07.04.2019 16:40

Я не знаю, нужен ли рут для MIME_TYPE_DIR, и клиенты могут узнать об этом только через queryDocument(). В общем, я бы попытался сделать очень мало предположений о порядке этих вызовов.

CommonsWare 07.04.2019 16:43

Попался, я уже начал помещать рут-хак "" dropbox в queryDocument. Я думаю, в конце концов это имеет смысл, просто делает код инициализации более круглым и немного уродливым (с хаками инициализации как в queryDocument, так и в queryChildDocuments

Anthony 07.04.2019 16:49
1
4
431
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Документация по реализации DocumentsProvider... ограничена. В частности, нет документально подтвержденной гарантии порядка звонков. Таким образом, DocumentsProvider действительно должен быть реализован, чтобы делать как можно меньше предположений о порядке этих вызовов.

Например, я бы не стал предполагать, что queryRoots() вызывается первым. Вероятно, это будет первым, если первым использованием DocumentsProvider для этого процесса будет пользовательский интерфейс Storage Access Framework. Однако, учитывая, что клиенты могут (с осторожностью) сохранять документ или дерево документов Uri, вы можете сначала вызвать что-то еще в вашем процессе, если первым делом окажется клиент, использующий сохраненный Uri.

И в вашем конкретном случае я бы не стал предполагать, что queryChildDocuments() происходит до или после queryDocument().

Я еще даже не начал думать о сохранении Uri. В моем случае возврат псевдопапки в queryDocument для ROOT_DOCUMENT_ID вызвал вызов queryChildDocuments. Интересно, что изначально у меня не было папки root, она заполнила собой фактическое содержимое корня (что и требовалось). github.com/rcetscientist/CloudProvider

Anthony 07.04.2019 20:21

Я также написал реализацию SAF DropBox, и поначалу я тоже был немного сбит с толку.

Из документация:

Обратите внимание на следующее:

  • Каждый поставщик документов сообщает об одном или нескольких «корнях», которые начинают указывает на изучение дерева документов. Каждый корень имеет уникальный COLUMN_ROOT_ID и указывает на документ (каталог) представляющий содержимое под этим корнем. Корни являются динамическими дизайн для поддержки вариантов использования, таких как несколько учетных записей, временный USB устройства хранения данных или вход/выход пользователя.
  • Под каждым корнем находится отдельный документ. Этот документ указывает на 1 к N документов, каждый из которых, в свою очередь, может указывать от 1 до N документов.
  • Каждая серверная часть хранилища отображает отдельные файлы и каталоги по ссылаясь на них с помощью уникального идентификатора COLUMN_DOCUMENT_ID. Идентификаторы документов должны быть уникальными и не изменяться после выпуска, так как они используются для постоянные гранты URI при перезагрузке устройства.
  • Документы могут быть открываемыми файлами (с определенным типом MIME), или каталог, содержащий дополнительные документы (с MIME_TYPE_DIR MIME-тип).
  • Каждый документ может иметь разные возможности, как описано в COLUMN_FLAGS. Например, FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE и FLAG_SUPPORTS_THUMBNAIL. Одинаковый COLUMN_DOCUMENT_ID может быть включен в несколько каталогов.

Эта вторая пуля является ключевой пулей. После возврата из queryRoots() для каждого корня, который вы передали обратно, SAF вызывает queryDocument(). По сути, это необходимо для создания документа «корневая папка с файлами», который отображается в списке. Что я сделал, так это в queryDocument(). Я проверяю, соответствует ли переданный documentId уникальному значению, которое я дал DocumentsContract.Root.COLUMN_ROOT_ID в вызове queryRoots(). Если это так, то вы знаете, что этот вызов queryDocument() должен вернуть папку, представляющую этот корень. В противном случае я везде использую путь из DropBox в качестве моего documentId, поэтому я использую это значение documentID в вызовах через DbxClientV2.

Вот пример кода — обратите внимание, что в моем случае я создал класс AbstractStorageProvider, из которого расширяются все мои различные поставщики (Dropbox, Instagram и т. д.). Базовый класс обрабатывает получение вызовов от SAF и выполняет некоторую работу (например, создание курсоров), а затем вызывает методы в реализующих классах для заполнения курсоров в соответствии с требованиями этой конкретной службы:

Базовый класс

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() в базовом классе:

@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;
}

И addRowToQueryDocumentCursor() в DropboxProvider:

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);
    }
}

Другие вопросы по теме