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

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

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

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

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

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

  1. 然后回到DefaultContactBrowseListFragment类:
    在执行onCreateView之前,会执行父类的一些方法,顺序如下:
onAttach()
setContext(activity);
setLoaderManager(super.getLoaderManager());

setLoaderManager中设置当前的LoaderManager实现类。
加载联系人列表数据的过程中,这个类是ProfileandContactsLoader
之后执行onCreate方法。

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

发现在这里创建了ListAdapter

DefaultContactListAdapter adapter = 
new DefaultContactListAdapter(getContext());

可以知道创建的ListAdapter类型是DefaultContactListAdapter
并返回到DefaultContactBrowseListFragment类。
执行完onCreate方法之后,
执行DefaultContactBrowseListFragmentonCreateView方法。

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

首先获取了ListView用以填充联系人数据,然后设置了适配器,但是此时适配器中的数据是空的,直到后面才会加载数据更新uI。
onCreateView方法执行完之后,在uI可见之前回调执行ActivityonStart方法。

  1. 进入DefaultContactBrowseListFragmentonStart方法:
mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
startLoading();

首先注册了一个ContentObserve的子类监听数据变化。
然后执行startLoading方法,目测这应当就是开始加载数据的方法了!

  1. 进入DefaultContactBrowseListFragmentstartLoading方法:
int partitionCount = mAdapter.getPartitionCount();
for (int i = 0; i < partitionCount; i++) {
……
Partition partition = mAdapter.getPartition(i);
startLoadingDirectoryPartition(i);
……}

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

从这里我们可以做出猜测:
联系人数据不是想象中的分页(每次N条联系人数据)加载,也不是说一次性全部加载,而是一个账户一个账户加载联系人数据,加载完毕一个账户就在uI刷新并显示数据。

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

发现在此方法中,首先调用createCursorLoader方法创建了Loader
然后通过configureLoader方法配置Loaderquery方法的查询参数,也就是配置SQL中select查询语句的参数。
这也同时意味着,ContactEntryListFragment类的子类们可以重写createCursorLoader方法以提供适合自身的Loader,重写configureLoader方法为Loader配置合适的参数,适配各种自定义的查询获取数据。

  1. 观察createCursorLoader方法在DefaultContactBrowseListFragment类中实现:
return new ProfileAndContactsLoader(context);

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

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

  1. 那么问题来了
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主要的几个参数:UriProjectionSelectionSortOrder
这些参数用于最后和ContactsProvider交互的方法Query方法中……


  1. 最终查询ContactsProvider2uri是:
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视图,因为这样会有更加高的效率。
这也就意味着如果想给联系人数据库新增一个字段供界面使用,仅修改对应的表结构是不行,还要修改对应的视图才能得到想要的效果。

  1. 查询完毕后,回调LoaderManageronLoadFinished方法,完成对Ui界面的更新:
onPartitionLoaded(loaderId, data);

接着进入onPartitionLoaded方法:

mAdapter.changeCursor(partitionIndex, data);

进入这个changeCursor方法:

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

发现在这里改变了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     //布局控制类。

原理类似列表显示,如下简要说明:
* 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
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值