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);
}
}
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); } }
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);
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, MySuggestionProvider.AUTHORITY, MySuggestionProvider.MODE); suggestions.saveRecentQuery(query, null);
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
        MySuggestionProvider.AUTHORITY, MySuggestionProvider.MODE);
suggestions.saveRecentQuery(query, null);

  以上的流程简单易用,但如果希望独立控制搜索历史读写等功能,需要了解SearchRecentSuggestionsProvider对搜索历史的具体读写方法。

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();
}
/** * 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(); }
/**
 * 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);
}
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); }
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";
}
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"; }
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");
mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
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());
}
...
}
/** * 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()); } ... }
/**
 * 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;
}
/** * 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; }
/**
 * 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";
private static final String sSuggestions = "suggestions";
private static final String sSuggestions = "suggestions";

可见,前面SearchRecentSuggestions里面mSuggestionsUri 中的“/suggestions”  与这里的sSuggestions 相匹配,所以SearchRecentSuggestions的saveRecentQuery() 方法可以成功通过SearchRecentSuggestionsProvider保存搜索历史。另外sSuggestions 的值也是2.1.中的表名,直接插入:
rowID = db.insert(sSuggestions, NULL_COLUMN, values);
rowID = db.insert(sSuggestions, NULL_COLUMN, values);
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;
}
...
}
/** * 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; } ... }
/**
 * 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);
mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST);
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";
public final static String SUGGEST_URI_PATH_QUERY = "search_suggest_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);
}
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); }
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);
}