Android拨号搜索机制源码分析(原)

 

Android拨号搜索机制源码分析(原)

分类: Telephony Android Dialer 搜索   396人阅读   评论(2)   收藏   举报

目录(?)[+]

          本文主要介绍Android4.4拨号界面的联系人搜索机制

        拨号搜索机制分为两个部分,引导搜索和搜索。其中引导搜索是指,从用户输入到开始搜索之间的流程,而搜索部分是指,从数据库搜索字符串的过程。


一、引导搜索部分


        默认的拨号界面的布局从上到下主要分为3个部分:显示列表、数字编辑框、拨号键盘。他们的作用是:用户直接在拨号键盘上输入数字,然后数字编辑框显示所输入的数字,同时在显示列表中体现此时的搜索结果。如图所示:

        从流程上来讲,需要拨号键盘将用户点击转换为按键事件并传递给编辑框,然后由编辑框传递给搜索框,再由搜索框传递给列表Fragment,然后在列表所加载的Adapter中体现当前的搜索结果。
        接下来我们详细分析这个过程。


1.1、从拨号键盘到编辑框


        用户在拨号键盘上的点击的数字按钮,都会在编辑框中体现出来,我们先来追踪这一过程。
        每个拨号键盘按钮都是DialpadKeyButton类型的View,他们继承自FrameLayout,当遇到点击事件时,就会触发setPressed()方法:

[java]   view plain copy
  1. @setPressed  
  2. public void setPressed(boolean pressed) {  
  3.     super.setPressed(pressed);  
  4.     if (mOnPressedListener != null) {  
  5.         mOnPressedListener.onPressed(this, pressed);  
  6.     }  
  7. }  
        然后将事件转换为onPressed()发送给mOnPressedListener,这个mOnPressedListener就是DialpadFragment,然后在DialpadFragment的onPressed()中,将当前的点击事件转换为标准的按键输入:
[java]   view plain copy
  1. @DialpadFragment.java  
  2. public void onPressed(View view, boolean pressed) {  
  3.     if (pressed) {  
  4.         switch (view.getId()) {  
  5.             case R.id.one: {  
  6.                //将当前点击事件转换为键盘事件  
  7.                keyPressed(KeyEvent.KEYCODE_1);  
  8.                break;  
  9.             }  
  10.             case R.id.two: {  
  11.                keyPressed(KeyEvent.KEYCODE_2);  
  12.                break;  
  13.             }  
  14.             default: {  
  15.                  Log.wtf(TAG, "Unexpected onTouch(ACTION_DOWN) event from: " + view);  
  16.                  break;  
  17.             }  
  18.         }  
  19.     } else {  
  20.     }  
  21. }  
        这里看到,当我们在拨号键盘上点击某个View时,将会通过onPressed()转换为标准的键盘消息,比如,在R.id.one控件上的点击,将会转换为KeyEvent.KEYCODE_1消息。然后在keyPressed()中将会把当前输入传递给编辑框:
[java]   view plain copy
  1. private void keyPressed(int keyCode) {  
  2.     mHaptic.vibrate();  
  3.     KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);  
  4.     //传递给编辑框控件  
  5.     mDigits.onKeyDown(keyCode, event);  
  6.   
  7.     // If the cursor is at the end of the text we hide it.  
  8.     final int length = mDigits.length();  
  9.     if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) {  
  10.         mDigits.setCursorVisible(false);  
  11.     }  
  12. }  

        上面的mDigits就是显示当前输入内容的编辑框控件。


1.2、从编辑框到搜索框


        搜索框的作用主要是,当拨号键盘隐藏时,显示当前的输入内容。而编辑框需要将当前的输入传递给搜索框。
        当编辑框检测到KeyDown事件后,就会将当前键盘的输入放入编辑框中,并触发TextWatcher的相关方法:
[java]   view plain copy
  1. @DialpadFragment.java  
  2. public void afterTextChanged(Editable input) {  
  3.     if (!mDigitsFilledByIntent && SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), mDigits)) {  
  4.         mDigits.getText().clear();  
  5.     }  
  6.   
  7.     if (isDigitsEmpty()) {  
  8.         mDigitsFilledByIntent = false;  
  9.         mDigits.setCursorVisible(false);  
  10.     }  
  11.   
  12.     if (mDialpadQueryListener != null) {  
  13.         //传递给mDialpadQueryListener  
  14.         mDialpadQueryListener.onDialpadQueryChanged(mDigits.getText().toString());  
  15.     }  
  16.     updateDialAndDeleteButtonEnabledState();  
  17. }  
        在这里,又将当前已经输入的文本传递给mDialpadQueryListener,它是在DialtactsActivity中实现的:
[java]   view plain copy
  1. @DialtactsActivity.java  
  2. public void onDialpadQueryChanged(String query) {  
  3.     final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);  
  4.     if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {  
  5.         if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {  
  6.             return;  
  7.         }  
  8.         //传递给搜索框  
  9.         mSearchView.setText(normalizedQuery);  
  10.     }  
  11. }  

        我们看到,在onDialpadQueryChanged()中将当前编辑框的内容通过setText()方法传递给了mSearchView,也就是最上方的搜索框。


1.3、从搜索框到搜索结果列表Fragment


        搜索框下面的列表用于在搜索时显示搜索结果,他所处的位置是复用的,可以选择性的加载三种Fragment, 当处于非搜索状态时,加载PhoneFavoriteFragment,这是进入拨号界面的默认加载项,将会显示瓦片式收藏界面,当在搜索模式时,将会加载SmartDialSearchFragment或者RegularSearchFragment用于显示当时的搜索结果。对于最常用的用户在拨号键盘输入内容触发的搜索,将会加载SmartDialSearchFragment。此时搜索框需要将要搜索的文本传递给SmartDialSearchFragment。
        在搜索时,由于搜索框注册了文本监听器,所以将会触发TextWatcher,此时需要暂存当前要搜索的文本,并进入搜索模式,然后再将搜索内容交给SmartDialSearchFragment。
[java]   view plain copy
  1. public void onTextChanged(CharSequence s, int start, int before, int count) {  
  2.     final String newText = s.toString();  
  3.     if (newText.equals(mSearchQuery)) {  
  4.         return;  
  5.     }  
  6.     //存储当前的搜索文本  
  7.     mSearchQuery = newText;  
  8.     final boolean dialpadSearch = isDialpadShowing();  
  9.   
  10.     // Show search result with non-empty text. Show a bare list otherwise.  
  11.     if (TextUtils.isEmpty(newText) && getInSearchUi()) {  
  12.         //退出搜索模式  
  13.         exitSearchUi();  
  14.         mSearchViewCloseButton.setVisibility(View.GONE);  
  15.         mVoiceSearchButton.setVisibility(View.VISIBLE);  
  16.         return;  
  17.     } else if (!TextUtils.isEmpty(newText)) {  
  18.         final boolean sameSearchMode = (dialpadSearch && mInDialpadSearch) || (!dialpadSearch && mInRegularSearch);  
  19.         if (!sameSearchMode) {  
  20.             //进入搜素模式  
  21.             enterSearchUi(dialpadSearch, newText);  
  22.         }  
  23.   
  24.         if (dialpadSearch && mSmartDialSearchFragment != null) {  
  25.             //将搜索文本转交给mSmartDialSearchFragment  
  26.             mSmartDialSearchFragment.setQueryString(newText, false);  
  27.         } else if (mRegularSearchFragment != null) {  
  28.             mRegularSearchFragment.setQueryString(newText, false);  
  29.         }  
  30.         mSearchViewCloseButton.setVisibility(View.VISIBLE);  
  31.         mVoiceSearchButton.setVisibility(View.GONE);  
  32.         return;  
  33.     }  
  34. }  
        在这里完成了三个重要任务:
        1、将当前要搜索的文本存储在mSearchQuery中,在当前界面被恢复时使用;
        2、进入/退出搜素界面,也就是配置当前需要加载的Fragment;

        3、将要搜素的文本传递给搜索列表的Fragment,也就是mSmartDialSearchFragment;


