Using AsyncQueryHandler to Access Content Providers Asynchronously in Android

In our previous posts we discussed SQLite access and Content Providers. Now access to a content provider will involve accessing some sort of persistent storage like a database or a file under the hood. Accessing a storage can be a long task and hence this should not be executed on the UI thread which will delay the rendering and could even lead to ANRs. Hence, background threads should be responsible for handling provider’s execution.

Spawning new threads in the ContentProvider itself is not a good idea especially in the case of query() as you’ll then have to block and wait for the background thread to return the result (cursor) making it not asynchronous. Ideally execution must occur in a background thread and then the result communicated back to the UI thread. Now this could be implemented with Runnables, Threads and Handlers but Android provides us with a special class called AsyncQueryHandler just for this purpose. This abstract class exposes an excellent interface to deal with the situation.

What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.

Note: Ideally the background threads should be created by the application component that uses the provider by creating a ContentResolver object. If the caller and provider are in the same application process, then the provider methods are invoked on the same thread as the ContentResolver methods. However, if the processes are different then the provider implementation is invoked on binder threads (processes incoming IPCs).

Using AsyncQueryHandler

AsyncQueryHandler is basically an abstract class that wraps the ContentResolver object and handles background execution of its operations (CRUD) as well as passing messages (result) from the between threads (background and main/UI). It has four methods that wraps that of a ContentResolver:

If you notice carefully, except the first two parameters, all others match their equivalent ContentResolver methods. Each of them, when called, execute the equivalent ContentResolver method on a background thread. When the provider is done with its operation, it sends the result back to AsyncQueryHandler that invokes the following callbacks that your implementation should override.

class MyQueryHandler extends AsyncQueryHandler {

    public MyQueryHandler(ContentResolver cr) {
        super(cr);
    }

    @Override
    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
        // query() completed
    }

    @Override
    protected void onInsertComplete(int token, Object cookie, Uri uri) {
        // insert() completed
    }

    @Override
    protected void onUpdateComplete(int token, Object cookie, int result) {
        // update() completed
    }

    @Override
    protected void onDeleteComplete(int token, Object cookie, int result) {
        // delete() completed
    }
}

The last parameter for all the callbacks are results which has the same type as that of the return value of the underlying ContentResolver methods (query(), insert(), update(), delete()). Let’s discuss the first two methods of the calls and callbacks:

  • Token: It’s a user-defined request code that identifies what kind of a request this is. This can be used with cancelOperation() to cancel unprocessed requests submitted with that token.
  • Cookie: It’s a data container object that can be passed from the request to the response (callback) so that it can be used for some purpose like identifying the request if necessary.

AsyncQueryHandler is mostly created and executes provider operations on the UI thread, but it can be done on any other thread. The callbacks are always called on the thread that creates the AsyncQueryHandler.

Let’s get in action with some code now. We’ll convert the query() code from our content provider article to make use of AsyncQueryHandler:

// MainActivity.onCreate()

// AsyncQueryHandler object

AsyncQueryHandler queryHandler = new AsyncQueryHandler(getContentResolver()) {
    @Override
    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
        int idIndex = cursor.getColumnIndex(UserDictionary.Words._ID);
        int wordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD);
        int localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);

        if (cursor == null) {
            // Some providers return null if an error occurs whereas others throw an exception
        }
        else if (cursor.getCount() < 1) {
            // No matches found
        }
        else {

            while (cursor.moveToNext()) {
                int id = cursor.getInt(idIndex);
                String word = cursor.getString(wordIndex);
                String locale = cursor.getString(localeIndex);

                // Dumps "ID: 1 Word: NewWord Locale: en_US"
                // I added this via Settings > Language & Input > Personal Dictionary
                Log.d(TAG, "ID: " + id + " Word: " + word + " Locale: " + locale);
            }

        }
    }
};


// Construct query and execute

// "projection" defines the columns that will be returned for each row
String[] projection = {
        UserDictionary.Words._ID,       // Contract class constant for the _ID column
        UserDictionary.Words.WORD,      // Contract class constant for the word column
        UserDictionary.Words.LOCALE     // Contract class constant for the locale column
};

// Defines WHERE clause columns and placeholders
String selectionClause = UserDictionary.Words._ID + " = ?";

// Define the WHERE clause's placeholder values
String[] selectionArgs = { "1" };

queryHandler.startQuery(
        1, null,
        UserDictionary.Words.CONTENT_URI,
        projection,
        selectionClause,
        selectionArgs,
        UserDictionary.Words.DEFAULT_SORT_ORDER // "frequency DESC"
);

It’s all pretty straightforward. We create a AsyncQueryHandler object that implements the onQueryComplete() callback. On the object we execute startQuery() which is similar to ContentResolver.query() except the first two arguments which are token (1) and cookie (null). The entire query operation happens in the background and finally when the result set is ready, onQueryComplete() is called in the UI thread where you can use the cursor however you want to (display results or just log).

Similarly you can also insert, update or delete records using relevant methods and callbacks.

Under the Hood

It’ll actually help us to understand how things work behidn the scenes with regards to AsyncQueryHandler. When one of these methods is called – startQuery(), startInsert(), startUpdate(), startDelete() – a Message object is created that is populated with the ContentResolver arguments, cookie object data and the token which becomes the what field of the Message (can be later used to cancelOperation()). This Message object is added to the MessageQueue of a single background thread. All AsynQueryHandler instances within an application adds these provider requests messages to this same queue on a single background thread. This is not really problematic as accessing providers (eventually data sources) are not very long tasks as a network operation.

Once the background thread processes a request, the result is passed back in a Message object to the calling AsyncQueryHandler instance. Then the data is extracted and passed to the respective callbacks. This also means all the provider requests are handled sequentially.

Wrapping Up

In this article we discussed how AsyncQueryHandler can be used to so conveniently to execute our provider’s operations in a background thread without blocking the UI thread. In the next article we’ll dive into another concept called CursorLoader that can be used for similar purpose.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

One thought on “Using AsyncQueryHandler to Access Content Providers Asynchronously in Android”

Leave a Reply

Your email address will not be published. Required fields are marked *