📜 ⬆️ ⬇️

Use Android Search Dialog. Part 3 - Custom Suggestions

image

This is the final article on using the Android Search Dialog (the previous ones are here and here ). In it, I will tell you how to add dynamic hints to search in the dialog, as well as how to integrate the search for your application into the system's Quick Search Box (QSB). The advantage of QSB is that it can be used to get information from virtually anywhere in the OS.


Theory


Search tips are created using the data from your application that is being searched. When a user selects one of them, Search Manager sends an Intent to the Activity that is responsible for the search. Usually, when a user clicks the search icon in a dialog, an Search type Search intent is sent, however, when choosing a prompt, in this case, you can define another type of Intent so that we can intercept it and perform appropriate actions, for example, creating a new dialog or calling Activity to display information, etc.
The search query data is transferred via the Intent as before, however, now we will use the URI to determine the type of query through the content provider.
')
Again, we don’t need to take any action to draw the dialog, Search Manager is doing this, all we have to do is submit the xml configuration file.

So, when Search Manager defines our Activity as responsible for searching and providing hints to the search, the following sequence of actions occurs:
  1. When Search Manager receives the text of a search request, it sends its request to the content provider that provides hints.
  2. The content provider returns a cursor pointing to prompts that match the text of the search query.
  3. Search Manager displays tooltips using the cursor.

After the hint list has been displayed, the following may occur:

So, we modify our application (which was discussed in Part 1 ) so that dynamic prompts are added, and, to refine the mechanism, when selecting a prompt, we will call a new Activity, which will display information on request. For implementation will require:


Modify configuration file


I remind you that the configuration file (res / xml / searchable.xml) is required to display the dialog and modify it, for example, to use voice search. To use dynamic prompts, you need to add the parameter to the file: android: searchSuggestAuthority. It will match the authorization string of the content provider. In addition, add the android parameter: searchMode = "queryRewriteFromText", its value indicates that the search string in the dialog will be overwritten when you navigate through prompts, for example, using a trackball. We also add the parameters that specify the selection operator, the type of Intent sent when the prompt is selected, and the minimum number of characters printed in the dialog that is required for a request to the content provider.

Res / xml / searchable.xml file
<?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/settings_description" android:searchMode="queryRewriteFromText" android:includeInGlobalSearch="true" android:searchSuggestAuthority="com.example.search.SuggestionProvider" android:searchSuggestIntentAction="android.intent.action.VIEW" android:searchSuggestIntentData="content://com.example.search.SuggestionProvider/records" android:searchSuggestThreshold="1" android:searchSuggestSelection=" ?"> </searchable> 


We create content provider


In fact, our content provider is no different from others. But you need to make sure that for each row the required columns are selected from the table of hints, those required by Search Manager. We will query the data for prompts using the content provider query () method. And it will be called every time the user types a new character in the dialog. Thus, the query () method must return the cursor to records in the table that match the query, and then Search Manager will be able to display hints. See the description of the method in the comments to the code.
The request text itself will be added to the URI, so there will be no problems with getting it; you just need to use the standard getLastPathSegment () method.

Create hints table


When Search Manager receives a cursor pointing to a record, it expects a specific set of columns for each record. Two are obligatory: _ID is the unique identifier of each hint, and SUGGEST_COLUMN_TEXT_1 is the hint text.
There are many optional columns, for example, using SUGGEST_COLUMN_ICON_1, you can define for each entry an icon displayed on the left side of the tooltip (very convenient, for example, for searching by contacts).

Determining the data type for an intent


Since we pass data on request through a URI, we need a mechanism to determine which hint was chosen. There are two ways. The first is to define a separate SUGGEST_COLUMN_INTENT_DATA column, which will have unique data for each record, then you can get data from the Intent via getData () or getDataString (). The second option is to determine the data type for all Intent in the configuration file (res / xml / searchable.xml) and then add unique data for each Intent to the URI using the SUGGEST_COLUMN_INTENT_DATA_ID column.
We will use the second option, and I did not create separate columns in the table, since you can simply create a mapping from SUGGEST_COLUMN_INTENT_DATA_ID to the rowId of the table. I also add that for the sake of sporting interest in SQLite, FTS3 was used for searching, that is, I had to create a virtual table for which you can’t impose constraints on columns, such as PRIMARY KEY or NULL / NOT NULL. But virtual tables have a unique row identifier, and set the display on it. That is, the data for Intent will be as follows: the "/" and rowId rows in the table will be added to the URI.

Creating an Activity to display information


The interface is in res / layout / record_activity.xml. All what the Activity does is getting data from the Intent, querying the cursor through the content provider and displaying the record in the text field.

Res / layout / record_activity.xml file
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:padding="10dp"> <TextView android:id="@+id/record_header" android:textSize="25dp" android:textColor="?android:textColorPrimary" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> 