1.4、从搜索列表的Fragment到Adapter


        先来看一下SmartDialSearchFragment的继承关系:
        SmartDialSearchFragment
            ----SearchFragment
                ----PhoneNumberPickerFragment
                    ----ContactEntryListFragment<ContactEntryListAdapter>
                        ----Fragment

        如下图所示:

        SmartDialSearchFragment拿到搜索的文本后,需要传递给自己的Adapter才能完成搜索任务,我们现在来分析这个交接的过程。
        从上面1.3节中我们看到,SmartDialSearchFragment通过setQueryString()拿到了要搜索的字串,我们来查看这个方法,他是在SmartDialSearchFragment的父类ContactEntryListFragment中被实现的:

[java]   view plain copy
  1. @ContactEntryListFragment.java  
  2. public void setQueryString(String queryString, boolean delaySelection) {  
  3.     if (TextUtils.isEmpty(queryString)) queryString = null;  
  4.   
  5.     if (!TextUtils.equals(mQueryString, queryString)) {  
  6.         mQueryString = queryString;  
  7.         setSearchMode(!TextUtils.isEmpty(mQueryString));  
  8.   
  9.         if (mAdapter != null) {  
  10.             //传递给Adapter  
  11.             mAdapter.setQueryString(queryString);  
  12.             //触发Adapter重新搜索  
  13.             reloadData();  
  14.         }  
  15.     }  
  16. }  
        在这里,Fragment将要搜索的文本通过setQueryString()的方法传递给当前的Adapter,然后通过reloadData()方法触发Adapter的搜索机制。那么这里的Adapter具体是指哪个呢?
        我们在SmartDialSearchFragment中找到了该Adapter的创建之处,他就是SmartDialNumberListAdapter:
[java]   view plain copy
  1. @SmartDialSearchFragment.java  
  2. protected ContactEntryListAdapter createListAdapter() {  
  3.     SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity());  
  4.     adapter.setUseCallableUri(super.usesCallableUri());  
  5.     adapter.setQuickContactEnabled(true);  
  6.     adapter.setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false);  
  7.     return adapter;  
  8. }  
        该Adapter的继承关系如下:
        SmartDialNumberListAdapter
            ----DialerPhoneNumberListAdapter
                ----PhoneNumberListAdapter
                    ----ContactEntryListAdapter
                        ----IndexerListAdapter
                            ----PinnedHeaderListAdapter
                                ----CompositeCursorAdapter

        如下图所示:

        接下来我们分析如何通过Fragment的reloadData()触发Adapter的搜索。


1.5、Adapter触发搜索机制


        刚才介绍到,SmartDialSearchFragment在setQueryString()时,通过reloadData()触发了Adapter的搜索,我们来看一下这个流程:

[java]   view plain copy
  1. @ContactEntryListFragment.java  
  2. protected void reloadData() {  
  3.     removePendingDirectorySearchRequests();  
  4.     mAdapter.onDataReload();  
  5.     mLoadPriorityDirectoriesOnly = true;  
  6.     mForceLoad = true;  
  7.     //触发新的Adapter  
  8.     startLoading();  
  9. }  
  10. protected void startLoading() {  
  11.     if (mAdapter == null) {  
  12.         return;  
  13.     }  
  14.   
  15.     //配置Adapter要搜索的文本  
  16.     configureAdapter();  
  17.     int partitionCount = mAdapter.getPartitionCount();  
  18.     for (int i = 0; i < partitionCount; i++) {  
  19.         Partition partition = mAdapter.getPartition(i);  
  20.         if (partition instanceof DirectoryPartition) {  
  21.             DirectoryPartition directoryPartition = (DirectoryPartition)partition;  
  22.             if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {  
  23.                 if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {  
  24.                     startLoadingDirectoryPartition(i);  
  25.                 }  
  26.             }  
  27.         } else {  
  28.             //通过LoaderManager进行异步查询  
  29.             getLoaderManager().initLoader(i, nullthis);  
  30.         }  
  31.     }  
  32.   
  33.     mLoadPriorityDirectoriesOnly = false;  
  34. }  
        在startLoading()时,通过configureAdapter()对当前的Adapter配置了要搜索的文本、排序方法以及显示主题等信息,然后就 通过LoaderManager进行异步查询
        我们来看Loader的流程。
        经过initLoader()的操作之后,就会触发SmartDialSearchFragment中的onCreateLoader()方法:
[java]   view plain copy
  1. @SmartDialSearchFragment.java  
  2. public Loader<Cursor> onCreateLoader(int id, Bundle args) {  
  3.     if (id == getDirectoryLoaderId()) {  
  4.         return super.onCreateLoader(id, args);  
  5.     } else {  
  6.         //创建当前的CursorLoader,也就是SmartDialCursorLoader  
  7.         final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter();  
  8.         SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext());  
  9.         adapter.configureLoader(loader);  
  10.         return loader;  
  11.     }  
  12. }  
        这里创建了SmartDialCursorLoader作为当前的CursorLoader。然后通过adapter的configureLoader()方法将该Loader传递给SmartDialNumberListAdapter,接下来就会在SmartDialCursorLoader中完成异步查询,并将查询结果传递给ContactEntryListFragment的onLoadFinished()方法:
[java]   view plain copy
  1. @ContactEntryListFragment.java  
  2. public void onLoadFinished(Loader<Cursor> loader, Cursor data) {  
  3.     if (!mEnabled) {  
  4.         return;  
  5.     }  
  6.   
  7.     int loaderId = loader.getId();  
  8.     if (loaderId == DIRECTORY_LOADER_ID) {  
  9.         mDirectoryListStatus = STATUS_LOADED;  
  10.         mAdapter.changeDirectories(data);  
  11.         startLoading();  
  12.     } else {  
  13.         //更新Adapter的Cursor  
  14.         onPartitionLoaded(loaderId, data);  
  15.         if (isSearchMode()) {  
  16.             int directorySearchMode = getDirectorySearchMode();  
  17.             if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {  
  18.                 if (mDirectoryListStatus == STATUS_NOT_LOADED) {  
  19.                     mDirectoryListStatus = STATUS_LOADING;  
  20.                     getLoaderManager().initLoader(DIRECTORY_LOADER_ID, nullthis);  
  21.                 } else {  
  22.                     startLoading();  
  23.                 }  
  24.             }  
  25.         } else {  
  26.             mDirectoryListStatus = STATUS_NOT_LOADED;  
  27.             getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);  
  28.         }  
  29.     }  
  30. }  
  31. protected void onPartitionLoaded(int partitionIndex, Cursor data) {  
  32.     if (partitionIndex >= mAdapter.getPartitionCount()) {  
  33.         return;  
  34.     }  
  35.   
  36.     //更新当前的Adapter  
  37.     mAdapter.changeCursor(partitionIndex, data);  
  38.     setProfileHeader();  
  39.     showCount(partitionIndex, data);  
  40.   
  41.     if (!isLoading()) {  
  42.         completeRestoreInstanceState();  
  43.     }  
  44. }  

        在onLoadFinished()中,通过onPartitionLoaded()对当前的Adapter所使用的Cursor进行更新,从而刷新列表。


二、字符搜索过程


        前面我们看到, 搜索时使用了LoaderManager的异步查询机制,而且CursorLoader使用的是SmartDialCursorLoader,那么具体的搜索过程就会在SmartDialCursorLoader中体现出来。根据AsyncTaskLoader的机制,SmartDialCursorLoader需要在自己的loadInBackground()中查询Cursor结果,现在来看这个过程:
[java]   view plain copy
  1. @SmartDialCursorLoader.java  
  2. public Cursor loadInBackground() {  
  3.     //从dialerDatabaseHelper中查找匹配结果  
  4.     final DialerDatabaseHelper dialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper( mContext);  
  5.     final ArrayList<ContactNumber> allMatches = dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher);  
  6.   
  7.     //构建Cursor给Adapter使用  
  8.     final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY);  
  9.     Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length];  
  10.     for (ContactNumber contact : allMatches) {  
  11.         row[PhoneQuery.PHONE_ID] = contact.dataId;  
  12.         row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber;  
  13.         row[PhoneQuery.CONTACT_ID] = contact.id;  
  14.         row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey;  
  15.         row[PhoneQuery.PHOTO_ID] = contact.photoId;  
  16.         row[PhoneQuery.DISPLAY_NAME] = contact.displayName;  
  17.         cursor.addRow(row);  
  18.     }  
  19.     return cursor;  
  20. }  

        原来,SmartDialCursorLoader是利用DialerDatabaseHelper进行的查找,他是SQLiteOpenHelper的子类,每次拨号盘进程的创建都会根据当前的通讯录内容创建表单,用于联系人搜索。下面我们从该数据库的创建、查询、更新三个方面来分析其内部原理。


