1,簡介:
本文基於Android4.4.2淺析Contacts及相關模塊的功能實現,以及數據庫的操作。
本篇博文主要分析contacts,後續會分析contactsProvider。
聯繫人模塊主要記錄用戶的聯繫人數據,方便用戶快捷的操作和使用,主要包括本機聯繫人和Sim卡聯繫人。
本機聯繫人主要存儲在手機內部存儲空間,Android平臺上是通過數據庫進行存儲,使用ContentProvider組件封裝,提供複雜的字段用於表示聯繫人數據,並提供用戶快捷的操作,比如增加,刪除,修改,查詢等等。
Sim卡聯繫人主要存儲在Sim卡內部存儲文件,包括adn、fdn、sdn。主要提供簡單的字段用於表示聯繫人數據。並通過IccProvider提供的接口進行數據的增加、刪除、修改、查詢操作。
2,軟件架構
聯繫人Contacts應用主要包括3個部分:
1. Contacts主要響應用戶的請求和交互,數據顯示。
2. ContactsProvider繼承自Android四大組件之一的ContentProvider組件,封裝了對底層數據庫contact2.db的添刪改查。
3. SQLite在底層物理性地存儲了聯繫人數據。
主要交互流程如下圖:
Contacts模塊的主要7塊功能:
3,各功能模塊分析:
3.1,聯繫人數據的顯示:
1,聯繫人列表顯示:
簡要說明:
* PeopleActivity類負責聯繫人列表的顯示。
* PeopleActivity包含4個Fragment,每個Fragment包含一個ListView。
* 各個Fragment中ListView的Adapter(BaseAdapter的子類)負責將數據填充到ListView。
* 各個Fragment的Loader類(CursorLoader的子類)負責加載數據。
* 實現LoadertManager接口負責管理這些CursorLoader。
爲什麼使用Loader?
1. Loaders確保所有的cursor操作是異步的,從而排除了UI線程中堵塞的可能性。
2. 當通過LoaderManager來管理,Loaders還可以在activity實例中保持當前的cursor數據,也就是不需要重新查詢(比如,當因爲橫豎屏切換需要重新啓動activity時)。
3. 當數據改變時,Loaders可以自動檢測底層數據的更新和重新檢索。
數據加載流程概覽:
流程具體分析:
先上圖:
- 進入Contacts應用,程序的主入口Activity是
PeopleActivity
。
進入onCreate
方法:
createViewsAndFragments(savedState);
此方法創建視圖和Fragments,進入此方法:
mFavoritesFragment = new ContactTileListFragment();
mAllFragment = new DefaultContactBrowseListFragment();
mGroupsFragment = new GroupBrowseListFragment();
- 1
- 2
- 3
- 1
- 2
- 3
發現創建了3個Fragment,分別是 收藏聯繫人列表、所有聯繫人列表、羣組列表。
- 進入
DefaultContactBrowseListFragment
:
發現DefaultContactBrowseListFragment
的祖父類是:
ContactEntryListFragment<T extends ContactEntryListAdapter>
首先分析此基類:
發現此基類實現了LoadManager
接口,實現了該接口3個重要的抽象方法:
public Loader<D> onCreateLoader(int id, Bundle args);//創建Loader
public void onLoadFinished(Loader<D> loader, D data);//數據加載完畢後的回調方法
public void onLoaderReset(Loader<D> loader);//數據重新加載
- 1
- 2
- 3
- 1
- 2
- 3
該類同時提供了重要的抽象方法:
protected abstract T createListAdapter();//創建適配器Adapter類。
- 1
- 1
這意味着,子類可以按需求創造自己的適配器Adapter類,完成各個子界面Listview的數據顯示,如3.1節圖1所示。
- 然後回到
DefaultContactBrowseListFragment
類:
在執行onCreateView
之前,會執行父類的一些方法,順序如下:
onAttach()
setContext(activity);
setLoaderManager(super.getLoaderManager());
- 1
- 2
- 3
- 1
- 2
- 3
setLoaderManager
中設置當前的LoaderManager
實現類。
加載聯繫人列表數據的過程中,這個類是ProfileandContactsLoader
。
之後執行onCreate
方法。
- 進入
DefaultContactBrowseListFragment
的onCreate(Bundle)
方法:
mAdapter = createListAdapter();
- 1
- 1
發現在這裏創建了ListAdapter
:
DefaultContactListAdapter adapter =
new DefaultContactListAdapter(getContext());
- 1
- 2
- 1
- 2
可以知道創建的ListAdapter類型是DefaultContactListAdapter
並返回到DefaultContactBrowseListFragment
類。
執行完onCreate
方法之後,
執行DefaultContactBrowseListFragment
的onCreateView
方法。
- 進入
DefaultContactBrowseListFragment
的onCreateView
方法:
mListView = (ListView)mView.findViewById(android.R.id.list);
mListView.setAdapter(mAdapter);
- 1
- 2
- 1
- 2
首先獲取了ListView用以填充聯繫人數據,然後設置了適配器,但是此時適配器中的數據是空的,直到後面纔會加載數據更新uI。
在onCreateView
方法執行完之後,在uI可見之前回調執行Activity
的onStart
方法。
- 進入
DefaultContactBrowseListFragment
的onStart
方法:
mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
startLoading();
- 1
- 2
- 1
- 2
首先註冊了一個ContentObserve
的子類監聽數據變化。
然後執行startLoading
方法,目測這應當就是開始加載數據的方法了!
- 進入
DefaultContactBrowseListFragment
的startLoading
方法:
int partitionCount = mAdapter.getPartitionCount();
for (int i = 0; i < partitionCount; i++) {
……
Partition partition = mAdapter.getPartition(i);
startLoadingDirectoryPartition(i);
……}
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
Partition
這個類持有一個Cursor
對象,用來存儲數據。
Adapter
持有的Partition
,Partition
類代表了當前需要加載的Directory
,可以理解爲一個聯繫人集合,比如說本地聯繫人、Google聯繫人……這裏我們假設只加載本地聯繫人數據,所以partitionCount=1。
從這裏我們可以做出猜測:
聯繫人數據不是想象中的分頁(每次N條聯繫人數據)加載,也不是說一次性全部加載,而是一個賬戶一個賬戶加載聯繫人數據,加載完畢一個賬戶就在uI刷新並顯示數據。
- 進入
DefaultContactBrowseListFragment
的startLoadingDirectoryPartition
方法:
loadDirectoryPartition(partitionIndex, partition);
- 1
- 1
進入此方法:
getLoaderManager().restartLoader(partitionIndex, args, this);
- 1
- 1
這個方法是LoaderManager
實現類的方法,參照文檔解釋:
這個方法會新建/重啓一個當前LoaderManager中的Loader,將回調方法註冊給他,並開始加載數據。也就是說會回調LoaderManager的onCreateLoader()方法。
Starts a new or restarts an existing android.content.Loader in this manager, registers the callbacks to it, and (if the activity/fragment is currently started) starts loading it
進入LoadManager接口的實現類:LoaderManagerImpl 的restartLoader方法內部:
LoaderInfo info = mLoaders.get(id);
Create info=
createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
//進入createAndInstallLoader方法:
LoaderInfo info = createLoader(id, args, callback);
installLoader(info);
//進入createLoader方法:
LoaderInfo info = new LoaderInfo(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
Loader<Object> loader = callback.onCreateLoader(id, args);
//關鍵方法出現了!LoadManager接口的抽象方法的onCreateLoader方法被回調了!
//然後installLoader方法啓動了這個Loader!
info.start();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 進入
ContactEntryListFragment
的onCreateLoader
方法,位於DefaultContactBrowseListFragment
的祖父類ContactEntryListFragment
中:
CursorLoader loader = createCursorLoader(mContext);//創建Loader
mAdapter.configureLoader(loader, directoryId);//配置Loader
- 1
- 2
- 1
- 2
發現在此方法中,首先調用createCursorLoader
方法創建了Loader
。
然後通過configureLoader
方法配置Loader
的query
方法的查詢參數,也就是配置SQL中select查詢語句的參數。
這也同時意味着,ContactEntryListFragment
類的子類們可以重寫createCursorLoader
方法以提供適合自身的Loader
,重寫configureLoader
方法爲Loader
配置合適的參數,適配各種自定義的查詢獲取數據。
- 觀察
createCursorLoader
方法在DefaultContactBrowseListFragment
類中實現:
return new ProfileAndContactsLoader(context);
- 1
- 1
直接返回了DefaultContactBrowseListFragment
的數據加載器:ProfileAndContactsLoader
這就是DefaultContactBrowseListFragment
的Loader
實現類(數據加載器)。
- 然後再看一下
ProfileAndContactsLoader
類是如何加載數據的呢?
發現它繼承自CursorLoader
,而CursorLoader
又繼承自AsyncTaskLoader<D>
在關鍵的LoadBackGround()
方法中:
異步調用了ContentResolver
的query
方法:
Cursor cursor = getContext()
.getContentResolver()
.query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder, mCancellationSignal);
cursor.registerContentObserver(mObserver);
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
通過這個Query
方法,實現了對聯繫人數據的查詢,返回Cursor
數據。並綁定了數據監聽器。
- 那麼問題來了
query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder, mCancellationSignal)
- 1
- 2
- 1
- 2
的這些參數那裏指定的呢?
configureLoader
方法在DefaultContactListAdapter
類中實現,實現了對query
參數的配置:
configureUri(loader, directoryId, filter);
loader.setProjection(getProjection(false));
configureSelection(loader, directoryId, filter);
loader.setSortOrder(sortOrder);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
可以看到,配置了Loader
主要的幾個參數:Uri
,Projection
,Selection
,SortOrder
。
這些參數用於最後和ContactsProvider
交互的方法Query
方法中……
-
最終查詢ContactsProvider2
的uri
是:
Uri:content://com.android.contacts/contacts?address_book_index_extras=true&directory=0
- 1
- 1
發現ContentProvider
的服務類似一個網站,uri
就是網址,而請求數據的方式類似使用Get
方式獲取數據。
最後通過ContentProvider2
構建的查詢語句是這樣的:
SELECT
_id, display_name, agg_presence.mode AS contact_presence,
contacts_status_updates.status AS contact_status, photo_id, photo_thumb_uri, lookup,
is_user_profile
FROM view_contacts
LEFT OUTER JOIN agg_presence ON (_id = agg_presence.presence_contact_id) LEFT OUTER JOIN
status_updates contacts_status_updates ON
(status_update_id=contacts_status_updates.status_update_data_id)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
可以發現最後通過ContactsProvider2
實現的查詢,並不是直接查詢相關的表(Contacts
表、rawcontacts
表,data
表……),而是直接查詢view_contacts
視圖,因爲這樣會有更加高的效率。
這也就意味着如果想給聯繫人數據庫新增一個字段供界面使用,僅修改對應的表結構是不行,還要修改對應的視圖才能得到想要的效果。
- 查詢完畢後,回調
LoaderManager
的onLoadFinished
方法,完成對Ui界面的更新:
onPartitionLoaded(loaderId, data);
- 1
- 1
接着進入onPartitionLoaded
方法:
mAdapter.changeCursor(partitionIndex, data);
- 1
- 1
進入這個changeCursor
方法:
mPartitions[partition].cursor = cursor;
notifyDataSetChanged();
- 1
- 2
- 1
- 2
發現在這裏改變了Adapter
的數據集Cursor
,併發出通知數據已經改變,UI進行更新。
至此,默認聯繫人數據的顯示分析到此結束。
其他Fragment
的數據填充基本仍然類似此流程,所不同的只是各自的Fragment
、Adapter
、CursorLoader
以及CursorLoader
配置的參數(uri,projection,selection,args,order……)有所不同。
可以參考下表:
Fragment | Adapter | CursorLoader |
---|---|---|
DefaultContactBrowseListFragment(默認聯繫人列表) | DefaultContactListAdapter | ProfileAndContactsLoader |
ContactTitleListFragment(收藏聯繫人列表) | ContactTileAdapter | ContactTileLoaderFactory StarredLoader |
ContactTitleFrequentFragment(常用聯繫人列表) | ContactTitleAdapter | ContactTileLoaderFactory |
FrequentLoader GroupBrowseListFragment(羣組列表) | GroupBrowseLIstAdapter | GroupListLoader |
GroupDetailFragment(指定ID羣組的聯繫人列表) | GroupMemberTileAdapter | GroupMemberLoader |
ContactDetailFragment(指定ID聯繫人信息) | ViewAdapter | ContactLoader |
2,聯繫人詳細信息數據的顯示:
關鍵類:
ContactDetailActivity
ContactDetailFragment
ContactLoaderFragment //不可見 負責加載聯繫人詳細數據,集成LoadManager對象。
ContactLoader //聯繫人詳細信息Loader。
ContactDetailLayoutController //佈局控制類。
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
原理類似列表顯示,如下簡要說明:
* ContactLoaderFragment
類創建了一個實現LoaderManager.LoaderCallbacks<Contact>
接口的對象,數據類型指定爲Contacts
。負責創建、管理ContactLoader
。
* 得到當前用戶選擇的聯繫人URI
,配置對應的ContactLoader
。
* 後臺數據查詢分完畢後,回調LoadManager
的onLoadFinished()
方法,並將數據以Contacts
的數據類型返回,然後回調ContactDetailLoaderFragmentListener
的onDetailsLoaded()
方法。
* onDetailsLoaded()
方法中,新開一個線程,通過ContactDetailLayoutController
類的setContactData(Conatct)
設置數據,刷新ContactDetailFragment
。
3.2,聯繫人數據的編輯和存儲:
1,編輯界面相關:
聯繫人數據所屬的賬號不同,加載的UI也是不同的,比如Sim卡聯繫人一般只有name,phone num,但是本地賬號聯繫人可能就會有email,address,website等信息……
聯繫人數據UI的加載是通過代碼動態加載的,而不是xml文件寫死的。
那麼問題來了,
新建聯繫人的界面是如何設計?
先進入新建聯繫人界面:
主界面PeopleActivity
中點擊新建聯繫人Button,觸發onOptionsItemSelected
方法中的
case R.id.menu_add_contact
分支:
執行startActivity(intent);
startActivity啓動Intent,Intent的Action設置爲android.intent.action.INSERT
找到匹配此Action的Activity:ContactEditorActivity
ContactEditorActivity
的佈局文件:
ContactEditorActivity
的onCreate()
方法中找到佈局:
setContentView(R.layout.contact_editor_activity);
在xml文件中找到這個佈局:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment class="com.android.contacts.editor.ContactEditorFragment"
android:id="@+id/contact_editor_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
只包含一個Fragment:ContactEditorFragment
。程序解析Xml文件到這裏就會執行ContactEditorFragment
類。
- 進入
ContactEditorFragment
的onCreateView
方法:
//展開佈局
final View view
= inflater.inflate(R.layout.contact_editor_fragment, container, false);
//找到佈局中的一個線性佈局
//關鍵的佈局是contact_editor_fragment中的一個iD爲editors的線性佈局!
mContent = (LinearLayout) view.findViewById(R.id.editors);
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
- 找到
contact_editor_fragment
:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:fadingEdge="none"
android:background="@color/background_primary"
>
<LinearLayout android:id="@+id/editors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
/>
</ScrollView>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
於是確認ContactEditorFragment
的根佈局就是一個id爲editors
的LinearLayout。
想到上一步的語句:
mContent = (LinearLayout) view.findViewById(R.id.editors);
- 1
- 1
所以關鍵就在於,接下來在代碼中爲mContent
這個線性佈局動態添加地了什麼UI,而這些UI纔是真正顯示的東西。
-
ContactEditorFragment
的onCreateView
方法執行完畢之後,會調用onActivityCreate()
方法:
if (Intent.ACTION_INSERT.equals(mAction))
{
final Account account = mIntentExtras == null ? null : (Account)
mIntentExtbindEditorsForNewContactras.getParcelable(Intents.Insert.ACCOUNT);
final String dataSet = mIntentExtras == null ? null :
mIntentExtras.getString(Intents.Insert.DATA_SET);
if (account != null) {
// Account specified in Intent
createContact(new AccountWithDataSet(account.name, account.type, dataSet));}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
上面代碼首先取出了當前Account信息,數據信息。封裝爲一個AccountWithDataSet
對象,作爲createContact
方法的參數。之前我們分析過,編輯界面和賬戶是高度相關的,所以對UI的動態操作必然和Account對象相關。進入createContact
方法。
- 看一下
ContactEditorFragment
中的createContact()
到底對界面幹了什麼!!
createContact
方法中調用了bindEditorsForNewContact(account, accountType)
:
關鍵代碼:
……
final RawContact rawContact = new RawContact();
if (newAccount != null) {
rawContact.setAccount(newAccount);
} else {
rawContact.setAccountToLocal();
}
final ValuesDelta valuesDelta = ValuesDelta.fromAfter(rawContact.getValues());
final RawContactDelta insert = new RawContactDelta(valuesDelta);
……
mState.add(insert);
bindEditors();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
發現暫時還是沒有對界面做什麼事情,任然處於醞釀階段……
首先使用傳入的Accout對象創建一個RawContact
對象,然後使用RawContact
對象構建了一個RawContactDelta
對象insert
,接着就將insert
對象放入RawContactDeltaList
對象mState
中。
RawContact
類:raw contacts數據表內的一條數據,表示一個聯繫人某一特定帳戶的信息。存儲Data表中一些數據行(電話號碼、Email、地址……)的集合及一些其他的信息。
他的存儲結構爲: HashMap<String, ArrayList<ValuesDelta>>
RawContactDelta
類:包含RawContact
對象(即一個聯繫人某一特定帳戶的信息),並具有記錄修改的功能。
RawContactDeltaList
類:內部的存儲結構是ArrayList<RawContactDelta>
,可以理解爲 單個聯繫人所有賬戶的數據集合。
- 然後調用了
bindEditors()
法。
關鍵代碼如下:
……
mContent.removeAllViews();
……
final BaseRawContactEditorView editor;
……
editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view,mContent, false);
//添加視圖了……………………
mContent.addView(editor);
//爲自定義視圖BaseRawContactEditorView設置狀態,必然是修改UI的操作!
editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
可以看到,mContent
這個LinearLayout添加的View是editor,而editor是一個自定義的視圖BaseRawContactEditorView
,佈局是R.layout.raw_contact_editor_view
。
- 找到
raw_contact_editor_view
佈局,發現該佈局包含新建聯繫人頁面所有的UI:
<com.android.contacts.editor.RawContactEditorView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/editor_padding_top">
<include
用戶賬戶相關UI
layout="@layout/editor_account_header_with_dropdown" />
<LinearLayout
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal"
android:paddingTop="8dip">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="0dip"
android:layout_weight="1"
android:orientation="vertical">
<include
Name相關的UI
android:id="@+id/edit_name"
layout="@layout/structured_name_editor_view" />
<include
拼音名
android:id="@+id/edit_phonetic_name"
layout="@layout/phonetic_name_editor_view" />
</LinearLayout>
<include
照片相關的UI
android:id="@+id/edit_photo"
android:layout_marginRight="8dip"
android:layout_marginEnd="8dip"
layout="@layout/item_photo_editor" />
</LinearLayout>
<LinearLayout
中間部分Item的顯示在此處
android:id="@+id/sect_fields"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dip"/>
添加其他字段 按鈕
<Button
android:id="@+id/button_add_field"
android:text="@string/add_field"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="32dip"/>
</LinearLayout>
</com.android.contacts.editor.RawContactEditorView>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
那麼問題來了:中間的那部分佈局(電話、地址……)去哪兒了?
搜索有可能包含這些內容的線性佈局sect_fields
,發現在RawContactEditorView
類中初始化爲mFields
:
mFields = (ViewGroup)findViewById(R.id.sect_fields);
那麼只需要看代碼中對mFields添加了什麼uI!回到之前的
bindEditors()
方法,RawContactEditorView
對象editor
從xml中解析完成後,執行了setState
方法:
editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
- 1
- 1
- 進入
RawContactEditorView
類,找到setState
方法:
public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,boolean isProfile)
……
// 遍歷當前賬戶所有可能的item種類,如電話,姓名,地址……,並分別創建自定義視圖KindSectionView
for (DataKind kind : type.getSortedDataKinds()) {
……
final KindSectionView section = (KindSectionView)mInflater.inflate(
R.layout.item_kind_section, mFields, false);
section.setEnabled(isEnabled());
section.setState(kind, state, false, vig);
mFields.addView(section);
……
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
手機賬戶下的imme
類型如下:
the mimeType isvnd.android.cursor.item/name
the mimeType is#displayName
the mimeType is#phoneticName
the mimeType isvnd.android.cursor.item/photo
the mimeType isvnd.android.cursor.item/phone_v2
the mimeType isvnd.android.cursor.item/email_v2
the mimeType isvnd.android.cursor.item/postal-address_v2
the mimeType isvnd.android.cursor.item/nickname
the mimeType isvnd.android.cursor.item/organization
the mimeType isvnd.android.cursor.item/note
the mimeType isvnd.android.cursor.item/im
the mimeType isvnd.android.cursor.item/sip_address
the mimeType isvnd.android.cursor.item/group_membership
the mimeType isvnd.android.cursor.item/website
發現遍歷了當前賬號類型中所有可能的數據類型(DataKind),
創建了相關的自定義視圖KindSectionView
對象section
,
再將section
對象添加到mFields
中顯示,
這個mFields
正是之前在RawContactEditorView
類中初始化的線性佈局:
mFields = (ViewGroup)findViewById(R.id.sect_fields)。
- 1
- 1
到這裏,基本可以確定,中間部分(也就是除了Name、Photo 和底部的添加字段Button之外的部分),就是通過這個mFields
動態的根據當前賬戶類型添加編輯的KindSectionView
條目來填充的。
首先觀察一下KindSectionView
的佈局文件item_kind_section
:
<com.android.contacts.editor.KindSectionView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include 這是一個TextView,title
android:id="@+id/kind_title_layout"
layout="@layout/edit_kind_title" />
<LinearLayout 線性佈局,用於添加EditText
android:id="@+id/kind_editors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<include 添加新條目的TextView,初始化狀態不可見
android:id="@+id/add_field_footer"
layout="@layout/edit_add_field" />
</com.android.contacts.editor.KindSectionView>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
KindSectionView
加載完xml文件之後,會執行onFinishInflate
方法:
mTitle = (TextView) findViewById(R.id.kind_title);
mEditors = (ViewGroup) findViewById(R.id.kind_editors);
mAddFieldFooter = findViewById(R.id.add_field_footer);
- 1
- 2
- 3
- 1
- 2
- 3
把Xml文件中三個主要的部分都得到了,接下來重點就是觀察代碼中對他們做了什麼。
在第12步中,加載完xml文件之後,執行KindSectionView
的setState
方法:
section.setState(kind, state, false, vig);
- 1
- 1
將rawContactDelta
對象state
傳遞給了KindSectionView
類的setState
方法:
進入KindSectionView
類的setState
方法:
mKind = kind;
mState = state;
rebuildFromState();
- 1
- 2
- 3
- 1
- 2
- 3
先進行局部變量的賦值。
- 然後進入到
rebuildFromState()
方法:
for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
//……遍歷當前賬戶可能的鍵值對,比如電話、Email、地址……
createEditorView(entry); //這個方法應當是創建EditText的方法!
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
在這個方法中,對mState
集合中所有Mime類型的ValuesDelta
集合(ArrayList<ValuesDelta>
類型)進行遍歷,而後將每一個 ValuesDelta
對象 entry
作爲參數調用了createEditorView(entry)
也就是創建各個種類的EditText
方法,根據entry
對象創建相應的EditText
!
簡單說,就是創建mState
中存在的類型的EditText
。
當然……這還都只是猜測,需要進入createEditorView
方法確認。
- 進入
createEditorView
方法:
view = mInflater.inflate(layoutResId, mEditors, false);
Editor editor = (Editor) view;
editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
- 1
- 2
- 3
- 1
- 2
- 3
第13步初始化的mEditors
對象(也就是那個被猜測應該是放EditTExt
的線性佈局)在這裏被使用!
- 聯繫上下文,實際上此時
editor
對象是TextFieldsEditorView
類的對象,進入TextFieldsEditorView
的setValues
方法,看看他是如何根據entry
對象創建EditText
的:
public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,ViewIdGenerator vig)
{
int fieldCount = kind.fieldList.size(); //獲取所有可能的datakind的總數
for (int index = 0; index < fieldCount; index++) //遍歷所有可能的datakind,
{
final EditText fieldView = new EditText(mContext); //創建EditText對象,之後進行配置
fieldView.setLayoutParams……
fieldView.setTextAppearance(getContext(), android.R.style.TextAppearance_Medium);
fieldView.setHint(field.titleRes); //EditText的Hint
……
fieldView.addTextChangedListener(new TextWatcher() //註冊TextChangedListener
{
@Override
public void afterTextChanged(Editable s) {
// Trigger event for newly changed value
onFieldChanged(column, s.toString());
}
mFields.addView(fieldView); //將EditText添加到當前的線性佈局中!
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
註釋基本解釋瞭如何通過一個ValuesDelta
(理解爲鍵值對集合)對象entry
創建佈局中的所有EditText
。
至此,聯繫人編輯界面的顯示原理基本分析完成。
2,數據存儲相關
對聯繫人數據的操作基本流程:
以新增聯繫人爲例:
基本流程圖如下:
總結這個流程:
1. 展開編輯界面視圖,同時創建相應的RawContactDeltaList對象mState。
2. 將用戶輸入的聯繫人信息實時地保存到mState對象中。
3. 用戶點擊保存按鈕,在服務中啓動新線程,根據mState中的對象構建ContentProviderOperation數組(理解爲構建Sql語句)。
4. 將ContentProviderOperation數組交給ContentResolver處理(理解爲執行Sql語句),操作數據庫。
代碼詳細邏輯分析:
第一步,從界面封裝數據: RawContactDeltaList
對象mState
1. 聯繫人編輯界面ContactEditorActivity
,輸入完畢後點擊Save按鈕,觸發ContactEditorFragment
類的save ()
方法:重要代碼如下:
Intent intent =ContactSaveService.createSaveContactIntent(
mContext,mState,SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED,
mUpdatedPhotos);
mContext.startService(intent);
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
可以看到存儲新建聯繫人是通過ContactSaveService
的createSaveContactIntent
方法開始的,重要的是其中第二個參數是RawContactDeltaList
對象mState
,顯然這是從Fragment
的各個EditText
控件返回到ContactSaveService
的數據,那問題就是這個mState
是如何組織數據的呢????
- 首先應該搞清楚這個
mState
是什麼類的對象。
mState
是RawContactDeltaList
類的對象,先分析一下這個類是什麼數據結構:
RawContactDeltaList
類:內部的存儲結構是ArrayList<RawContactDelta>
,可以理解爲 單個聯繫人所有賬戶的數據集合。
RawContactDelta
類:包含RawContact
對象(即一個聯繫人某一特定帳戶的信息)。
RawContact
類:raw contacts
數據表內的一條數據。存儲Data表中一些數據行的集合及一些其他的信息,表示一個聯繫人某一特定帳戶的信息。
所以他的存儲結構爲: HashMap<String, ArrayList<ValuesDelta>>
ValuesDelta
:類似ContentValues
的鍵值對數據結構,是一個HashMap
。用來存儲data表的數據,key爲Mime
類型。
- 從聯繫人編輯界面
ContactEditorFragment
開始:
在Fragment可見之前會執行onActivityCreate
方法:
方法內調用:
bindEditors();
- 1
- 1
進入此方法:
int numRawContacts = mState.size();
for (int i = 0; i < numRawContacts; i++) {
// TODO ensure proper ordering of entities in the list
final RawContactDelta rawContactDelta = mState.get(i);
editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
發現他遍歷了mState
集合
將每一個rawContactDelta
對象作爲參數傳入RawContactEditorView
類的setState
方法。
實際上如果只是保存單個賬戶的聯繫人信息,這裏mState
內的rawContactDelta
對象只會有一個。
- 進入
RawContactEditorView
類的setState
此方法:
public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,
boolean isProfile) {
for (DataKind kind : type.getSortedDataKinds()) {
final KindSectionView section = (KindSectionView)mInflater.inflate(
R.layout.item_kind_section, mFields, false);
section.setEnabled(isEnabled());
section.setState(kind, state, false, vig);
mFields.addView(section);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
可以發現,首先遍歷了當前用戶賬戶所有的可能條目種類,
- 然後又將
rawContactDelta
對象傳遞給了KindSectionView
類的setState
方法:
mKind = kind;
mState = state;
rebuildFromState();
- 1
- 2
- 3
- 1
- 2
- 3
先進行局部變量的賦值,然後進入到rebuildFromState()
方法:
for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
createEditorView(entry);
}
- 1
- 2
- 3
- 1
- 2
- 3
在這個方法中,又對mState
集合中所有Mime
類型的ValuesDelta
集合(ArrayList<ValuesDelta>
類型)進行遍歷,而後將每一個 ValuesDelta entry
對象作爲參數調用了createEditorView
(也就是創建各個種類的EditText)方法,根據entry對象創建相應的EditText
!
- 進入
createEditorView
方法:
editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
- 1
- 1
- 聯繫上下文,實際上此時
editor
對象是TextFieldsEditorView
類,進入TextFieldsEditorView
的setValues
方法,看看他是如何根據entry對象創建EditText的:
public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly, ViewIdGenerator vig)
{
int fieldCount = kind.fieldList.size(); //獲取所有可能的datakind的總數
for (int index = 0; index < fieldCount; index++)//遍歷所有可能的datakind
{
final EditText fieldView = new EditText(mContext);//創建EditText對象,之後進行配置
fieldView.setLayoutParams……
fieldView.setTextAppearance(getContext(), android.R.style.TextAppearance_Medium);
fieldView.setHint(field.titleRes); //EditText的Hint
……
fieldView.addTextChangedListener(new TextWatcher() //註冊TextChangedListener
{
@Override
public void afterTextChanged(Editable s) {
// Trigger event for newly changed value
onFieldChanged(column, s.toString());
}
mFields.addView(fieldView); //將EditText添加到當前的線性佈局中!
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
註釋基本解釋瞭如何通過一個ValuesDelta
(理解爲HashMap)對象entry創建佈局中的所有EditText
。
TextFieldsEditorView
:
Ui中每一個EditText
綁定了監聽器addTextChangedListener
,當EditTest內容發生改變時回調onFieldChanged
方法:進入此方法:
saveValue(column, value);
- 1
- 1
TextFieldsEditorView
的saveValue
方法:
protected void saveValue(String column, String value) {
mEntry.put(column, value);
}
- 1
- 2
- 3
- 1
- 2
- 3
發現這個方法將EditText
中用戶輸入的字符串實時地放到entry
這個以當前column
爲key的ValueData
鍵值對中。
回顧到第5步中:KindSectionView
類中的for循環遍歷操作:entry
是mState
集合的一個對象,因此也就是說:當用戶編輯EditText
的同時,也改變了mState
集合。
以上,就是ContactSaveService
的createSaveContactIntent
中第二個關鍵參數mSTate
對象的由來。
第二步,將數據封裝爲ContactsProviderOperation
數組,並提交:
這個對象mState
很重要,因爲當用戶點擊保存Button時,
就會啓動ContactSaveService
的createSaveContactIntent
方法,開始保存聯繫人的操作:
1, 進入此方法:
public static Intent createNewRawContactIntent(Context context,
ArrayList<ContentValues> values, AccountWithDataSet account,Class<? extends Activity> callbackActivity, String callbackAction)
{
serviceIntent.putParcelableArrayListExtra(
ContactSaveService.EXTRA_CONTENT_VALUES, values);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
第二個參數values
就是上文中分析的mState
,可以看到放到了Intent中傳遞,key是EXTRA_CONTENT_VALUES,後面會通過Intent傳遞這個對象(已經實現parcelable接口)。
2, 完成上述操作之後,在ContactEditorFragment
中會調用startService
啓動ContactService
:
此Service繼承自IntentService
,在單獨的Thread執行聯繫人的添刪改查(耗時)操作。
在ContactSaveService
啓動之後,在onHandleIntent(Intent)
中對Intent的action進行匹配:
if (ACTION_SAVE_CONTACT.equals(action)) {
saveContact(intent);
CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
3, 可以發現對存儲聯繫人匹配的分支調用了ContactSaveService
的saveContact
方法:
private void saveContact(Intent intent) {
//1,得到之前保存的mState對象,賦值給state
RawContactDeltaList state = intent
.getParcelableExtra(EXTRA_CONTACT_STATE);
//2,在這裏建立ContentProviderOperation操作數組
final ArrayList<ContentProviderOperation> diff=
state.buildDiff();
ContentProviderResult[] results = null;
//3,提交給ContentResolver,批量提交操作。
results = resolver.applyBatch(ContactsContract.AUTHORITY,
diff);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
4, 首先取出Intent中傳遞過來的mState
,執行buildDiff()
構造ContentProviderOperation
數組diff
。
那麼這段程序的的迷惑之處在於ContentProviderOperation
數組是怎麼通過一個
RawContactDeltaList
對象mState
構建的?
5, 進入RawContactDeltaList
的diff
方法:
for (RawContactDelta delta : this) {
//此處傳入的diff是一個空ArrayList<ContentProviderOperation>對象:
delta.buildDiff(diff);
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
在for循環中遍歷了this對象,也就是mState
對象,取出了其中所有的RawContactDelta
對象,如果只是保存到一個賬戶,這裏的RawContactDelta
對象只會有一個。
然後調用delta.buildDiff(diff)
,diff
是一個空ArrayList<ContentProviderOperation>
對象,並且此時是空對象。
6, 進入RawContactDelta
的buildDiff(ArrayList<ContentProviderOperation> buildInto)
方法:
for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
for (ValuesDelta child : mimeEntries) {
//根據child鍵值對構建builder對象
builder = child.buildDiff(Data.CONTENT_URI);
//構造ContentProviderOperation數組!
possibleAdd(buildInto, builder);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
有兩層for循環,完成了對ContentProviderOperation
數組的構建。
外層的循環mEntries
對象包含了被保存的聯繫人所有的數據,本身的數據結構是HashMap<String, ArrayList<ValuesDelta>>
,
此時mEntries
的數據類似這樣:
可以觀察到,mEntries
的key是mime
類型,value是對應的data
值組成的ArrayList
。
{
vnd.android.cursor.item/phone_v2=[{ IdColumn=_id, FromTemplate=false, is_super_primary=0, data_version=0, mimetype=vnd.android.cursor.item/phone_v2, data2=2, is_primary=0, _id=593, data1=1111-111-1111, }],
vnd.android.cursor.item/postal-address_v2=[{ IdColumn=_id, FromTemplate=false,
data_version=0, mimetype=vnd.android.cursor.item/postal-address_v2, _id=594,
data1=ManchesterUnitid, is_super_primary=0, data4=ManchesterUnitid, data2=1,
is_primary=0, }],
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
7, 最終在內層循環調用了ValuesDelta
的buildDiff
方法,參數targetUri=Data.CONTENT_URI
:
public ContentProviderOperation.Builder buildDiff(Uri targetUri)
{
ContentProviderOperation.Builder builder = null;
if (isInsert()) {
mAfter.remove(mIdColumn);
//創建builder對象,關聯目標table,這裏是Data表
builder = ContentProviderOperation.newInsert(targetUri);
//配置builder參數
builder.withValues(mAfter);
}
//返回
return builder;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
這裏用到了ContentProviderOperation
的內部類Builder
,那Builder
類是什麼?
簡單說來就是爲ContentProviderOperation
數組的構建提供服務的。
一個ContentProviderOperation
對象構建的基本流程就是這樣的:
* ContentProviderOperation.newInsert(targetUri)
創建builder對象
* Builder.withValues(ContentValues values)
按照ContentValues構建Builder的參數
* 最後調用Builder
的build()
方法返回一個ContentProviderOperation
對象!
Builder簡介:
Used to add parameters to a ContentProviderOperation. The Builder is first created by calling ContentProviderOperation.newInsert(android.net.Uri),
ContentProviderOperation.newUpdate(android.net.Uri),
ContentProviderOperation.newDelete(android.net.Uri)
or ContentProviderOperation.newAssertQuery(Uri).
The withXXX methods can then be used to add parameters to the builder. See the specific methods to find for which Builder type each is allowed. Call build to create the ContentProviderOperation once all the parameters have been supplied.
8, 然後看方法體內執行:
可以看到執行了ContentProviderOperation
的靜態方法:newInsert(targetUri)
,完成了對ContentProviderOperation.builder
的創建,然後使用builder.withValues(mAfter)
完成了對builder
添加的參數。
9, 方法返回後回到第六步:
//根據child鍵值對構建builder對象
builder = child.buildDiff(Data.CONTENT_URI);
// 構造ContentProviderOperation數組!
possibleAdd(buildInto, builder);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
發現構建builder
對象並在其中配置參數之後,馬上執行了possibleAdd
方法。
10, 進入possibleAdd
方法:
private void possibleAdd(ArrayList<ContentProviderOperation> diff,
ContentProviderOperation.Builder builder) {
if (builder != null) {
//在這裏構建了ContactsProviderOperation數組!
diff.add(builder.build());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
經過以上分析,
經過層層遍歷,完成了ContentProviderOperation
數組的構造,
這時候構建完畢的ContentProviderOperation
數組diff
是類似這樣的:
[
mType: 1, mUri: content://com.android.contacts/raw_contacts,
mSelection: null, mExpectedCount: null, mYieldAllowed: false, mValues: aggregation_mode=2 data_set=null account_type=null account_name=null, mValuesBackReferences: null,
mSelectionArgsBackReferences: null,
mType: 1, mUri: content://com.android.contacts/data, mSelection: null, mExpectedCount: null, mYieldAllowed: false, mValues: data2=2 mimetype=vnd.android.cursor.item/phone_v2
data1=1111-111-1111, mValuesBackReferences: raw_contact_id=0, mSelectionArgsBackReferences: null,
mType: 1, mUri: content://com.android.contacts/data, mSelection: null, mExpectedCount: null, mYieldAllowed: false, mValues: data2=1
mimetype=vnd.android.cursor.item/postal-address_v2
data1=ManchesterUnitid, mValuesBackReferences: raw_contact_id=0,
mSelectionArgsBackReferences: null,
]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
11, 程序返回到第3步
最初的ContactSaveService
類:
回到saveContact
方法,將構造完畢ContentProviderOperation
數組diff
作爲參數,調了用contentResolver
的
applyBatch(ContactsContract.AUTHORITY,diff)
提交修改……
基本流程就是到這裏就OK了,接下去就是ContatcsProvider
與數據庫的操作了。
與ContactsProvider2、數據庫的交互
1, 之後深入Contacts的數據操作層ContactsProvider2
:
然後追溯ContentResolver
的applyBatch()
方法:
ContentProviderClient provider = acquireContentProviderClient(authority);
return provider.applyBatch(operations);
- 1
- 2
- 1
- 2
2, 根據authority
參數,可以知道acquireContentProviderClient
方法返回的provider
是ContactsProvider2
,所以之後調用了ContactsProvider2
的applyBatch
方法:
return super.applyBatch(operations);
- 1
- 1
3, 調用了父類AbstractContentProvider
中的applyBatch
方法:
final int numOperations = operations.size();
final ContentProviderResult[] results =
new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
results[i] = operation.apply(this, results, i);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
4, 發現最終執行了ContentProviderOperation.apply()
方法:
if (mType == TYPE_INSERT) {
Uri newUri = provider.insert(mUri, values); }
if (mType == TYPE_DELETE) {
numRows = provider.delete(mUri, mSelection, selectionArgs);
} else if (mType == TYPE_UPDATE) {
numRows = provider.update(mUri, values, mSelection, selectionArgs);
} else if (mType == TYPE_ASSERT) {
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
我們執行的新增聯繫人操作,也就是Insert操作。
5, 因此進入Insert分支,調用了ContentProvider2
的insert
方法:
Uri result = insertInTransaction(uri, values);
- 1
- 1
6, 追蹤到ContactsProvider2.insertInTransaction(Uri uri, ContentValues values)
方法:
實現了對URI的匹配,確定執行對哪個數據庫的進行插入操作,如果uri是對Data表的操作:
//匹配URI對應的Insert操作表:
case DATA:
case PROFILE_DATA: {
invalidateFastScrollingIndexCache();
id = insertData(values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
7, 最後ContactsProvider2. insertData
方法實現了對底層數據庫的直接操作:
final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
//根據mime類型獲取合適 的DataRowHandler類對象
DataRowHandler rowHandler = getDataRowHandler(mimeType);
//使用合適的DataRowHandler對象直接對數據庫執行操作
id = rowHandler.insert(db, mTransactionContext.get(), rawContactId, mValues);
return id;
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
至此,一個聯繫人的插入操作分析完畢。
添刪改查操作的基本流程都類似。
值得注意的是刪除聯繫人並不是真正的刪除聯繫人數據。
用戶在聯繫人列表選擇聯繫人的刪除,本地聯繫人url匹配只是刪除contacts
表中的數據,標記raw_contacts
表的字段deletde
爲1,而Data
表的數據並沒有發生變化。url匹配刪除Sim卡聯繫人或者同步聯繫人時刪除,會直接刪除raw_contacts
表的數據,並觸發觸發器raw_contacts_deleted
,將data
表,agg_exceptions
表,contacts
表的數據全部刪除。
當用戶進入到聯繫人編輯界面,刪除某個數據。也就是隻對聯繫人的data數據進行刪除,而聯繫人數據未發生變化,這樣會根據刪除內容獲得ContentProviderOperation
數組。最後會調用applyBatch()
函數進行數據更新。
調用applyBatch()
函數過程中,會讀取ContentProviderOperation
數組,而數組的每一條記錄都會帶有一個URI,通過匹配URI
,找到對應的表進行刪除操作。操作成功後得到返回結果。
最後根據mimetype
類型數據,獲得不同的DataRowHandler
,進行data
數據的刪除。
3.3,Sim聯繫人數據的整合
實時獲得Sim卡的狀態,對Sim上的聯繫人導入到本地數據庫,或者將本地數據中Sim卡聯繫人刪除。數據庫Contacts表和raw_Contacts表表中有字段indicate_phone_or_sim_contact表示是否爲Sim卡聯繫人,並區分出Sim1,Sim2上的聯繫人。
Mtk平臺中實現了開機自動導入SIm卡聯繫人數據的功能。
1,Sim卡聯繫人數據的顯示:
- 由
SimContacts
類負責顯示Sim卡中的聯繫人數據,並與用戶交互。 AdnList
負責與Sim卡交互,SimaContacts
繼承自AdnList
,而AdnList
繼承自ListActivity
。
如何交互Sim卡數據?
這裏主要是簡單應用層的操作,IccProvider
屏蔽了sim卡內部複雜的操作。
使用一個繼承自AsyncHandler
的QueryHandler
類封裝了異步查詢操作:
AsyncHandler類的定義
說明:A helper class to help make handling asynchronous ContentResolver queries easier.
ContentResolver異步查詢操作的幫助類,其實它同樣可以處理增刪改。
AsyncQueryHandler的作用
查詢其API便可知,它擔供:
startInsert
startDelete
startUpdate
startQuery
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
這四個操作,並提供相對應的onXXXComplete
方法,以供操作完數據庫後進行其它的操作,這四個 onXXXComplete
方法都是抽象方法,可以在子類中實現想要的操作,在本例中是使用QueryHandler類實現了這幾個方法,查詢完畢後將數據填充到ListView
中。
爲什麼要使用AsyncQueryHandler
當然也可以使用ContentProvider去操作數據庫。這在數據量很小的時候是沒有問題的,但是如果數據量大了,可能導致UI線程發生ANR事件。當然你也可以寫個Handler去做這些操作,只是你每次使用ContentProvider時都要再寫個Handler,必然降低了效率。
因此API提供了一個操作數據庫的通用方法。
如何使用AsyncQueryHandler
指定需要查詢的數據的URI,與ContentResolver的query方法中的參數並無太大區別。
本例中查詢Sim卡聯繫人數據的的uri是:
Uri.parse("content://icc/adn");
- 1
- 1
然後調用查詢方法:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri, COLUMN_NAMES,
null, null, null);
- 1
- 2
- 1
- 2
2,開機自動導入Sim卡聯繫人:
先上流程圖:
具體分析:
1. 註冊一個BootCmpReceiver
檢測開機廣播事件,被觸發之後啓動SIMProcessorService
。
2. SIMProcessorService
繼承service
組件,主要實例化了SIMProcessorManager
對象,註冊了ProcessorManagerListener
監聽器。接受到BootCmpReceiver
發送過來的處理Intent
之後,調用SIMProcessorManager
的handleProcessor
方法。
3. SIMProcessorManager
的handleProcessor
方法根據傳遞過來的Intent
,在createProcessor
方法中創建相應的Processor
,比如SIMImportProcessor
、SIMRemoveProcessor
、SIMEditProcessor
、SIMDeleteProcessor
。可以觀察到有導入SIm卡數據、移除Sim卡數據、編輯Sim卡數據、刪除Sim卡數據。
4. 本次操作爲導入SIm卡聯繫人,所以創建的Processor爲SIMImportProcessor
,發現其基類爲ProcessorBase
,實現了Runnable
接口,因此SIMImportProcessor
類可以理解爲Thread
的target
,其run()
方法是線程執行體。
5. SIMImportProcessor
的run()
方法實現了什麼功能?首先調用ContentResolver
的query
方法,指定uri
爲SIm卡聯繫人數據的uri
,並進行查詢操作(通過匹配Authority
可以得知這裏調用的其實是IccProvider
類),得到聯繫人數據遊標Cursor
對象。這一步完成了Sim卡聯繫人數據的讀取。
6. 然後再執行importAllSimContacts
方法,構建ContentProviderOperation
數組OperatioList
,通過ContentResolver
的applyBatch(uri, OperatioList)
方法批量提交對IccProvider
的操作,也就是對數據庫的操作,。這一步完成了Sim卡聯繫人數據寫入到Sqlite。
7. 那麼,這個線程池是什麼時候啓動的呢?在第三步的createProcessor
方法之後,將創建的Processor
添加到ProcessorManager
,ProcessorManagerListener
監聽器會執行Excute
方法執行被添加的Processor
其內部機制在線程池中執行Processor
類。
3,telephony中IccProvider淺析:
預備知識:
Sim卡中存儲的號碼的類型:
ADN: Abbreviated dialing number, 就是常規的用戶號碼,用戶可以存儲/刪除。
FDN:Fixed dialer number,固定撥號,固定撥號功能讓您設置話機的使用限制,當您開啓固定撥號功能後,您只可以撥打存儲的固定撥號列表中的號碼。固定號碼錶存放在SIM卡中。能否使用固定撥號功能取決於SIM卡類型以及網絡商是否提供此功能。
SDN:Service dialing number,系統撥叫號碼,網絡服務撥號,固化的用戶不能編輯。
從以上的描述,可以看到,一般情況下訪問SIM卡聯繫人數據就是訪問ADN。
時序圖:
上述第二節,第五步執行ContentResolver
的query
方法時,根據Authority
可以得知匹配的ContentProvider
是IccProvider
:
在
IccProvider
的Query
方法中,會執行loadFromEf
方法。在
loadFromEf
中,要先得到一個IIccPhoneBook
對象:
IIccPhoneBook iccIpb =
IIccPhoneBook.Stub.asInterface(ServiceManager.getService("simphonebook");
- 1
- 2
- 1
- 2
發現這個對象是用AIDL接口來獲取到的,
那麼 ServiceManager.getService("simphonebook")
究竟獲取了一個什麼實體對象呢?
- 先不着急找到這個實體對象,
發現程序之後在ICCProvider
中調用Stub
的實體類的getAdnRecordsInEf
方法:
adnRecords = iccIpb.getAdnRecordsInEf(efType);
- 1
- 1
這是通過AIDL接口實現的方法調用,最終是調用到了Stub
實體類的getAdnRecordsInEf
方法.
那麼可以知道,這個實體類首先必然存在getAdnRecordsInEf()
方法,這個實體類的對象是通過ServiceManager.getService
來獲取的,那麼找到addService
的地方就可以發現它了。
- 全局搜索後,發現
IccPhoneBookInterfaceManagerProxy
類符合需求:在他的構造函數中執行了addService()
方法,而且存在getAdnRecordsInEf
方法,
判斷IccPhoneBookInterfaceManagerProxy
類就是上文中的Stub
實體類。
IccPhoneBookInterfaceManagerProxy
類繼承了IIccPhoneBook.Stub
,
在它的構造函數中執行了addService
方法:
String serviceName =
PhoneFactory.getServiceName("simphonebook", phoneId);
if(ServiceManager.getService(serviceName) == null) {
ServiceManager.addService(serviceName, this);
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
addService
方法傳入參數爲當前IccPhoneBookInterfaceManagerProxy
類的對象,
因此,在IIccPhoneBook iccIpb =IIccPhoneBook.Stub.asInterface(ServiceManager.getService("simphonebook");
是獲得的就是IccPhoneBookInterfaceManagerProxy
類的對象。
- 那麼在前面第3步提到的
ICCProvider
類中iccIpb.getAdnRecordsInEf
方法實際就調用到了IccPhoneBookInterfaceManagerProxy
類的getAdnRecordsInEf
方法。
在getAdnRecordsInEf
方法中,執行:
mIccPhoneBookInterfaceManager.getAdnRecordsInEf(efid);
- 1
- 1
看到getAdnRecordsInEf
這個方法名就可以知道,這個方法是獲取Sim卡內Adn類型聯繫人數據的方法。
- 類
IccPhoneBookInterfaceManager
中實現了getAdnRecordsInEf
方法
在getAdnRecordsInEf
方法中,執行:
adnCache.requestLoadAllAdnLike(efid, adnCache.extensionEfForEf(efid), response);
- 1
- 1
adnCache
是類AdnRecordCache
的對象
7. AdnRecordCache
:
在類AdnRecordCache
中實現了requestLoadAllAdnLike
方法,
requestLoadAllAdnLike
中,執行:
new AdnRecordLoader(mFh).loadAllFromEF
(efid, extensionEf,obtainMessage(EVENT_LOAD_ALL_ADN_LIKE_DONE, efid, 0))
- 1
- 2
- 1
- 2
這裏實例化一個類AdnRecordLoader
的對象,並且調用該對象的loadAllFromEF
方法
- 進入
AdnRecordLoader
在類AdnRecordLoader
中實現了loadAllFromEF
方法,
在loadAllFromEF
方法中,執行:
mFh.loadEFLinearFixedAll(ef, obtainMessage(EVENT_ADN_LOAD_ALL_DONE));
- 1
- 1
mFh
是類IccFileHandler
的對象,實際上是它的子類TDUSIMFileHandler
的對象,繼承關係是:TDUSIMFileHandler
繼承自SIMFileHandler
繼承自IccFileHandler
9. 進入IccFileHandler
在類IccFileHandler
中實現了loadEFLinearFixedAll
方法,
在loadEFLinearFixedAll
方法中,執行:
mCi.iccIOForApp(COMMAND_GET_RESPONSE,fileid, getEFPath(fileid, is7FFF),
0, 0, GET_RESPONSE_EF_SIZE_BYTES,null, null, mAid, response);
- 1
- 2
- 1
- 2
聯繫上下文,mCi
是RIL
類的對象,RIL
是Radio Interface Layer
的縮寫,即無線接口通信層。
之後涉及的東西比較底層……以後再慢慢分析……
- 經過與底層數據的交互,
可以在IccPhoneBookInterfaceManager
類的IccPhoneBookInterfaceManager
方法返回得到Sim卡Adn聯繫人數據List<AndRecord>
數據。 - 返回
IccProvider
的loadFromEf
方法:
for (int i = 0; i < size; i++) {
loadRecord(adnRecords.get(i), cursor, i);
}
- 1
- 2
- 3
- 1
- 2
- 3
發現遍歷了List<AndRecord>
中的數據,放到cursor
對象中,最後返回這個cursor
對象,也就是返回給了最初IccProvider
調用的query
方法返回的Cursor
對象。
4,Sim卡聯繫人的手動導入導出:
導入的基本流程與開機導入Sim卡聯繫人類似,同樣是先query得到SIM卡聯繫人數據,然後寫入聯繫人數據庫,不再做分析。
導出流程就是反過來……
華爲的需求:手機聯繫人詳情界面增加一個導出到卡1/卡2/Exchange賬戶的optionMenu。
具體做的時候完全可以走SIm卡聯繫人導入導出的流程,只需要指定導入導出數據的uri即可。
3.4,SD卡備份/恢復聯繫人
SD卡導入導出主要是通過vCard的形式,存儲到sd卡或者從sd卡讀取指定的vCard文件並進行解析。
1,從Sd卡恢復/import聯繫人數據
從Sd卡導入聯繫人主要流程:
1. 聯繫人主界面PeopleActivity
響應選項菜單的onOptionsItemSelected
事件。
2. 彈出Import/Export對話框ImportExportDialogFragment
中選擇Import,啓動導入界面ImportVCardActivity
.
3. ImportVCardActivity
最終通過startService
啓動VCardService
服務。
4. ImportVCardActivity
啓動VCardCacheThread
來進行將Vcard文件從外部Sd卡複製到手機內部存儲,然後構建Importrequests
數組,該數組封裝了被導入Vcard文件的信息。
5. 通過調用VCardService
的HandleImportRequest
通知VCardService
導入VCard。
6. VCardService
啓動ImportProcessor
線程,通過實現的一個Vcard文件解析類VCardEntryConstructor
類,第4步構建的Importrequests
數組作爲參數,依次導入VCard中的每個聯繫人(readOneVCard
方法)。
7. 在VCardService
處理導入的過程中,會把過程狀態通知給NotificationImportExportListener
,後者負責更新通知狀態欄中的信息。
visio時序圖如下:
2,聯繫人數據導出到Sd卡
聯繫人導出到Sd卡與導入流程類似,略。
3.5,聯繫人搜索:
在Contacts應用內搜索聯繫人, 主要步驟:
1. 在主界面PeopleActivity
點擊聯繫人搜索按鈕,觸發onAction
方法。
2. 調用restartLoader
來啓動Loader
異步更新數據。
3. 在LoadeManager
的回調接口onCreateLoader
創建、配置Loader
,包括查詢的Uri
等,此階段配置的uri參數爲Contacts.CONTENT_FILTER_URI
。
4. Loader
啓動後調用ContactProvider2
的query
方法,
匹配的uri爲Contacts.CONTENT_FILTER_URI
的分支。
5. 在ContactProvider2
的appendSearchIndexJoin
方法中拼接Sql語句,並調用Sqlite的底層query
語句查詢。
6. 完成查詢後回調LoaderManager
的onLoaderFinish
刷新UI。
visio時序圖如下:
搜索tan字符串的時候,sql語句爲:
SELECT _id, display_name, agg_presence.mode AS contact_presence, contacts_status_updates.status AS contact_status, photo_id, photo_thumb_uri, lookup, is_user_profile, snippet
FROM view_contacts
JOIN (SELECT contact_id AS snippet_contact_id, content AS snippet FROM search_index WHERE search_index
MATCH 'content:tan* OR name:B791AB* OR tokens:tan*'
AND snippet_contact_id IN default_directory) ON (_id=snippet_contact_id) LEFT OUTER JOIN agg_presence ON (_id = agg_presence.presence_contact_id) LEFT OUTER JOIN status_updates contacts_status_updates ON (status_update_id=contacts_status_updates.status_update_data_id) ORDER BY phonebook_bucket, sort_key
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
發現其實最後經過sqlite語句的拼接,查詢的是view_contacts
視圖。
android聯繫人的搜索機制如下:
當新建了一個聯繫人的時候,例如名字爲:【abcd】,那麼會在raw_contacts表的對應數據中的display_name顯示【abcd】,同時在insert的時候會在name_lookup表中存儲進去一個normallized_name字段,這個字段是根據名字【abcd】轉換成的16進制碼,使用的方法是NameNormalize.normalize()方法。
在查詢的時候使用的是ContactsProvider2裏面的query方法,當輸入查詢條件時【a】,會使用NameNormalize.normalize()方法將【a】轉換成16進制碼,然後再進入name_lookup中去查詢對應的raw_contacts_id,從而對應出contact顯示在界面上。
也就是說,google 的查詢並不是根據display_name來進行的,而是通過轉換的normallized_name來進行匹配查詢,在NameNormalizer.Java文件中定義了normalize方法,這個方法是進行轉換的方法,對於數字和字母,做了直接轉換的處
理,對於一些特殊字符做了特別的處理,舉例如下:
如果輸入的是【,】【.】那麼google會將這種字符當作分隔符,即輸入【a,b,c.d】的話,名字就是【a,b,c.d】,在處理這個名字的時候首先按照【,】【.】來進行分割,分割成【a】【b】【c】【d】後再轉換成lookup條件,那麼此時在查詢的時候輸入了【a】,匹配到【a,b,c.d】,再輸入【,】時,系統會認爲輸入的是分隔符,那麼會認爲用戶想要查詢結果的基礎上再次進行查詢,也就是常說的在搜索結果中繼續查詢,所以此時再輸入【a】的時候系統就會認爲是在上一次的結果中(【a,b,c.d】)再此查詢【a】,那麼還是可以匹配到【a,b,c.d】,所以造成了下面的現象:
1.輸入【a,a】/【a,c】/【d,d】/…..
2.查詢出結果【a,b,c.d】.
而對於一些其他的特殊字符(非數字,非字符),如【@】【_】等等,在轉換的時候會自動將這些字符過濾掉,但卻保留了分割的特性,即
出現瞭如下的現象:
1.保存聯繫人名稱爲【first@second#three】
2.輸入條件【firstsecond】,結果爲:【first@second#three】
3.輸入條件【three,second】,結果爲:【first@second#three】(因爲保留的分割特性)
4.輸入條件【first@se】,無結果(因爲轉換時去掉了字符@)
上述即爲google default對於查詢的機制,關於轉換的代碼可以在NameNormalizer.java中進行分析。
3.6,Google聯繫人同步
通過添加Google帳號,並開啓同步,則會將Gmail中聯繫人同步到本地數據庫,也可以將本地聯繫人同步到Gmail中。而且也支持Exchange服務帳號同步。
3.7,其他零碎功能:
1,聯繫人分享
關鍵類:
ContactDetailActivity
:聯繫人詳細信息顯示界面。
ContactLoaderFragment
:被包含於Aty中的Fragment。
ContactDetailLayoutController
:控制佈局,填充數據。
分享聯繫人的實現步驟:
* 在聯繫人詳細信息界面,選擇分享。
* 得到當前聯繫人的uri
* 設置Intent屬性、攜帶指定聯繫的uri:
final Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(Contacts.CONTENT_VCARD_TYPE);
intent.putExtra(Intent.EXTRA_STREAM, shareUri);
- 1
- 2
- 3
- 1
- 2
- 3
- 創建Intent選擇器:藍牙/email/Nfc/其他應用……
final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
- 1
- 1
- 跳轉到用戶選擇的分享聯繫人方式的應用(藍牙/Nfc/Email/其他應用……),聯繫人數據通過intent傳遞。
2,桌面快捷方式和文件夾
createLauncherShortcutWithContact()
ShortcutIntentBuilder
- 1
- 2
- 3
- 1
- 2
- 3
3,聯繫人字符分組、字母表導航效果實現機制:
關鍵問題:需要知道聯繫人名字的首字母。
把中文轉換爲拼音字符,這樣就可以實現排序,按照字母導航的效果。
發現在rawContacts
表中:
發現Android已經在Sqlite中自動實現了 漢字-拼音 轉換功能,直接讀取sort_key
這個列的數據就可以。
1,得到聯繫人數據,並按照sort_key
排序,通過listview顯示。
2,用戶拖動滑動塊時顯示字母提示框(A_Z)。
上面的實現都比較簡單,問題是sort_key是如何自動生成的?
HanziToPinyin.java
在ContactsProvider2
下,負責將漢字轉化爲拼音
4,聯繫人側邊欄字母導航條如何實現?
Android L中contact應用是沒有側邊欄的,但是字母導航的數據仍然是可以讀到,我們只需要搞個自定義控件,畫出A-Z的字母導航條,並監測觸摸事件,在Contacts中的listview
中setSelection
點擊的字母位置就可以。
public class SideBar extends View {
public static String[] b = { "#", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z"};
protected void onDraw(Canvas canvas) {
int height = getHeight()-20;
int width = getWidth();
int singleHeight = height / index.length;
for (int i = 0; i < index.length; i++) {
paint.setColor(android.graphics.Color.parseColor("#b6b6b6"));
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setAntiAlias(true);
paint.setTextSize(20);
if (i == choose) {
paint.setColor(Color.parseColor("#28c0c6"));
paint.setFakeBoldText(true);
}
float xPos = width / 2 - paint.measureText(index[i]) / 2;
float yPos = singleHeight * i + singleHeight;
canvas.drawText(index[i], xPos, yPos, paint);
paint.reset();
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final int action = event.getAction();
final float y = event.getY();
final int oldChoose = choose;
final OnTouchingLetterChangedListener listener =
onTouchingLetterChangedListener;
final int c = (int) (y / getHeight() * index.length);
if (event.getX() > getWidth()) {
setBackgroundDrawable(new ColorDrawable(0x00000000));
choose = -1;
invalidate();
if (mTextDialog != null) {
mTextDialog.setVisibility(View.INVISIBLE);
}
return true;
}
switch (action) {
case MotionEvent.ACTION_UP:
setBackgroundDrawable(new ColorDrawable(0x00000000));
choose = -1;//
invalidate();
if (mTextDialog != null) {
mTextDialog.setVisibility(View.INVISIBLE);
}
break;
default:
// setBackgroundResource(R.drawable.sidebar_background);
if (oldChoose != c) {
if (c >= 0 && c < index.length) {
if (listener != null) {
listener.onTouchingLetterChanged(index[c]);
}
if (mTextDialog != null) {
mTextDialog.setText(index[c]);
mTextDialog.setVisibility(View.VISIBLE);
}
choose = c;
invalidate();
}
}
break;
}
return true;
}
}
至此contacts應用層的分析基本結束,後面會再寫數據層contactsProvider2的分析。