dimanche 9 juin 2013

Adding search capabilities to an Android application

To set up a search assistant in an application, you need to go through the following steps :
  • Define a searchable configuration: An XML file that configures some settings for the search dialog or widget. It includes settings for features such as hint text, search suggestion, voice search, etc.
  • A searchable activity that will receive the search query, to perform the search on the application data data, then to display the results.
  • A search interface, provided by either: a search dialog that will appear at the top of the screen when the user presses the device SEARCH button (if available), it can also called programmatically from the code, or a SearchView widget.
First, the XML searchable configuration res/xml/searchable.xml:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/search_label"
    android:hint="@string/search_hint"
    android:searchSuggestAuthority="com.fontself.sms.provider.ContactProvider"    
    android:searchSuggestIntentAction="android.intent.action.VIEW">
</searchable> 

Second, the ContentProvider that will be called by the Android system to perform search:
public class MyContentProvider extends ContentProvider {
   public static String AUTHORITY = "com.app.package.MyContentProvider";
   private static final int SEARCH_SUGGEST = 0;
   private static final int SHORTCUT_REFRESH = 1;
   private static final UriMatcher sURIMatcher = buildUriMatcher();

   private static final String[] COLUMNS = {
      "_id",  // must include this column
      SearchManager.SUGGEST_COLUMN_TEXT_1,
      SearchManager.SUGGEST_COLUMN_TEXT_2,
      SearchManager.SUGGEST_COLUMN_INTENT_DATA,     
   };
   private static UriMatcher buildUriMatcher() {
      UriMatcher matcher =  new UriMatcher(UriMatcher.NO_MATCH);
      matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
      matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
      matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT, SHORTCUT_REFRESH);
      matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SHORTCUT_REFRESH);
      return matcher;
   }
   @Override public String getType(Uri uri) {
      switch (sURIMatcher.match(uri)) {  
      case SEARCH_SUGGEST:
         return SearchManager.SUGGEST_MIME_TYPE;
      case SHORTCUT_REFRESH:
         return SearchManager.SHORTCUT_MIME_TYPE;
      default:
         throw new IllegalArgumentException("Unknown URL " + uri);
      }
   }
   @Override public boolean onCreate() {  
      return false;
   }
   @Override
   public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {  
      if (!TextUtils.isEmpty(selection)) {
         throw new IllegalArgumentException("selection not allowed for " + uri);
      }
      if (selectionArgs != null && selectionArgs.length != 0) {
         throw new IllegalArgumentException("selectionArgs not allowed for " + uri);
      }
      if (!TextUtils.isEmpty(sortOrder)) {
         throw new IllegalArgumentException("sortOrder not allowed for " + uri);
      }
      switch (sURIMatcher.match(uri)) {
         case SEARCH_SUGGEST:
            String query = null;
            if (uri.getPathSegments().size() > 1) {
               query = uri.getLastPathSegment().toLowerCase();
            }
            return getSuggestions(query, projection);
         case SHORTCUT_REFRESH:
            String shortcutId = null;
            if (uri.getPathSegments().size() > 1) {
               shortcutId = uri.getLastPathSegment();
            }
            return refreshShortcut(shortcutId, projection);
         default:
            throw new IllegalArgumentException("Unknown URL " + uri);
      }  
   }
   private Cursor getSuggestions(String query, String[] projection) {
      String processedQuery = query == null ? "" : query.toLowerCase();
      List<MyObject> rows = doSearch(processedQuery);
        
      MatrixCursor cursor = new MatrixCursor(COLUMNS);
      for (MyObject row : rows) {
          cursor.addRow(columnValuesOfWord(row));
      }        
      return cursor;
   }
   private Object[] columnValuesOfWord(MyObject obj) {
      return new String[] {
         obj._id,    // _id
         obj.name,   // text1
         obj.phone,  // text2
         obj.phone,  // intent_data (included in the Intent when clicking on item)
      };
   }
   private Cursor refreshShortcut(String shortcutId, String[] projection) {
      return null;
   }
    
   @Override public Uri insert(Uri uri, ContentValues values) {
      throw new UnsupportedOperationException();
   }

   @Override public int delete(Uri uri, String selection, String[] selectionArgs) {
      throw new UnsupportedOperationException();
   }

   @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
      throw new UnsupportedOperationException();
   }
}

Third, define the SearchActivity:
public class MySearchActivity extends ListActivity {
   public void onCreate(Bundle savedInstanceState) { 
      super.onCreate(savedInstanceState); 
   //setContentView(R.layout.search);  
        setListAdapter(new MyAdapter.getInstance(MySearchActivity.this));
        handleIntent(getIntent());  
   } 

   @Override protected void onNewIntent(Intent intent) { 
      setIntent(intent); 
      handleIntent(intent);  
   } 

   public void onListItemClick(ListView l, View v, int position, long id) {       
      // call detail activity for clicked entry     
   } 

   private void handleIntent(Intent intent) {    
      if (Intent.ACTION_SEARCH.equals(intent.getAction())) {            
         // Handle the normal search query case
         String query = intent.getStringExtra(SearchManager.QUERY);            
         doSearch(query);       
      } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
         // Handle a suggestions click (because the suggestions all use ACTION_VIEW)
         Uri data = intent.getData();
         showResult(data);
      }
   }    
   
   private void showResult(Uri data) {}

   private void doSearch(String queryStr) {    
      // get a Cursor, prepare the ListAdapter and set it
   } 

}
Finally, update the AndroidManifest.xml file:
<application>
<meta-data android:name="android.app.default_searchable" android:value=".MySearchActivity" />
     <activity android:name=".MySearchActivity" android:label="@string/app_name" android:launchMode="singleTop" > 

<!-- enable the search dialog to send searches to MessageActivity -->       
         <intent-filter >
             <action android:name="android.intent.action.SEARCH" /> 
        </intent-filter> 
        <intent-filter > 
           <action android:name="android.intent.action.VIEW" /> 
        </intent-filter> 
        <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />

     </activity>
<provider android:name=".MyContentProvider" android:authorities="com.app.package.MyContentProvider" />
</application>

Resources

For more details check official documentation on ContentProvider, Search dialogs, how to implement custom suggestions, and how to use recent search suggestions. In case, you are using ActionBarSherlock check this blog post.
You may also have to check the SearchableDitionary sample project that comes with the Android SDK.