2.1、数据库的创建及搜索机制


        该数据库是单例模式,只存在一个实例对象,第一次被调用时就会完成初始化任务。可以通过DatabaseHelperManager的getDatabaseHelper()方法来得到DialerDatabaseHelper的实例对象:
[java]   view plain copy
  1. @DatabaseHelperManager.java  
  2. public static DialerDatabaseHelper getDatabaseHelper(Context context) {  
  3.     return DialerDatabaseHelper.getInstance(context);  
  4. }  
        然后就会在DialerDatabaseHelper中判断,是否已经有实例对象,没有的话就创建。
[java]   view plain copy
  1. @DialerDatabaseHelper.java  
  2. public static synchronized DialerDatabaseHelper getInstance(Context context) {  
  3.     if (sSingleton == null) {  
  4.         sSingleton = new DialerDatabaseHelper(context.getApplicationContext(), DATABASE_NAME);  
  5.     }  
  6.     return sSingleton;  
  7. }  
        接下来我们看该数据库的创建过程,主要在onCreate()方法中体现:
[java]   view plain copy
  1. public void onCreate(SQLiteDatabase db) {  
  2.     setupTables(db);  
  3. }  
  4. private void setupTables(SQLiteDatabase db) {  
  5.     //删除旧表单  
  6.     dropTables(db);  
  7.     //创建新表“smartdial_table”  
  8.     db.execSQL("CREATE TABLE " + Tables.SMARTDIAL_TABLE + " (" +  
  9.             SmartDialDbColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +  
  10.             SmartDialDbColumns.DATA_ID + " INTEGER, " +  
  11.             SmartDialDbColumns.NUMBER + " TEXT," +  
  12.             SmartDialDbColumns.CONTACT_ID + " INTEGER," +  
  13.             SmartDialDbColumns.LOOKUP_KEY + " TEXT," +  
  14.             SmartDialDbColumns.DISPLAY_NAME_PRIMARY + " TEXT, " +  
  15.             SmartDialDbColumns.PHOTO_ID + " INTEGER, " +  
  16.             SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " LONG, " +  
  17.             SmartDialDbColumns.LAST_TIME_USED + " LONG, " +  
  18.             SmartDialDbColumns.TIMES_USED + " INTEGER, " +  
  19.             SmartDialDbColumns.STARRED + " INTEGER, " +  
  20.             SmartDialDbColumns.IS_SUPER_PRIMARY + " INTEGER, " +  
  21.             SmartDialDbColumns.IN_VISIBLE_GROUP + " INTEGER, " +  
  22.             SmartDialDbColumns.IS_PRIMARY + " INTEGER" +  
  23.             ");");  
  24.   
  25.     //创建新表“prefix_table”  
  26.     db.execSQL("CREATE TABLE " + Tables.PREFIX_TABLE + " (" +  
  27.             PrefixColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +  
  28.             PrefixColumns.PREFIX + " TEXT COLLATE NOCASE, " +  
  29.             PrefixColumns.CONTACT_ID + " INTEGER" +  
  30.             ");");  
  31.   
  32.     //创建新表“properties”  
  33.     db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " (" +  
  34.             PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, " +  
  35.             PropertiesColumns.PROPERTY_VALUE + " TEXT " +  
  36.             ");");  
  37.   
  38.     //设置属性  
  39.     setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));  
  40.     //更新时间  
  41.     resetSmartDialLastUpdatedTime();  
  42. }  
        在上面的初始化过程中,最主要的任务就是创建了三张表单:smartdial_table、prefix_table、properties。其中properties表用于存储当前数据库的版本号,与搜索任务无关,我们主要分析其他两个表。
prefix_table表单
        该表是对所有联系人的电话号码以及英文姓名进行解析,形成搜索的索引表单。

        如果是姓名,则将姓名的英文单词解析为相应的数字,比如:
            英文名  dushaofeng 将会被解析为:3(d)8(u)7(s)4(h)2(a)6(o)3(f)3(e)6(n)4(g)
        如果是号码,除了要保存号码本身外,对于包含国家码的号码,还要保存除去国家码以外的有效号码。
          经过上面的解析,每个联系人至少包含两条记录,即姓名对应的数字以及号码所拆分出来的数字。当搜索时,就会利用用户所输入的内容在该表中进行匹配,匹配成功的记录,将根据该条记录的contact_id在smartdial_table表中查找该联系人的详细信息。
smartdial_table表单
         每个联系人都对应该表中一条记录,每条记录都包含了该联系人的phone_number、contact_id、display_name、photo_id、starred、last_smartdial_update_time等信息,在搜索时,会利用用户输入区prefix_table中进行匹配,对于匹配成功的记录,根据prefix_table表中对应的contact_id再来smartdial_table中查找该联系人的详细信息,也就是头像、姓名、收藏状态等,并把这些信息构建为Cursor类数据,返回给查询者。

        这就是该数据库搜索的原理。


2.2、数据库的更新


        前面说到了数据库的结构和搜索原理,下面来介绍数据库表单的初始化和更新过程。
        每次拨号界面经过onResume(),都会触发数据库的更新:
[java]   view plain copy
  1. @DialtactsActivity.java  
  2. protected void onResume() {  
  3.     super.onResume();  
  4.     //进入数据库更新入口  
  5.     mDialerDatabaseHelper.startSmartDialUpdateThread();  
  6. }  
        然后会在数据库中开启异步线程更新数据:
[java]   view plain copy
  1. @DialerDatabaseHelper.java  
  2. public void startSmartDialUpdateThread() {  
  3.     new SmartDialUpdateAsyncTask().execute();  
  4. }  
  5. private class SmartDialUpdateAsyncTask extends AsyncTask {  
  6.     @Override  
  7.     protected Object doInBackground(Object[] objects) {  
  8.         updateSmartDialDatabase();  
  9.         return null;  
  10.     }  
  11.   
  12.     @Override  
  13.     protected void onCancelled() {  
  14.         super.onCancelled();  
  15.     }  
  16.   
  17.     @Override  
  18.     protected void onPostExecute(Object o) {  
  19.         super.onPostExecute(o);  
  20.     }  
  21. }  
        在线程中执行updateSmartDialDatabase()来更新数据:
