android - 如何向存储访问框架表明我不再需要加载动画?

标签 android frameworks storage storage-access-framework dropbox-sdk-js

我正在为 Dropbox 编写一个 DocumentsProvider。我试图按照 Google guidelines 来创建自定义提供程序,以及 Ian Lake 的 post on Medium 也是如此。

我正在尝试将该功能合并到存储访问框架中,从而表明有更多数据要加载。

我的 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 会自动在屏幕顶部放置一个视觉效果,用于向用户发送文本(“请求数据”)和动画栏(在我的三星 Galaxy S7 运行 API 27) 来回移动以指示光标正在加载:

screenshot of 'loading' bar and text

我的问题是 - 一旦我退出 fetch 循环并完成加载,我如何以编程方式摆脱屏幕顶部的 EXTRA_INFO 文本和 EXTRA_LOADING 动画?我已经搜索了 API,但没有看到任何看起来像是告诉 SAF 加载已完成的“信号”。

android 文档没有过多讨论这个功能,Ian 的 Medium 帖子只是简单地提到发送通知,以便光标知道自己刷新。两者都没有关于动画的任何说法。

最佳答案

我根据查看 com.android.documentsui 中的代码以及 AOSP 的其他区域以了解如何调用和使用自定义 DocumentsProvider 来回答这个问题:

  • 当目录的内容显示在选择器中时,它是通过 DirectoryFragment 实例完成的。
  • DirectoryFragment 最终管理 DirectoryLoader 的一个实例。
  • DirectoryLoader 异步调用 DocumentsProvider 来填充 Cursor,该 Cursor 被包装在 DirectoryResult 实例中并移交给作为 DirectoryFragment 中 RecyclerView 的底层数据存储的 Model 实例。重要的是,加载器在完成后会挂起对该光标的引用 - 当我们需要通知加载器执行另一个加载时,这将发挥作用。
  • 模型接收 DirectoryResult,使用封闭的 Cursor 填充其数据结构,并通过查询 Cursor 的 getExtras() 中的 EXTRA_LOADING 键来更新“isLoading”的状态。然后它通知同样由 DirectoryFragment 管理的监听器数据已更新。
  • DirectoryFragment 通过此监听器检查模型是否指示 EXTRA_LOADING 设置为 TRUE,如果是,将显示进度条,否则将删除它。然后它在与 RecyclerView 关联的适配器上执行 notifyDataSetChanged()。

  • 我们的解决方案的关键是在模型通过加载器的返回更新自身之后显示/删除进度条。

    此外,当 Model 实例被要求更新自身时,它会完全清除先前的数据并遍历当前光标以再次填充自身。这意味着我们的“第二次获取”应该只在所有数据都被检索到之后进行,并且它需要包括完整的数据集,而不仅仅是“第二次获取”。

    最后 - 只有在从 queryChildDocuments() 返回 Cursor 之后,DirectoryLoader 才会在 Cursor 中注册一个内部类作为 ContentObserver。

    因此,我们的解决方案变为:

    在 DocumentsProvider.queryChildDocuments() 中,确定是否可以在一次传递中满足完整的结果集。

    如果可以,那么只需加载并返回 Cursor,我们就完成了。

    如果不能,那么:
  • 确保初始加载的 Cursor 的 getExtras() 将为 EXTRA_LOADING 键返回 TRUE
  • 收集第一批数据并用它加载 Cursor,并利用内部缓存保存这些数据以用于下一个查询(更多原因见下文)。我们将在下一步之后返回这个 Cursor 并且由于 EXTRA_LOADING 为真,进度条将出现。
  • 现在是棘手的部分。 queryChildDocuments() 的 JavaDoc 说:

  • If your provider is cloud-based, and you have some data cached or pinned locally, you may return the local data immediately, setting DocumentsContract.EXTRA_LOADING on the Cursor to indicate that you are still fetching additional data. Then, when the network data is available, you can send a change notification to trigger a requery and return the complete contents.


  • 问题是这个通知是从哪里来的?在这一点上,我们深入到我们的 Provider 代码中,使用初始加载请求填充 Cursor。 Provider 对 Loader 一无所知——它只是响应 queryChildDocuments() 调用。此时,Loader 对 Cursor 一无所知——它只是在系统中执行一个 query(),最终调用我们的 Provider。一旦我们将 Cursor 返回给 Loader,就不会在没有某种外部事件(例如用户单击文件或目录)的情况下进一步调用 Provider。来自 DirectoryLoader:

  •  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() 是在最终调用我们的 Provider 的类上完成的。请注意,在上面的代码中,在 Cursor 返回之后,Loader 使用“mObserver”将自己注册为 Cursor 作为 ContentObserver。 mObserver 是 Loader 内部类的一个实例,当收到内容更改的通知时,将导致 loader 再次重新查询。
  • 因此我们需要采取两个步骤。首先是因为 Loader 没有销毁它从初始 query() 接收到的 Cursor,在对 queryChildDocuments() 的初始调用期间,Provider 需要使用 Cursor.setNotificationUri() 方法向 ContentResolver 注册 Cursor 并传递一个 Uri表示当前子目录(传递给 queryChildDocuments() 的 parentDocumentId):

    cursor.setNotificationUri(getContext().getContentResolver(), DocumentsContract.buildChildDocumentsUri(, parentDocumentId));

  • 然后再次启动 Loader 以收集其余数据,产生一个单独的线程来执行一个循环,a) 获取数据,b) 将其连接到用于填充第一个查询中的 Cursor 的缓存结果(这是为什么我在步骤 2) 中说要保存它,并且 c) 通知 Cursor 数据已更改。
  • 从初始查询返回 Cursor。由于 EXTRA_LOADING 设置为 true,将出现进度条。
  • 由于加载器注册了自己在内容更改时收到通知,当通过步骤 7 在 Provider 中生成的线程完成获取时,它需要使用与在步骤 ( 6):

    getContext().getContentResolver().notifyChange(DocumentsContract.buildChildDocumentsUri(, parentDocumentId), null);

  • Cursor 收到来自 Resolver 的通知,然后通知 Loader 使其重新查询。这一次,当加载程序查询我的提供程序时,提供程序指出这是一个重新查询,并使用缓存中的当前内容集填充游标。它还必须注意线程在获取缓存的当前快照时是否仍在运行 - 如果是,则设置 getExtras() 以指示加载仍在进行。如果不是,则设置 GetExtras() 以指示未发生加载,以便删除进度条。
  • Thread获取数据后,数据集会加载到Model中,RecyclerView会刷新。当线程在最后一次批量获取后死亡时,进度条将被删除。

  • 我在此过程中学到的一些重要提示:
  • 在调用 queryChildDocuments() 时,提供者必须决定是否可以一次获取所有条目,并适当调整 Cursor.getExtras() 的结果。文档建议是这样的:

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


    如果您知道何时创建 Cursor 是否在一次获取中获取所有内容,这很好。

    相反,如果您需要创建光标,填充它,然后在需要不同的模式后进行调整,例如:

    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 类以一种使 ArrayMap 不可变的方式定义它,因此如果您尝试改变它。
  • 我认识到在我启动填充其余内容的线程和我将初始光标返回到加载器之间有一个非常小的时间窗口。从理论上讲,线程可以在 Loader 向 Cursor 注册自己之前完成。如果发生这种情况,那么即使线程将更改通知解析器,由于 Cursor 尚未注册为监听器,因此它不会收到消息,并且加载器也不会再次启动。 知道一种方法 以确保不会发生这种情况会很好,但除了诸如延迟线程 250 毫秒之类的事情之外,我还没有研究过。
  • 另一个问题是在提取进度仍在发生时处理用户离开当前目录的情况。这可以由 Provider 进行检查,跟踪每次传递给 queryChildDocuments() 的 parentDocumentId - 当它们相同时,它是一个重新查询。当不同时,它是一个新的查询。在新查询中,如果线程处于 Activity 状态,我们将取消该线程并清除缓存,然后处理查询。
  • 另一个需要处理的问题是对同一目录的重新查询可能有多个来源。第一个是当线程在完成为目录获取条目后通过 Uri 通知触发它时。其他是当 Loader 被请求刷新时,这可以通过几种方式发生(例如,用户在屏幕上向下滑动)。检查的关键是是否为同一目录调用了 queryChildDocuments() 并且 Thread 尚未完成,然后我们收到了从某种刷新重新加载的请求 - 我们通过执行同步加载到游标来尊重这一点从缓存的当前状态,但预计我们会在线程完成时再次被调用。
  • 在我的测试中,从来没有并行调用同一个 Provider - 当用户浏览目录时,一次只请求一个目录。因此,我们可以用单个线程来满足我们的“批量获取”,当我们检测到一个新目录被请求时(例如用户从一个加载时间过长的目录移开),然后我们可以取消线程并启动根据需要在新目录上创建它的新实例。

  • 我发布了我的代码的相关部分来展示我是如何做到的,还有一些注释:
  • 我的应用程序支持多种提供者类型,所以我创建了一个抽象类“AbstractStorageProvider”,它扩展了 DocumentsProvider 以封装提供者从系统获取的常见调用(如 queryRoots、queryChildDocuments 等)。这些又委托(delegate)给每个类的类我想要支持的服务(我自己的本地存储、Dropbox、Spotify、Instagram 等)来填充光标。我还在这里放置了一个标准方法来检查并确保用户没有在应用程序之外更改我的 Android 权限设置,这会导致抛出异常。
  • 同步访问内部缓存至关重要,因为线程将在后台工作,当多个调用请求更多数据时填充缓存。
  • 为清楚起见,我发布了此代码的相对“基本”版本。生产代码中需要多个处理程序来处理网络故障、配置更改等。

  • 我的抽象 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() 方法是我的抽象 Provider 类在调用它的 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());
            }
    
        }
    
    }
    

    关于android - 如何向存储访问框架表明我不再需要加载动画?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51663222/

    相关文章:

    c++ - 存储动态创建对象的列表/vector 的最佳方式是什么?

    Android 滚动条一闪而过

    java - 如果 get volley 请求为 true,则需要从 cardview 中的可绘制对象更新图像(200 ok)

    Perl 网络爬虫框架

    iphone - Xcode:在现有项目中找不到库文件,它应该在哪里?

    linux - 无法在 RHEL 5.2 上安装 ntfs-fuse,缺少依赖项错误

    kubernetes - Persistent Volumes 和 Persistent Volume Claims 之间有什么关系 (1 :1 or 1:n)

    android - 状态更改时播放声音(Android)

    Google Play 更新后 Android AlarmManager 未触发

    ios - 最小化嵌入式 swift 框架的大小