queryRoots 首先调用 queryDocument,而不是 queryChildDocuments
queryRoots calls queryDocument first, not queryChildDocuments
我正在为 Dropbox 的 SAF 包装器编写一个包装器,因为每个人(包括 Google)都懒得实施这个 "very rich"(即:糟糕)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
。
我在这里错过了什么?
实现 DocumentsProvider
的文档……有限。特别是,没有记录调用顺序的保证。因此,确实应该实施 DocumentsProvider
以尽可能少地假设这些调用的顺序。
例如,我不会假定 queryRoots()
首先被调用。如果此过程的第一个 DocumentsProvider
碰巧是存储访问框架 UI,它可能会是第一个。但是,鉴于客户端可以(小心地)持久化文档或文档树 Uri
,如果第一件事恰好是使用持久化 Uri
.
并且,在您的具体情况下,我不会假设 queryChildDocuments()
发生在 queryDocument()
之前或之后。
我也写过一个SAF DropBox实现,一开始我也有点困惑。
注意以下几点:
- 每个文档提供者报告一个或多个 "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);
}
}
我正在为 Dropbox 的 SAF 包装器编写一个包装器,因为每个人(包括 Google)都懒得实施这个 "very rich"(即:糟糕)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
。
我在这里错过了什么?
实现 DocumentsProvider
的文档……有限。特别是,没有记录调用顺序的保证。因此,确实应该实施 DocumentsProvider
以尽可能少地假设这些调用的顺序。
例如,我不会假定 queryRoots()
首先被调用。如果此过程的第一个 DocumentsProvider
碰巧是存储访问框架 UI,它可能会是第一个。但是,鉴于客户端可以(小心地)持久化文档或文档树 Uri
,如果第一件事恰好是使用持久化 Uri
.
并且,在您的具体情况下,我不会假设 queryChildDocuments()
发生在 queryDocument()
之前或之后。
我也写过一个SAF DropBox实现,一开始我也有点困惑。
注意以下几点:
- 每个文档提供者报告一个或多个 "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);
}
}