[java]   view plain copy
  1. public void updateSmartDialDatabase() {  
  2.     final SQLiteDatabase db = getWritableDatabase();  
  3.   
  4.     synchronized(mLock) {  
  5.         final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null;  
  6.   
  7.         //获取上一次更新的时间  
  8.         final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences( DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);  
  9.         final String lastUpdateMillis = String.valueOf(databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0));  
  10.   
  11.         //得到当前的通讯录数据  
  12.         final Cursor updatedContactCursor = mContext.getContentResolver().query(PhoneQuery.URI,  
  13.                 PhoneQuery.PROJECTION, PhoneQuery.SELECT_UPDATED_CLAUSE,  
  14.                 new String[]{lastUpdateMillis}, null);  
  15.   
  16.         //获取当前的时间  
  17.         final Long currentMillis = System.currentTimeMillis();  
  18.         if (updatedContactCursor == null) {  
  19.             return;  
  20.         }  
  21.         sInUpdate.getAndSet(true);  
  22.   
  23.         //删掉已经删除的和无效的联系人记录  
  24.         removeDeletedContacts(db, lastUpdateMillis);  
  25.         removePotentiallyCorruptedContacts(db, lastUpdateMillis);  
  26.   
  27.   
  28.         try {  
  29.             if (!lastUpdateMillis.equals("0")) {  
  30.                 removeUpdatedContacts(db, updatedContactCursor);  
  31.             }  
  32.   
  33.             //向smartdial_table表中插入当前所有有效的联系人数据,以及向prefix_table表中添加联系人号码添加为搜索索引  
  34.             insertUpdatedContactsAndNumberPrefix(db, updatedContactCursor, currentMillis);  
  35.         } finally {  
  36.             updatedContactCursor.close();  
  37.         }  
  38.   
  39.         //从smartdial_table表中读取当前联系人的姓名和号码  
  40.         final Cursor nameCursor = db.rawQuery(  
  41.                 "SELECT DISTINCT " +  
  42.                 SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + SmartDialDbColumns.CONTACT_ID +  
  43.                 " FROM " + Tables.SMARTDIAL_TABLE +  
  44.                 " WHERE " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME +  
  45.                 " = " + Long.toString(currentMillis),  
  46.                 new String[] {});  
  47.   
  48.         if (nameCursor != null) {  
  49.             try {  
  50.                 //根据联系人姓名生成相应的数字索引  
  51.                 insertNamePrefixes(db, nameCursor);  
  52.             } finally {  
  53.                 nameCursor.close();  
  54.             }  
  55.         }  
  56.   
  57.         //创建数据库相应的列  
  58.         /** Creates index on contact_id for fast JOIN operation. */  
  59.         db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON " + Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns.CONTACT_ID  + ");");  
  60.         /** Creates index on last_smartdial_update_time for fast SELECT operation. */  
  61.         db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON " +  
  62.                 Tables.SMARTDIAL_TABLE + " (" +  
  63.                 SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ");");  
  64.         /** Creates index on sorting fields for fast sort operation. */  
  65.         db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_sort_index ON " +  
  66.                 Tables.SMARTDIAL_TABLE + " (" +  
  67.                 SmartDialDbColumns.STARRED + ", " +  
  68.                 SmartDialDbColumns.IS_SUPER_PRIMARY + ", " +  
  69.                 SmartDialDbColumns.LAST_TIME_USED + ", " +  
  70.                 SmartDialDbColumns.TIMES_USED + ", " +  
  71.                 SmartDialDbColumns.IN_VISIBLE_GROUP +  ", " +  
  72.                 SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +  
  73.                 SmartDialDbColumns.CONTACT_ID + ", " +  
  74.                 SmartDialDbColumns.IS_PRIMARY +  
  75.                 ");");  
  76.         /** Creates index on prefix for fast SELECT operation. */  
  77.         db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_index ON " +  
  78.                 Tables.PREFIX_TABLE + " (" + PrefixColumns.PREFIX + ");");  
  79.         /** Creates index on contact_id for fast JOIN operation. */  
  80.         db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON " +  
  81.                 Tables.PREFIX_TABLE + " (" + PrefixColumns.CONTACT_ID + ");");  
  82.         /** Updates the database index statistics.*/  
  83.         db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE);  
  84.         db.execSQL("ANALYZE " + Tables.PREFIX_TABLE);  
  85.         db.execSQL("ANALYZE smartdial_contact_id_index");  
  86.         db.execSQL("ANALYZE smartdial_last_update_index");  
  87.         db.execSQL("ANALYZE nameprefix_index");  
  88.         db.execSQL("ANALYZE nameprefix_contact_id_index");  
  89.         sInUpdate.getAndSet(false);  
  90.   
  91.         final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();  
  92.         editor.putLong(LAST_UPDATED_MILLIS, currentMillis);  
  93.         editor.commit();  
  94.     }  
  95. }  
        在上面这个更新数据库的过程中,一次完成如下任务:
        1、从联系人数据库中读取从上次更新到现在时间段内更新的联系人记录;
        2、在removeDeletedContacts()中,删除已经被删除的联系人记录;
        3、在removePotentiallyCorruptedContacts()中,删除已经损坏的联系人记录;
        4、在removeUpdatedContacts()中,删除需要更新的联系人记录;
        5、在insertUpdatedContactsAndNumberPrefix()中,将更新的联系人数据插入到smartdial_table中,并把联系人号码插入到prefix_table中;
        6、在insertNamePrefixes()中,将本次需要更新的联系人的姓名转换为数字存入prefix_table中;
        7、为数据库建立索引;
          需要注意两点:
        1、从联系人数据库查询时,并不是查询所有联系人,而是查询从上次查询到现在之间所更新的联系人数据;

        2、解析联系人姓名为号码时,只对英文姓名有效,这就决定了,无法通过拼音搜索联系人;


2.3、数据库的查询


        在2.1中已经介绍过搜索机制,本节就结合具体代码来看搜索的详细过程。
        在1.5节中我们分析到,在SmartDialCursorLoader中通过DialerDatabaseHelper的getLooseMatches()方法进行搜索任务,现在来看具体的操作:
[java]   view plain copy
  1. @DialerDatabaseHelper.java  
  2. public ArrayList<ContactNumber>  getLooseMatches(String query, SmartDialNameMatcher nameMatcher) {  
  3.     final boolean inUpdate = sInUpdate.get();  
  4.     if (inUpdate) {  
  5.         return Lists.newArrayList();  
  6.     }  
  7.     final SQLiteDatabase db = getReadableDatabase();  
  8.   
  9.     //准备搜索匹配语句  
  10.     final String looseQuery = query + "%";  
  11.     final ArrayList<ContactNumber> result = Lists.newArrayList();  
  12.     final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null;  
  13.     final String currentTimeStamp = Long.toString(System.currentTimeMillis());  
  14.   
  15.     //搜索语句,从prefix_table中搜索匹配项,并从smartdial_table中读取匹配项的详细信息  
  16.     final Cursor cursor = db.rawQuery("SELECT " +  
  17.             SmartDialDbColumns.DATA_ID + ", " +  
  18.             SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +  
  19.             SmartDialDbColumns.PHOTO_ID + ", " +  
  20.             SmartDialDbColumns.NUMBER + ", " +  
  21.             SmartDialDbColumns.CONTACT_ID + ", " +  
  22.             SmartDialDbColumns.LOOKUP_KEY +  
  23.             " FROM " + Tables.SMARTDIAL_TABLE + " WHERE " +  
  24.             SmartDialDbColumns.CONTACT_ID + " IN " +  
  25.             " (SELECT " + PrefixColumns.CONTACT_ID +  
  26.             " FROM " + Tables.PREFIX_TABLE +  
  27.             " WHERE " + Tables.PREFIX_TABLE + "." + PrefixColumns.PREFIX +  
  28.             " LIKE '" + looseQuery + "')" +  
  29.             " ORDER BY " + SmartDialSortingOrder.SORT_ORDER,  
  30.             new String[] {currentTimeStamp});  
  31.   
  32.   
  33.     final int columnDataId = 0;  
  34.     final int columnDisplayNamePrimary = 1;  
  35.     final int columnPhotoId = 2;  
  36.     final int columnNumber = 3;  
  37.     final int columnId = 4;  
  38.     final int columnLookupKey = 5;  
  39.   
  40.     final Set<ContactMatch> duplicates = new HashSet<ContactMatch>();  
  41.     int counter = 0;  
  42.     try {  
  43.         //对匹配项去重,并构建搜索结果  
  44.         while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) {  
  45.             final long dataID = cursor.getLong(columnDataId);  
  46.             final String displayName = cursor.getString(columnDisplayNamePrimary);  
  47.             final String phoneNumber = cursor.getString(columnNumber);  
  48.             final long id = cursor.getLong(columnId);  
  49.             final long photoId = cursor.getLong(columnPhotoId);  
  50.             final String lookupKey = cursor.getString(columnLookupKey);  
  51.   
  52.             final ContactMatch contactMatch = new ContactMatch(lookupKey, id);  
  53.             //该匹配项已经被收录,无需重复添加到结果中  
  54.             if (duplicates.contains(contactMatch)) {  
  55.                 continue;  
  56.             }  
  57.   
  58.             final boolean nameMatches = nameMatcher.matches(displayName);  
  59.             final boolean numberMatches = (nameMatcher.matchesNumber(phoneNumber, query) != null);  
  60.             if (nameMatches || numberMatches) {  
  61.                 //匹配成功,且没有重复项  
  62.                 duplicates.add(contactMatch);  
  63.                 result.add(new ContactNumber(id, dataID, displayName, phoneNumber, lookupKey, photoId));  
  64.                 counter++;  
  65.             }  
  66.         }  
  67.     } finally {  
  68.         cursor.close();  
  69.     }  
  70.     return result;  
  71. }  
        在上面的过程中,主要完成两个任务:搜索和去重。
          搜索的过程就是从prefix_table中匹配当前的搜索字串,对于匹配到的项,再去smartdial_table中查找该联系人的详细记录,但是由于同一条联系人有可能既匹配姓名又匹配号码,因此搜索结果中可能包含相同的联系人记录。所以对于拿到的Cursor进行遍历,整理其每一条数据,遇到重复项则忽略,最终整理得到有效的结果组合ArrayList<ContactNumber>,返回给查询者。
        这就是拨号界面的搜索机制。

          本文主要介绍Android4.4拨号界面的联系人搜索机制

        拨号搜索机制分为两个部分,引导搜索和搜索。其中引导搜索是指,从用户输入到开始搜索之间的流程,而搜索部分是指,从数据库搜索字符串的过程。


