In this article, we will develop an Android application containing a search dialog, that can search a set of countries stored in an SQLite database. The search dialog is facilitated with custom search suggestions containing texts and images. The search dialog fetches the data from SQLite database using cursor loader via content provider.
Major files used in this application are listed below :
- MainActivity.java : Search dialog is invoked from the MainActivity class defined in this file.
- SearchableActivity.java : Actions performed on search dialog is handled by SearchableActivity.
- CountryActivity.java : The details of the selected country is displayed in this activity. The selection can be done either from suggestion for search dialog or from listview of SearchableActivity.
- searchable.xml : This is the configuration file for search dialog which defines various properties like content provider for custom suggestions, intent data for selected suggested item, search suggestions to quick search box etc
- CountryContentProvider.java : The content provider that provides necessary data for the search dialog from table class.
- CountryDB.java : A table class that fetches data from the SQLite database.
1. Create new Android application project namely “SearchDialogDemo” with the given below details
Application Name : SearchDialogDemo
Project Name : SearchDialogDemo
Package Name : in.wptrafficanalyzer.searchdialogdemo
Minimum Required SDK : API 8 : Android 2.2 ( Froyo )
Target SDK : API 17 : Android 4.2 ( Jelly Bean )
2. Add Android Support library to this project
By default, Android support library (android-support-v4.jar ) is added to this project by Eclipse IDE to the directory libs. If it is not added, we can do it manually by doing the following steps :
- Open Project Explorer by Clicking “Window -> Show View -> Project Explorer”
- Right click this project
- Then from popup menu, Click “Android Tools -> Add Support Library “
3. Create a folder namely res/drawable and extract the zip file to into it.
4. Update the file res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">SearchDialogDemo</string> <string name="action_settings">Settings</string> <string name="hello_world">Hello world!</string> <string name="search">Search Country</string> <string name="search_hint">Search names</string> <string name="flag_description">Country Flag</string> <string name="search_settings">Search Country Names</string> </resources>
5. Create the layout file res/layout/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <Button android:id="@+id/btn_search" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/search" android:layout_centerInParent="true" /> </RelativeLayout>
6. Create the layout file res/layout/activity_searchable.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ListView android:id="@+id/lv_countries" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
7. Create the layout file res/layout/activity_country.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ImageView android:id="@+id/iv_flag" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:contentDescription="@string/flag_description"/> <TextView android:id="@+id/tv_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/iv_flag" android:textSize="25sp" /> <TextView android:id="@+id/tv_currency" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_name" android:textSize="25sp" /> </RelativeLayout>
8. Create a class Country in the file src/in/wptrafficanalyzer/searchdialogdemo/Country.java
package in.wptrafficanalyzer.searchdialogdemo; /** Country details are stored in this class and is used to populate the table countries * in CountryDb.java */ public class Country { // Array of strings storing country names static String[] countries = new String[] { "India", "Pakistan", "Sri Lanka", "China", "Bangladesh", "Nepal", "Afghanistan", "North Korea", "South Korea", "Japan" }; // Array of integers points to images stored in /res/drawable static int[] flags = new int[]{ R.drawable.india, R.drawable.pakistan, R.drawable.srilanka, R.drawable.china, R.drawable.bangladesh, R.drawable.nepal, R.drawable.afghanistan, R.drawable.nkorea, R.drawable.skorea, R.drawable.japan }; // Array of strings to store currencies static String[] currency = new String[]{ "Indian Rupee", "Pakistani Rupee", "Sri Lankan Rupee", "Renminbi", "Bangladeshi Taka", "Nepalese Rupee", "Afghani", "North Korean Won", "South Korean Won", "Japanese Yen" }; }
9. Create the class CountryDB in the file src/in/wptrafficanalyzer/searchdialogdemo/CountryDB.java
package in.wptrafficanalyzer.searchdialogdemo; import java.util.HashMap; import android.app.SearchManager; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; public class CountryDB{ private static final String DBNAME = "country"; private static final int VERSION = 1; private CountryDBOpenHelper mCountryDBOpenHelper; private static final String FIELD_ID = "_id"; private static final String FIELD_NAME = "name"; private static final String FIELD_FLAG = "flag"; private static final String FIELD_CURRENCY = "currency"; private static final String TABLE_NAME = "countries"; private HashMap<String, String> mAliasMap; public CountryDB(Context context){ mCountryDBOpenHelper = new CountryDBOpenHelper(context, DBNAME, null, VERSION); // This HashMap is used to map table fields to Custom Suggestion fields mAliasMap = new HashMap<String, String>(); // Unique id for the each Suggestions ( Mandatory ) mAliasMap.put("_ID", FIELD_ID + " as " + "_id" ); // Text for Suggestions ( Mandatory ) mAliasMap.put(SearchManager.SUGGEST_COLUMN_TEXT_1, FIELD_NAME + " as " + SearchManager.SUGGEST_COLUMN_TEXT_1); // Icon for Suggestions ( Optional ) mAliasMap.put( SearchManager.SUGGEST_COLUMN_ICON_1, FIELD_FLAG + " as " + SearchManager.SUGGEST_COLUMN_ICON_1); // This value will be appended to the Intent data on selecting an item from Search result or Suggestions ( Optional ) mAliasMap.put( SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, FIELD_ID + " as " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID ); } /** Returns Countries */ public Cursor getCountries(String[] selectionArgs){ String selection = FIELD_NAME + " like ? "; if(selectionArgs!=null){ selectionArgs[0] = "%"+selectionArgs[0] + "%"; } SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setProjectionMap(mAliasMap); queryBuilder.setTables(TABLE_NAME); Cursor c = queryBuilder.query(mCountryDBOpenHelper.getReadableDatabase(), new String[] { "_ID", SearchManager.SUGGEST_COLUMN_TEXT_1 , SearchManager.SUGGEST_COLUMN_ICON_1 , SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID } , selection, selectionArgs, null, null, FIELD_NAME + " asc ","10" ); return c; } /** Return Country corresponding to the id */ public Cursor getCountry(String id){ SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(TABLE_NAME); Cursor c = queryBuilder.query(mCountryDBOpenHelper.getReadableDatabase(), new String[] { "_id", "name", "flag", "currency" } , "_id = ?", new String[] { id } , null, null, null ,"1" ); return c; } class CountryDBOpenHelper extends SQLiteOpenHelper{ public CountryDBOpenHelper( Context context, String name, CursorFactory factory, int version ) { super(context, DBNAME, factory, VERSION); } @Override public void onCreate(SQLiteDatabase db) { String sql = ""; // Defining table structure sql = " create table " + TABLE_NAME + "" + " ( " + FIELD_ID + " integer primary key autoincrement, " + FIELD_NAME + " varchar(100), " + FIELD_FLAG + " int, " + FIELD_CURRENCY + " varchar(100) " + " ) " ; // Creating table db.execSQL(sql); for(int i=0;i<Country.countries.length;i++){ // Defining insert statement sql = "insert into " + TABLE_NAME + " ( " + FIELD_NAME + " , " + FIELD_FLAG + " , " + FIELD_CURRENCY + " ) " + " values ( " + " '" + Country.countries[i] + "' ," + " " + Country.flags[i] + " ," + " '" + Country.currency[i] + "' ) "; // Inserting values into table db.execSQL(sql); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // TODO Auto-generated method stub } } }
10. Creating the content provider in the file “src/in/wptrafficanalyzer/searchdialogdemo/CountryContentProvider.java
package in.wptrafficanalyzer.searchdialogdemo; import android.app.SearchManager; import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.net.Uri; public class CountryContentProvider extends ContentProvider { public static final String AUTHORITY = "in.wptrafficanalyzer.searchdialogdemo.CountryContentProvider"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/countries" ); CountryDB mCountryDB = null; private static final int SUGGESTIONS_COUNTRY = 1; private static final int SEARCH_COUNTRY = 2; private static final int GET_COUNTRY = 3; UriMatcher mUriMatcher = buildUriMatcher(); private UriMatcher buildUriMatcher(){ UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); // Suggestion items of Search Dialog is provided by this uri uriMatcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,SUGGESTIONS_COUNTRY); // This URI is invoked, when user presses "Go" in the Keyboard of Search Dialog // Listview items of SearchableActivity is provided by this uri // See android:searchSuggestIntentData="content://in.wptrafficanalyzer.searchdialogdemo.provider/countries" of searchable.xml uriMatcher.addURI(AUTHORITY, "countries", SEARCH_COUNTRY); // This URI is invoked, when user selects a suggestion from search dialog or an item from the listview // Country details for CountryActivity is provided by this uri // See, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID in CountryDB.java uriMatcher.addURI(AUTHORITY, "countries/#", GET_COUNTRY); return uriMatcher; } @Override public boolean onCreate() { mCountryDB = new CountryDB(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Cursor c = null; switch(mUriMatcher.match(uri)){ case SUGGESTIONS_COUNTRY : c = mCountryDB.getCountries(selectionArgs); break; case SEARCH_COUNTRY : c = mCountryDB.getCountries(selectionArgs); break; case GET_COUNTRY : String id = uri.getLastPathSegment(); c = mCountryDB.getCountry(id); } return c; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override public String getType(Uri uri) { throw new UnsupportedOperationException(); } @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException(); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } }
11. Create a class namely MainActivity in the file src/in/wptrafficanalyzer/searchdialogdemo/MainActivity.java
package in.wptrafficanalyzer.searchdialogdemo; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class MainActivity extends FragmentActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn = (Button) findViewById(R.id.btn_search); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onSearchRequested(); } }); } }
12. Create the class namely “SearchableActivity” in the file src/in/wptrafficanalyzer/searchdialogdemo/SearchableActivity.java
package in.wptrafficanalyzer.searchdialogdemo; import android.app.SearchManager; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.SearchRecentSuggestions; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; public class SearchableActivity extends FragmentActivity implements LoaderCallbacks<Cursor> { ListView mLVCountries; SimpleCursorAdapter mCursorAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_searchable); // Getting reference to Country List mLVCountries = (ListView)findViewById(R.id.lv_countries); // Setting item click listener mLVCountries.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent countryIntent = new Intent(getApplicationContext(), CountryActivity.class); // Creating a uri to fetch country details corresponding to selected listview item Uri data = Uri.withAppendedPath(CountryContentProvider.CONTENT_URI, String.valueOf(id)); // Setting uri to the data on the intent countryIntent.setData(data); // Open the activity startActivity(countryIntent); } }); // Defining CursorAdapter for the ListView mCursorAdapter = new SimpleCursorAdapter(getBaseContext(), android.R.layout.simple_list_item_1, null, new String[] { SearchManager.SUGGEST_COLUMN_TEXT_1}, new int[] { android.R.id.text1}, 0); // Setting the cursor adapter for the country listview mLVCountries.setAdapter(mCursorAdapter); // Getting the intent that invoked this activity Intent intent = getIntent(); // If this activity is invoked by selecting an item from Suggestion of Search dialog or // from listview of SearchActivity if(intent.getAction().equals(Intent.ACTION_VIEW)){ Intent countryIntent = new Intent(this, CountryActivity.class); countryIntent.setData(intent.getData()); startActivity(countryIntent); finish(); }else if(intent.getAction().equals(Intent.ACTION_SEARCH)){ // If this activity is invoked, when user presses "Go" in the Keyboard of Search Dialog String query = intent.getStringExtra(SearchManager.QUERY); doSearch(query); } } private void doSearch(String query){ Bundle data = new Bundle(); data.putString("query", query); // Invoking onCreateLoader() in non-ui thread getSupportLoaderManager().initLoader(1, data, this); } /** This method is invoked by initLoader() */ @Override public Loader<Cursor> onCreateLoader(int arg0, Bundle data) { Uri uri = CountryContentProvider.CONTENT_URI; return new CursorLoader(getBaseContext(), uri, null, null , new String[]{data.getString("query")}, null); } /** This method is executed in ui thread, after onCreateLoader() */ @Override public void onLoadFinished(Loader<Cursor> arg0, Cursor c) { mCursorAdapter.swapCursor(c); } @Override public void onLoaderReset(Loader<Cursor> arg0) { // TODO Auto-generated method stub } }
13. Create the class “CountryActivity” in the file src/in/wptrafficanalyzer/searchdialogdemo/CountryActivity.java
package in.wptrafficanalyzer.searchdialogdemo; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.widget.ImageView; import android.widget.TextView; public class CountryActivity extends FragmentActivity implements LoaderCallbacks<Cursor>{ private Uri mUri; private ImageView mIvFlag; private TextView mTvName; private TextView mTvCurrency; @Override protected void onCreate(Bundle arg0) { super.onCreate(arg0); setContentView(R.layout.activity_country); Intent intent = getIntent(); mUri = intent.getData(); mIvFlag = (ImageView) findViewById(R.id.iv_flag); mTvName = (TextView) findViewById(R.id.tv_name); mTvCurrency = (TextView) findViewById(R.id.tv_currency); // Invokes the method onCreateloader() in non-ui thread getSupportLoaderManager().initLoader(0, null, this); } /** Invoked by initLoader() */ @Override public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) { return new CursorLoader(getBaseContext(), mUri, null, null , null, null); } /** Invoked by onCreateLoader(), will be executed in ui-thread */ @Override public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) { if(cursor.moveToFirst()){ mIvFlag.setImageResource(cursor.getInt(cursor.getColumnIndex(cursor.getColumnName(2)))); mTvName.setText("Country: "+cursor.getString(cursor.getColumnIndex(cursor.getColumnName(1)))); mTvCurrency.setText("Currency: "+cursor.getString(cursor.getColumnIndex(cursor.getColumnName(3)))); } } @Override public void onLoaderReset(Loader<Cursor> arg0) { // TODO Auto-generated method stub } }
14. Create the configuration file for search dialog in the file res/xml/searchable.xml
<?xml version="1.0" encoding="utf-8"?> <searchable xmlns:android="http://schemas.android.com/apk/res/android" android:label="@string/app_name" android:hint="@string/search_hint" android:searchSettingsDescription="@string/search_settings" android:searchSuggestAuthority="in.wptrafficanalyzer.searchdialogdemo.CountryContentProvider" android:searchSuggestIntentAction="android.intent.action.VIEW" android:searchSuggestIntentData="content://in.wptrafficanalyzer.searchdialogdemo.CountryContentProvider/countries" android:searchSuggestSelection=" ?" android:searchSuggestThreshold="1" android:includeInGlobalSearch="true" > </searchable>
15. Update the file AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="in.wptrafficanalyzer.searchdialogdemo" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <!-- Activity with SearchDialog enabled --> <activity android:name="in.wptrafficanalyzer.searchdialogdemo.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <!-- Enabling Search Dialog --> <meta-data android:name="android.app.default_searchable" android:value=".SearchableActivity" /> </activity> <!-- A Searchable activity, that handles the searches --> <activity android:name=".SearchableActivity" > <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/> </activity> <!-- Activity that shows the country details --> <activity android:name=".CountryActivity" /> <!-- Content Provider to query sqlite database --> <provider android:name=".CountryContentProvider" android:authorities="in.wptrafficanalyzer.searchdialogdemo.CountryContentProvider" android:exported="true" /> </application> </manifest>
16. Screenshots of the application
17. Download Source Code