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);
}