Now, we will enter information about the content provider and the new Activity into the manifest, also, since we now have two Activities, then we specify the one that is responsible for the search by default.

AndroidManifest.xml file
 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.search" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".Main" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" /> </activity> <activity android:name=".RecordActivity" android:theme="@android:style/Theme.NoTitleBar" /> <provider android:name=".SuggestionProvider" android:authorities="com.example.search.SuggestionProvider" /> <meta-data android:name="android.app.default_searchable" android:value=".Main" /> </application> <uses-sdk android:minSdkVersion="5" /> </manifest> 

Interception of Intent in the activity responsible for the search


After all the above steps, you need to process the Intent in the main Activity, which is responsible for the search. Since we defined the type of Intent for prompts as View, we just need to add a check for it. If the condition is fulfilled, then RecordActivity is launched using an Intent, into the data of which the URI + "/" + id prompts are written in the table.

Integration with Quick Search Box


After you have modified your application to use custom suggestions, you can add it to the system search. To do this, add two parameters to the searchable.xml file:
  1. android: includeInGlobalSearch = “true” - indicates that QSB can search your application.
  2. android: searchSettingsDescription = "@ string / settings_description" - indicates the description of your application, which is displayed when you configure Quick Search Box. These settings are in settings-> search.

These options are available with the version of Android 1.6, that is, for the versions below, you will not be able to configure your application for QSB.

Source


I present the full source code of all necessary classes. Main.java - the main Activity responsible for searching and sending requests to the content provider, RecordActivity.java - receives an Intent with data on a specific record, receives a link to the record and displays information. SuggestionProvider.java is a content provider that processes requests from Search Manager to the hint table. RecordsDbHelper.java is responsible for creating the table, filling it in, establishing the necessary mapping, and for the “matching” records itself.

