概述
本文主要解读Android系统联系人相关的知识,以及展示联系人相关的操作实例。参考Android官方API。由于Contact在API 5的时候废弃,因为其只支持单账号联系人。所以本文就直接介绍ContactContract
类
ContactsContract
ContactsContract
是什么呢?官方文档是这样描述的:
The contract between the contacts provider and applications. Contains definitions for the supported URIs and columns. These APIs supersede ContactsContract.Contacts.
联系人应用和提供者包括自定义URL可字段都被新的ContactsContract
API取代。也就是说新的API支持多个URL也就是多账号,是Contact类的升级版。
ContactsContract
定义了可扩展的联系人关联数据库,联系人信息存储为三层表模型
ContactsContract.Data
:表中一行可以存储任何个人数据,例如手机号或者邮箱地址。并且表中的所有数据皆为开放的。表中有一些常规的预定字段,当然任何应用可以根据自身需求去定义自己数据字段。ContactsContract.RawContacts
:这个表表示用户单个账号下面的联系人,比如用户的Gmail邮箱内的联系人。ContactsContract.Contact
:此表存放着描述同一个人的一个或者多个RowContact(账号)的聚合数据。当单个账号数据修改后,此表会根据需要更新对应的聚合联系人。
其它表
ContactsContract.Groups
:其中包含有关Gmail联系人组等原始联系人群组的信息。当前的API不支持跨多个账户的组的概念。ContactsContract.StatusUpdates
:其中包含社交状态更新,包括IM可用性。ContactsContract.AggregationExceptions
:用于手动聚合和分解原生数据。ContactsContract.Settings
:其中包含账号和组的可见性和同步设置。ContactsContract.SyncState
: 其中包含代表同步适配器维护的自由格式数据。ContactsContract.PhoneLookup
:用于紧急呼叫者的ID查找。
ContactsContact.Contacts
联系人表的常熟,其中包含代表同一人的原始联系人的每个汇总的记录
操作
Insert
无法明确的创建联系人。当一条联系人数据插入的时候,provider首先去查找是否存在同一个联系人。如果找到,会从聚合表中获取联系人的CONTACT_ID
。如果没有找到匹配的联系人,provider会自动的插入一个新的联系人,并将新的联系人插入原始的联系人数据表中。
update
当某个确定的列被修改如: TIMES_CONTACTED
, LAST_TIME_CONTACTED
, STARRED
, CUSTOM_RINGTONE
, SEND_TO_VOICEMAIL
. 更改联系人表的以上字段都会更改所有构成联系人的原始数据。
Query
如果只是阅读个别联系人,可以使用
CONTENT_LOOKUP_URI
去代替CONTENT_URI
。如果你要根据电话号码来查找联系人,使用
PhoneLookup.CONTENT_FILTER_URI
,对此进行了优化如果你要根据部分名称查询联系人,例如某些过滤条件,可以使用
CONTENT_FILTER_URI
如果你想根据某些数据元素如邮箱地址,昵称等查询联系人,可以对
ContactsContract.Data
表进行查询,该表中包含联系人ID,姓名等。
ContactsContract.Data
表中的常量包含与联系人绑定的数据点。且每一行的数据表中存放着单个一些列的联系人信息(比如手机号)和他相关的数据如(家庭电话或者是工作电话)。
数据类型
数据表可以容纳任何种类的联系数据。存储在一个特定行的数据由MIMETYPE
值指定,该值决定了数据存放在通用的数据表DATA1到DATA15行中。比如,如果数据类型是 Phone.CONTENT_ITEM_TYPE
则DATA1列存放手机号,如果数据类型是 Email.CONTENT_ITEM_TYPE
则DATA1存放为邮箱地址。同步的适配器和应用可以定义他们自己的数据类型。
ContactsContract
预设了很少一部分数据类型,例如:ContactsContract.CommonDataKinds.Phone
, ContactsContract.CommonDataKinds.Email
等等。为了方便起见,为DATA1起了别名。Phone.NUMBER
与DATA1是相同的意思。
DATA1位索引列,用于预期在查询选择中最常用的数据元素。例如在代表电子邮件地址的DATA1行的情况下,应用于邮件地址本身,而DATA2等可以用于诸如电子邮件类型的辅助信息。
按照惯例,DATA15用于存储BLOB(二进制数据)
给定账户类型的同步适配器必须正确的处理相应原始联系人中使用的每种数据类型。否则可能导致数据丢失或者损坏。
同样的你不能为已有的账号类型引入新的数据类型。例如,你给谷歌账号的联系人数据表行中中新增一个数据类型“favorite song”,它将不会同步到服务器上面,因为谷歌同步适配器并不能解析这种数据类型。所以新的数据类型应该去建立新的账户类型或者新的同步适配器。
批量操作
可以使用传统的插入(Uri,ContentValues),update(Uri,ContentValues,String,String [])和delete(Uri,String,String [])方法来插入/更新/删除数据行,但是基于在几乎所有情况下,批处理ContentProviderOperation
将被证明是一个更好的选择。批处理中的所有操作都在单个事务中执行,这确保原始联系人的电话端和服务器端状态始终保持一致。此外,基于批处理的方法效率更高:不仅数据库操作在单个事务中执行时更快,而且还向内容提供者发送一批命令可以节省大量时间在进程和内容提供商运行的过程。
使用批处理操作的另一面是大批量可能长时间锁定数据库,从而阻止其他应用程序访问数据并潜在地导致ANR(“应用程序无响应”对话框)。)
为了避免数据库的这种锁定,请确保在批处理中插入“产生点”。产生点向内容提供者指出,在执行下一个操作之前,它可以提交已经进行的更改,产生其他请求,打开另一个事务并继续处理操作。屈服点不会自动提交事务,但只有在数据库上有其他请求等待时。通常,同步适配器应在批次中的每个原始接触操作序列的开始处插入屈服点。
操作符
Insert
可以使用传统插入(Uri,ContentValues)方法插入单个数据行。应该始终以批次的形式插入多个行。
例如:
ContentValues values = new ContentValues(); values.put(Data.RAW_CONTACT_ID, rawContactId); values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); values.put(Phone.NUMBER, "1-800-GOOG-411"); values.put(Phone.TYPE, Phone.TYPE_CUSTOM); values.put(Phone.LABEL, "free directory assistance"); Uri dataUri = getContentResolver().insert(Data.CONTENT_URI, values);
同样的可以使用ContentProviderOperations
:
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValue(Data.RAW_CONTACT_ID, rawContactId) .withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE) .withValue(Phone.NUMBER, "1-800-GOOG-411") .withValue(Phone.TYPE, Phone.TYPE_CUSTOM) .withValue(Phone.LABEL, "free directory assistance") .build()); getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
Update
就像插入一样,更新可以逐个或者批处理,批处理模式是首选方法:
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newUpdate(Data.CONTENT_URI) .withSelection(Data._ID + "=?", new String[]{String.valueOf(dataId)}) .withValue(Email.DATA, "somebody@android.com") .build()); getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
Delete
就像插入和更新一样,删除可以使用delete(Uri,String,String [])方法或使用ContentProviderOperation完成:
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newDelete(Data.CONTENT_URI) .withSelection(Data._ID + "=?", new String[]{String.valueOf(dataId)}) .build()); getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
Query
查找给定联系人的给定类型的所有数据
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);
查找给定联系人的给定类型的所有原始数据
Cursor c = getContentResolver().query(Data.CONTENT_URI, new String[] {Data._ID, Phone.NUMBER, Phone.TYPE, Phone.LABEL}, Data.RAW_CONTACT_ID + "=?" + " AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'", new String[] {String.valueOf(rawContactId)}, null);
插入通讯录实例
插入demo
/** * 插入本地联系人数据库 * 名片夹的插入地方目前全部是以新增模式保存通讯录,待放出名片夹功能时需要做是否是更新操作的判断 */ public static int insertContact(Context context, List<CardCaseBean> list, boolean... action) { boolean isUpdate = false; if (action.length > 0) { if (action[0]) { isUpdate = action[0]; } } if (list == null || list.size() < 0) { return 0; } ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); int rawContactInsertIndex = 0; int resutl = 0; for (CardCaseBean card : list) { rawContactInsertIndex = ops.size(); if (isUpdate) { ops.add(ContentProviderOperation.newUpdate(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) .build()); } else { ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) .build()); } //插入姓名 if (!TextUtils.isEmpty(card.name)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, card.name) .build()); } //插入电话 if (!TextUtils.isEmpty(card.mobile)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_WORK) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, card.mobile) .build()); } //插入传真 if (!TextUtils.isEmpty(card.fax)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, card.fax) .build()); } //插入Email if (!TextUtils.isEmpty(card.email)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, card.email) .withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_WORK) .build()); } //插入工作网址 if (!TextUtils.isEmpty(card.web)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Website.URL, card.web) .withValue(ContactsContract.CommonDataKinds.Website.TYPE, ContactsContract.CommonDataKinds.Website.TYPE_WORK) .build()); } //插入地址 if (!TextUtils.isEmpty(card.address)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK) .withValue(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, card.address) .build()); } //插入部门 if (!TextUtils.isEmpty(card.department)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, card.department) .build()); } //插入职务 if (!TextUtils.isEmpty(card.jobTitle)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, card.jobTitle) .build()); } //插入公司 if (!TextUtils.isEmpty(card.company)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, card.company) .withYieldAllowed(true) .build()); } //插入备注【公司简介】 if (!TextUtils.isEmpty(card.note)) { ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Note.DATA1, card.note) .withYieldAllowed(true) .build()); } } // 真正添加 try { ContentProviderResult[] results = context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); if (results != null && results.length > 0) { for (ContentProviderResult result : results) { SoutUtils.out("URI:" + result.uri, "count:" + result.count); } resutl++; } } catch (RemoteException | OperationApplicationException e) { e.printStackTrace(); } return resutl; }
插入前要进行一些权限的判断以及开启
/*** * 执行插入用户的通讯录 * * @param activity:当前的Activity * @param items:待插入的数据 * @param isUpdate:是否是更新通讯录 */ private static boolean executeInsertToAddressBook(final Activity activity, List<CardCaseBean> items, boolean isUpdate) { int hasWriteContactsPermission = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CONTACTS); if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) { //用户拒绝授予权限并且不再提示 if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_CONTACTS)) { CommonMsgDialog.Builder commonMsgDialog = new CommonMsgDialog.Builder(activity) .setMessage("没有写入通讯录的权限").setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }).setPositiveButton("开启", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_CONTACTS}, REQUEST_WRITE_CONSTACTS_PERMISSIONS); } }); commonMsgDialog.create().show(); return false; } ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_CONTACTS}, REQUEST_WRITE_CONSTACTS_PERMISSIONS); return false; } int result = UserUtils.insertContact(activity, items, isUpdate); boolean isSuccess = result > 0; if (isSuccess) { if (isUpdate) { Util.showShortToast(activity, "更新成功"); } else { Util.showShortToast(activity, "已存入通讯录"); } } else { Util.showShortToast(activity, "存入失败"); } return isSuccess; }
获取所有联系人常用字段数据
/** * 获取所有联系人的姓名、手机号、公司、职务、地址、邮箱 */ public static List<CardCaseBean> getAllContact(Context context) { //读取通讯录的全部的联系人 //需要先在raw_contact表中遍历id,并根据id到data表中获取数据 //uri = content://com.android.contacts/contacts List<CardCaseBean> list = new ArrayList<>(); Uri uri = Uri.parse("content://com.android.contacts/contacts"); //访问raw_contacts表 ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(uri, new String[]{ContactsContract.Contacts.Data._ID}, null, null, null); //获得_id属性 while (cursor.moveToNext()) { CardCaseBean contact = new CardCaseBean(); int id = cursor.getInt(0);//获得id并且在data中寻找数据 uri = Uri.parse("content://com.android.contacts/contacts/" + id + "/data"); //如果要获得data表中某个id对应的数据,则URI为content://com.android.contacts/contacts/#/data Cursor cursor2 = resolver.query(uri, new String[]{ContactsContract.Contacts.Data.DATA1, ContactsContract.Contacts.Data.MIMETYPE}, null, null, null); //data1存储各个记录的总数据,mimetype存放记录的类型,如电话、email等 while (cursor2.moveToNext()) { String data = cursor2.getString(cursor2.getColumnIndex("data1")); if (isHasValue(data)) { if (cursor2.getString(cursor2.getColumnIndex(ContactsContract.Data.MIMETYPE)).equals(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) { //如果是名字 contact.name = data; } else if (cursor2.getString(cursor2.getColumnIndex(ContactsContract.Data.MIMETYPE)).equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) { //如果是电话 contact.mobile = data; } else if (cursor2.getString(cursor2.getColumnIndex(ContactsContract.Data.MIMETYPE)).equals(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)) { //如果是email contact.email = data; } else if (cursor2.getString(cursor2.getColumnIndex(ContactsContract.Data.MIMETYPE)).equals(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)) { //如果公司 contact.company = data; } } } Cursor cursor3 = resolver.query(uri, new String[]{ContactsContract.Contacts.Data.DATA4, ContactsContract.Contacts.Data.MIMETYPE}, null, null, null); //data4 获取公司地址以及职务 while (cursor3.moveToNext()) { String data = cursor3.getString(cursor3.getColumnIndex("data4")); if (isHasValue(data) && cursor3.getString(cursor3.getColumnIndex(ContactsContract.Data.MIMETYPE)).equals(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)) { contact.jobTitle = data; } } Cursor cursor4 = resolver.query(uri, new String[]{ContactsContract.Contacts.Data.DATA9, ContactsContract.Contacts.Data.MIMETYPE}, null, null, null); //data4 获取公司地址以及职务 while (cursor4.moveToNext()) { String data = cursor4.getString(cursor4.getColumnIndex("data9")); if (isHasValue(data) && cursor4.getString(cursor4.getColumnIndex(ContactsContract.Data.MIMETYPE)).equals(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)) { contact.address = data; } } list.add(contact); } return list; }