一、引导搜索部分


        默认的拨号界面的布局从上到下主要分为3个部分:显示列表、数字编辑框、拨号键盘。他们的作用是:用户直接在拨号键盘上输入数字,然后数字编辑框显示所输入的数字,同时在显示列表中体现此时的搜索结果。如图所示:

        从流程上来讲,需要拨号键盘将用户点击转换为按键事件并传递给编辑框,然后由编辑框传递给搜索框,再由搜索框传递给列表Fragment,然后在列表所加载的Adapter中体现当前的搜索结果。
        接下来我们详细分析这个过程。


1.1、从拨号键盘到编辑框


        用户在拨号键盘上的点击的数字按钮,都会在编辑框中体现出来,我们先来追踪这一过程。
        每个拨号键盘按钮都是DialpadKeyButton类型的View,他们继承自FrameLayout,当遇到点击事件时,就会触发setPressed()方法:

[java]   view plain copy
  1. @setPressed  
  2. public void setPressed(boolean pressed) {  
  3.     super.setPressed(pressed);  
  4.     if (mOnPressedListener != null) {  
  5.         mOnPressedListener.onPressed(this, pressed);  
  6.     }  
  7. }  
        然后将事件转换为onPressed()发送给mOnPressedListener,这个mOnPressedListener就是DialpadFragment,然后在DialpadFragment的onPressed()中,将当前的点击事件转换为标准的按键输入:
[java]   view plain copy
  1. @DialpadFragment.java  
  2. public void onPressed(View view, boolean pressed) {  
  3.     if (pressed) {  
  4.         switch (view.getId()) {  
  5.             case R.id.one: {  
  6.                //将当前点击事件转换为键盘事件  
  7.                keyPressed(KeyEvent.KEYCODE_1);  
  8.                break;  
  9.             }  
  10.             case R.id.two: {  
  11.                keyPressed(KeyEvent.KEYCODE_2);  
  12.                break;  
  13.             }  
  14.             default: {  
  15.                  Log.wtf(TAG, "Unexpected onTouch(ACTION_DOWN) event from: " + view);  
  16.                  break;  
  17.             }  
  18.         }  
  19.     } else {  
  20.     }  
  21. }  
        这里看到,当我们在拨号键盘上点击某个View时,将会通过onPressed()转换为标准的键盘消息,比如,在R.id.one控件上的点击,将会转换为KeyEvent.KEYCODE_1消息。然后在keyPressed()中将会把当前输入传递给编辑框:
[java]   view plain copy
  1. private void keyPressed(int keyCode) {  
  2.     mHaptic.vibrate();  
  3.     KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);  
  4.     //传递给编辑框控件  
  5.     mDigits.onKeyDown(keyCode, event);  
  6.   
  7.     // If the cursor is at the end of the text we hide it.  
  8.     final int length = mDigits.length();  
  9.     if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) {  
  10.         mDigits.setCursorVisible(false);  
  11.     }  
  12. }  

        上面的mDigits就是显示当前输入内容的编辑框控件。


1.2、从编辑框到搜索框


        搜索框的作用主要是,当拨号键盘隐藏时,显示当前的输入内容。而编辑框需要将当前的输入传递给搜索框。
        当编辑框检测到KeyDown事件后,就会将当前键盘的输入放入编辑框中,并触发TextWatcher的相关方法:
[java]   view plain copy
  1. @DialpadFragment.java  
  2. public void afterTextChanged(Editable input) {  
  3.     if (!mDigitsFilledByIntent && SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), mDigits)) {  
  4.         mDigits.getText().clear();  
  5.     }  
  6.   
  7.     if (isDigitsEmpty()) {  
  8.         mDigitsFilledByIntent = false;  
  9.         mDigits.setCursorVisible(false);  
  10.     }  
  11.   
  12.     if (mDialpadQueryListener != null) {  
  13.         //传递给mDialpadQueryListener  
  14.         mDialpadQueryListener.onDialpadQueryChanged(mDigits.getText().toString());  
  15.     }  
  16.     updateDialAndDeleteButtonEnabledState();  
  17. }  
        在这里,又将当前已经输入的文本传递给mDialpadQueryListener,它是在DialtactsActivity中实现的:
[java]   view plain copy
  1. @DialtactsActivity.java  
  2. public void onDialpadQueryChanged(String query) {  
  3.     final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);  
  4.     if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {  
  5.         if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {  
  6.             return;  
  7.         }  
  8.         //传递给搜索框  
  9.         mSearchView.setText(normalizedQuery);  
  10.     }  
  11. }  

        我们看到,在onDialpadQueryChanged()中将当前编辑框的内容通过setText()方法传递给了mSearchView,也就是最上方的搜索框。


1.3、从搜索框到搜索结果列表Fragment


        搜索框下面的列表用于在搜索时显示搜索结果,他所处的位置是复用的,可以选择性的加载三种Fragment, 当处于非搜索状态时,加载PhoneFavoriteFragment,这是进入拨号界面的默认加载项,将会显示瓦片式收藏界面,当在搜索模式时,将会加载SmartDialSearchFragment或者RegularSearchFragment用于显示当时的搜索结果。对于最常用的用户在拨号键盘输入内容触发的搜索,将会加载SmartDialSearchFragment。此时搜索框需要将要搜索的文本传递给SmartDialSearchFragment。
        在搜索时,由于搜索框注册了文本监听器,所以将会触发TextWatcher,此时需要暂存当前要搜索的文本,并进入搜索模式,然后再将搜索内容交给SmartDialSearchFragment。
[java]   view plain copy
  1. public void onTextChanged(CharSequence s, int start, int before, int count) {  
  2.     final String newText = s.toString();  
  3.     if (newText.equals(mSearchQuery)) {  
  4.         return;  
  5.     }  
  6.     //存储当前的搜索文本  
  7.     mSearchQuery = newText;  
  8.     final boolean dialpadSearch = isDialpadShowing();  
  9.   
  10.     // Show search result with non-empty text. Show a bare list otherwise.  
  11.     if (TextUtils.isEmpty(newText) && getInSearchUi()) {  
  12.         //退出搜索模式  
  13.         exitSearchUi();  
  14.         mSearchViewCloseButton.setVisibility(View.GONE);  
  15.         mVoiceSearchButton.setVisibility(View.VISIBLE);  
  16.         return;  
  17.     } else if (!TextUtils.isEmpty(newText)) {  
  18.         final boolean sameSearchMode = (dialpadSearch && mInDialpadSearch) || (!dialpadSearch && mInRegularSearch);  
  19.         if (!sameSearchMode) {  
  20.             //进入搜素模式  
  21.             enterSearchUi(dialpadSearch, newText);  
  22.         }  
  23.   
  24.         if (dialpadSearch && mSmartDialSearchFragment != null) {  
  25.             //将搜索文本转交给mSmartDialSearchFragment  
  26.             mSmartDialSearchFragment.setQueryString(newText, false);  
  27.         } else if (mRegularSearchFragment != null) {  
  28.             mRegularSearchFragment.setQueryString(newText, false);  
  29.         }  
  30.         mSearchViewCloseButton.setVisibility(View.VISIBLE);  
  31.         mVoiceSearchButton.setVisibility(View.GONE);  
  32.         return;  
  33.     }  
  34. }  
        在这里完成了三个重要任务:
        1、将当前要搜索的文本存储在mSearchQuery中,在当前界面被恢复时使用;
        2、进入/退出搜素界面,也就是配置当前需要加载的Fragment;

        3、将要搜素的文本传递给搜索列表的Fragment,也就是mSmartDialSearchFragment;


1.4、从搜索列表的Fragment到Adapter


        先来看一下SmartDialSearchFragment的继承关系:
        SmartDialSearchFragment
            ----SearchFragment
                ----PhoneNumberPickerFragment
                    ----ContactEntryListFragment<ContactEntryListAdapter>
                        ----Fragment

        如下图所示:

        SmartDialSearchFragment拿到搜索的文本后,需要传递给自己的Adapter才能完成搜索任务,我们现在来分析这个交接的过程。
        从上面1.3节中我们看到,SmartDialSearchFragment通过setQueryString()拿到了要搜索的字串,我们来查看这个方法,他是在SmartDialSearchFragment的父类ContactEntryListFragment中被实现的:

[java]   view plain copy
  1. @ContactEntryListFragment.java  
  2. public void setQueryString(String queryString, boolean delaySelection) {  
  3.     if (TextUtils.isEmpty(queryString)) queryString = null;  
  4.   
  5.     if (!TextUtils.equals(mQueryString, queryString)) {  
  6.         mQueryString = queryString;  
  7.         setSearchMode(!TextUtils.isEmpty(mQueryString));  
  8.   
  9.         if (mAdapter != null) {  
  10.             //传递给Adapter  
  11.             mAdapter.setQueryString(queryString);  
  12.             //触发Adapter重新搜索  
  13.             reloadData();  
  14.         }  
  15.     }  
  16. }  
        在这里,Fragment将要搜索的文本通过setQueryString()的方法传递给当前的Adapter,然后通过reloadData()方法触发Adapter的搜索机制。那么这里的Adapter具体是指哪个呢?
        我们在SmartDialSearchFragment中找到了该Adapter的创建之处,他就是SmartDialNumberListAdapter:
[java]   view plain copy
  1. @SmartDialSearchFragment.java  
  2. protected ContactEntryListAdapter createListAdapter() {  
  3.     SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity());  
  4.     adapter.setUseCallableUri(super.usesCallableUri());  
  5.     adapter.setQuickContactEnabled(true);  
  6.     adapter.setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false);  
  7.     return adapter;  
  8. }  
        该Adapter的继承关系如下:
        SmartDialNumberListAdapter
            ----DialerPhoneNumberListAdapter
                ----PhoneNumberListAdapter
                    ----ContactEntryListAdapter
                        ----IndexerListAdapter
                            ----PinnedHeaderListAdapter
                                ----CompositeCursorAdapter

        如下图所示:

        接下来我们分析如何通过Fragment的reloadData()触发Adapter的搜索。


1.5、Adapter触发搜索机制


        刚才介绍到,SmartDialSearchFragment在setQueryString()时,通过reloadData()触发了Adapter的搜索,我们来看一下这个流程:

[java]   view plain copy
  1. @ContactEntryListFragment.java  
  2. protected void reloadData() {  
  3.     removePendingDirectorySearchRequests();  
  4.     mAdapter.onDataReload();  
  5.     mLoadPriorityDirectoriesOnly = true;  
  6.     mForceLoad = true;  
  7.     //触发新的Adapter  
  8.     startLoading();  
  9. }  
  10. protected void startLoading() {  
  11.     if (mAdapter == null) {  
  12.         return;  
  13.     }  
  14.   
  15.     //配置Adapter要搜索的文本  
  16.     configureAdapter();  
  17.     int partitionCount = mAdapter.getPartitionCount();  
  18.     for (int i = 0; i < partitionCount; i++) {  
  19.         Partition partition = mAdapter.getPartition(i);  
  20.         if (partition instanceof DirectoryPartition) {  
  21.             DirectoryPartition directoryPartition = (DirectoryPartition)partition;  
  22.             if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {  
  23.                 if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {  
  24.                     startLoadingDirectoryPartition(i);  
  25.                 }  
  26.             }  
  27.         } else {  
  28.             //通过LoaderManager进行异步查询  
  29.             getLoaderManager().initLoader(i, nullthis);  
  30.         }  
  31.     }  
  32.   
  33.     mLoadPriorityDirectoriesOnly = false;  
  34. }  
        在startLoading()时,通过configureAdapter()对当前的Adapter配置了要搜索的文本、排序方法以及显示主题等信息,然后就 通过LoaderManager进行异步查询
        我们来看Loader的流程。
        经过initLoader()的操作之后,就会触发SmartDialSearchFragment中的onCreateLoader()方法:
[java]   view plain copy
  1. @SmartDialSearchFragment.java  
  2. public Loader<Cursor> onCreateLoader(int id, Bundle args) {  
  3.     if (id == getDirectoryLoaderId()) {  
  4.         return super.onCreateLoader(id, args);  
  5.     } else {  
  6.         //创建当前的CursorLoader,也就是SmartDialCursorLoader  
  7.         final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter();  
  8.         SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext());  
  9.         adapter.configureLoader(loader);  
  10.         return loader;  
  11.     }  
  12. }  
        这里创建了SmartDialCursorLoader作为当前的CursorLoader。然后通过adapter的configureLoader()方法将该Loader传递给SmartDialNumberListAdapter,接下来就会在SmartDialCursorLoader中完成异步查询,并将查询结果传递给ContactEntryListFragment的onLoadFinished()方法:
[java]   view plain copy
  1. @ContactEntryListFragment.java  
  2. public void onLoadFinished(Loader<Cursor> loader, Cursor data) {  
  3.     if (!mEnabled) {  
  4.         return;  
  5.     }  
  6.   
  7.     int loaderId = loader.getId();  
  8.     if (loaderId == DIRECTORY_LOADER_ID) {  
  9.         mDirectoryListStatus = STATUS_LOADED;  
  10.         mAdapter.changeDirectories(data);  
  11.         startLoading();  
  12.     } else {  
  13.         //更新Adapter的Cursor  
  14.         onPartitionLoaded(loaderId, data);  
  15.         if (isSearchMode()) {  
  16.             int directorySearchMode = getDirectorySearchMode();  
  17.             if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {  
  18.                 if (mDirectoryListStatus == STATUS_NOT_LOADED) {  
  19.                     mDirectoryListStatus = STATUS_LOADING;  
  20.                     getLoaderManager().initLoader(DIRECTORY_LOADER_ID, nullthis);  
  21.                 } else {  
  22.                     startLoading();  
  23.                 }  
  24.             }  
  25.         } else {  
  26.             mDirectoryListStatus = STATUS_NOT_LOADED;  
  27.             getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);  
  28.         }  
  29.     }  
  30. }  
  31. protected void onPartitionLoaded(int partitionIndex, Cursor data) {  
  32.     if (partitionIndex >= mAdapter.getPartitionCount()) {  
  33.         return;  
  34.     }  
  35.   
  36.     //更新当前的Adapter  
  37.     mAdapter.changeCursor(partitionIndex, data);  
  38.     setProfileHeader();  
  39.     showCount(partitionIndex, data);  
  40.   
  41.     if (!isLoading()) {  
  42.         completeRestoreInstanceState();  
  43.     }  
  44. }  

        在onLoadFinished()中,通过onPartitionLoaded()对当前的Adapter所使用的Cursor进行更新,从而刷新列表。


二、字符搜索过程


        前面我们看到, 搜索时使用了LoaderManager的异步查询机制,而且CursorLoader使用的是SmartDialCursorLoader,那么具体的搜索过程就会在SmartDialCursorLoader中体现出来。根据AsyncTaskLoader的机制,SmartDialCursorLoader需要在自己的loadInBackground()中查询Cursor结果,现在来看这个过程:
[java]   view plain copy
  1. @SmartDialCursorLoader.java  
  2. public Cursor loadInBackground() {  
  3.     //从dialerDatabaseHelper中查找匹配结果  
  4.     final DialerDatabaseHelper dialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper( mContext);  
  5.     final ArrayList<ContactNumber> allMatches = dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher);  
  6.   
  7.     //构建Cursor给Adapter使用  
  8.     final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY);  
  9.     Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length];  
  10.     for (ContactNumber contact : allMatches) {  
  11.         row[PhoneQuery.PHONE_ID] = contact.dataId;  
  12.         row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber;  
  13.         row[PhoneQuery.CONTACT_ID] = contact.id;  
  14.         row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey;  
  15.         row[PhoneQuery.PHOTO_ID] = contact.photoId;  
  16.         row[PhoneQuery.DISPLAY_NAME] = contact.displayName;  
  17.         cursor.addRow(row);  
  18.     }  
  19.     return cursor;  
  20. }  

        原来,SmartDialCursorLoader是利用DialerDatabaseHelper进行的查找,他是SQLiteOpenHelper的子类,每次拨号盘进程的创建都会根据当前的通讯录内容创建表单,用于联系人搜索。下面我们从该数据库的创建、查询、更新三个方面来分析其内部原理。


