Android contacts 聯繫人 通訊錄 源碼 完全解析

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可以自動檢測底層數據的更新和重新檢索。

數據加載流程概覽:

這裏寫圖片描述

流程具體分析:

先上圖:

這裏寫圖片描述

  1. 進入Contacts應用,程序的主入口Activity是PeopleActivity
    進入onCreate方法:
    createViewsAndFragments(savedState);
    此方法創建視圖和Fragments,進入此方法:
mFavoritesFragment = new ContactTileListFragment();
mAllFragment = new DefaultContactBrowseListFragment();
mGroupsFragment = new GroupBrowseListFragment();
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

發現創建了3個Fragment,分別是 收藏聯繫人列表、所有聯繫人列表、羣組列表。

  1. 進入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所示。

  1. 然後回到DefaultContactBrowseListFragment類:
    在執行onCreateView之前,會執行父類的一些方法,順序如下:
onAttach()
setContext(activity);
setLoaderManager(super.getLoaderManager());
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

setLoaderManager中設置當前的LoaderManager實現類。
加載聯繫人列表數據的過程中,這個類是ProfileandContactsLoader
之後執行onCreate方法。

  1. 進入DefaultContactBrowseListFragmentonCreate(Bundle)方法:
mAdapter = createListAdapter();
  • 1
  • 1

發現在這裏創建了ListAdapter

DefaultContactListAdapter adapter = 
new DefaultContactListAdapter(getContext());
  • 1
  • 2
  • 1
  • 2

可以知道創建的ListAdapter類型是DefaultContactListAdapter
並返回到DefaultContactBrowseListFragment類。
執行完onCreate方法之後,
執行DefaultContactBrowseListFragmentonCreateView方法。

  1. 進入DefaultContactBrowseListFragmentonCreateView方法:
mListView = (ListView)mView.findViewById(android.R.id.list);
mListView.setAdapter(mAdapter);
  • 1
  • 2
  • 1
  • 2

首先獲取了ListView用以填充聯繫人數據,然後設置了適配器,但是此時適配器中的數據是空的,直到後面纔會加載數據更新uI。
onCreateView方法執行完之後,在uI可見之前回調執行ActivityonStart方法。

  1. 進入DefaultContactBrowseListFragmentonStart方法:
mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
startLoading();
  • 1
  • 2
  • 1
  • 2

首先註冊了一個ContentObserve的子類監聽數據變化。
然後執行startLoading方法,目測這應當就是開始加載數據的方法了!

  1. 進入DefaultContactBrowseListFragmentstartLoading方法:
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持有的PartitionPartition類代表了當前需要加載的Directory,可以理解爲一個聯繫人集合,比如說本地聯繫人、Google聯繫人……這裏我們假設只加載本地聯繫人數據,所以partitionCount=1。

從這裏我們可以做出猜測:
聯繫人數據不是想象中的分頁(每次N條聯繫人數據)加載,也不是說一次性全部加載,而是一個賬戶一個賬戶加載聯繫人數據,加載完畢一個賬戶就在uI刷新並顯示數據。

  1. 進入DefaultContactBrowseListFragmentstartLoadingDirectoryPartition方法:
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
  1. 進入ContactEntryListFragmentonCreateLoader方法,位於DefaultContactBrowseListFragment的祖父類ContactEntryListFragment中:
CursorLoader loader = createCursorLoader(mContext);//創建Loader
mAdapter.configureLoader(loader, directoryId);//配置Loader
  • 1
  • 2
  • 1
  • 2

發現在此方法中,首先調用createCursorLoader方法創建了Loader
然後通過configureLoader方法配置Loaderquery方法的查詢參數,也就是配置SQL中select查詢語句的參數。
這也同時意味着,ContactEntryListFragment類的子類們可以重寫createCursorLoader方法以提供適合自身的Loader,重寫configureLoader方法爲Loader配置合適的參數,適配各種自定義的查詢獲取數據。

  1. 觀察createCursorLoader方法在DefaultContactBrowseListFragment類中實現:
return new ProfileAndContactsLoader(context);
  • 1
  • 1

