自Android 2.0(API Level 5)开始,Android平台采用了改进后的Contacts API- ContactsContract,用于管理和集成来自多账户和多数据来源的联系人信息。新的联系人API在android.provider.ContactsContract及相关的类来定义,老的API已经废弃,但仍然可以使用。
- Contact API的结构和使用方法
和老的API有所不同的是,在新的Contacts API中,联系人数据被安排三个主要的表中:contacts, raw contacts和 data。这种新的结构可以使系统更加容易储存和管理从多个数据源得到的特定的联系人数据。结构如下图所示:
(图片来源于:developer.android.com)
1) Data表:储存所有与RawContract相关具体的信息。表的每一条记录对应一个特定信息,如:名字、电话,email地址, 头像和组信息等。每一记录通过一个mimetype_id字段来表明该行所记录的数据类型。例如,如果row的数据类型是Phone.CONTENT_ITEM_TYPE,那么第一个字段应该保存电话号码,如果数据类型为Email.CONTENT_ITEM_TYPE,那么这一记录的字段应该保存邮件地址。
Data表的字段主要有:
mimetype_id | 表示该行存储的信息的类型 |
raw_contact_id | 表示该行所属的RawContact |
is_primary | 多个data数据组成一个raw contact,该字段表示此data是否是其所属的raw contact的主data,即其display name会作为raw contact的display name |
is_super_primary | 该data是否是其所属的contact的主data,如果is_super_primary为1则is_primary一定为1 |
data1~data15 | 15个数据字段,对于不同类型的信息,表示不同的含义,ContactsContract.CommomDataKinds类中定义了与常用的数据类型相对应的一些类,这些类中分别定义了相应数据类型中这些字段表示的含义。一般data1表是主信息(如电话,Email地址等),data2表示副信息,data15表示Blob数据。 |
data_sync1~data_sync4 | sync_adapter要用的字段(sync_adapter用于数据的同步,比如你手机中的Gmail帐户与Google服务器的同步)。 |
data_version | 数据的版本,用于数据的同步。 |
2)RawContact表中的一行存储Data表中一些数据行的集合及一些其他的信息,表示一个联系人某一特定帐户的信息,比如Facebook或Exchange的一个联系人。
当插入一个raw contact或当一个raw contact所属的一个data改变时,系统会检查这个raw contact跟其他的raw contact是否可以匹配(比如如果两个raw contact的data包含相同的电话号码或名字),如果匹配他们就会被综合到一起,也就是说他们会属于同一个cantact,表现为在RawContact表中他们引用的cantact_id是一样的。
联系人姓名、组织、电话号码、Email或昵称的改变会引发raw contact的重新聚合。有两个方法控制聚合的行为Aggregaton Mode与ContactsContract.AggregationExceptions。
Aggregaton Mode:
RawContact表中有一个字段aggregation_mode,通过向特定raw contact行中插入这个字段可以修改系统对这个raw contact的聚合行为,其允许的值如下:AGGREGATION_MODE_DEFAULT:正常模式,允许自动聚合;
AGGREGATION_MODE_DISABLE:不允许聚合;
AGGREGATION_MODE_SUSPENDED:当一个raw contact的aggregation mode修改为suspended时,如果其已是一个已聚合的contact的一部分,那么它仍会保持与原来聚合到一起的raw contact的关系,即使它已改变不再跟其他raw contact匹配。AGGREGATIONEXCEPTIONS:
在数据库中存在一个表:agg_exceptions。通过字段raw_contact_id1、raw_contact_id2、mode存储两个raw contact聚合的方法,系统定义的聚合行为有3个:
TYPE_AUTOMATIC=0 由系统决定聚合行为,默认值。
TYPE_KEEP_SEPARATE=2 不聚合
TYPE_KEEP_TOGETHER=1 聚合
3) Contact表中的一行表示一个联系人,它是RawContact表中的一行或多行的数据的组合,这些RawContact表中的行表示同一个人的不同的帐户信息。Contact中的数据由系统组合RawContact表中的数据自动生成。不可以直接向这个表中插入数据,当一个raw contact被插入的时候,系统会首先查找Contact表看是否有记录跟插入的raw contact表示同一个人,如果找到了,则把找到的这个contact的_ID插入raw contact记录的CONTACT_ID字段,如果没有找到,则系统自动插入一个Contact记录并把它的_ID插入新插入的raw contact的CONTACT_ID列。
Contact表中只有TIMES_CONTACTED、LAST_TIME_CONTACTED、STARRED、CUSTOM_RINGTONE、SENE_TO_VOICEMAIL列可更改,这些列的更改会导致相应的raw contact被更改。
数据删除
Contacts文档说:
Be careful with deleting Contacts! Deleting an aggregate contact deletes all constituent raw contacts.
The corresponding sync adapters will notice the deletions of their respective raw contacts
and remove them from their back end storage.
删除一个Contacts,会把它所包含所有raw contacts都删除掉。
但是用下面语句居然删除不了任何Contacts.
getContentResolver().delete(Contacts.CONTENT_URI, null, null);
不过在删除Contacts的所有raw contacts后,Contacts也被删除了。
如果需要读取一个联系人的信息用CONTENT_LOOKUP_URI代替CONTENT_URI;
如果需要通过电话号码查找一个联系人,用PhoneLookup.CONTENT_FIILTER_URI,这个URI为这个目的进行了优化;
如果需要通过部分名字的匹配查找,用CONTENT_FILTER_URI;
如果需要通过email,address等信息查找,查找表ContactsContract.Data,结果包含contact ID,名字...
android.provider.ontactsContract.Data类
其定义如下:
public final static class Data implements DataColumnsWithJoins { private Data() {} public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "data"); public static final String CONTENT_TYPE = "vnd.android.cursor.dir/data"; //This flag is useful (currently) only for vCard exporter in Contacts app public static final String FOR_EXPORT_ONLY = "for_export_only"; /*Build a CONTENT_LOOKUP_URI style Uri for the parent ContactsContract.Contacts entry of the given ContactsContract.Data entry.*/ public static Uri getContactLookupUri(ContentResolver resolver, Uri dataUri) {...} }
Data类定义了CONTENT_URI及CONTENT_TYPE,主要是继承了DataColumnsWithJoins接口中的字段。DataColumnsWithJoins定义如下:
protected interface DataColumnsWithJoins extends BaseColumns, DataColumns, StatusColumns, RawContactsColumns, ContactsColumns, ContactNameColumns, ContactOptionsColumns, ContactStatusColumns { }
该接口只是综合了一些接口,其中:
BaseColumns定义了_ID与_COUNT字段,ContentProvider查询中都要用到的字段。
DataColumns定义了与Data表中字段一一对应的常量
RawContactsColumns, ContactsColumns, ContactNameColumns, ContactOptionsColumns表示RawContact与Contact中的一些字段,定义到此类中以方便访问。
由于联系人的信息都是按类别存储到了这个Data表中,所以我们要查找联系人的信息只需要查这个表。Data类中的Data.CONTACT_ID与Data.RAW_CONTACT_ID分别表示该表项对应的联系人在Contact与RawContract表中的ID,我们只需要知道某一个联系人的contactId或rawContractId,并根据其查找的数据的类型就可以查到相应类型的信息。
Cursor c = getContentResolver().query(Data.CONTENT_URI,
new String[] {Data._ID, Phone.NUMBER, Phone.TYPE, Phone.LABEL},
Data.CONTACT_ID + "=?" + " AND "
+ Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'",
new String[] {String.valueOf(contactId)}, null);
以上代码根据联系人的contractId,并通过设置其MIMETYPE为Phone.CONTENT_ITEM_TYPE查到了该联系人的电话号码,把Data.CONTACT_ID换为Data.RAW_CONTACT_ID即可查找相应rawContactId对应的电话号码。其中用到了Phone.CONTENT_ITEM_TYPE,这是什么呢?
CommonDataKinds类
在前面讲Data表的结构时讲到,Data的data1~data15字段用于存储各类型的数据信息,那么这15个字段分别表示什么信息呢?
前面提到了,Data表中有一个mimetype_id字段,通过这个字段关联mimetypes表表示该行代表的信息类型,因为Data表中的每一行可以表示如Phone或Address等不同类型的信息,所以对于不同类型的信息,data1~data15这15列表示不同的含义,如果要靠记忆记住这15列对于特定的类型分别表示什么意义自然不行,于是Google就预定义了一些类,每一个类对应一些预先定义好的数据类型,在每个类中定义了一些语义地、方便记忆的常量,用来对应这15个字段,比如在CommonDataKinds.Email类中有如下定义
public static final String ADDRESS = DATA1;
public static final String DISPLAY_NAME = DATA4;
DATA1与DATA4为继承自DataColumns中的常量,在DataColumns中是这样定义的:
public static final String DATA1 = "data1";
public static final String DATA4 = "data4";
这样,当于们要查找Email地址时,只需要通过ContactsContract.CommonDataKinds.Email.ADDRESS引用,而不需要知道它是存储在Data表中的data1列中。
回到上一个问题,上面查询电话号码的例子中用到了Phone.CONTENT_ITEM_TYPE,即是表示CommonDataKinds.Phone.CONTENT_ITEM_TYPE,在Phone类中有如下定义
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone_v2";
vnd.android.cursor.item/phone_v2 就是表示Data表中相应的行存储的是电话号码的信息,通过相应类型(如Phone、Email)的CONTENT_ITEM_TYPE,我们就可以指定要查询的MIMETYPE,即要查询的数据类型。
其实在上面的例子中还有更简单的方法:
Cursor phone = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[] { CommonDataKinds.Phone.NUMBER }, CommonDataKinds.Phone.CONTACT_ID + " =? ",
new String[] { String.valueOf(contactId) }, null);
只需把query的第一个参数处传递想查找的数据类型的相应的类中的CONTENT_URI就可以了。
Lookup Key
在新的Contact API中,为contact引入了lookup key的概念,当你的程序需要保存对联系人的引用时,用lookup key而别用row id,lookup key是contacts表中的一列,当你有一个lookup key时,可以这样构造一个Uri:
Uri lookupUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
然后用这个Uri查询:
Cursor c = getContentResolver().query(lookupUri, new String[]{Contacts.DISPLAY_NAME}, ...);
try {
c.moveToFirst();
String displayName = c.getString(0);
} finally {
c.close();
}
用lookup key 而不用row id的原因是因为row id容易改变,用户把原先两个联系人合并成一个或因为同步的问题都会导致row id的改变。lookup key是一个字符串,它由raw contact的标识连接组成。
使用row id会比使用lookup key效率高,你可以同时保存二者,然后联合二者生成一个lookup uri:
Uri lookupUri = getLookupUri(contactId, lookupKey)
当同时有row id跟lookup key时,系统会优先以row id查询,如果无查找结果或找到的结果跟lookup key不匹配,则再用lookup key查找。
参考1:http://developer.android.com/resources/articles/contacts.html
The new Contacts API introduces the notion of a lookup key for a contact. If your application needs to maintain references to contacts, you should use lookup keys instead of the traditional row ids. You can acquire a lookup key from the contact itself, it is a column on the ContactsContract.Contacts table.
The reason for this complication is that regular contact row IDs are inherently volatile. Let's say your app stored a long ID of a contact. Then the user goes and manually joins the contact with some other contact. Now there is a single contact where there used to be two, and the stored long contact ID points nowhere.
The lookup key helps resolve the contact in this case. The key is a string that concatenates the server-side identities of the raw contacts. Your application can use that string to find a contact, regardless whether the raw contact is aggregated with others or not.
示例代码如下:
package com.memo;
import java.util.ArrayList;
import android.app.ListActivity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class Main extends ListActivity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri contactsUri=ContactsContract.Contacts.CONTENT_URI;
String[] proj1=new String[]{ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER,
ContactsContract.Contacts.LOOKUP_KEY};
Cursor curContacts=getContentResolver().query(contactsUri,proj1, null, null, null);
//declare a ArrayList object to store the data that will present to the user
ArrayList<String> contactsList=new ArrayList<String>();
String allPhoneNo="";
if(curContacts.getCount()>0){
while(curContacts.moveToNext()){
// get all the phone numbers if exist
if(curContacts.getInt(1)>0){
allPhoneNo=getAllPhoneNumbers(curContacts.getString(2));
}
contactsList.add(curContacts.getString(0)+" , "+allPhoneNo);
allPhoneNo="";
}
}
// binding the data to ListView
setListAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1, contactsList));
ListView lv=getListView();
lv.setTextFilterEnabled(true);
}
/**
* Get all the phone numbers of a specific contact person
*
* @param lookUp_Key lookUp key for a specific contact
* @return a string containing all the phone numbers
*/
public String getAllPhoneNumbers(String lookUp_Key){
String allPhoneNo="";
// Phone info are stored in the ContactsContract.Data table
Uri phoneUri=ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
String[] proj2={ContactsContract.CommonDataKinds.Phone.NUMBER};
// using lookUp key to search the phone numbers
String selection=ContactsContract.Data.LOOKUP_KEY+"=?";
String[] selectionArgs={lookUp_Key};
Cursor cur=getContentResolver().query(phoneUri,proj2,selection, selectionArgs, null);
while(cur.moveToNext()){
allPhoneNo+=cur.getString(0)+" ";
}
return allPhoneNo;
}
}
5. 注意事项:
1)由于要读取联系人信息,所以必须在AndroidMenifest.xml里加入相关uses-permission用于请求使用许可:
<uses-permission android:name="android.permission.READ_CONTACTS" />
参考1:http://developer.android.com/guide/topics/security/security.html
参考2:http://www.higherpass.com/Android/Tutorials/Working-With-Android-Contacts/
2) 怎样获取特定联系人的电话号码?由于使用新的API,所以编写方法与使用旧的API不同。
在查询是主要使用到lookUp key 的概念。
参考:http://stackoverflow.com/questions/4729551/contactscontract-lookup-phone-number-by-contact-id