您的位置:首页 > 移动开发 > Android开发

Android contacts 联系人 通讯录 源码 完全解析

2016-02-15 21:41 513 查看

Android contacts 联系人 通讯录 源码 完全解析

Android contacts 联系人 通讯录 源码 完全解析
1简介

2软件架构

3各功能模块分析
1联系人数据的显示
1联系人列表显示

2联系人详细信息数据的显示

2联系人数据的编辑和存储
1编辑界面相关

2数据存储相关

3Sim联系人数据的整合
1Sim卡联系人数据的显示

2开机自动导入Sim卡联系人

3telephony中IccProvider浅析

4Sim卡联系人的手动导入导出

4SD卡备份恢复联系人
1从Sd卡恢复import联系人数据

2联系人数据导出到Sd卡

5联系人搜索

6Google联系人同步

7其他零碎功能

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();


发现创建了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);//数据重新加载


该类同时提供了重要的抽象方法:

protected abstract T createListAdapter();//创建适配器Adapter类。


这意味着,子类可以按需求创造自己的适配器Adapter类,完成各个子界面Listview的数据显示,如3.1节图1所示。

然后回到
DefaultContactBrowseListFragment
类:

在执行
onCreateView
之前,会执行父类的一些方法,顺序如下:

onAttach()
setContext(activity);
setLoaderManager(super.getLoaderManager());


setLoaderManager
中设置当前的
LoaderManager
实现类。

加载联系人列表数据的过程中,这个类是
ProfileandContactsLoader


之后执行
onCreate
方法。

进入
DefaultContactBrowseListFragment
onCreate(Bundle)
方法:

mAdapter = createListAdapter();


发现在这里创建了
ListAdapter


DefaultContactListAdapter adapter =
new DefaultContactListAdapter(getContext());


可以知道创建的ListAdapter类型是
DefaultContactListAdapter


并返回到
DefaultContactBrowseListFragment
类。

执行完
onCreate
方法之后,

执行
DefaultContactBrowseListFragment
onCreateView
方法。

进入
DefaultContactBrowseListFragment
onCreateView
方法:

mListView = (ListView)mView.findViewById(android.R.id.list);
mListView.setAdapter(mAdapter);


首先获取了ListView用以填充联系人数据,然后设置了适配器,但是此时适配器中的数据是空的,直到后面才会加载数据更新uI。

onCreateView
方法执行完之后,在uI可见之前回调执行
Activity
onStart
方法。

进入
DefaultContactBrowseListFragment
onStart
方法:

mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
startLoading();


首先注册了一个
ContentObserve
的子类监听数据变化。

然后执行
startLoading
方法,目测这应当就是开始加载数据的方法了!


进入
DefaultContactBrowseListFragment
startLoading
方法:

int partitionCount = mAdapter.getPartitionCount();
for (int i = 0; i < partitionCount; i++) {
……
Partition partition = mAdapter.getPartition(i);
startLoadingDirectoryPartition(i);
……}


Partition
这个类持有一个
Cursor
对象,用来存储数据。

Adapter
持有的
Partition
Partition
类代表了当前需要加载的
Directory
,可以理解为一个联系人集合,比如说本地联系人、Google联系人……这里我们假设只加载本地联系人数据,所以partitionCount=1。

从这里我们可以做出猜测:

联系人数据不是想象中的分页(每次N条联系人数据)加载,也不是说一次性全部加载,而是一个账户一个账户加载联系人数据,加载完毕一个账户就在uI刷新并显示数据。

进入
DefaultContactBrowseListFragment
startLoadingDirectoryPartition
方法:

loadDirectoryPartition(partitionIndex, partition);


进入此方法:

getLoaderManager().restartLoader(partitionIndex, args, this);


这个方法是
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();


进入
ContactEntryListFragment
onCreateLoader
方法,位于
DefaultContactBrowseListFragment
的祖父类
ContactEntryListFragment
中:

CursorLoader loader = createCursorLoader(mContext);//创建Loader
mAdapter.configureLoader(loader, directoryId);//配置Loader


发现在此方法中,首先调用
createCursorLoader
方法创建了
Loader