2.1、数据库的创建及搜索机制


        该数据库是单例模式,只存在一个实例对象,第一次被调用时就会完成初始化任务。可以通过DatabaseHelperManager的getDatabaseHelper()方法来得到DialerDatabaseHelper的实例对象:
[java]   view plain copy
  1. @DatabaseHelperManager.java  
  2. public static DialerDatabaseHelper getDatabaseHelper(Context context) {  
  3.     return DialerDatabaseHelper.getInstance(context);  
  4. }  
        然后就会在DialerDatabaseHelper中判断,是否已经有实例对象,没有的话就创建。
[java]   view plain copy
  1. @DialerDatabaseHelper.java  
  2. public static synchronized DialerDatabaseHelper getInstance(Context context) {  
  3.     if (sSingleton == null) {  
  4.         sSingleton = new DialerDatabaseHelper(context.getApplicationContext(), DATABASE_NAME);  
  5.     }  
  6.     return sSingleton;  
  7. }  
        接下来我们看该数据库的创建过程,主要在onCreate()方法中体现:
[java]   view plain copy
  1. public void onCreate(SQLiteDatabase db) {  
  2.     setupTables(db);  
  3. }  
  4. private void setupTables(SQLiteDatabase db) {  
  5.     //删除旧表单  
  6.     dropTables(db);  
  7.     //创建新表“smartdial_table”  
  8.     db.execSQL("CREATE TABLE " + Tables.SMARTDIAL_TABLE + " (" +  
  9.             SmartDialDbColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +  
  10.             SmartDialDbColumns.DATA_ID + " INTEGER, " +  
  11.             SmartDialDbColumns.NUMBER + " TEXT," +  
  12.             SmartDialDbColumns.CONTACT_ID + " INTEGER," +  
  13.             SmartDialDbColumns.LOOKUP_KEY + " TEXT," +  
  14.             SmartDialDbColumns.DISPLAY_NAME_PRIMARY + " TEXT, " +  
  15.             SmartDialDbColumns.PHOTO_ID + " INTEGER, " +  
  16.             SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " LONG, " +  
  17.             SmartDialDbColumns.LAST_TIME_USED + " LONG, " +  
  18.             SmartDialDbColumns.TIMES_USED + " INTEGER, " +  
  19.             SmartDialDbColumns.STARRED + " INTEGER, " +  
  20.             SmartDialDbColumns.IS_SUPER_PRIMARY + " INTEGER, " +  
  21.             SmartDialDbColumns.IN_VISIBLE_GROUP + " INTEGER, " +  
  22.             SmartDialDbColumns.IS_PRIMARY + " INTEGER" +  
  23.             ");");  
  24.   
  25.     //创建新表“prefix_table”  
  26.     db.execSQL("CREATE TABLE " + Tables.PREFIX_TABLE + " (" +  
  27.             PrefixColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +  
  28.             PrefixColumns.PREFIX + " TEXT COLLATE NOCASE, " +  
  29.             PrefixColumns.CONTACT_ID + " INTEGER" +  
  30.             ");");  
  31.   
  32.     //创建新表“properties”  
  33.     db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " (" +  
  34.             PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, " +  
  35.             PropertiesColumns.PROPERTY_VALUE + " TEXT " +  
  36.             ");");  
  37.   
  38.     //设置属性  
  39.     setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));  
  40.     //更新时间  
  41.     resetSmartDialLastUpdatedTime();  
  42. }  
        在上面的初始化过程中,最主要的任务就是创建了三张表单:smartdial_table、prefix_table、properties。其中properties表用于存储当前数据库的版本号,与搜索任务无关,我们主要分析其他两个表。
prefix_table表单
        该表是对所有联系人的电话号码以及英文姓名进行解析,形成搜索的索引表单。

        如果是姓名,则将姓名的英文单词解析为相应的数字,比如:
            英文名  dushaofeng 将会被解析为:3(d)8(u)7(s)4(h)2(a)6(o)3(f)3(e)6(n)4(g)
        如果是号码,除了要保存号码本身外,对于包含国家码的号码,还要保存除去国家码以外的有效号码。
          经过上面的解析,每个联系人至少包含两条记录,即姓名对应的数字以及号码所拆分出来的数字。当搜索时,就会利用用户所输入的内容在该表中进行匹配,匹配成功的记录,将根据该条记录的contact_id在smartdial_table表中查找该联系人的详细信息。
smartdial_table表单
         每个联系人都对应该表中一条记录,每条记录都包含了该联系人的phone_number、contact_id、display_name、photo_id、starred、last_smartdial_update_time等信息,在搜索时,会利用用户输入区prefix_table中进行匹配,对于匹配成功的记录,根据prefix_table表中对应的contact_id再来smartdial_table中查找该联系人的详细信息,也就是头像、姓名、收藏状态等,并把这些信息构建为Cursor类数据,返回给查询者。

        这就是该数据库搜索的原理。


2.2、数据库的更新


        前面说到了数据库的结构和搜索原理,下面来介绍数据库表单的初始化和更新过程。
        每次拨号界面经过onResume(),都会触发数据库的更新:
[java]   view plain copy
  1. @DialtactsActivity.java  
  2. protected void onResume() {  
  3.     super.onResume();  
  4.     //进入数据库更新入口  
  5.     mDialerDatabaseHelper.startSmartDialUpdateThread();  
  6. }  
        然后会在数据库中开启异步线程更新数据:
[java]   view plain copy
  1. @DialerDatabaseHelper.java  
  2. public void startSmartDialUpdateThread() {  
  3.     new SmartDialUpdateAsyncTask().execute();  
  4. }  
  5. private class SmartDialUpdateAsyncTask extends AsyncTask {  
  6.     @Override  
  7.     protected Object doInBackground(Object[] objects) {  
  8.         updateSmartDialDatabase();  
  9.         return null;  
  10.     }  
  11.   
  12.     @Override  
  13.     protected void onCancelled() {  
  14.         super.onCancelled();  
  15.     }  
  16.   
  17.     @Override  
  18.     protected void onPostExecute(Object o) {  
  19.         super.onPostExecute(o);  
  20.     }  
  21. }  
        在线程中执行updateSmartDialDatabase()来更新数据:
