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可以自动检测底层数据的更新和重新检索。
数据加载流程概览:
流程具体分析:
先上图:
- 进入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……)有所不同。
可以参考下表:
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 //布局控制类。
原理类似列表显示,如下简要说明:
* 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