拨号搜索机制分为两个部分,引导搜索和搜索。其中引导搜索是指,从用户输入到开始搜索之间的流程,而搜索部分是指,从数据库搜索字符串的过程。
默认的拨号界面的布局从上到下主要分为3个部分:显示列表、数字编辑框、拨号键盘。他们的作用是:用户直接在拨号键盘上输入数字,然后数字编辑框显示所输入的数字,同时在显示列表中体现此时的搜索结果。如图所示:
从流程上来讲,需要拨号键盘将用户点击转换为按键事件并传递给编辑框,然后由编辑框传递给搜索框,再由搜索框传递给列表Fragment,然后在列表所加载的Adapter中体现当前的搜索结果。
接下来我们详细分析这个过程。
用户在拨号键盘上的点击的数字按钮,都会在编辑框中体现出来,我们先来追踪这一过程。
每个拨号键盘按钮都是DialpadKeyButton类型的View,他们继承自FrameLayout,当遇到点击事件时,就会触发setPressed()方法:
@setPressed public void setPressed(boolean pressed) { super.setPressed(pressed); if (mOnPressedListener != null) { mOnPressedListener.onPressed(this, pressed); } }然后将事件转换为onPressed()发送给mOnPressedListener,这个mOnPressedListener就是DialpadFragment,然后在DialpadFragment的onPressed()中,将当前的点击事件转换为标准的按键输入:
@DialpadFragment.java public void onPressed(View view, boolean pressed) { if (pressed) { switch (view.getId()) { case R.id.one: { //将当前点击事件转换为键盘事件 keyPressed(KeyEvent.KEYCODE_1); break; } case R.id.two: { keyPressed(KeyEvent.KEYCODE_2); break; } default: { Log.wtf(TAG, "Unexpected onTouch(ACTION_DOWN) event from: " + view); break; } } } else { } }这里看到,当我们在拨号键盘上点击某个View时,将会通过onPressed()转换为标准的键盘消息,比如,在R.id.one控件上的点击,将会转换为KeyEvent.KEYCODE_1消息。然后在keyPressed()中将会把当前输入传递给编辑框:
private void keyPressed(int keyCode) { mHaptic.vibrate(); KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); //传递给编辑框控件 mDigits.onKeyDown(keyCode, event); // If the cursor is at the end of the text we hide it. final int length = mDigits.length(); if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) { mDigits.setCursorVisible(false); } }
上面的mDigits就是显示当前输入内容的编辑框控件。
@DialpadFragment.java public void afterTextChanged(Editable input) { if (!mDigitsFilledByIntent && SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), mDigits)) { mDigits.getText().clear(); } if (isDigitsEmpty()) { mDigitsFilledByIntent = false; mDigits.setCursorVisible(false); } if (mDialpadQueryListener != null) { //传递给mDialpadQueryListener mDialpadQueryListener.onDialpadQueryChanged(mDigits.getText().toString()); } updateDialAndDeleteButtonEnabledState(); }在这里,又将当前已经输入的文本传递给mDialpadQueryListener,它是在DialtactsActivity中实现的:
@DialtactsActivity.java public void onDialpadQueryChanged(String query) { final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { return; } //传递给搜索框 mSearchView.setText(normalizedQuery); } }
我们看到,在onDialpadQueryChanged()中将当前编辑框的内容通过setText()方法传递给了mSearchView,也就是最上方的搜索框。
public void onTextChanged(CharSequence s, int start, int before, int count) { final String newText = s.toString(); if (newText.equals(mSearchQuery)) { return; } //存储当前的搜索文本 mSearchQuery = newText; final boolean dialpadSearch = isDialpadShowing(); // Show search result with non-empty text. Show a bare list otherwise. if (TextUtils.isEmpty(newText) && getInSearchUi()) { //退出搜索模式 exitSearchUi(); mSearchViewCloseButton.setVisibility(View.GONE); mVoiceSearchButton.setVisibility(View.VISIBLE); return; } else if (!TextUtils.isEmpty(newText)) { final boolean sameSearchMode = (dialpadSearch && mInDialpadSearch) || (!dialpadSearch && mInRegularSearch); if (!sameSearchMode) { //进入搜素模式 enterSearchUi(dialpadSearch, newText); } if (dialpadSearch && mSmartDialSearchFragment != null) { //将搜索文本转交给mSmartDialSearchFragment mSmartDialSearchFragment.setQueryString(newText, false); } else if (mRegularSearchFragment != null) { mRegularSearchFragment.setQueryString(newText, false); } mSearchViewCloseButton.setVisibility(View.VISIBLE); mVoiceSearchButton.setVisibility(View.GONE); return; } }在这里完成了三个重要任务:
3、将要搜素的文本传递给搜索列表的Fragment,也就是mSmartDialSearchFragment;
如下图所示:
SmartDialSearchFragment拿到搜索的文本后,需要传递给自己的Adapter才能完成搜索任务,我们现在来分析这个交接的过程。
从上面1.3节中我们看到,SmartDialSearchFragment通过setQueryString()拿到了要搜索的字串,我们来查看这个方法,他是在SmartDialSearchFragment的父类ContactEntryListFragment中被实现的:
@ContactEntryListFragment.java public void setQueryString(String queryString, boolean delaySelection) { if (TextUtils.isEmpty(queryString)) queryString = null; if (!TextUtils.equals(mQueryString, queryString)) { mQueryString = queryString; setSearchMode(!TextUtils.isEmpty(mQueryString)); if (mAdapter != null) { //传递给Adapter mAdapter.setQueryString(queryString); //触发Adapter重新搜索 reloadData(); } } }在这里,Fragment将要搜索的文本通过setQueryString()的方法传递给当前的Adapter,然后通过reloadData()方法触发Adapter的搜索机制。那么这里的Adapter具体是指哪个呢?
@SmartDialSearchFragment.java protected ContactEntryListAdapter createListAdapter() { SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity()); adapter.setUseCallableUri(super.usesCallableUri()); adapter.setQuickContactEnabled(true); adapter.setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false); return adapter; }该Adapter的继承关系如下:
如下图所示:
接下来我们分析如何通过Fragment的reloadData()触发Adapter的搜索。
刚才介绍到,SmartDialSearchFragment在setQueryString()时,通过reloadData()触发了Adapter的搜索,我们来看一下这个流程:
@ContactEntryListFragment.java protected void reloadData() { removePendingDirectorySearchRequests(); mAdapter.onDataReload(); mLoadPriorityDirectoriesOnly = true; mForceLoad = true; //触发新的Adapter startLoading(); } protected void startLoading() { if (mAdapter == null) { return; } //配置Adapter要搜索的文本 configureAdapter(); int partitionCount = mAdapter.getPartitionCount(); for (int i = 0; i < partitionCount; i++) { Partition partition = mAdapter.getPartition(i); if (partition instanceof DirectoryPartition) { DirectoryPartition directoryPartition = (DirectoryPartition)partition; if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) { if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) { startLoadingDirectoryPartition(i); } } } else { //通过LoaderManager进行异步查询 getLoaderManager().initLoader(i, null, this); } } mLoadPriorityDirectoriesOnly = false; }在startLoading()时,通过configureAdapter()对当前的Adapter配置了要搜索的文本、排序方法以及显示主题等信息,然后就 通过LoaderManager进行异步查询。
@SmartDialSearchFragment.java public Loader<Cursor> onCreateLoader(int id, Bundle args) { if (id == getDirectoryLoaderId()) { return super.onCreateLoader(id, args); } else { //创建当前的CursorLoader,也就是SmartDialCursorLoader final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter(); SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext()); adapter.configureLoader(loader); return loader; } }这里创建了SmartDialCursorLoader作为当前的CursorLoader。然后通过adapter的configureLoader()方法将该Loader传递给SmartDialNumberListAdapter,接下来就会在SmartDialCursorLoader中完成异步查询,并将查询结果传递给ContactEntryListFragment的onLoadFinished()方法:
@ContactEntryListFragment.java public void onLoadFinished(Loader<Cursor> loader, Cursor data) { if (!mEnabled) { return; } int loaderId = loader.getId(); if (loaderId == DIRECTORY_LOADER_ID) { mDirectoryListStatus = STATUS_LOADED; mAdapter.changeDirectories(data); startLoading(); } else { //更新Adapter的Cursor onPartitionLoaded(loaderId, data); if (isSearchMode()) { int directorySearchMode = getDirectorySearchMode(); if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) { if (mDirectoryListStatus == STATUS_NOT_LOADED) { mDirectoryListStatus = STATUS_LOADING; getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this); } else { startLoading(); } } } else { mDirectoryListStatus = STATUS_NOT_LOADED; getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); } } } protected void onPartitionLoaded(int partitionIndex, Cursor data) { if (partitionIndex >= mAdapter.getPartitionCount()) { return; } //更新当前的Adapter mAdapter.changeCursor(partitionIndex, data); setProfileHeader(); showCount(partitionIndex, data); if (!isLoading()) { completeRestoreInstanceState(); } }
在onLoadFinished()中,通过onPartitionLoaded()对当前的Adapter所使用的Cursor进行更新,从而刷新列表。
@SmartDialCursorLoader.java public Cursor loadInBackground() { //从dialerDatabaseHelper中查找匹配结果 final DialerDatabaseHelper dialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper( mContext); final ArrayList<ContactNumber> allMatches = dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher); //构建Cursor给Adapter使用 final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; for (ContactNumber contact : allMatches) { row[PhoneQuery.PHONE_ID] = contact.dataId; row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; row[PhoneQuery.CONTACT_ID] = contact.id; row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; row[PhoneQuery.PHOTO_ID] = contact.photoId; row[PhoneQuery.DISPLAY_NAME] = contact.displayName; cursor.addRow(row); } return cursor; }
原来,SmartDialCursorLoader是利用DialerDatabaseHelper进行的查找,他是SQLiteOpenHelper的子类,每次拨号盘进程的创建都会根据当前的通讯录内容创建表单,用于联系人搜索。下面我们从该数据库的创建、查询、更新三个方面来分析其内部原理。
@DatabaseHelperManager.java public static DialerDatabaseHelper getDatabaseHelper(Context context) { return DialerDatabaseHelper.getInstance(context); }然后就会在DialerDatabaseHelper中判断,是否已经有实例对象,没有的话就创建。
@DialerDatabaseHelper.java public static synchronized DialerDatabaseHelper getInstance(Context context) { if (sSingleton == null) { sSingleton = new DialerDatabaseHelper(context.getApplicationContext(), DATABASE_NAME); } return sSingleton; }接下来我们看该数据库的创建过程,主要在onCreate()方法中体现:
public void onCreate(SQLiteDatabase db) { setupTables(db); } private void setupTables(SQLiteDatabase db) { //删除旧表单 dropTables(db); //创建新表“smartdial_table” db.execSQL("CREATE TABLE " + Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + SmartDialDbColumns.DATA_ID + " INTEGER, " + SmartDialDbColumns.NUMBER + " TEXT," + SmartDialDbColumns.CONTACT_ID + " INTEGER," + SmartDialDbColumns.LOOKUP_KEY + " TEXT," + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + " TEXT, " + SmartDialDbColumns.PHOTO_ID + " INTEGER, " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " LONG, " + SmartDialDbColumns.LAST_TIME_USED + " LONG, " + SmartDialDbColumns.TIMES_USED + " INTEGER, " + SmartDialDbColumns.STARRED + " INTEGER, " + SmartDialDbColumns.IS_SUPER_PRIMARY + " INTEGER, " + SmartDialDbColumns.IN_VISIBLE_GROUP + " INTEGER, " + SmartDialDbColumns.IS_PRIMARY + " INTEGER" + ");"); //创建新表“prefix_table” db.execSQL("CREATE TABLE " + Tables.PREFIX_TABLE + " (" + PrefixColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + PrefixColumns.PREFIX + " TEXT COLLATE NOCASE, " + PrefixColumns.CONTACT_ID + " INTEGER" + ");"); //创建新表“properties” db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " (" + PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, " + PropertiesColumns.PROPERTY_VALUE + " TEXT " + ");"); //设置属性 setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION)); //更新时间 resetSmartDialLastUpdatedTime(); }在上面的初始化过程中,最主要的任务就是创建了三张表单:smartdial_table、prefix_table、properties。其中properties表用于存储当前数据库的版本号,与搜索任务无关,我们主要分析其他两个表。
这就是该数据库搜索的原理。
@DialtactsActivity.java protected void onResume() { super.onResume(); //进入数据库更新入口 mDialerDatabaseHelper.startSmartDialUpdateThread(); }然后会在数据库中开启异步线程更新数据:
@DialerDatabaseHelper.java public void startSmartDialUpdateThread() { new SmartDialUpdateAsyncTask().execute(); } private class SmartDialUpdateAsyncTask extends AsyncTask { @Override protected Object doInBackground(Object[] objects) { updateSmartDialDatabase(); return null; } @Override protected void onCancelled() { super.onCancelled(); } @Override protected void onPostExecute(Object o) { super.onPostExecute(o); } }在线程中执行updateSmartDialDatabase()来更新数据:
public void updateSmartDialDatabase() { final SQLiteDatabase db = getWritableDatabase(); synchronized(mLock) { final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null; //获取上一次更新的时间 final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences( DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE); final String lastUpdateMillis = String.valueOf(databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0)); //得到当前的通讯录数据 final Cursor updatedContactCursor = mContext.getContentResolver().query(PhoneQuery.URI, PhoneQuery.PROJECTION, PhoneQuery.SELECT_UPDATED_CLAUSE, new String[]{lastUpdateMillis}, null); //获取当前的时间 final Long currentMillis = System.currentTimeMillis(); if (updatedContactCursor == null) { return; } sInUpdate.getAndSet(true); //删掉已经删除的和无效的联系人记录 removeDeletedContacts(db, lastUpdateMillis); removePotentiallyCorruptedContacts(db, lastUpdateMillis); try { if (!lastUpdateMillis.equals("0")) { removeUpdatedContacts(db, updatedContactCursor); } //向smartdial_table表中插入当前所有有效的联系人数据,以及向prefix_table表中添加联系人号码添加为搜索索引 insertUpdatedContactsAndNumberPrefix(db, updatedContactCursor, currentMillis); } finally { updatedContactCursor.close(); } //从smartdial_table表中读取当前联系人的姓名和号码 final Cursor nameCursor = db.rawQuery( "SELECT DISTINCT " + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + SmartDialDbColumns.CONTACT_ID + " FROM " + Tables.SMARTDIAL_TABLE + " WHERE " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " = " + Long.toString(currentMillis), new String[] {}); if (nameCursor != null) { try { //根据联系人姓名生成相应的数字索引 insertNamePrefixes(db, nameCursor); } finally { nameCursor.close(); } } //创建数据库相应的列 /** Creates index on contact_id for fast JOIN operation. */ db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON " + Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns.CONTACT_ID + ");"); /** Creates index on last_smartdial_update_time for fast SELECT operation. */ db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON " + Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ");"); /** Creates index on sorting fields for fast sort operation. */ db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_sort_index ON " + Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns.STARRED + ", " + SmartDialDbColumns.IS_SUPER_PRIMARY + ", " + SmartDialDbColumns.LAST_TIME_USED + ", " + SmartDialDbColumns.TIMES_USED + ", " + SmartDialDbColumns.IN_VISIBLE_GROUP + ", " + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + SmartDialDbColumns.CONTACT_ID + ", " + SmartDialDbColumns.IS_PRIMARY + ");"); /** Creates index on prefix for fast SELECT operation. */ db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_index ON " + Tables.PREFIX_TABLE + " (" + PrefixColumns.PREFIX + ");"); /** Creates index on contact_id for fast JOIN operation. */ db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON " + Tables.PREFIX_TABLE + " (" + PrefixColumns.CONTACT_ID + ");"); /** Updates the database index statistics.*/ db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE); db.execSQL("ANALYZE " + Tables.PREFIX_TABLE); db.execSQL("ANALYZE smartdial_contact_id_index"); db.execSQL("ANALYZE smartdial_last_update_index"); db.execSQL("ANALYZE nameprefix_index"); db.execSQL("ANALYZE nameprefix_contact_id_index"); sInUpdate.getAndSet(false); final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit(); editor.putLong(LAST_UPDATED_MILLIS, currentMillis); editor.commit(); } }在上面这个更新数据库的过程中,一次完成如下任务:
2、解析联系人姓名为号码时,只对英文姓名有效,这就决定了,无法通过拼音搜索联系人;
@DialerDatabaseHelper.java public ArrayList<ContactNumber> getLooseMatches(String query, SmartDialNameMatcher nameMatcher) { final boolean inUpdate = sInUpdate.get(); if (inUpdate) { return Lists.newArrayList(); } final SQLiteDatabase db = getReadableDatabase(); //准备搜索匹配语句 final String looseQuery = query + "%"; final ArrayList<ContactNumber> result = Lists.newArrayList(); final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null; final String currentTimeStamp = Long.toString(System.currentTimeMillis()); //搜索语句,从prefix_table中搜索匹配项,并从smartdial_table中读取匹配项的详细信息 final Cursor cursor = db.rawQuery("SELECT " + SmartDialDbColumns.DATA_ID + ", " + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + SmartDialDbColumns.PHOTO_ID + ", " + SmartDialDbColumns.NUMBER + ", " + SmartDialDbColumns.CONTACT_ID + ", " + SmartDialDbColumns.LOOKUP_KEY + " FROM " + Tables.SMARTDIAL_TABLE + " WHERE " + SmartDialDbColumns.CONTACT_ID + " IN " + " (SELECT " + PrefixColumns.CONTACT_ID + " FROM " + Tables.PREFIX_TABLE + " WHERE " + Tables.PREFIX_TABLE + "." + PrefixColumns.PREFIX + " LIKE '" + looseQuery + "')" + " ORDER BY " + SmartDialSortingOrder.SORT_ORDER, new String[] {currentTimeStamp}); final int columnDataId = 0; final int columnDisplayNamePrimary = 1; final int columnPhotoId = 2; final int columnNumber = 3; final int columnId = 4; final int columnLookupKey = 5; final Set<ContactMatch> duplicates = new HashSet<ContactMatch>(); int counter = 0; try { //对匹配项去重,并构建搜索结果 while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) { final long dataID = cursor.getLong(columnDataId); final String displayName = cursor.getString(columnDisplayNamePrimary); final String phoneNumber = cursor.getString(columnNumber); final long id = cursor.getLong(columnId); final long photoId = cursor.getLong(columnPhotoId); final String lookupKey = cursor.getString(columnLookupKey); final ContactMatch contactMatch = new ContactMatch(lookupKey, id); //该匹配项已经被收录,无需重复添加到结果中 if (duplicates.contains(contactMatch)) { continue; } final boolean nameMatches = nameMatcher.matches(displayName); final boolean numberMatches = (nameMatcher.matchesNumber(phoneNumber, query) != null); if (nameMatches || numberMatches) { //匹配成功,且没有重复项 duplicates.add(contactMatch); result.add(new ContactNumber(id, dataID, displayName, phoneNumber, lookupKey, photoId)); counter++; } } } finally { cursor.close(); } return result; }在上面的过程中,主要完成两个任务:搜索和去重。