We are the Dev Teams of
  • brands
  • ebay_main
  • ebay
  • mobile
<
>
BLOG

Autosuggestions from API - A Deep Dive into Android Cursors

by Danny Preussler
in Tutorials

ebayk-and

Cursors are one of the key building blocks of the Android SDK but still one of the hardest to understand, mostly because they feel so low level in our object-orientated abstracted world.

However, sometimes we don't have a choice and have to work with them, the same also applies to ContentProviders.  For example, implementing auto-suggestion using SearchView, which is a feature we recently added to the eBay Kleinanzeigen Android App.

This article describes how to work with Android Cursors.

Let's Dive in.

Our aim is to provide search suggestions for user queries. When a user types in a common search phrase and/or category, the server API presents end-users with a list of search suggestions.

We wanted to adopt the look and feel of other native android apps on the Google Play Store. So that whenever a user starts to type a query, Google Play gives "live" search suggestions on the device and displays the results in real-time.

google play autosuggestions

This can be achieved using the basic SearchView and Actionbar features. Good documentation is available on the Android developer site: http://developer.android.com/guide/topics/search/search-dialog.html

Basically you define an xml file, extend the SearchRecentSuggestionsProvider and glue them together. Now your application can store your suggestion data in the provider database which is used the next time the user searches for something. We had already implemented search into our Android application. So most of the work was done.

The aim now is to merge the data (a Cursor) from the recent search suggestions with data provided from the API. This means merging a cursor from SearchRecentSuggestionsProvider with our own data.

The SDK provides an easy way to achieve this using the MergeCursor, which merges the results of 2 cursors by simply putting them into the contstructor.

The important thing here is that both need to use the same column definition! This might be obvious, but it means that we have to dig a little deeper into how search suggestions work.

The column names can be found in android.app.SearchManager.  To see how they are used, take a look at the following source code of SearchRecentSuggestionsProvider:

     mSuggestionProjection = new String [] {
                    "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT,
                    "'android.resource://system/"
                            + com.android.internal.R.drawable.ic_menu_recent_history + "' AS "
                            + SearchManager.SUGGEST_COLUMN_ICON_1,
                    "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
                    "display2 AS " + SearchManager.SUGGEST_COLUMN_TEXT_2,
                    "query AS " + SearchManager.SUGGEST_COLUMN_QUERY,
                    "_id"                    

So to summarise, we have 2 texts for the 2 rows that we want to show and an icon shown next to it. All we have to do is stick to the column ids (indices). This is how we create our API Cursor, which is the one that will be merged with the original one.

Let's do it

To create a Cursor from scratch we have 2 helper classes that we can use — MatrixCursor and AbstractCursor.  The MatrixCursor is a concrete class that can easily be filled with data.

The AbstractCursor implements most of the methods needed for a cursor but leaves us with some important ones (e.g. getColumnNames(), getCount() and the getters for the data).

We decided to use the AbstractCursor to return data on the fly instead of feeding them into an object first.

To sum up — we now have a MergeCursor with the Cursor from SearchRecentSuggestionsProvider and our implementation of AbstractCursor with the API data.

Our ContentProvider could also be used to implement the query by doing something like this:

   @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {                     
        String query = selectionArgs != null ? selectionArgs[0] : null;
        return new MergeCursor(
                       new Cursor[]{super.query(uri, projection, selection, selectionArgs, sortOrder),
                       createCursorFromBackend(query)});
    }

​This would work pretty well but has a problem. The query would be blocked until we receive the data from server. This might be ok for the definition for a normal ContentProvider but as the user types (fast) a lot of query calls are being made. At some point the system will simply drop incoming queries which basically results in losing the user input. This means — we need to return queries as fast as possible! 

A simple way to do this would be to cache the last results from the backend call in the provider and use that cache in the cursor.

This will work but you will always be at least one character behind your user. When the user types in the word "car", it would return the suggestion for "ca" or even "c". If the network is slow, for example a mobile connection, then we would have to assume that the same connection would be used in your application.  To resolve this we would need data that can be updated.

The good thing is — ContentProviders where designed this way!

Let's add some magic into our query:

   @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {                      
        String query = selectionArgs != null ? selectionArgs[0] : null;                                
        final Cursor backendCursor = createCursorFromBackend(query, uri);
        backendCursor.setNotificationUri(getContext().getContentResolver(), uri);
        return new MergeCursor(
                       new Cursor[]{super.query(uri, projection, selection, selectionArgs, sortOrder), 
                       backendCursor});
   
    }

In the above, we register a Uri to apply changes to the cursor. Our backend implementation classes can then notify the application when something is done.

This is done using — context.getContentResolver().notifyChange(uri, null);

Does it work?

When trying this yourself you may run in to the problem that the view is still not refreshing the content.  The adapter may not be prepared for the content change.  This basically means that you have to notify your Adapter when the ContentProvider notification occurs!

For example, this can easily be solved using ActionbarSherlock :

public class UpdateableSuggestionsAdapter extends SuggestionsAdapter {
...
    @Override
    protected void onContentChanged() {
        super.onContentChanged();
        notifyDataSetChanged();
    }
}

The result

You should now see something similar to the following:

Autosuggestions ebayK android

You may have noticed that we are also using the same icons as Google Play Store, which shows the user if this is a cached recent suggestion (clock icon) or a live one (magnifier icon). This is done using the icon column that was mentioned at the beginning of this article.

Simply return 
"android.R.drawable.ic_menu_search"

in your cursor's
public String getString(final int column) 

when asked for your icon column.

See it live in the app: https://play.google.com/store/apps/details?id=com.ebay.kleinanzeigen

There's one little problem.

If supporting older android versions, there's one more thing to be aware of. The icon that was introduced in Android 4.0 (SearchRecentSuggestionsProviders) uses a different column definition for cursors.

As a workaround you can use one of the other cursor classes that are available — CursorWrapper, which wraps an existing Cursor and allows you to override the methods and change its behaviour i.e. by changing the column definition or the column ids.

Title graphics: Wikimedia Commons

android

?>