直接返回了DefaultContactBrowseListFragment的數據加載器:ProfileAndContactsLoader
這就是DefaultContactBrowseListFragmentLoader實現類(數據加載器)。

  1. 然後再看一下ProfileAndContactsLoader類是如何加載數據的呢?
    發現它繼承自CursorLoader,而CursorLoader又繼承自AsyncTaskLoader<D>
    在關鍵的LoadBackGround()方法中:
    異步調用了ContentResolverquery方法:
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數據。並綁定了數據監聽器

  1. 那麼問題來了
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主要的幾個參數:UriProjectionSelectionSortOrder
這些參數用於最後和ContactsProvider交互的方法Query方法中……


  1. 最終查詢ContactsProvider2uri是:
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視圖,因爲這樣會有更加高的效率。
這也就意味着如果想給聯繫人數據庫新增一個字段供界面使用,僅修改對應的表結構是不行,還要修改對應的視圖才能得到想要的效果。

  1. 查詢完畢後,回調LoaderManageronLoadFinished方法,完成對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的數據填充基本仍然類似此流程,所不同的只是各自的FragmentAdapterCursorLoader以及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
* 後臺數據查詢分完畢後,回調LoadManageronLoadFinished()方法,並將數據以Contacts的數據類型返回,然後回調ContactDetailLoaderFragmentListeneronDetailsLoaded()方法。
* onDetailsLoaded()方法中,新開一個線程,通過ContactDetailLayoutController類的setContactData(Conatct)設置數據,刷新ContactDetailFragment

3.2,聯繫人數據的編輯和存儲:

1,編輯界面相關:

聯繫人數據所屬的賬號不同,加載的UI也是不同的,比如Sim卡聯繫人一般只有name,phone num,但是本地賬號聯繫人可能就會有email,address,website等信息……
聯繫人數據UI的加載是通過代碼動態加載的,而不是xml文件寫死的。

那麼問題來了,
新建聯繫人的界面是如何設計?
這裏寫圖片描述

  1. 先進入新建聯繫人界面:
    主界面PeopleActivity中點擊新建聯繫人Button,觸發onOptionsItemSelected方法中的
    case R.id.menu_add_contact分支:
    執行startActivity(intent);
    startActivity啓動Intent,Intent的Action設置爲android.intent.action.INSERT
    找到匹配此Action的Activity:ContactEditorActivity

  2. ContactEditorActivity的佈局文件:
    ContactEditorActivityonCreate()方法中找到佈局:
    setContentView(R.layout.contact_editor_activity);

  3. 在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類。

  1. 進入ContactEditorFragmentonCreateView方法:
//展開佈局 
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
  1. 找到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纔是真正顯示的東西。


  1. ContactEditorFragmentonCreateView方法執行完畢之後,會調用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方法。

  1. 看一下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>,可以理解爲 單個聯繫人所有賬戶的數據集合。


  1. 然後調用了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

  1. 找到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
  1. 那麼問題來了:中間的那部分佈局(電話、地址……)去哪兒了?
    搜索有可能包含這些內容的線性佈局sect_fields,發現在RawContactEditorView類中初始化爲mFields
    mFields = (ViewGroup)findViewById(R.id.sect_fields);
    那麼只需要看代碼中對mFields添加了什麼uI!

  2. 回到之前的bindEditors()方法,RawContactEditorView 對象editor從xml中解析完成後,執行了setState方法:

editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
  • 1
  • 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                   這是一個TextViewtitle
        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
  1. 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文件之後,執行KindSectionViewsetState方法:

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

先進行局部變量的賦值。

  1. 然後進入到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方法確認。

  1. 進入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的線性佈局)在這裏被使用!

  1. 聯繫上下文,實際上此時editor對象是TextFieldsEditorView類的對象,進入TextFieldsEditorViewsetValues方法,看看他是如何根據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

可以看到存儲新建聯繫人是通過ContactSaveServicecreateSaveContactIntent方法開始的,重要的是其中第二個參數是RawContactDeltaList 對象mState,顯然這是從Fragment的各個EditText控件返回到ContactSaveService的數據,那問題就是這個mState是如何組織數據的呢????

  1. 首先應該搞清楚這個mState是什麼類的對象。
    mStateRawContactDeltaList類的對象,先分析一下這個類是什麼數據結構:

RawContactDeltaList類:內部的存儲結構是ArrayList<RawContactDelta>,可以理解爲 單個聯繫人所有賬戶的數據集合。

RawContactDelta類:包含RawContact對象(即一個聯繫人某一特定帳戶的信息)。

RawContact類:raw contacts數據表內的一條數據。存儲Data表中一些數據行的集合及一些其他的信息,表示一個聯繫人某一特定帳戶的信息。
所以他的存儲結構爲: HashMap<String, ArrayList<ValuesDelta>>

ValuesDelta:類似ContentValues的鍵值對數據結構,是一個HashMap。用來存儲data表的數據,key爲Mime類型。

  1. 從聯繫人編輯界面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對象只會有一個。

  1. 進入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

可以發現,首先遍歷了當前用戶賬戶所有的可能條目種類,

  1. 然後又將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

  1. 進入createEditorView方法:
editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
  • 1
  • 1
  1. 聯繫上下文,實際上此時editor對象是TextFieldsEditorView類,進入TextFieldsEditorViewsetValues方法,看看他是如何根據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

  1. TextFieldsEditorView
    Ui中每一個EditText綁定了監聽器addTextChangedListener,當EditTest內容發生改變時回調onFieldChanged方法:進入此方法:
saveValue(column, value);
  • 1
  • 1
  1. TextFieldsEditorViewsaveValue方法:
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循環遍歷操作:entrymState集合的一個對象,因此也就是說:當用戶編輯EditText的同時,也改變了mState集合。

以上,就是ContactSaveServicecreateSaveContactIntent中第二個關鍵參數mSTate對象的由來。

