Batch fetching over the network in SAF


#1

I am attempting to incorporate the feature within the Storage Access Framework whereby one indicates that there is more data to load. I am writing a provider for Dropbox, and have included the following code in my queryChildDocuments() method:

final MatrixCursor cursor = new MatrixCursor(projection != null ? projection : getDefaultDocumentProjection()){
    @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;
    }

};

I then proceed normally within my method, utilizing DropBox specific objects/API to batch fetch file/folder objects, and notify the cursor per the documentation:

            // Notify cursor this batch is ready
            Uri updatedUri = DocumentsContract.buildChildDocumentsUri(
                    BuildConfig.DROPBOX_DOCUMENTS_AUTHORITY, parentDocumentId);
            getContext().getContentResolver().notifyChange(updatedUri, null);

This all works fine. I notice that - presumably due to the

DocumentsContract.EXTRA_LOADING

value in extras bundle - that I automatically get a visual at the top of my screen “for free” of a bar going back and forth, and the text that I sent in the EXTRA_INFO portion of the bundle (“Requesting data”).

My question is - how do I get rid of both the EXTRA_INFO and the EXTRA_LOADING animation once I have finished gathering all the data? Even if the user presses the “dimiss” button, all that does is dismiss the text. The bar that is moving back and forth across the screen stays there.

Here is the relevant part of the body of the code that is doing the fetch as per Dropbox API:

        // Setup notification so cursor will continue to build
        cursor.setNotificationUri(getContext().getContentResolver(),
                                  getChildDocumentsUri(parentDocumentId));

        Log.d(TAG, "Called addRowsToQueryChildDocumentsCursor starting fetch loop");
        while (true) {

            // Load the entries and notify listener
            for (Metadata metadata : result.getEntries()) {

                Log.d(TAG, "Adding to childDocumentsCursor " + metadata.getName());

                if (metadata instanceof FolderMetadata) {
                    includeFolder(cursor, (FolderMetadata) metadata);


                } else if (metadata instanceof FileMetadata) {
                    includeFile(cursor, (FileMetadata) metadata);
                }

            }

            // Notify for this batch
            Uri updatedUri = DocumentsContract.buildChildDocumentsUri(
                    BuildConfig.DROPBOX_DOCUMENTS_AUTHORITY, parentDocumentId);
            getContext().getContentResolver().notifyChange(updatedUri, null);

            // See if we are ready to exit
            if (!result.getHasMore()) {
                break;
            }
             // Not ready to leave yet, get the next batch
            result = mDbxClient.files().listFolderContinue(result.getCursor());
        }

        Log.d(TAG, "Called addRowsToQueryChildDocumentsCursor finished fetch loop");

Once I have exited the loop that is populating the cursor, how do I indicate to Android that I no longer need the “spinner bar” at the top of the window?

Thanks!


#2

I have never done anything much like this.

However, I notice that your notifyChange() call is using updatedUri. Is that the right Uri to use? It should be the Uri that you associated with the Cursor via setNotificationUri(), if I am reading the documentation correctly.


#3

Yes, they are actually the same. I was refactoring

DocumentsContract.buildChildDocumentsUri(BuildConfig.DROPBOX_DOCUMENTS_AUTHORITY, parentDocumentId)

into the private method

getChildDocumentsUri()

and just hadn’t done that one yet. They are the same.


#4

All that I can suggest is that you poke through the AOSP and see who is all using EXTRA_LOADING, and how. That link will bring up a search result showing those uses in Android 8.1, courtesy of Opersys’ OpenGrok instance.


#5

Mark -

I’m updating my StackOverflow post with the full answer - and hopefully earn the bounty :slight_smile: - but thought I’d answer here as well. In summary after spending many hours last weekend and this week pouring through the code in com.android.documentsui as well as other areas of the AOSP to see how a custom DocumentsProvider is called and used, I can say the following:

Picker uses a DirectoryFragment, which utilizes a RecyclerView and underlying Model for its data.

A DirectoryLoader asynchronously performs the query which ultimately reaches the Provider to return a Cursor. this Cursor instance is saved until the next query is requested of the Loader.

After it has the Cursor, DirectoryLoader registers itself as a ContentObserver and dispatches the Cursor to the Model, which updates itself

DirectoryFragment is a listener for Model data changes, and when the Model notifies it that the data has been updated, it queries the status of whether loading is happening and also refreshes its RecyclerView.

If loading is marked as happening (EXTRA_LOADING == true), DirectoryFragment displays a progress bar, otherwise it removes it.

Because Loader is a listener for changes in content, if it gets a content change notification it will requery.

Since the query must return asap in order for the UI to refresh, under the covers the Provider must continue to fetch data after it returns the initial query, then notify via standard Resolver notification when it is complete.

Upon the requery, the Provider must return a Cursor with the complete data set, and that Cursor set to indicate EXTRA_LOADING is false. This will cause the model to refresh with the full data set, and DirectoryFragment to remove the progress bar.


#6

That certainly sounds reasonable. Glad to hear that you worked it out! Sad to hear that it required so much poking around Android’s innards!