Main.java file
 package com.example.search; import android.app.ListActivity; import android.app.SearchManager; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.SimpleCursorAdapter; import android.widget.Toast; public class Main extends ListActivity { private EditText text; private Button add; private RecordsDbHelper mDbHelper; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mDbHelper = new RecordsDbHelper(this); Intent intent = getIntent(); if (Intent.ACTION_SEARCH.equals(intent.getAction())) { //     String query = intent.getStringExtra(SearchManager.QUERY); //  showResults(query); } else if (Intent.ACTION_VIEW.equals(intent.getAction())){ // Intent   RecordActivity Intent recordIntent = new Intent(this, RecordActivity.class); recordIntent.setData(intent.getData()); startActivity(recordIntent); finish(); } add = (Button) findViewById(R.id.add); text = (EditText) findViewById(R.id.text); add.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { String data = text.getText().toString(); if (!data.equals("")) { saveTask(data); text.setText(""); } } }); } private void saveTask(String data) { mDbHelper.createRecord(data); } private void showResults(String query) { //  -    Cursor cursor = managedQuery(SuggestionProvider.CONTENT_URI, null, null, new String[] {query}, null); if (cursor == null) { Toast.makeText(this, "There are no results", Toast.LENGTH_SHORT).show(); } else { //  String[] from = new String[] { RecordsDbHelper.KEY_DATA }; int[] to = new int[] { R.id.text1 }; SimpleCursorAdapter records = new SimpleCursorAdapter(this, R.layout.record, cursor, from, to); getListView().setAdapter(records); } } //     (  res/menu/main_menu.xml) public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); return true; } public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.search_record: onSearchRequested(); return true; default: return super.onOptionsItemSelected(item); } } } 


RecordActivity.java file
 package com.example.search; import android.app.Activity; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.TextView; public class RecordActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.record_activity); // URI    Intent     - Uri uri = getIntent().getData(); Cursor cursor = managedQuery(uri, null, null, null, null); if (cursor == null) { finish(); } else { //     cursor.moveToFirst(); TextView record = (TextView) findViewById(R.id.record_header); int rIndex = cursor.getColumnIndexOrThrow(RecordsDbHelper.KEY_DATA); record.setText(cursor.getString(rIndex)); } } //         @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.search_record: onSearchRequested(); return true; default: return false; } } } 


File SuggestionProvider.java
 package com.example.search; import android.app.SearchManager; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; public class SuggestionProvider extends ContentProvider{ private RecordsDbHelper mDbHelper; public static String AUTHORITY = "com.example.search.SuggestionProvider"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/records"); //MIME   getType() public static final String RECORDS_MIME_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.example.search"; public static final String RECORD_MIME_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.example.search"; //   URI private static final int SEARCH_RECORDS = 0; private static final int GET_RECORD = 1; private static final int SEARCH_SUGGEST = 2; private static final UriMatcher sURIMatcher = makeUriMatcher(); @Override public boolean onCreate() { mDbHelper = new RecordsDbHelper(getContext()); return true; } /** *    Search Manager'a. *    ,    URI. *      ,     selectionArgs   . *    . */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // UriMatcher,      .       switch (sURIMatcher.match(uri)) { case SEARCH_SUGGEST: if (selectionArgs == null) { throw new IllegalArgumentException( "selectionArgs must be provided for the Uri: " + uri); } return getSuggestions(selectionArgs[0]); case SEARCH_RECORDS: if (selectionArgs == null) { throw new IllegalArgumentException( "selectionArgs must be provided for the Uri: " + uri); } return search(selectionArgs[0]); case GET_RECORD: return getRecord(uri); default: throw new IllegalArgumentException("Unknown Uri: " + uri); } } private Cursor getSuggestions(String query) { query = query.toLowerCase(); String[] columns = new String[] { BaseColumns._ID, RecordsDbHelper.KEY_DATA, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID}; return mDbHelper.getRecordMatches(query, columns); } private Cursor search(String query) { query = query.toLowerCase(); String[] columns = new String[] { BaseColumns._ID, RecordsDbHelper.KEY_DATA}; return mDbHelper.getRecordMatches(query, columns); } private Cursor getRecord(Uri uri) { String rowId = uri.getLastPathSegment(); String[] columns = new String[] { RecordsDbHelper.KEY_DATA}; return mDbHelper.getRecord(rowId, columns); } /** *   *     URI   */ private static UriMatcher makeUriMatcher() { UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); //   matcher.addURI(AUTHORITY, "records", SEARCH_RECORDS); matcher.addURI(AUTHORITY, "records/#", GET_RECORD); //   matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST); matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST); return matcher; } //  (   ContentProvider) @Override public String getType(Uri uri) { switch (sURIMatcher.match(uri)) { case SEARCH_RECORDS: return RECORDS_MIME_TYPE; case SEARCH_SUGGEST: return SearchManager.SUGGEST_MIME_TYPE; case GET_RECORD: return RECORD_MIME_TYPE; default: throw new IllegalArgumentException("Unknown URL " + uri); } } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException(); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } } 


RecordsDbHelper.java file
 package com.example.search; import java.util.HashMap; import android.app.SearchManager; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.provider.BaseColumns; import android.util.Log; public class RecordsDbHelper { //    -  public static final String KEY_DATA = SearchManager.SUGGEST_COLUMN_TEXT_1; private static final String TAG = "RecordsDbHelper"; private DatabaseHelper mDbHelper; private SQLiteDatabase mDb; private static final String DATABASE_NAME = "datas"; private static final String DATABASE_TABLE = "records"; private static final int DATABASE_VERSION = 2; //   private static final String DATABASE_CREATE = "CREATE VIRTUAL TABLE " + DATABASE_TABLE + " USING fts3 (" + KEY_DATA + ");"; private static final HashMap<String,String> mColumnMap = buildColumnMap(); /** *  ,     rowId * @param rowId id   * @param columns   ;  null,   * @return ,    , null -      */ public Cursor getRecord(String rowId, String[] columns) { String selection = "rowid = ?"; String[] selectionArgs = new String[] {rowId}; return query(selection, selectionArgs, columns); } /** *  ,    ,    * @param query    * @param columns   ;  null,   * @return ,   ,   , null -      */ public Cursor getRecordMatches(String query, String[] columns) { String selection = KEY_DATA + " MATCH ?"; String[] selectionArgs = new String[] {query+"*"}; return query(selection, selectionArgs, columns); } /** *     . *      SQLiteQueryBuilder. *   ,        SUGGEST_COLUMN_INTENT_DATA_ID *        URI. */ private static HashMap<String,String> buildColumnMap() { HashMap<String,String> map = new HashMap<String,String>(); map.put(KEY_DATA, KEY_DATA); map.put(BaseColumns._ID, "rowid AS " + BaseColumns._ID); map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); return map; } /** * * @param selection   * @param selectionArgs ,  "?"     * @param columns    * @return ,    ,     */ private Cursor query(String selection, String[] selectionArgs, String[] columns) { /* SQLiteBuilder       *   ,     - *   . */ SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); builder.setTables(DATABASE_TABLE); builder.setProjectionMap(mColumnMap); Cursor cursor = builder.query(mDbHelper.getReadableDatabase(), columns, selection, selectionArgs, null, null, null); if (cursor == null) { return null; } else if (!cursor.moveToFirst()) { cursor.close(); return null; } return cursor; } /** */  */ private static class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(DATABASE_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS records"); onCreate(db); } } public RecordsDbHelper(Context context) { mDbHelper = new DatabaseHelper(context); } /** *     * @param data ,    * @return id ,  -1,     */ public long createRecord(String data) { mDb = mDbHelper.getWritableDatabase(); ContentValues initialValues = new ContentValues(); initialValues.put(KEY_DATA, data); return mDb.insert(DATABASE_TABLE, null, initialValues); } } 


The whole project can be taken at code.google.com
Thanks for attention!

Source: https://habr.com/ru/post/111961/


All Articles