SearchRecentSuggestionsProvider读写搜索历史代码分析
SearchRecentSuggestionsProvider是用于实现简易搜索历史Provider的父类,官方API Guide在Adding Recent Query Suggestions一节介绍了添加搜索历史的方法,具体来说,除了必要的xml配置,只需实现自己的Provider继承SearchRecentSuggestionsProvider:
public class MySuggestionProvider extends SearchRecentSuggestionsProvider { public final static String AUTHORITY = "com.example.MySuggestionProvider"; public final static int MODE = DATABASE_MODE_QUERIES; public MySuggestionProvider() { setupSuggestions(AUTHORITY, MODE); } }
然后就可以使用SearchRecentSuggestions保存搜索历史:
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, MySuggestionProvider.AUTHORITY, MySuggestionProvider.MODE); suggestions.saveRecentQuery(query, null);
以上的流程简单易用,但如果希望独立控制搜索历史读写等功能,需要了解SearchRecentSuggestionsProvider对搜索历史的具体读写方法。
Contents
1. 向SearchRecentSuggestionsProvider写入搜索历史
首先查看SearchRecentSuggestions的saveRecentQuery() 方法:
/** * Add a query to the recent queries list. Returns immediately, performing the save * in the background. * * @param queryString The string as typed by the user. This string will be displayed as * the suggestion, and if the user clicks on the suggestion, this string will be sent to your * searchable activity (as a new search query). * @param line2 If you have configured your recent suggestions provider with * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can * pass a second line of text here. It will be shown in a smaller font, below the primary * suggestion. When typing, matches in either line of text will be displayed in the list. * If you did not configure two-line mode, or if a given suggestion does not have any * additional text to display, you can pass null here. */ public void saveRecentQuery(final String queryString, final String line2) { if (TextUtils.isEmpty(queryString)) { return; } if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) { throw new IllegalArgumentException(); } new Thread("saveRecentQuery") { @Override public void run() { saveRecentQueryBlocking(queryString, line2); sWritesInProgress.release(); } }.start(); }
saveRecentQuery() 新建线程调用了saveRecentQueryBlocking() ,避免对ContentProvider的读写阻塞UI。saveRecentQueryBlocking() 里就是具体的写入逻辑:
private void saveRecentQueryBlocking(String queryString, String line2) { ContentResolver cr = mContext.getContentResolver(); long now = System.currentTimeMillis(); // Use content resolver (not cursor) to insert/update this query try { ContentValues values = new ContentValues(); values.put(SuggestionColumns.DISPLAY1, queryString); if (mTwoLineDisplay) { values.put(SuggestionColumns.DISPLAY2, line2); } values.put(SuggestionColumns.QUERY, queryString); values.put(SuggestionColumns.DATE, now); cr.insert(mSuggestionsUri, values); } catch (RuntimeException e) { Log.e(LOG_TAG, "saveRecentQuery", e); } // Shorten the list (if it has become too long) truncateHistory(cr, MAX_HISTORY_COUNT); }
saveRecentQueryBlocking() 主要是常规的ContentProvider写入流程,mTwoLineDisplay 为使用双行搜索历史时添加一行额外的内容,几个Column名称定义如下:
private static class SuggestionColumns implements BaseColumns { public static final String DISPLAY1 = "display1"; public static final String DISPLAY2 = "display2"; public static final String QUERY = "query"; public static final String DATE = "date"; }
mSuggestionsUri 内容为:
mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
至此,知道了Uri和各Column的定义,就可以按照saveRecentQueryBlocking()的方法,手动对搜索历史的ContentProvider进行插入操作。
2. SearchRecentSuggestionsProvider
2.1. 搜索历史的存储
可以看到SearchRecentSuggestionsProvider使用SQLite数据库来存储搜索历史:
/** * Builds the database. This version has extra support for using the version field * as a mode flags field, and configures the database columns depending on the mode bits * (features) requested by the extending class. * * @hide */ private static class DatabaseHelper extends SQLiteOpenHelper { ... @Override public void onCreate(SQLiteDatabase db) { StringBuilder builder = new StringBuilder(); builder.append("CREATE TABLE suggestions (" + "_id INTEGER PRIMARY KEY" + ",display1 TEXT UNIQUE ON CONFLICT REPLACE"); if (0 != (mNewVersion & DATABASE_MODE_2LINES)) { builder.append(",display2 TEXT"); } builder.append(",query TEXT" + ",date LONG" + ");"); db.execSQL(builder.toString()); } ... }
这里建立了一张名为“suggestions” 的表,如果设置了DATABASE_MODE_2LINES ,会额外添加“display2” 字段。
2.2. SearchRecentSuggestionsProvider的insert()
接下来关注SearchRecentSuggestionsProvider把上面的搜索历史写入存储的流程。SearchRecentSuggestionsProvider的insert() 方法如下:
/** * This method is provided for use by the ContentResolver. Do not override, or directly * call from your own code. */ @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int length = uri.getPathSegments().size(); if (length < 1) { throw new IllegalArgumentException("Unknown Uri"); } // Note: This table has on-conflict-replace semantics, so insert() may actually replace() long rowID = -1; String base = uri.getPathSegments().get(0); Uri newUri = null; if (base.equals(sSuggestions)) { if (length == 1) { rowID = db.insert(sSuggestions, NULL_COLUMN, values); if (rowID > 0) { newUri = Uri.withAppendedPath(mSuggestionsUri, String.valueOf(rowID)); } } } if (rowID < 0) { throw new IllegalArgumentException("Unknown Uri"); } getContext().getContentResolver().notifyChange(newUri, null); return newUri; }
这里的逻辑非常简单,连UriMatcher都没用,只是比对了sSuggestions ,然后就直接向数据库插入数据,其中sSuggestions 为:
private static final String sSuggestions = "suggestions";
可见,前面SearchRecentSuggestions里面mSuggestionsUri 中的“/suggestions” 与这里的sSuggestions 相匹配,所以SearchRecentSuggestions的saveRecentQuery() 方法可以成功通过SearchRecentSuggestionsProvider保存搜索历史。另外sSuggestions 的值也是2.1.中的表名,直接插入:
rowID = db.insert(sSuggestions, NULL_COLUMN, values);
2.3. SearchRecentSuggestionsProvider的query()
接下来继续看SearchRecentSuggestionsProvider的query()方法,其逻辑根据请求Uri是否能够被UriMatcher匹配而分成两段,先看能够匹配的情况:
/** * This method is provided for use by the ContentResolver. Do not override, or directly * call from your own code. */ // TODO: Confirm no injection attacks here, or rewrite. @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = mOpenHelper.getReadableDatabase(); // special case for actual suggestions (from search manager) if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) { String suggestSelection; String[] myArgs; if (TextUtils.isEmpty(selectionArgs[0])) { suggestSelection = null; myArgs = null; } else { String like = "%" + selectionArgs[0] + "%"; if (mTwoLineDisplay) { myArgs = new String [] { like, like }; } else { myArgs = new String [] { like }; } suggestSelection = mSuggestSuggestionClause; } // Suggestions are always performed with the default sort order Cursor c = db.query(sSuggestions, mSuggestionProjection, suggestSelection, myArgs, null, null, ORDER_BY, null); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } ... }
这里mUriMatcher 只有一种匹配:
mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST);
SearchManager.SUGGEST_URI_PATH_QUERY 为:
public final static String SUGGEST_URI_PATH_QUERY = "search_suggest_query";
在query() 中,如果请求Uri成功匹配URI_MATCH_SUGGEST ,则向sSuggestions 表查询包含selectionArgs[0] 的所有条目。
对于请求Uri不能被mUriMatcher 匹配的情况,会从Uri提取表名,并向该表发起查询,具体流程可以查看源码。
3. 从SearchRecentSuggestionsProvider读取搜索历史
通过上面的分析,可以得到从SearchRecentSuggestionsProvider读取搜索历史的方法:
public Cursor getRecentSuggestions(String query) { Uri.Builder uriBuilder = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SimpleSearchSuggestionsProvider.AUTHORITY); uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); String selection = " ?"; String[] selArgs = new String[] { query }; Uri uri = uriBuilder.build(); return getContentResolver().query(uri, null, selection, selArgs, null); }