[java]   view plain copy
  1. public void updateSmartDialDatabase() {  
  2.     final SQLiteDatabase db = getWritableDatabase();  
  3.   
  4.     synchronized(mLock) {  
  5.         final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null;  
  6.   
  7.         //获取上一次更新的时间  
  8.         final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences( DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);  
  9.         final String lastUpdateMillis = String.valueOf(databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0));  
  10.   
  11.         //得到当前的通讯录数据  
  12.         final Cursor updatedContactCursor = mContext.getContentResolver().query(PhoneQuery.URI,  
  13.                 PhoneQuery.PROJECTION, PhoneQuery.SELECT_UPDATED_CLAUSE,  
  14.                 new String[]{lastUpdateMillis}, null);  
  15.   
  16.         //获取当前的时间  
  17.         final Long currentMillis = System.currentTimeMillis();  
  18.         if (updatedContactCursor == null) {  
  19.             return;  
  20.         }  
  21.         sInUpdate.getAndSet(true);  
  22.   
  23.         //删掉已经删除的和无效的联系人记录  
  24.         removeDeletedContacts(db, lastUpdateMillis);  
  25.         removePotentiallyCorruptedContacts(db, lastUpdateMillis);  
  26.   
  27.   
  28.         try {  
  29.             if (!lastUpdateMillis.equals("0")) {  
  30.                 removeUpdatedContacts(db, updatedContactCursor);  
  31.             }  
  32.   
  33.             //向smartdial_table表中插入当前所有有效的联系人数据,以及向prefix_table表中添加联系人号码添加为搜索索引  
  34.             insertUpdatedContactsAndNumberPrefix(db, updatedContactCursor, currentMillis);  
  35.         } finally {  
  36.             updatedContactCursor.close();  
  37.         }  
  38.   
  39.         //从smartdial_table表中读取当前联系人的姓名和号码  
  40.         final Cursor nameCursor = db.rawQuery(  
  41.                 "SELECT DISTINCT " +  
  42.                 SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + SmartDialDbColumns.CONTACT_ID +  
  43.                 " FROM " + Tables.SMARTDIAL_TABLE +  
  44.                 " WHERE " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME +  
  45.                 " = " + Long.toString(currentMillis),  
  46.                 new String[] {});  
  47.   
  48.         if (nameCursor != null) {  
  49.             try {  
  50.                 //根据联系人姓名生成相应的数字索引  
  51.                 insertNamePrefixes(db, nameCursor);  
  52.             } finally {  
  53.                 nameCursor.close();  
  54.             }  
  55.         }  
  56.   
  57.         //创建数据库相应的列  
  58.         /** Creates index on contact_id for fast JOIN operation. */  
  59.         db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON " + Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns.CONTACT_ID  + ");");  
  60.         /** Creates index on last_smartdial_update_time for fast SELECT operation. */  
  61.         db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON " +  
  62.                 Tables.SMARTDIAL_TABLE + " (" +  
  63.                 SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ");");  
  64.         /** Creates index on sorting fields for fast sort operation. */  
  65.         db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_sort_index ON " +  
  66.                 Tables.SMARTDIAL_TABLE + " (" +  
  67.                 SmartDialDbColumns.STARRED + ", " +  
  68.                 SmartDialDbColumns.IS_SUPER_PRIMARY + ", " +  
  69.                 SmartDialDbColumns.LAST_TIME_USED + ", " +  
  70.                 SmartDialDbColumns.TIMES_USED + ", " +  
  71.                 SmartDialDbColumns.IN_VISIBLE_GROUP +  ", " +  
  72.                 SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +  
  73.                 SmartDialDbColumns.CONTACT_ID + ", " +  
  74.                 SmartDialDbColumns.IS_PRIMARY +  
  75.                 ");");  
  76.         /** Creates index on prefix for fast SELECT operation. */  
  77.         db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_index ON " +  
  78.                 Tables.PREFIX_TABLE + " (" + PrefixColumns.PREFIX + ");");  
  79.         /** Creates index on contact_id for fast JOIN operation. */  
  80.         db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON " +  
  81.                 Tables.PREFIX_TABLE + " (" + PrefixColumns.CONTACT_ID + ");");  
  82.         /** Updates the database index statistics.*/  
  83.         db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE);  
  84.         db.execSQL("ANALYZE " + Tables.PREFIX_TABLE);  
  85.         db.execSQL("ANALYZE smartdial_contact_id_index");  
  86.         db.execSQL("ANALYZE smartdial_last_update_index");  
  87.         db.execSQL("ANALYZE nameprefix_index");  
  88.         db.execSQL("ANALYZE nameprefix_contact_id_index");  
  89.         sInUpdate.getAndSet(false);  
  90.   
  91.         final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();  
  92.         editor.putLong(LAST_UPDATED_MILLIS, currentMillis);  
  93.         editor.commit();  
  94.     }  
  95. }  
        在上面这个更新数据库的过程中,一次完成如下任务:
        1、从联系人数据库中读取从上次更新到现在时间段内更新的联系人记录;
        2、在removeDeletedContacts()中,删除已经被删除的联系人记录;
        3、在removePotentiallyCorruptedContacts()中,删除已经损坏的联系人记录;
        4、在removeUpdatedContacts()中,删除需要更新的联系人记录;
        5、在insertUpdatedContactsAndNumberPrefix()中,将更新的联系人数据插入到smartdial_table中,并把联系人号码插入到prefix_table中;
        6、在insertNamePrefixes()中,将本次需要更新的联系人的姓名转换为数字存入prefix_table中;
        7、为数据库建立索引;
          需要注意两点:
        1、从联系人数据库查询时,并不是查询所有联系人,而是查询从上次查询到现在之间所更新的联系人数据;

        2、解析联系人姓名为号码时,只对英文姓名有效,这就决定了,无法通过拼音搜索联系人;


2.3、数据库的查询


        在2.1中已经介绍过搜索机制,本节就结合具体代码来看搜索的详细过程。
        在1.5节中我们分析到,在SmartDialCursorLoader中通过DialerDatabaseHelper的getLooseMatches()方法进行搜索任务,现在来看具体的操作:
[java]   view plain copy
  1. @DialerDatabaseHelper.java  
  2. public ArrayList<ContactNumber>  getLooseMatches(String query, SmartDialNameMatcher nameMatcher) {  
  3.     final boolean inUpdate = sInUpdate.get();  
  4.     if (inUpdate) {  
  5.         return Lists.newArrayList();  
  6.     }  
  7.     final SQLiteDatabase db = getReadableDatabase();  
  8.   
  9.     //准备搜索匹配语句  
  10.     final String looseQuery = query + "%";  
  11.     final ArrayList<ContactNumber> result = Lists.newArrayList();  
  12.     final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null;  
  13.     final String currentTimeStamp = Long.toString(System.currentTimeMillis());  
  14.   
  15.     //搜索语句,从prefix_table中搜索匹配项,并从smartdial_table中读取匹配项的详细信息  
  16.     final Cursor cursor = db.rawQuery("SELECT " +  
  17.             SmartDialDbColumns.DATA_ID + ", " +  
  18.             SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +  
  19.             SmartDialDbColumns.PHOTO_ID + ", " +  
  20.             SmartDialDbColumns.NUMBER + ", " +  
  21.             SmartDialDbColumns.CONTACT_ID + ", " +  
  22.             SmartDialDbColumns.LOOKUP_KEY +  
  23.             " FROM " + Tables.SMARTDIAL_TABLE + " WHERE " +  
  24.             SmartDialDbColumns.CONTACT_ID + " IN " +  
  25.             " (SELECT " + PrefixColumns.CONTACT_ID +  
  26.             " FROM " + Tables.PREFIX_TABLE +  
  27.             " WHERE " + Tables.PREFIX_TABLE + "." + PrefixColumns.PREFIX +  
  28.             " LIKE '" + looseQuery + "')" +  
  29.             " ORDER BY " + SmartDialSortingOrder.SORT_ORDER,  
  30.             new String[] {currentTimeStamp});  
  31.   
  32.   
  33.     final int columnDataId = 0;  
  34.     final int columnDisplayNamePrimary = 1;  
  35.     final int columnPhotoId = 2;  
  36.     final int columnNumber = 3;  
  37.     final int columnId = 4;  
  38.     final int columnLookupKey = 5;  
  39.   
  40.     final Set<ContactMatch> duplicates = new HashSet<ContactMatch>();  
  41.     int counter = 0;  
  42.     try {  
  43.         //对匹配项去重,并构建搜索结果  
  44.         while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) {  
  45.             final long dataID = cursor.getLong(columnDataId);  
  46.             final String displayName = cursor.getString(columnDisplayNamePrimary);  
  47.             final String phoneNumber = cursor.getString(columnNumber);  
  48.             final long id = cursor.getLong(columnId);  
  49.             final long photoId = cursor.getLong(columnPhotoId);  
  50.             final String lookupKey = cursor.getString(columnLookupKey);  
  51.   
  52.             final ContactMatch contactMatch = new ContactMatch(lookupKey, id);  
  53.             //该匹配项已经被收录,无需重复添加到结果中  
  54.             if (duplicates.contains(contactMatch)) {  
  55.                 continue;  
  56.             }  
  57.   
  58.             final boolean nameMatches = nameMatcher.matches(displayName);  
  59.             final boolean numberMatches = (nameMatcher.matchesNumber(phoneNumber, query) != null);  
  60.             if (nameMatches || numberMatches) {  
  61.                 //匹配成功,且没有重复项  
  62.                 duplicates.add(contactMatch);  
  63.                 result.add(new ContactNumber(id, dataID, displayName, phoneNumber, lookupKey, photoId));  
  64.                 counter++;  
  65.             }  
  66.         }  
  67.     } finally {  
  68.         cursor.close();  
  69.     }  
  70.     return result;  
  71. }  
        在上面的过程中,主要完成两个任务:搜索和去重。
          搜索的过程就是从prefix_table中匹配当前的搜索字串,对于匹配到的项,再去smartdial_table中查找该联系人的详细记录,但是由于同一条联系人有可能既匹配姓名又匹配号码,因此搜索结果中可能包含相同的联系人记录。所以对于拿到的Cursor进行遍历,整理其每一条数据,遇到重复项则忽略,最终整理得到有效的结果组合ArrayList<ContactNumber>,返回给查询者。
        这就是拨号界面的搜索机制。

猜你喜欢

转载自blog.csdn.net/synola/article/details/42459513