然后通过
configureLoader
方法配置
Loader
query
方法的查询参数,也就是配置SQL中select查询语句的参数。

这也同时意味着,
ContactEntryListFragment
类的子类们可以重写
createCursorLoader
方法以提供适合自身的
Loader
,重写
configureLoader
方法为
Loader
配置合适的参数,适配各种自定义的查询获取数据。

观察
createCursorLoader
方法在
DefaultContactBrowseListFragment
类中实现:

return new ProfileAndContactsLoader(context);


直接返回了
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);


通过这个
Query
方法,实现了对联系人数据的查询,返回
Cursor
数据。并绑定了数据监听器

那么问题来了

query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder, mCancellationSignal)


的这些参数那里指定的呢?

configureLoader
方法在
DefaultContactListAdapter
类中实现,实现了对
query
参数的配置:

configureUri(loader, directoryId, filter);
loader.setProjection(getProjection(false));
configureSelection(loader, directoryId, filter);
loader.setSortOrder(sortOrder);


可以看到,配置了
Loader
主要的几个参数:
Uri
Projection
Selection
SortOrder


这些参数用于最后和
ContactsProvider
交互的方法
Query
方法中……

最终查询
ContactsProvider2
uri
是:

Uri:content://com.android.contacts/contacts?address_book_index_extras=true&directory=0


发现
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)


可以发现最后通过
ContactsProvider2
实现的查询,并不是直接查询相关的表(
Contacts
表、
rawcontacts
表,
data
表……),而是直接查询
view_contacts
视图,因为这样会有更加高的效率。

这也就意味着如果想给联系人数据库新增一个字段供界面使用,仅修改对应的表结构是不行,还要修改对应的视图才能得到想要的效果。

查询完毕后,回调
LoaderManager
onLoadFinished
方法,完成对Ui界面的更新:

onPartitionLoaded(loaderId, data);


接着进入
onPartitionLoaded
方法:

mAdapter.changeCursor(partitionIndex, data);


进入这个
changeCursor
方法:

mPartitions[partition].cursor = cursor;
notifyDataSetChanged();


发现在这里改变了
Adapter
的数据集
Cursor
,并发出通知数据已经改变,UI进行更新。

至此,默认联系人数据的显示分析到此结束。

其他
Fragment
的数据填充基本仍然类似此流程,所不同的只是各自的
Fragment
Adapter
CursorLoader
以及
CursorLoader
配置的参数(uri,projection,selection,args,order……)有所不同。

可以参考下表:

FragmentAdapterCursorLoader
DefaultContactBrowseListFragment(默认联系人列表)DefaultContactListAdapterProfileAndContactsLoader
ContactTitleListFragment(收藏联系人列表)ContactTileAdapterContactTileLoaderFactory StarredLoader
ContactTitleFrequentFragment(常用联系人列表)ContactTitleAdapterContactTileLoaderFactory
FrequentLoader GroupBrowseListFragment(群组列表)GroupBrowseLIstAdapterGroupListLoader
GroupDetailFragment(指定ID群组的联系人列表)GroupMemberTileAdapterGroupMemberLoader
ContactDetailFragment(指定ID联系人信息)ViewAdapterContactLoader

2,联系人详细信息数据的显示:

关键类:

ContactDetailActivity
ContactDetailFragment
ContactLoaderFragment //不可见 负责加载联系人详细数据,集成LoadManager对象。
ContactLoader   //联系人详细信息Loader。
ContactDetailLayoutController     //布局控制类。


原理类似列表显示,如下简要说明:

*
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>


只包含一个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);


找到
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>


于是确认
ContactEditorFragment
的根布局就是一个id为
editors
的LinearLayout。

想到上一步的语句:

mContent = (LinearLayout) view.findViewById(R.id.editors);


所以关键就在于,接下来在代码中为
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));}


上面代码首先取出了当前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();


发现暂时还是没有对界面做什么事情,任然处于酝酿阶段……

首先使用传入的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());


可以看到,
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>


那么问题来了:中间的那部分布局(电话、地址……)去哪儿了?

搜索有可能包含这些内容的线性布局
sect_fields
,发现在
RawContactEditorView
类中初始化为
mFields


