Я пишу обертку для SAF-оболочки для Dropbox, так как всем (включая Google) лень реализовывать этот «очень богатый» (т.е. ужасный) API. У меня есть корень в сборщике, но я подумал, что сначала нужно вызвать queryChildren
. Однако queryChildren is never called and it goes straight to
queryDocument`.
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
сработает, как только будет нажато что-то, чего не было DocumentsContract.Document.MIME_TYPE_DIR
.
Я не знаю, нужен ли рут для MIME_TYPE_DIR
, и клиенты могут узнать об этом только через queryDocument()
. В общем, я бы попытался сделать очень мало предположений о порядке этих вызовов.
Попался, я уже начал помещать рут-хак ""
dropbox в queryDocument
. Я думаю, в конце концов это имеет смысл, просто делает код инициализации более круглым и немного уродливым (с хаками инициализации как в queryDocument
, так и в queryChildDocuments
Документация по реализации DocumentsProvider
... ограничена. В частности, нет документально подтвержденной гарантии порядка звонков. Таким образом, DocumentsProvider
действительно должен быть реализован, чтобы делать как можно меньше предположений о порядке этих вызовов.
Например, я бы не стал предполагать, что queryRoots()
вызывается первым. Вероятно, это будет первым, если первым использованием DocumentsProvider
для этого процесса будет пользовательский интерфейс Storage Access Framework. Однако, учитывая, что клиенты могут (с осторожностью) сохранять документ или дерево документов Uri
, вы можете сначала вызвать что-то еще в вашем процессе, если первым делом окажется клиент, использующий сохраненный Uri
.
И в вашем конкретном случае я бы не стал предполагать, что queryChildDocuments()
происходит до или после queryDocument()
.
Я еще даже не начал думать о сохранении Uri
. В моем случае возврат псевдопапки в queryDocument
для ROOT_DOCUMENT_ID
вызвал вызов queryChildDocuments
. Интересно, что изначально у меня не было папки root
, она заполнила собой фактическое содержимое корня (что и требовалось). github.com/rcetscientist/CloudProvider
Я также написал реализацию SAF DropBox, и поначалу я тоже был немного сбит с толку.
Из документация:
Обратите внимание на следующее:
Эта вторая пуля является ключевой пулей. После возврата из 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);
}
}
Почему вы ожидаете, что
queryChildDocuments()
будет либо до, либо послеqueryDocument()
? IOW, почему вы делаете предположения о порядке этих вызовов?