我正在为Dropbox编写DocumentsProvider。我试图遵循Google guidelines来创建自定义提供程序,以及Ian Lake的post on Medium来创建自定义提供程序。
我正在尝试将该功能合并到Storage Access Framework中,从而表明有更多数据要加载。
queryChildDocuments()方法的相关部分如下:
@Override
public Cursor queryChildDocuments(final String parentDocumentId,
final String[] projection,
final String sortOrder) {
if (selfPermissionsFailed(getContext())) {
// Permissions have changed, abort!
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 : getDefaultDocumentProjection()){
// Indicate we will be batch loading
@Override
public Bundle getExtras() {
Bundle bundle = new Bundle();
bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
bundle.putString(DocumentsContract.EXTRA_INFO, getContext().getResources().getString(R.string.requesting_data));
return bundle;
}
};
ListFolderResult result = null;
DbxClientV2 mDbxClient = DropboxClientFactory.getClient();
result = mDbxClient.files().listFolderBuilder(parentDocumentId).start();
if (result.getEntries().size() == 0) {
// Nothing in the dropbox folder
Log.d(TAG, "addRowsToQueryChildDocumentsCursor called mDbxClient.files().listFolder() but nothing was there!");
return;
}
// Setup notification so cursor will continue to build
cursor.setNotificationUri(getContext().getContentResolver(),
getChildDocumentsUri(parentDocumentId));
while (true) {
// Load the entries and notify listener
for (Metadata metadata : result.getEntries()) {
if (metadata instanceof FolderMetadata) {
includeFolder(cursor, (FolderMetadata) metadata);
} else if (metadata instanceof FileMetadata) {
includeFile(cursor, (FileMetadata) metadata);
}
}
// Notify for this batch
getContext().getContentResolver().notifyChange(getChildDocumentsUri(parentDocumentId), null);
// See if we are ready to exit
if (!result.getHasMore()) {
break;
}
result = mDbxClient.files().listFolderContinue(result.getCursor());
}
一切正常。我得到了预期的数据加载游标。我得到的“免费”(大概是由于附加服务包)是因为SAF会自动在屏幕顶部放置一个视觉效果,以显示给用户的文本(“请求数据”)和动画条(位于我的运行API 27的Samsung Galaxy S7来回移动以指示光标正在加载:
我的问题是-退出提取循环并完成加载后,如何以编程方式消除屏幕顶部的EXTRA_INFO文本和EXTRA_LOADING动画?我已经仔细检查过API,并且看不到任何看起来像“信号”的东西来告诉SAF加载已完成。
android文档对此功能的讨论不多,Ian的Medium帖子只是简短提及发送通知,因此光标知道自己会刷新。两者都无话可说。
答案 0 :(得分:2)
基于对com.android.documentsui以及AOSP其他区域中的代码的审查,我对这个问题有一个答案,以了解如何调用和使用自定义DocumentsProvider:
我们解决方案的关键是,进度条的显示/删除是在之后进行的,是根据加载程序的返回来更新模型本身。
此外,当要求Model实例更新自身时,它会完全清除先前的数据,并在当前游标上进行迭代以再次填充自身。这意味着只有在检索完所有数据之后才应该执行“第二次提取”,并且它需要包括完整的数据集,而不仅仅是“第二次提取”。
最后-仅在从queryChildDocuments()返回了游标之后,DirectoryLoader才将游标内部类注册为ContentObserver 。
因此,我们的解决方案变为:
在DocumentsProvider.queryChildDocuments()中,确定是否可以通过一次遍历即可满足完整的结果集。
如果可以的话,只需加载并返回Cursor就可以了。
如果不能,则:
确保用于初始加载的Cursor的getExtras()对于EXTRA_LOADING键将返回TRUE
收集第一批数据并为其加载游标,并利用内部缓存为下次查询保存该数据(下面会详细说明)。下一步后,我们将返回此Cursor,并且由于EXTRA_LOADING为true,因此将显示进度条。
现在是棘手的部分。 queryChildDocuments()的JavaDoc说:
如果提供商是基于云的,并且您有一些数据在本地缓存或固定,则可以立即返回本地数据,并在Cursor上设置DocumentsContract.EXTRA_LOADING以指示您仍在获取其他数据。然后,当网络数据可用时,您可以发送更改通知以触发重新查询并返回完整内容。
if (mFeatures.isContentPagingEnabled()) { Bundle queryArgs = new Bundle(); mModel.addQuerySortArgs(queryArgs); // TODO: At some point we don't want forced flags to override real paging... // and that point is when we have real paging. DebugFlags.addForcedPagingArgs(queryArgs); cursor = client.query(mUri, null, queryArgs, mSignal); } else { cursor = client.query( mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal); } if (cursor == null) { throw new RemoteException("Provider returned null"); } cursor.registerContentObserver(mObserver);
client.query()在最终调用我们的提供程序的类上完成。请注意,在上面的代码中,在返回游标之后,加载程序立即使用“ mObserver”将游标注册为ContentObserver。 mObserver是Loader中一个内部类的实例,当收到内容更改通知时,它将导致该Loader再次重新查询。
因此,我们需要采取两个步骤。首先是因为加载程序不会破坏它从初始query()中收到的Cursor,所以在对queryChildDocuments()的初始调用期间,提供程序需要使用Cursor.setNotificationUri()方法向ContentResolver注册Cursor,并传递一个Uri。表示当前子目录(传递给queryChildDocuments()的parentDocumentId):
cursor.setNotificationUri(getContext()。getContentResolver(), DocumentsContract.buildChildDocumentsUri(,parentDocumentId));
然后再次启动加载程序以收集其余数据,生成单独的线程以执行循环,以便a)提取数据,b)将其连接到用于填充Cursor的缓存结果中。第一个查询(这就是为什么我说要在步骤2中保存它),并且c)通知Cursor数据已更改。
从初始查询返回游标。由于EXTRA_LOADING设置为true,因此将显示进度条。
由于加载程序已注册自己,以便在内容更改时收到通知,因此当通过步骤7在提供程序中生成的线程完成获取操作时,它需要使用与注册时相同的Uri值在解析器上调用notifyChange()。步骤(6)中的游标:
getContext()。getContentResolver()。notifyChange(DocumentsContract.buildChildDocumentsUri(,parentDocumentId),null);
Cursor接收来自解析程序的通知,然后通知加载程序,使其重新查询。这次,当加载程序查询我的提供程序时,提供程序会指出它是重新查询,并使用缓存中的当前内容填充游标。它还必须注意线程在获取缓存的当前快照时是否仍在运行-如果是这样,它将设置getExtras()来指示加载仍在进行。如果没有,它将设置GetExtras()来指示没有加载,以便删除进度条。
线程读取数据后,数据集将加载到模型中,并且RecyclerView将刷新。当线程在最后一次批量获取后死掉时,进度条将被删除。
我从中学到的一些重要技巧:
MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION) { @Override public Bundle getExtras() { Bundle bundle = new Bundle(); bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true); return bundle; } };
这很好,如果您在创建游标时知道是否要一次获取所有内容。
如果相反,您需要创建游标,填充它,然后在需要其他模式后进行调整,这有点不对:
private final Bundle b = new Bundle() MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION) { @Override public Bundle getExtras() { return b; } };
然后您可以执行以下操作:
result.getExtras()。putBoolean(DocumentsContract.EXTRA_LOADING,true);
如果您需要像上面的示例中那样修改从getExtras()返回的Bundle,则必须对getExtras()进行编码,以使其可以像上面的示例中那样进行更新。如果不这样做,则默认情况下无法修改从getExtras()返回的Bundle实例。这是因为默认情况下,getExtras()将返回Bundle.EMPTY的实例,该实例本身由ArrayMap.EMPTY支持,ArrayMap.EMPTY定义了ArrayMap类以使ArrayMap不可变的方式,因此,如果您遇到了运行时异常尝试更改它。
我认识到,从启动填充其余内容的线程到将初始Cursor返回给Loader的时间之间只有很小的时间窗口。从理论上讲,线程有可能在装载程序向游标注册之前完成。如果发生这种情况,那么即使线程将更改通知了Resolver,由于Cursor尚未注册为侦听器,它也不会收到消息,并且Loader也不会再次启动。 最好知道一种方法,以确保不会发生这种情况,但是除了诸如将线程延迟250ms之类的东西之外,我没有对此进行研究。
另一个问题是要处理这种情况,即当用户仍在获取进度的同时离开当前目录导航时。提供商可以跟踪每次传递给queryChildDocuments()的parentDocumentId进行检查-当它们相同时,这是重新查询。当不同时,它是一个新查询。在新查询中,如果线程处于活动状态,我们将取消该线程并清除缓存,然后处理该查询。
要处理的另一个问题是,可以有多个源重新查询到同一目录。第一种是在完成获取目录条目后,线程通过Uri通知触发线程。其他情况是请求加载程序进行刷新时,这可能以几种方式发生(例如,用户在屏幕上向下滑动)。检查的关键是是否在同一目录中调用了queryChildDocuments(),而线程尚未完成,那么我们已经收到了从某种刷新中重新加载的请求-我们通过从从缓存的当前状态开始,但是希望我们在线程完成时再次被调用。
在我的测试中,从来没有一次并行调用同一提供程序的-当用户浏览目录时,一次只请求一个目录。因此,我们可以用一个线程来满足“批量获取”的要求,并且当我们检测到请求了新目录(例如,用户离开了加载时间太长的目录)时,我们可以取消线程并开始根据需要在新目录中添加它的新实例。
我正在发布代码的相关部分以显示我是如何做到的,并有一些注意事项:
我的抽象Provider类中的queryChildDocuments()方法调用createDocumentMatrixCursor()方法,该方法可以根据Provider子类的不同实现:
@Override
public Cursor queryChildDocuments(final String parentDocumentId,
final String[] projection,
final String sortOrder) {
if (selfPermissionsFailed(getContext())) {
return null;
}
Log.d(TAG, "queryChildDocuments called for: " + parentDocumentId + ", calling createDocumentMatrixCursor");
// Create a cursor with either the requested fields, or the default projection if "projection" is null.
final MatrixCursor cursor = createDocumentMatrixCursor(projection != null ? projection : getDefaultDocumentProjection(), parentDocumentId);
addRowsToQueryChildDocumentsCursor(cursor, parentDocumentId, projection, sortOrder);
return cursor;
}
以及我的createDocumentMatrixCursor的DropboxProvider实现:
@Override
/**
* Called to populate a sub-directory of the parent directory. This could be called multiple
* times for the same directory if (a) the user swipes down on the screen to refresh it, or
* (b) we previously started a BatchFetcher thread to gather data, and the BatchFetcher
* notified our Resolver (which then notifies the Cursor, which then kicks the Loader).
*/
protected MatrixCursor createDocumentMatrixCursor(String[] projection, final String parentDocumentId) {
MatrixCursor cursor = null;
final Bundle b = new Bundle();
cursor = new MatrixCursor(projection != null ? projection : getDefaultDocumentProjection()){
@Override
public Bundle getExtras() {
return b;
}
};
Log.d(TAG, "Creating Document MatrixCursor" );
if ( !(parentDocumentId.equals(oldParentDocumentId)) ) {
// Query in new sub-directory requested
Log.d(TAG, "New query detected for sub-directory with Id: " + parentDocumentId + " old Id was: " + oldParentDocumentId );
oldParentDocumentId = parentDocumentId;
// Make sure prior thread is cancelled if it was started
cancelBatchFetcher();
// Clear the cache
metadataCache.clear();
} else {
Log.d(TAG, "Requery detected for sub-directory with Id: " + parentDocumentId );
}
return cursor;
}
addrowsToQueryChildDocumentsCursor()方法是我的抽象提供程序类的queryChildDocuments()方法被调用时所调用的方法,也是子类实现的方法,也是所有获取大量目录内容的妙招。例如,我的Dropbox提供程序子类利用Dropbox API获取所需的数据,如下所示:
protected void addRowsToQueryChildDocumentsCursor(MatrixCursor cursor,
final String parentDocumentId,
String[] projection,
String sortOrder) {
Log.d(TAG, "addRowstoQueryChildDocumentsCursor called for: " + parentDocumentId);
try {
if ( DropboxClientFactory.needsInit()) {
Log.d(TAG, "In addRowsToQueryChildDocumentsCursor, initializing DropboxClientFactory");
DropboxClientFactory.init(accessToken);
}
final ListFolderResult dropBoxQueryResult;
DbxClientV2 mDbxClient = DropboxClientFactory.getClient();
if ( isReQuery() ) {
// We are querying again on the same sub-directory.
//
// Call method to populate the cursor with the current status of
// the pre-loaded data structure. This method will also clear the cache if
// the thread is done.
boolean fetcherIsLoading = false;
synchronized(this) {
populateResultsToCursor(metadataCache, cursor);
fetcherIsLoading = fetcherIsLoading();
}
if (!fetcherIsLoading) {
Log.d(TAG, "I believe batchFetcher is no longer loading any data, so clearing the cache");
// We are here because of the notification from the fetcher, so we are done with
// this cache.
metadataCache.clear();
clearCursorLoadingNotification(cursor);
} else {
Log.d(TAG, "I believe batchFetcher is still loading data, so leaving the cache alone.");
// Indicate we are still loading and bump the loader.
setCursorForLoadingNotification(cursor, parentDocumentId);
}
} else {
// New query
if (parentDocumentId.equals(accessToken)) {
// We are at the Dropbox root
dropBoxQueryResult = mDbxClient.files().listFolderBuilder("").withLimit(batchSize).start();
} else {
dropBoxQueryResult = mDbxClient.files().listFolderBuilder(parentDocumentId).withLimit(batchSize).start();
}
Log.d(TAG, "New query fetch got " + dropBoxQueryResult.getEntries().size() + " entries.");
if (dropBoxQueryResult.getEntries().size() == 0) {
// Nothing in the dropbox folder
Log.d(TAG, "I called mDbxClient.files().listFolder() but nothing was there!");
return;
}
// See if we are ready to exit
if (!dropBoxQueryResult.getHasMore()) {
// Store our results to the query
populateResultsToCursor(dropBoxQueryResult.getEntries(), cursor);
Log.d(TAG, "First fetch got all entries so I'm clearing the cache");
metadataCache.clear();
clearCursorLoadingNotification(cursor);
Log.d(TAG, "Directory retrieval is complete for parentDocumentId: " + parentDocumentId);
} else {
// Store our results to both the cache and cursor - cursor for the initial return,
// cache for when we come back after the Thread finishes
Log.d(TAG, "Fetched a batch and need to load more for parentDocumentId: " + parentDocumentId);
populateResultsToCacheAndCursor(dropBoxQueryResult.getEntries(), cursor);
// Set the getExtras()
setCursorForLoadingNotification(cursor, parentDocumentId);
// Register this cursor with the Resolver to get notified by Thread so Cursor will then notify loader to re-load
Log.d(TAG, "registering cursor for notificationUri on: " + getChildDocumentsUri(parentDocumentId).toString() + " and starting BatchFetcher");
cursor.setNotificationUri(getContext().getContentResolver(),getChildDocumentsUri(parentDocumentId));
// Start new thread
batchFetcher = new BatchFetcher(parentDocumentId, dropBoxQueryResult);
batchFetcher.start();
}
}
} catch (Exception e) {
Log.d(TAG, "In addRowsToQueryChildDocumentsCursor got exception, message was: " + e.getMessage());
}
线程(“ BatchFetcher”)负责填充缓存,并在每次提取后通知解析器:
private class BatchFetcher extends Thread {
String mParentDocumentId;
ListFolderResult mListFolderResult;
boolean keepFetchin = true;
BatchFetcher(String parentDocumentId, ListFolderResult listFolderResult) {
mParentDocumentId = parentDocumentId;
mListFolderResult = listFolderResult;
}
@Override
public void interrupt() {
keepFetchin = false;
super.interrupt();
}
public void run() {
Log.d(TAG, "Starting run() method of BatchFetcher");
DbxClientV2 mDbxClient = DropboxClientFactory.getClient();
try {
mListFolderResult = mDbxClient.files().listFolderContinue(mListFolderResult.getCursor());
// Double check
if ( mListFolderResult.getEntries().size() == 0) {
// Still need to notify so that Loader will cause progress bar to be removed
getContext().getContentResolver().notifyChange(getChildDocumentsUri(mParentDocumentId), null);
return;
}
while (keepFetchin) {
populateResultsToCache(mListFolderResult.getEntries());
if (!mListFolderResult.getHasMore()) {
keepFetchin = false;
} else {
mListFolderResult = mDbxClient.files().listFolderContinue(mListFolderResult.getCursor());
// Double check
if ( mListFolderResult.getEntries().size() == 0) {
// Still need to notify so that Loader will cause progress bar to be removed
getContext().getContentResolver().notifyChange(getChildDocumentsUri(mParentDocumentId), null);
return;
}
}
// Notify Resolver of change in data, it will contact cursor which will restart loader which will load from cache.
Log.d(TAG, "BatchFetcher calling contentResolver to notify a change using notificationUri of: " + getChildDocumentsUri(mParentDocumentId).toString());
getContext().getContentResolver().notifyChange(getChildDocumentsUri(mParentDocumentId), null);
}
Log.d(TAG, "Ending run() method of BatchFetcher");
//TODO - need to have this return "bites" of data so text can be updated.
} catch (DbxException e) {
Log.d(TAG, "In BatchFetcher for parentDocumentId: " + mParentDocumentId + " got error, message was; " + e.getMessage());
}
}
}