mFields = (ViewGroup)findViewById(R.id.sect_fields);


那么只需要看代码中对mFields添加了什么uI!

回到之前的
bindEditors()
方法,
RawContactEditorView
对象
editor
从xml中解析完成后,执行了
setState
方法:

editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());


进入
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);
……
}


手机账户下的
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)。


到这里,基本可以确定,中间部分(也就是除了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>


KindSectionView
加载完xml文件之后,会执行
onFinishInflate
方法:

mTitle = (TextView) findViewById(R.id.kind_title);
mEditors = (ViewGroup) findViewById(R.id.kind_editors);
mAddFieldFooter = findViewById(R.id.add_field_footer);


把Xml文件中三个主要的部分都得到了,接下来重点就是观察代码中对他们做了什么。

在第12步中,加载完xml文件之后,执行
KindSectionView
setState
方法:

section.setState(kind, state, false, vig);


rawContactDelta
对象
state
传递给了
KindSectionView
类的
setState
方法:

进入
KindSectionView
类的
setState
方法:

mKind = kind;
mState = state;
rebuildFromState();


先进行局部变量的赋值。

然后进入到
rebuildFromState()
方法:

for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
//……遍历当前账户可能的键值对,比如电话、Email、地址……
createEditorView(entry);  //这个方法应当是创建EditText的方法!
}


在这个方法中,对
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);


第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添加到当前的线性布局中!
}


注释基本解释了如何通过一个
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);


可以看到存储新建联系人是通过
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();


进入此方法:

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());


发现他遍历了
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);


可以发现,首先遍历了当前用户账户所有的可能条目种类,

然后又将
rawContactDelta
对象传递给了
KindSectionView
类的
setState
方法:

mKind = kind;
mState = state;
rebuildFromState();


先进行局部变量的赋值,然后进入到
rebuildFromState()
方法:

for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
createEditorView(entry);
}


在这个方法中,又对
mState
集合中所有
Mime
类型的
ValuesDelta
集合(
ArrayList<ValuesDelta>
类型)进行遍历,而后将每一个
ValuesDelta entry
对象作为参数调用了
createEditorView
(也就是创建各个种类的EditText)方法,根据entry对象创建相应的
EditText


进入
createEditorView
方法:

editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);


联系上下文,实际上此时
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添加到当前的线性布局中!
}


注释基本解释了如何通过一个
ValuesDelta
(理解为HashMap)对象entry创建布局中的所有
EditText


TextFieldsEditorView


Ui中每一个
EditText
绑定了监听器
addTextChangedListener
,当EditTest内容发生改变时回调
onFieldChanged
方法:进入此方法:

saveValue(column, value);


TextFieldsEditorView
saveValue
方法:

protected void saveValue(String column, String value) {
mEntry.put(column, value);
}


发现这个方法将
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);
}


第二个参数
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);
}


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);


4, 首先取出Intent中传递过来的
mState
,执行
buildDiff()
构造
ContentProviderOperation
数组
diff


那么这段程序的的迷惑之处在于
ContentProviderOperation
数组是怎么通过一个

RawContactDeltaList
对象
mState
构建的?

5, 进入
RawContactDeltaList
diff
方法:

for (RawContactDelta delta : this) {
//此处传入的diff是一个空ArrayList<ContentProviderOperation>对象:
delta.buildDiff(diff);
}


在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);
}
}


有两层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, }],
}


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;
}


这里用到了
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);


发现构建
builder
对象并在其中配置参数之后,马上执行了
possibleAdd
方法。

10, 进入
possibleAdd
方法:

private void possibleAdd(ArrayList<ContentProviderOperation> diff,
ContentProviderOperation.Builder builder) {
if (builder != null) {
//在这里构建了ContactsProviderOperation数组!
diff.add(builder.build());
}
}


经过以上分析,

经过层层遍历,完成了
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,
]


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);


2, 根据
authority
参数,可以知道
acquireContentProviderClient
方法返回的
provider
ContactsProvider2
,所以之后调用了
ContactsProvider2
applyBatch
方法:

return super.applyBatch(operations);


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);
}


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) {
}


