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对搜索历史的具体读写方法。

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