T9搜索流程
packages/apps/Dialer/src/com/mediatek/dialer/util/DialerFeatureOptions.java
/**
* [MTK Dialer Search] whether DialerSearch feature enabled on this device
* @return ture if allowed to enable
*/
public static boolean isDialerSearchEnabled() {
return sIsRunTestCase ?
false : SystemProperties.get("ro.mtk_dialer_search_support").equals("1");
}
ro.mtk_dialer_search_support系统属性定义了是否启用mtk的T9搜索
packages/apps/Dialer/src/com/android/dialer/list/SmartDialSearchFragment.java
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
...
final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter();
/// M: [MTK Dialer Search] @{
if (DialerFeatureOptions.isDialerSearchEnabled()) {
DialerSearchCursorLoader loader = new DialerSearchCursorLoader(super.getContext(),
usesCallableUri());
adapter.configureLoader(loader);
return loader;
}
...
}
启用的情况下,走mtk流程
packages/apps/Dialer/src/com/mediatek/dialer/dialersearch/DialerSearchCursorLoader.java
public Cursor loadInBackground() {
...
cursor = dialerSearchHelper.getSmartDialerSearchResults(mQuery);
...
}
packages/apps/Dialer/src/com/mediatek/dialer/dialersearch/DialerSearchHelper.java
public Cursor getSmartDialerSearchResults(String query) {
...
int displayOrder = sContactsPrefs.getDisplayOrder();
int sortOrder = sContactsPrefs.getSortOrder();
Uri baseUri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "dialer_search");
Uri dialerSearchUri = baseUri.buildUpon().appendPath(query).build();
Uri dialerSearchParamUri = dialerSearchUri.buildUpon().appendQueryParameter(
ContactsContract.Preferences.DISPLAY_ORDER, String.valueOf(displayOrder))
.appendQueryParameter(ContactsContract.Preferences.SORT_ORDER,
String.valueOf(sortOrder)).build();
cursor = resolver.query(dialerSearchParamUri, null, null, null, null);
...
}
packages/providers/ContactsProvider/src/com/android/providers/contacts/ContactsProvider2.java
matcher.addURI(ContactsContract.AUTHORITY, "dialer_search/*", DIALER_SEARCH);
protected Cursor queryLocal(final Uri uri, final String[] projection, String selection,
String[] selectionArgs, String sortOrder, final long directoryId,
final CancellationSignal cancellationSignal) {
...
/// M: Support MTK-DialerSearch @{
case DIALER_SEARCH: {
LogUtils.d(TAG, "MTK-DialerSearch");
return mDialerSearchSupport.query(db, uri);
}
/// M: @}
...
}
packages/providers/ContactsProvider/src/com/mediatek/providers/contacts/DialerSearchSupport.java
public Cursor query(SQLiteDatabase db, Uri uri) {
String filterParam = uri.getLastPathSegment();
...
Cursor cursor = null;
String offsetsSelectedTable = "selected_offsets_temp_table";
int firstNum = -1;
try {
firstNum = Integer.parseInt(String.valueOf(filterParam.charAt(0)));
} catch (NumberFormatException e) {
LogUtils.d(TAG, "MTK-DialerSearch, Cannot Parse as Int:" + filterParam);
}
try {
String currentRawContactsSelect = DialerSearchLookupColumns.RAW_CONTACT_ID
+ " in (SELECT _id FROM " + Tables.RAW_CONTACTS + ")";
String currentCallsSelect = DialerSearchLookupColumns.CALL_LOG_ID
+ " in (SELECT _id FROM " + Tables.CALLS + ")";
String currentSelect = "(" + currentCallsSelect + ") OR (" + currentRawContactsSelect
+ ")"; //_id要在联系人表或通话记录表中
// Only when first char is number and contacts preference is primary,
// can using cached table to enhance dialer search performance.
if (firstNum >= 0 && firstNum <= 9 && mIsCached
&& mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY
&& mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
//传入字符序列首位是数字并且有缓存的时候优先从缓存中获取数据,缓存在10个临时表中
//从ds_offsets_temp_table_0~ds_offsets_temp_table_9
String cachedOffsetsTable = sCachedOffsetsTempTableHead + firstNum;
String offsetsSelect = null;
// CREATE TEMP TABLE called in transaction, Cannot be used next time.
db.execSQL("CREATE TEMP TABLE IF NOT EXISTS " + cachedOffsetsTable
+ " AS " + createCacheOffsetsTableSelect(String.valueOf(firstNum)));
// If length of user input(e.g: 1) is 1, then select the first
// 150 data from cached table directly;
// Otherwise choose the data matched the user input(e.g: 123)
// base on the cached table.
if (filterParam.length() == 1) {
offsetsSelect = "SELECT * FROM " + cachedOffsetsTable + " WHERE "
+ currentSelect + " LIMIT 150 ";
//传入参数只有一位的时候限制获取row数最大为150,例如传入“1”则会有很多号码可以匹配
} else {
offsetsSelect = " SELECT "
+ getOffsetsTempTableColumns(
ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY, filterParam)
+ "," + RawContacts.CONTACT_ID
+ " FROM " + cachedOffsetsTable
+ " JOIN " + "(select "
+ DialerSearchLookupColumns._ID + ","
+ DialerSearchLookupColumns.NORMALIZED_NAME + ","
+ DialerSearchLookupColumns.SEARCH_DATA_OFFSETS + " from "
+ Tables.DIALER_SEARCH + ") AS ds_info " + " ON (_id = ds_id)"
+ " WHERE (" + currentSelect + ") AND "
+ "DIALER_SEARCH_MATCH_FILTER("
+ DialerSearchLookupColumns.NORMALIZED_NAME + ","
+ DialerSearchLookupColumns.SEARCH_DATA_OFFSETS + ","
+ DialerSearchLookupColumns.NAME_TYPE + ",'"
+ filterParam + "'" + ")"
+ " ORDER BY offset COLLATE MATCHTYPE DESC, "
+ DialerSearchLookupColumns.SORT_KEY + " COLLATE PHONEBOOK,"
+ DialerSearchLookupColumns.CALL_LOG_ID + " DESC "
+ " LIMIT 150";
}
db.execSQL("DROP TABLE IF EXISTS " + offsetsSelectedTable + ";");
db.execSQL("CREATE TEMP TABLE " + offsetsSelectedTable + " AS " + offsetsSelect);
//依据缓存表创建selected_offsets_temp_table临时表
} else { //一般情况
String offsetsTable = "ds_offsets_temp_table";
String dsOffsetsTable = " SELECT "
+ getOffsetsTempTableColumns(mDisplayOrder, filterParam)
+ "," + RawContacts.CONTACT_ID
+ " FROM " + Tables.DIALER_SEARCH
+ " LEFT JOIN ("
+ "SELECT _id as raw_id, contact_id,"
+ ((mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE)
? RawContacts.SORT_KEY_ALTERNATIVE + " AS "
+ RawContacts.SORT_KEY_PRIMARY : RawContacts.SORT_KEY_PRIMARY)
+ " FROM " + Tables.RAW_CONTACTS + ") AS raw_contact_info ON "
+ "raw_contact_info.raw_id=" + DialerSearchLookupColumns.RAW_CONTACT_ID
+ " WHERE " + DialerSearchLookupColumns.IS_VISIABLE + " = 1"
+ " AND (" + currentSelect + ")"
+ " AND " + DialerSearchLookupColumns.RAW_CONTACT_ID + " IN ( SELECT "
+ DialerSearchLookupColumns.RAW_CONTACT_ID + " FROM "
+ Tables.DIALER_SEARCH + " WHERE "
+ DialerSearchLookupColumns.NAME_TYPE + "="
+ DialerSearchLookupType.PHONE_EXACT + ")"
+ " AND DIALER_SEARCH_MATCH_FILTER("
+ DialerSearchLookupColumns.NORMALIZED_NAME + ","
+ DialerSearchLookupColumns.SEARCH_DATA_OFFSETS + ","
+ DialerSearchLookupColumns.NAME_TYPE + ",'"
+ filterParam + "'" + ")";
db.execSQL("DROP TABLE IF EXISTS " + offsetsTable + ";");
db.execSQL("CREATE TEMP TABLE " + offsetsTable + " AS " + dsOffsetsTable);
//依据dialer_search表创建ds_offsets_temp_table临时表,这个表中含有所有匹配项目
String offsetsSelect = "SELECT * FROM " + offsetsTable
+ " ORDER BY offset COLLATE MATCHTYPE DESC, "
+ DialerSearchLookupColumns.SORT_KEY + " COLLATE PHONEBOOK,"
+ DialerSearchLookupColumns.CALL_LOG_ID + " DESC "
+ " LIMIT 150 ";
db.execSQL("DROP TABLE IF EXISTS " + offsetsSelectedTable + ";");
db.execSQL("CREATE TEMP TABLE " + offsetsSelectedTable + " AS " + offsetsSelect);
//加入150最大row数的限制,并创建selected_offsets_temp_table临时表
}
String contactsSelect = "contact_id IN (SELECT contact_id FROM " + offsetsSelectedTable
+ ")";
String rawContactsSelect = "raw_contact_id IN (SELECT raw_contact_id FROM "
+ offsetsSelectedTable + ")";
String calllogSelect = Calls._ID + " IN (SELECT call_log_id FROM "
+ offsetsSelectedTable + ")";
createDialerSearchTempTable(db, contactsSelect, rawContactsSelect, calllogSelect);
//创建了dialer_search_temp_table临时表,把data表,calls表和dialer_search表连接在了一起,
//目的是添加通话记录或者联系人的相关信息,此为最终查询用的表1,
String nameOffsets = "SELECT raw_contact_id as name_raw_id, offset as name_offset FROM "
+ offsetsSelectedTable
+ " WHERE name_type=" + DialerSearchLookupType.NAME_EXACT;
//该字符串是生成了所有名字匹配条目的表
String joinedOffsetTable = "SELECT"
+ " ds_id, raw_contact_id, offset as offset_order, name_raw_id, name_type, "
+ " (CASE WHEN " + DialerSearchLookupColumns.NAME_TYPE
+ " = " + DialerSearchLookupType.PHONE_EXACT
+ " THEN offset ELSE NULL END) AS " + DialerSearch.MATCHED_DATA_OFFSET + ", "
+ " (CASE WHEN " + DialerSearchLookupColumns.NAME_TYPE + " = "
+ DialerSearchLookupType.NAME_EXACT
+ " THEN offset ELSE name_offset END) AS " + DialerSearch.MATCHED_NAME_OFFSET
+ " FROM "
+ offsetsSelectedTable
+ " LEFT JOIN (" + nameOffsets + ") AS name_offsets"
+ " ON (" + offsetsSelectedTable
+ ".name_type=" + DialerSearchLookupType.PHONE_EXACT
+ " AND " + offsetsSelectedTable + ".raw_contact_id=name_offsets.name_raw_id)";
//joinedOffsetTable这个字符串创建offset_table表的语句,最大的作用是把匹配号码或者匹配名字合
//并到了一条数据中,本来是一个字段offset,现在分成了matched_data_offset和matched_name_offset,
//此为最终查询用到的表2.
cursor = db.rawQuery("SELECT "
+ getDialerSearchResultColumns(mDisplayOrder, mSortOrder)
+ ", offset_order"
+ ", " + DialerSearchLookupColumns.TIMES_USED
+ " FROM "
+ " (" + joinedOffsetTable + " ) AS offset_table"
+ " JOIN "
+ DIALER_SEARCH_TEMP_TABLE
+ " ON (" + DIALER_SEARCH_TEMP_TABLE + "."
+ DialerSearch._ID + "=offset_table.ds_id)"
+ " OR ( offset_table.name_type="
+ DialerSearchLookupType.NAME_EXACT + " AND "
+ DIALER_SEARCH_TEMP_TABLE + "."
+ DialerSearch.RAW_CONTACT_ID + "=offset_table.raw_contact_id )"
+ " WHERE NOT" + "( offset_table.name_type="
+ DialerSearchLookupType.NAME_EXACT + " AND "
+ "_id IN (SELECT ds_id as _id FROM " + offsetsSelectedTable
+ " WHERE name_type=" + DialerSearchLookupType.PHONE_EXACT + ") )"
+ " ORDER BY " + DialerSearch.MATCHED_NAME_OFFSET + " COLLATE MATCHTYPE DESC,"
+ DialerSearch.MATCHED_DATA_OFFSET + " COLLATE MATCHTYPE DESC,"
+ DialerSearchLookupColumns.TIMES_USED + " DESC,"
+ DialerSearch.SORT_KEY_PRIMARY + " COLLATE PHONEBOOK,"
+ DialerSearch.CALL_LOG_ID + " DESC ", null);
//最终的查询,依据之前的dialer_search_temp_table和offset_table表获取数据。
db.setTransactionSuccessful();
} catch (SQLiteException e) {
LogUtils.w(TAG, "handleDialerSearchQueryEx SQLiteException:" + e);
} finally {
db.endTransaction();
}
if (cursor == null) {
LogUtils.d(TAG, "DialerSearch Cusor is null, Uri: " + uri);
cursor = new MatrixCursor(DialerSearchQuery.COLUMNS);
}
return cursor;
}
其中用到的getOffsetsTempTableColumns如下:
private static String getOffsetsTempTableColumns(int displayOrder, String filterParam) {
StringBuilder builder = new StringBuilder();
builder.append(DialerSearchLookupColumns._ID + " AS ds_id");
builder.append(", ");
builder.append(DialerSearchLookupColumns.RAW_CONTACT_ID);
builder.append(", ");
builder.append(DialerSearchLookupColumns.CALL_LOG_ID);
builder.append(", ");
builder.append(DialerSearchLookupColumns.NAME_TYPE);
builder.append(", ");
builder.append(RawContacts.SORT_KEY_PRIMARY + " AS " + DialerSearchLookupColumns.SORT_KEY);
builder.append(", ");
builder.append(getOffsetColumn(displayOrder, filterParam) + " AS " + "offset");
return builder.toString();
}
private static String getOffsetColumn(int displayOrder, String filterParam) {
String searchParamList = "";
if (displayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
searchParamList = DialerSearchLookupColumns.NORMALIZED_NAME_ALTERNATIVE
+ "," + DialerSearchLookupColumns.SEARCH_DATA_OFFSETS_ALTERNATIVE
+ "," + DialerSearchLookupColumns.NAME_TYPE
+ ",'" + filterParam + "'";
} else {
searchParamList = DialerSearchLookupColumns.NORMALIZED_NAME
+ "," + DialerSearchLookupColumns.SEARCH_DATA_OFFSETS
+ "," + DialerSearchLookupColumns.NAME_TYPE
+ ",'" + filterParam + "'";
}
return "DIALER_SEARCH_MATCH(" + searchParamList + ")";
}
整个查询中做是否匹配判断的DIALER_SEARCH_MATCH_FILTER和获取匹配偏移DIALER_SEARCH_MATCH的两个方法是sqlite内的函数。可惜的是mtk是不给这两个关键方法的代码的,只提供so库,名字是libmtksqlite3_android.so,在vendor目录下可以找到。
T9搜索的实现原理
建表
packages/providers/ContactsProvider/src/com/android/providers/contacts/ContactsDatabaseHelper.java
public void onCreate(SQLiteDatabase db) {
...
DialerSearchSupport.createDialerSearchTable(db);
...
}
建表方法:
public static void createDialerSearchTable(SQLiteDatabase db) {
if (ContactsProviderUtils.isSearchDbSupport()) {
db.execSQL("CREATE TABLE " + Tables.DIALER_SEARCH + " ("
+ DialerSearchLookupColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," //id
+ DialerSearchLookupColumns.DATA_ID //对应data表的id
+ " INTEGER REFERENCES data(_id) NOT NULL,"
+ DialerSearchLookupColumns.RAW_CONTACT_ID //对应rawcontacts表中的id
+ " INTEGER REFERENCES raw_contacts(_id) NOT NULL,"
+ DialerSearchLookupColumns.NAME_TYPE + " INTEGER NOT NULL," //类型有三种,解释见后文
+ DialerSearchLookupColumns.CALL_LOG_ID + " INTEGER DEFAULT 0," //对应通话记录id
+ DialerSearchLookupColumns.NUMBER_COUNT + " INTEGER NOT NULL DEFAULT 0, " //
+ DialerSearchLookupColumns.IS_VISIABLE + " INTEGER NOT NULL DEFAULT 1, " //联系人是否可见
+ DialerSearchLookupColumns.NORMALIZED_NAME + " VARCHAR DEFAULT NULL," //名字
+ DialerSearchLookupColumns.SEARCH_DATA_OFFSETS + " VARCHAR DEFAULT NULL," //匹配偏移
+ DialerSearchLookupColumns.NORMALIZED_NAME_ALTERNATIVE //give name在后的名字,后文解释
+ " VARCHAR DEFAULT NULL,"
+ DialerSearchLookupColumns.SEARCH_DATA_OFFSETS_ALTERNATIVE //give name在后的名字匹配偏移
+ " VARCHAR DEFAULT NULL " + ");");
db.execSQL("CREATE INDEX dialer_data_id_index ON " //后续是建立索引...
+ Tables.DIALER_SEARCH + " ("
+ DialerSearchLookupColumns.DATA_ID + ");");
db.execSQL("CREATE INDEX dialer_search_raw_contact_id_index ON "
+ Tables.DIALER_SEARCH + " ("
+ DialerSearchLookupColumns.RAW_CONTACT_ID + ","
+ DialerSearchLookupColumns.NAME_TYPE + ");");
db.execSQL("CREATE INDEX dialer_search_call_log_id_index ON "
+ Tables.DIALER_SEARCH + " ("
+ DialerSearchLookupColumns.CALL_LOG_ID + ");");
}
}
name_type类型有三种:
public final static class DialerSearchLookupType {
public static final int PHONE_EXACT = 8; //匹配号码
public static final int NO_NAME_CALL_LOG = 8; //匹配通话记录,实质还是匹配号码
public static final int NAME_EXACT = 11; //匹配名字
}
可见实际就是两种,匹配号码或者匹配名字。
名字分为三种,family name,given name和middle name,翻译成中文分别是姓、名和中间名。ContactsProvider中的NAME对应名在前(这个是西方标准的格式),NAME_ALTERNATIVE是对应姓在前,不过对CJK(China、Japan和Korea)名字来说这些无所谓,中文只使用NAME字段,带ALTERNATIVE后缀的一律不用(或者是和NAME相同)。
维护数据
DialerSearchSupport中有8个handle*方法:
public void handleContactsJoinOrSplit(SQLiteDatabase db) //处理联系人聚合
public void handleContactsInserted(SQLiteDatabase db, long rawContactId, long dataId,
String dataValue, String mimeType) //处理联系人插入
public void handleContactsDeleted (SQLiteDatabase db) //处理联系人删除
public void handleContactsUpdated (SQLiteDatabase db, long rawContactId,
long dataId, String dataValue, String dataValueAlt, String mimeType) //处理联系人更新
public void handleCallLogsInserted(SQLiteDatabase db, long callLogId, String callable) //处理通话记录插入
public void handleCallLogsDeleted(SQLiteDatabase db) //处理通话记录删除
public void handleCallLogsUpdated(SQLiteDatabase db, boolean isUpdatedByContactsRemoved) //处理通话记录更新
public void handleDataDeleted (SQLiteDatabase db) //处理data表删除
这8个方法涵盖了所有的情况,而数据中最难处理的就是名字和偏移,看下代码中更新数据的方法:
SQLiteStatement mUpdateNameWhenContactsUpdated;
private void updateNameValueForContactUpdated(SQLiteDatabase db, long rawContactId,
String displayNamePrimary, String displayNameAlternative) {
if (mUpdateNameWhenContactsUpdated == null) {
mUpdateNameWhenContactsUpdated = db.compileStatement("UPDATE "
+ Tables.DIALER_SEARCH + " SET "
+ DialerSearchLookupColumns.NORMALIZED_NAME + "=?,"
+ DialerSearchLookupColumns.SEARCH_DATA_OFFSETS + "=?,"
+ DialerSearchLookupColumns.NORMALIZED_NAME_ALTERNATIVE + "=?,"
+ DialerSearchLookupColumns.SEARCH_DATA_OFFSETS_ALTERNATIVE + "=?"
+ " WHERE " + DialerSearchLookupColumns.RAW_CONTACT_ID + "=? AND "
+ DialerSearchLookupColumns.NAME_TYPE + "="
+ DialerSearchLookupType.NAME_EXACT);
}
StringBuilder dialerSearchNameOffsets = new StringBuilder();
String normalizedDialerSearchName = HanziToPinyin.getInstance()
.getTokensForDialerSearch(displayNamePrimary, dialerSearchNameOffsets);
StringBuilder dialerSearchNameOffsetsAlt = new StringBuilder();
String normalizedDialerSearchNameAlt = HanziToPinyin.getInstance()
.getTokensForDialerSearch(displayNameAlternative, dialerSearchNameOffsetsAlt);
bindStringOrNull(mUpdateNameWhenContactsUpdated, 1, normalizedDialerSearchName);
bindStringOrNull(mUpdateNameWhenContactsUpdated, 2, dialerSearchNameOffsets.toString());
bindStringOrNull(mUpdateNameWhenContactsUpdated, 3, normalizedDialerSearchNameAlt);
bindStringOrNull(mUpdateNameWhenContactsUpdated, 4, dialerSearchNameOffsetsAlt.toString());
mUpdateNameWhenContactsUpdated.bindLong(5, rawContactId);
mUpdateNameWhenContactsUpdated.execute();
}
使用HanziToPinyin的方法赋值给这两个字段
packages/providers/ContactsProvider/src/com/android/providers/contacts/HanziToPinyin.java
public String getTokensForDialerSearch(final String input, StringBuilder offsets) {
...
StringBuilder subStrSet = new StringBuilder();
ArrayList<Token> tokens = new ArrayList<Token>(); //token队列
ArrayList<String> shortSubStrOffset = new ArrayList<String>(); //每个token对应的offset队列
final int inputLength = input.length();
final StringBuilder subString = new StringBuilder();
final StringBuilder subStrOffset = new StringBuilder();
int tokenType = Token.LATIN;
int caseTypePre = DialerSearchToken.FIRSTCASE;
int caseTypeCurr = DialerSearchToken.UPPERCASE;
int mPos = 0;
// Go through the input, create a new token when 将名字变为Token队列
// a. Token type changed Token类型变化
// b. Get the Pinyin of current charater.遇到拼音
// c. current character is space. 遇到空格
// d. Token case changed from lower case to upper case,小写变大写
// e. the first character is always a separated one 分隔符
// f character == '+' || character == '#' || character == '*' || character == ',' ||
// character == ';'
for (int i = 0; i < inputLength; i++) {
final char character = input.charAt(i);
...
} else { //忽略了其它分支,只看处理汉字的分支
Token t = new Token();
tokenize(character, t);
int tokenSize = t.target.length();
//Current type is PINYIN
if (t.type == Token.PINYIN) {
if (subString.length() > 0) {
addToken(subString, tokens, tokenType);
addOffsets(subStrOffset, shortSubStrOffset);
}
tokens.add(t);
for (int j = 0; j < tokenSize; j++) {
subStrOffset.append((char) mPos); //这里可以看出每个token对应的offset就是token开始位置重复tokensize遍
}
addOffsets(subStrOffset, shortSubStrOffset);
tokenType = Token.PINYIN;
caseTypePre = DialerSearchToken.FIRSTCASE;
mPos++;
} else {
mPos++;
}
}
// If the name string is too long, cut it off to meet the storage request of dialer
// search.
if (mPos > 127) {
break;
}
}
if (subString.length() > 0) {
addToken(subString, tokens, tokenType);
addOffsets(subStrOffset, shortSubStrOffset);
}
addSubString(tokens, shortSubStrOffset, subStrSet, offsets); //最后生成要插入到数据库中的字段值
return subStrSet.toString();
}
这里先要看Token到底代表什么:
* A token is defined as a range in the display name delimited by characters that have no
* latin alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese
* characters - '王'). Transliteration from non-latin characters to latin character will be
* done on a best effort basis - e.g. 'Ü' - 'u'.
*
* For example,
* the display name "Phillips Thomas Jr" contains three tokens: "phillips", "thomas", and "jr".
*
* A match must begin at the start of a token.
* For example, typing 846(Tho) would match "Phillips Thomas", but 466(hom) would not.
*
* Also, a match can extend across tokens.
* For example, typing 37337(FredS) would match (Fred S)mith.
英文解释如上,我的理解:Token就是匹配的最小单位,每次匹配是从一个token的开始匹配,不可能从token的中间或者尾部开始匹配,例如T9搜索中只能是从声母开始匹配,不可能从韵母开始匹配。token的分界有空格、分隔符、小写变大写(如java中的方法名字就常用这个方法)等,还有每个汉字的拼音就是一个token。
在addSubString生成最终的值:
private void addSubString(final ArrayList<Token> tokens,
final ArrayList<String> shortSubStrOffset,
StringBuilder subStrSet, StringBuilder offsets) {
//参数subStrSet和offsets就是返回值
...
int size = tokens.size();
int len = 0;
StringBuilder mShortSubStr = new StringBuilder(); //临时变量
StringBuilder mShortSubStrOffsets = new StringBuilder(); //临时变量
StringBuilder mShortSubStrSet = new StringBuilder(); //结果变量
StringBuilder mShortSubStrOffsetsSet = new StringBuilder(); //结果变量
for (int i = size - 1; i >= 0; i--) { //倒序添加
String mTempStr = tokens.get(i).target; //每个token对应的字符串,中文对应就是拼音
len = mTempStr.length(); //字符串长度
String mTempOffset = shortSubStrOffset.get(i);
if (mShortSubStr.length() > 0) { //初始化临时变量
mShortSubStr.setLength(0);
mShortSubStrOffsets.setLength(0);
}
mShortSubStr.insert(0, mTempStr); //填充字符串
mShortSubStr.insert(0, (char) len);//在字符串之前填充长度
mShortSubStrOffsets.insert(0, mTempOffset); //填充偏移字符串
mShortSubStrOffsets.insert(0, (char) len); //在偏移字符串之前填充长度
mShortSubStrSet.insert(0, mShortSubStr); //添加到结果变量之前
mShortSubStrOffsetsSet.insert(0, mShortSubStrOffsets);//添加到结果变量之前
}
subStrSet.append(mShortSubStrSet); //返回结果
offsets.append(mShortSubStrOffsetsSet); //返回结果
tokens.clear();
shortSubStrOffset.clear();
}
最后生成的两个值有相同的结构,例如名字“李光宇”,拼音是liguangyu,normalized_name字段的值是2li5guang2yu(2+li+5+guang+2+yu),search_data_offsets字段的值是200511111222(2+00+5+11111+2+22)。从search_data_offsets就能直接得出对应匹配的字符串的token范围是多少,这个目的就是为了匹配字符的高亮显示。如果名字是号码,那么normalized_name就是号码,不做任何处理,search_data_offsets为空。
sqlite方法原理推想
DIALER_SEARCH_MATCH_FILTER和DIALER_SEARCH_MATCH实际是一个实现,只不过返回值不同,一个是返回boolean(是用于where后的判断语句),一个是返回匹配偏移(生成一个字段,这个字段只包含两个数字,一个是开始位置,另外一个是结束位置,这个在Dialer中的高亮代码中可以得到验证)。先看下函数的参数:
searchParamList = DialerSearchLookupColumns.NORMALIZED_NAME //处理后的名字字符串
+ "," + DialerSearchLookupColumns.SEARCH_DATA_OFFSETS //处理后的偏移字符串
+ "," + DialerSearchLookupColumns.NAME_TYPE //名字类型
+ ",'" + filterParam + "'"; //用户输入的字符串
有四个参数,见注释,前两个上一小节已经详细解释了;名字类型8为匹配号码,11为匹配名字;filterParam就是用户输入的字符串,在拨号盘输入的一般就是一串数字。匹配号码的实质就是判断用户输入的字符串是否为normalized_name字段的子串,例如著名的kmp算法什么的,java的话直接用String.contains方法就可以了;匹配名字的话就比较复杂了,依据filterParam生成n多可能的字母序列,然后和normalized_name比较,网上早有讲解T9的文章,例如
android T9 搜索联系人分析与实现(支持多音字)。