第二步,將數據封裝爲ContactsProviderOperation數組,並提交:
這個對象mState很重要,因爲當用戶點擊保存Button時,
就會啓動ContactSaveServicecreateSaveContactIntent方法,開始保存聯繫人的操作:
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, 可以發現對存儲聯繫人匹配的分支調用了ContactSaveServicesaveContact方法:

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, 進入RawContactDeltaListdiff方法:

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, 進入RawContactDeltabuildDiff(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, 最終在內層循環調用了ValuesDeltabuildDiff方法,參數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的參數
* 最後調用Builderbuild()方法返回一個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

然後追溯ContentResolverapplyBatch()方法:

ContentProviderClient provider = acquireContentProviderClient(authority);
return provider.applyBatch(operations);
  • 1
  • 2
  • 1
  • 2

2, 根據authority參數,可以知道acquireContentProviderClient方法返回的providerContactsProvider2,所以之後調用了ContactsProvider2applyBatch方法:

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分支,調用了ContentProvider2insert方法:

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卡內部複雜的操作。
使用一個繼承自AsyncHandlerQueryHandler類封裝了異步查詢操作:

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之後,調用SIMProcessorManagerhandleProcessor方法。
3. SIMProcessorManagerhandleProcessor方法根據傳遞過來的Intent,在createProcessor方法中創建相應的Processor,比如SIMImportProcessorSIMRemoveProcessorSIMEditProcessorSIMDeleteProcessor。可以觀察到有導入SIm卡數據、移除Sim卡數據、編輯Sim卡數據、刪除Sim卡數據。
4. 本次操作爲導入SIm卡聯繫人,所以創建的Processor爲SIMImportProcessor,發現其基類爲ProcessorBase,實現了Runnable接口,因此SIMImportProcessor類可以理解爲Threadtarget,其run()方法是線程執行體。
5. SIMImportProcessorrun()方法實現了什麼功能?首先調用ContentResolverquery方法,指定uri爲SIm卡聯繫人數據的uri,並進行查詢操作(通過匹配Authority可以得知這裏調用的其實是IccProvider類),得到聯繫人數據遊標Cursor對象。這一步完成了Sim卡聯繫人數據的讀取。
6. 然後再執行importAllSimContacts方法,構建ContentProviderOperation數組OperatioList,通過ContentResolverapplyBatch(uri, OperatioList)方法批量提交對IccProvider的操作,也就是對數據庫的操作,。這一步完成了Sim卡聯繫人數據寫入到Sqlite。
7. 那麼,這個線程池是什麼時候啓動的呢?在第三步的createProcessor方法之後,將創建的Processor添加到ProcessorManagerProcessorManagerListener監聽器會執行Excute方法執行被添加的Processor其內部機制在線程池中執行Processor類。

3,telephony中IccProvider淺析:

預備知識:
Sim卡中存儲的號碼的類型:
ADN: Abbreviated dialing number, 就是常規的用戶號碼,用戶可以存儲/刪除。
FDN:Fixed dialer number,固定撥號,固定撥號功能讓您設置話機的使用限制,當您開啓固定撥號功能後,您只可以撥打存儲的固定撥號列表中的號碼。固定號碼錶存放在SIM卡中。能否使用固定撥號功能取決於SIM卡類型以及網絡商是否提供此功能。
SDN:Service dialing number,系統撥叫號碼,網絡服務撥號,固化的用戶不能編輯。

從以上的描述,可以看到,一般情況下訪問SIM卡聯繫人數據就是訪問ADN。

時序圖:

這裏寫圖片描述

上述第二節,第五步執行ContentResolverquery方法時,根據Authority可以得知匹配的ContentProviderIccProvider

  1. IccProviderQuery方法中,會執行loadFromEf方法。

  2. loadFromEf中,要先得到一個IIccPhoneBook對象:

IIccPhoneBook iccIpb =
IIccPhoneBook.Stub.asInterface(ServiceManager.getService("simphonebook");
  • 1
  • 2
  • 1
  • 2

發現這個對象是用AIDL接口來獲取到的,
那麼 ServiceManager.getService("simphonebook") 究竟獲取了一個什麼實體對象呢?

  1. 先不着急找到這個實體對象,
    發現程序之後在ICCProvider中調用Stub的實體類的getAdnRecordsInEf方法:
adnRecords = iccIpb.getAdnRecordsInEf(efType);
  • 1
  • 1

這是通過AIDL接口實現的方法調用,最終是調用到了Stub實體類的getAdnRecordsInEf方法.
那麼可以知道,這個實體類首先必然存在getAdnRecordsInEf()方法,這個實體類的對象是通過ServiceManager.getService來獲取的,那麼找到addService的地方就可以發現它了。

  1. 全局搜索後,發現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類的對象。

  1. 那麼在前面第3步提到的ICCProvider類中iccIpb.getAdnRecordsInEf方法實際就調用到了IccPhoneBookInterfaceManagerProxy類的getAdnRecordsInEf方法。
    getAdnRecordsInEf方法中,執行:
mIccPhoneBookInterfaceManager.getAdnRecordsInEf(efid);
  • 1
  • 1

看到getAdnRecordsInEf這個方法名就可以知道,這個方法是獲取Sim卡內Adn類型聯繫人數據的方法。

  1. 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方法

  1. 進入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

聯繫上下文,mCiRIL類的對象,RILRadio Interface Layer的縮寫,即無線接口通信層

之後涉及的東西比較底層……以後再慢慢分析……

  1. 經過與底層數據的交互,
    可以在IccPhoneBookInterfaceManager類的IccPhoneBookInterfaceManager方法返回得到Sim卡Adn聯繫人數據List<AndRecord>數據。
  2. 返回IccProviderloadFromEf方法:
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. 通過調用VCardServiceHandleImportRequest通知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啓動後調用ContactProvider2query方法,
匹配的uri爲Contacts.CONTENT_FILTER_URI的分支。
5. 在ContactProvider2appendSearchIndexJoin方法中拼接Sql語句,並調用Sqlite的底層query語句查詢。
6. 完成查詢後回調LoaderManageronLoaderFinish刷新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.javaContactsProvider2下,負責將漢字轉化爲拼音

4,聯繫人側邊欄字母導航條如何實現?

Android L中contact應用是沒有側邊欄的,但是字母導航的數據仍然是可以讀到,我們只需要搞個自定義控件,畫出A-Z的字母導航條,並監測觸摸事件,在Contacts中的listviewsetSelection點擊的字母位置就可以。


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的分析。

      發表評論
      所有評論
      還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
      相關文章