MTK的T9搜索流程分析

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 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 tokens = new ArrayList(); //token队列
        ArrayList shortSubStrOffset = new ArrayList(); //每个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 tokens,
            final ArrayList 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 搜索联系人分析与实现(支持多音字)。

你可能感兴趣的:(android)