我们执行的新增联系人操作,也就是Insert操作。

5, 因此进入Insert分支,调用了
ContentProvider2
insert
方法:

Uri result = insertInTransaction(uri, values);


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;
}


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;


至此,一个联系人的插入操作分析完毕。

添删改查操作的基本流程都类似。

值得注意的是删除联系人并不是真正的删除联系人数据。

用户在联系人列表选择联系人的删除,本地联系人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


这四个操作,并提供相对应的
onXXXComplete
方法,以供操作完数据库后进行其它的操作,这四个
onXXXComplete
方法都是抽象方法,可以在子类中实现想要的操作,在本例中是使用QueryHandler类实现了这几个方法,查询完毕后将数据填充到
ListView
中。

为什么要使用AsyncQueryHandler

当然也可以使用ContentProvider去操作数据库。这在数据量很小的时候是没有问题的,但是如果数据量大了,可能导致UI线程发生ANR事件。当然你也可以写个Handler去做这些操作,只是你每次使用ContentProvider时都要再写个Handler,必然降低了效率。

因此API提供了一个操作数据库的通用方法。

如何使用AsyncQueryHandler

指定需要查询的数据的URI,与ContentResolver的query方法中的参数并无太大区别。

本例中查询Sim卡联系人数据的的uri是:

Uri.parse("content://icc/adn");


然后调用查询方法:

mQueryHandler.startQuery(QUERY_TOKEN, null, uri, COLUMN_NAMES,
null, null, null);


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");


发现这个对象是用AIDL接口来获取到的,

那么
ServiceManager.getService("simphonebook")
究竟获取了一个什么实体对象呢?

先不着急找到这个实体对象,

发现程序之后在
ICCProvider
中调用
Stub
的实体类的
getAdnRecordsInEf
方法:

adnRecords = iccIpb.getAdnRecordsInEf(efType);


这是通过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);
}


addService
方法传入参数为当前
IccPhoneBookInterfaceManagerProxy
类的对象,

因此,在
IIccPhoneBook iccIpb =IIccPhoneBook.Stub.asInterface(ServiceManager.getService("simphonebook");


是获得的就是
IccPhoneBookInterfaceManagerProxy
类的对象。

那么在前面第3步提到的
ICCProvider
类中
iccIpb.getAdnRecordsInEf
方法实际就调用到了
IccPhoneBookInterfaceManagerProxy
类的
getAdnRecordsInEf
方法。

getAdnRecordsInEf
方法中,执行:

mIccPhoneBookInterfaceManager.getAdnRecordsInEf(efid);


看到
getAdnRecordsInEf
这个方法名就可以知道,这个方法是获取Sim卡内Adn类型联系人数据的方法。

IccPhoneBookInterfaceManager
中实现了
getAdnRecordsInEf
方法

getAdnRecordsInEf
方法中,执行:

adnCache.requestLoadAllAdnLike(efid, adnCache.extensionEfForEf(efid), response);


adnCache
是类
AdnRecordCache
的对象

7.
AdnRecordCache


在类
AdnRecordCache
中实现了
requestLoadAllAdnLike
方法,

requestLoadAllAdnLike
中,执行:

new AdnRecordLoader(mFh).loadAllFromEF
(efid, extensionEf,obtainMessage(EVENT_LOAD_ALL_ADN_LIKE_DONE, efid, 0))


这里实例化一个类
AdnRecordLoader
的对象,并且调用该对象的
loadAllFromEF
方法

进入
AdnRecordLoader


在类
AdnRecordLoader
中实现了
loadAllFromEF
方法,

loadAllFromEF
方法中,执行:

mFh.loadEFLinearFixedAll(ef, obtainMessage(EVENT_ADN_LOAD_ALL_DONE));


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);


联系上下文,
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);
}


发现遍历了
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


发现其实最后经过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);


创建Intent选择器:蓝牙/email/Nfc/其他应用……

final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);


跳转到用户选择的分享联系人方式的应用(蓝牙/Nfc/Email/其他应用……),联系人数据通过intent传递。

2,桌面快捷方式和文件夹

createLauncherShortcutWithContact()

ShortcutIntentBuilder


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的分析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: