Grokking Android

Getting Down to the Nitty Gritty of Android Development

Android Tutorial: Adding Search Suggestions

By

In this tutorial I am going to cover search suggestions. It’s the second and last part of a tutorial on Android search. In the first part I covered the basics of how to use the search framework of Android within your app. If you haven’t read it yet you might want to do so now.

What are search suggestions

When a user enters a search-query, Android starts to provide suggestions for possible search queries, based on what the user has entered so far. Given the shortcomings of onscreen-keyboards this eases searching a lot - which is why you should think about adding search suggestions to your app as well.

Search suggestions of the sample app
Search suggestions of the sample app

Types of suggestions

For search suggestions to work you need to add a content provider to your app and also to configure your app's search meta data.

Android supports two suggestion models:

Recent Query Suggestions

First I am going to show how to display recent queries as suggestions. This is really easy to do.

Android provides the class SearchRecentSuggestionsProvider which offers a complete solution for recent suggestions. All you have to do, is to inherit from this class and to configure it within your constructor:


import android.content.SearchRecentSuggestionsProvider; 

public class SampleRecentSuggestionsProvider 
      extends SearchRecentSuggestionsProvider { 
   
   public static final String AUTHORITY = 
      SampleRecentSuggestionsProvider.class.getName(); 

   public static final int MODE = DATABASE_MODE_QUERIES; 

   public SampleRecentSuggestionsProvider() { 
      setupSuggestions(AUTHORITY, MODE); 
   } 
}

Now your content provider is able to return all recent queries - or would be if it knew about those queries in the first place. What you have done so far is to configure your content provider to provide suggestions.

But you also have to save the queries so that Android can display them when the user starts another search. This is done from within your search activity when it reacts to the query:


SearchRecentSuggestions suggestions = 
   new SearchRecentSuggestions(this, 
      SampleRecentSuggestionsProvider.AUTHORITY, 
      SampleRecentSuggestionsProvider.MODE); 
suggestions.saveRecentQuery(query, null);

As you can see SearchRecentSuggestionsProvider's saveRecentQuery method does that for you.

Next you have to add your SearchRecentSuggestionsProvider subclass as a content provider within the AndroidManifest.xml file. This is not different from the configuration of any other content provider.


<provider
   android:authorities="de.openminds.SampleRecentSuggestionsProvider"
   android:name=".SampleRecentSuggestionsProvider" >
</provider>

Finally you need to add two lines to your search configuration file to enable your recent suggestions provider:


android:searchSuggestAuthority = 
   "com.grokkingandroid.SampleRecentSuggestionsProvider" 
android:searchSuggestSelection = " ?"

The database containing your app's recent suggestions

The SearchRecentSuggestionsProvider stores the search history in a database called suggestions.db within your app's databases directory. This db stores an id, the query, a display text and the timestamp of the time the query was made.

The timestamp is needed since suggestions are sorted by time so that the most recent queries show up first.

You should also consider to clear the search history from within your app – maybe even offer the user the possibility to do so. Clearing the history is also easily done. You simply have to call the method clearHistory() of the SearchRecentSuggestions object that you used above to save the search terms.

I you do not clear the history, Android will eventually do so itself. At most up to 250 search terms are kept in the database.

App-specific suggestions

For many apps you want to customize what suggestions to display. Most often it's not interesting what the user has already searched for, but what you can actually provide.

For this you need to implement a content provider accessing an app-specific data source. But your provider can have rudimentary implementations for many methods a normal content provider would have to implement in detail. For search to work only the query() and getContentType() methods are of interest.

Android's search framework calls your query() method to find out about the suggestions of your app. But to supply suggestions you first have to know what the user has typed into the search box so far. You get this information in one of two ways:

The query as part of the URI

The URI Android uses for search is a bit weird at first glance. As usual it consists of the authority of your content provider plus an optional path element you can configure in the search configuration. But in addition to these two elements Android always adds another constant to the path to make it unique for search. And as the last element Android adds the query text - the text the user has entered so far.

The constant added is search_suggest_query, defined as the final field SUGGEST_URI_PATH_QUERY within the class SearchManager.

So the URI looks like this:

content://authority/optionalPath/search_suggest_query/queryText

The next code snippet shows what your code would look like if your content provider is not used for anything else than search:


public Cursor query(Uri uri, String[] projection, String selection, 
      String[] selectionArgs, String sortOrder) { 
   String query = uri.getLastPathSegment(); 
   if (SearchManager.SUGGEST_URI_PATH_QUERY.equals(query)) { 
      // user hasn't entered anything 
      // thus return a default cursor
   } 
   else { 
      // query contains the users search
      // return a cursor with appropriate data
   } 
}

The query as part of a configured where clause

The other possibility to get the query is to define a where clause in your search configuration. You have to add the attribute android:searchSuggestSelection. For the sample app I use the following configuration:


android:searchSuggestSelection="name like ?"

The resulting code for a search only content provider looks pretty simple:


public Cursor query(Uri uri, String[] projection, String selection, 
      String[] selectionArgs, String sortOrder) { 
   if (selectionArgs != null && selectionArgs.length > 0 && selectionArgs[0].length() > 0) { 
      // the entered text can be found in selectionArgs[0] 
      // return a cursor with appropriate data
   } 
   else { 
      // user hasn't entered anything 
      // thus return a default cursor
   } 
}

In the end it is up to your preferences which of the two query styles you choose. I prefer configuring the where clause since it's a bit easier to handle and still very flexible.

Suggestions threshold

By default Android queries your search suggestions provider after any change to the text of the search box.

If your data source contains many data and you are unlikely to provide meaningful suggestions for few characters it might be best to show suggestions only when more characters have been typed. You can change the threshold value to achieve this. A value of "3" would mean that Android presents suggestions only when the user has entered at least three characters:


android:searchSuggestThreshold="3"

Preparing the suggestions list for later evaluation

The cursor you return at the end of the query() method must follow strict conventions. The framework expects columns with specific names - some of which you can also configure alternatively in your configuration file.

Android uses the returned values to display the suggestions and also to call your search activity with an intent object that contains all relevant information for you to react to a selected suggestion.

Configuration options for suggestions clicks

To create the intent for your search activity, Android needs to know which action to use and which URI to use as value for the intent-data. You can configure both in the search configuration file using the attributes android:searchSuggestIntentAction and android:searchSuggestIntentData respectively.


android:searchSuggestIntentAction = 
   "android.intent.action.VIEW" 
android:searchSuggestIntentData = 
   "content://someAuthority/somePath"

Returning a well-formed cursor for search suggestions

All other values must be part of your cursor. As mentioned above, the columns of your cursor must adhere to a strict naming convention.

All possible columns are defined within the class SearchManager. But the following three are those that you most likely need.

Common constants to use for your suggestions cursor
Constant Usage
SUGGEST_COLUMN_TEXT_1 The text to be displayed in the first line
SUGGEST_COLUMN_TEXT_2 The text to be displayed in the second line
SUGGEST_COLUMN_INTENT_DATA_ID An additional id to append to the data-URI

Of these only the column Searchmanager.SUGGEST_COLUMN_TEXT_1 is mandatory. But normally you would need at least the data id as well – after all you need to know how to react to a selected suggestion. And for this you might want to have an id ready, so that you can query your data source.

The problem with the strict naming conventions of course is, that your data source is unlikely to follow these conventions. Normally you would use names that represent some real life attributes. Like first and last names of customers, locations for events and so on.

Thus you need a mapping. The method setProjectionMap() of the class SQLiteQueryBuilder helps with that. Alas, the usage of the projection map is not without some downsides. I will cover these and how to deal with them in a follow-up post. For now I ignore these problems and simply show you how to use the projection map.


Map<String, String> projectionMap = new HashMap<String, String>(); 
projectionMap.put(COL_BAND, COL_BAND + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1); 
projectionMap.put(COL_ID, COL_ID); 
projectionMap.put(COL_ROW_ID, COL_ROW_ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 
projectionMap.put(COL_LOCATION, COL_LOCATION + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2); 
projectionMap.put(COL_DATE, COL_DATE); 
builder.setProjectionMap(projectionMap);

Reacting to a suggestion click

When a user selects a search suggestions, Android's search framework calls the search activity that you configured in your manifest file. It uses an explicit intent to do so. In case of an app-specific suggestions provider this intent contains data from your configuration file and your returned cursor.

How to proceed depends on whether you use a recent suggestions or an app-specific suggestions provider.

If you used a recent suggestions provider you have to start the search for this term again to present a list of results. The action for the intent is Intent.ACTION_SEARCH. In this case the search activity would look exactly like the one, you have seen in the first part of this tutorial.

With an app-specific suggestions provider the suggestions list most often shows concrete data. If a user clicks on one of these suggestions, he does not want to see a list of results again - instead he wants to see a detail view for the item that he selected. You can configure which action to use, but most often it will be Intent.ACTION_VIEW.

Since your search activity probably inherits from ListActivity it is not the appropriate activity for a detail view. In this case your search activity must read the id and other information contained within the intent and start a new activity for the detail view. That's the reason your cursor needs the data id as described above. The next snippet shows how to start the details activity.


private void handleIntent(Intent intent) { 
   if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 
      String query = intent.getStringExtra(SearchManager.QUERY); 
      doSearch(query); 
   } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { 
      Uri detailUri = intent.getData(); 
      String id = detailUri.getLastPathSegment(); 
      Intent detailsIntent = new Intent(getApplicationContext(), DetailsActivity.class); 
      detailsIntent.putExtra("ID", id); 
      startActivity(detailsIntent); 
      finish();	 
   } 
}

Global search

Basically all you have to do is to add one line to your search configuration file:


android:includeInGlobalSearch="true"

Of course there is a catch. Your search provider only gets used when the user adds it to the list of searchable items for the Quick Search Box. Without this one-time user interaction your provider won't be asked for suggestions.

The problem is not so much with the code but with how you get your user to add your app to this list:

List of globally searchable items
List of globally searchable items

Your app itself is not allowed to change this value - only the user can. But luckily Android provides you with an intent you can use to directly jump to the configuration page for globally searchable items.


android.app.SearchManager.INTENT_ACTION_GLOBAL_SEARCH

You should consider offering your user a way to trigger this intent from within your app.

When the user has added your app to the list of searchable items, your provider's results are shown within the search results. As you can see on the screenshot Android displays concerts of the sample app within the global suggestions list.

Global search suggestions showing results of the sample app
Global search suggestions showing results of the sample app

The biggest problem with search suggestions

Search suggestions have one big problem: You have no control over how they are going to be displayed.

This can be a big problem if you have customized the appearance of your app a lot and applied a custom style. In this case the suggestions displayed by Android would most likely ruin the consistency within your app.

I think there is a difference between switching to other apps for necessary tasks (like picking a contact or adding an event) or searching for content of your app. The latter should always fit your style.

For a Holo-only app, the UI of the standard search suggestions poses no problem. For customized apps it often does.

It gets worse if you want to present distinct data. Search in a concert app might yield results for locations as well as for bands. You might want to separate both - maybe even add section headers. You are out of luck with the standard suggestions. You need to roll out your own suggestions solution in this case.

There is no reason for despair though. What you have learned in this tutorial is needed for global search as well. And for many apps, global search makes sense. Also most apps do not need section headers and might even stick more or less to the standard Android design. In this case you do not need to worry.

Wrapping up

As you can see, Android offers you a powerful search framework that you can tweak to your app's need without too much hassle. You control whether your app's content is searchable only locally or also globally.

You can offer suggestions based on recent search queries or based on your apps data and tweak what to do once the user has selected a search suggestion.

Of course I couldn't cover everything in this tutorial. The biggest topic missing here is how to use search shortcuts. I will cover them in a later post. Stay tuned!

Wolfram Rittmeyer lives in Germany and has been developing with Java for many years.

He has been interested in Android for quite a while and has been blogging about all kind of topics around Android.

You can find him on Google+ and Twitter.