7、更新联系人:更新RawContacts显示名称、更新电话号码、更新邮箱、ContentProvider事务与同步
好,咱们接着聊联系人更新这块。说实话,更新操作比插入要麻烦不少。为什么?因为你要考虑的事情多了——数据原本就在那里,你不能瞎改,改错了用户可要骂娘的。我在项目里见过不少新手,一上来就对着RawContacts表直接UPDATE,结果把系统联系人搞崩了。嗯,咱们今天就把这事捋清楚。
7.1 更新RawContacts的显示名称
先说说最基础的——改名字。显示名称存在RawContacts表的DISPLAY_NAME字段里。但你要注意,这个字段其实是系统从结构化姓名里自动生成的。你直接改它,系统可能会跟你对着干。
我个人习惯的做法是:先查一下这个联系人有没有结构化姓名数据(StructuredName),如果有,就改那个;如果没有,再直接改DISPLAY_NAME。这样更稳妥。
ContentValues values = new ContentValues();
values.put(RawContacts.DISPLAY_NAME, "新名字");
Uri rawContactUri = ContentUris.withAppendedId(
RawContacts.CONTENT_URI, rawContactId);
int updated = getContentResolver().update(
rawContactUri, values, null, null);
这段代码看着简单吧?但坑在后面。你更新完DISPLAY_NAME,系统可能会触发同步,把其他账号的数据也给改了。我曾经遇到过,改了一个本地联系人的名字,结果Google账号里的同名联系人也变了。嗯,这里要注意——如果你只想改本地数据,最好加上账号类型的筛选条件。
ACCOUNT_TYPE和ACCOUNT_NAME。否则可能误改其他账号的数据。
7.2 更新电话号码
电话号码存在Data表里,MIMETYPE是Phone.CONTENT_ITEM_TYPE。更新它,你得先找到那条数据。怎么找?按RawContacts_ID和MIMETYPE查。
我一般这么干:先查一遍,拿到Data表的_ID,然后再更新。这样更精确,不会误伤其他号码。
// 先查出现有的电话号码
String selection = Data.RAW_CONTACT_ID + "=? AND " +
Data.MIMETYPE + "=?";
String[] selectionArgs = new String[]{
String.valueOf(rawContactId),
Phone.CONTENT_ITEM_TYPE
};
Cursor cursor = getContentResolver().query(
Data.CONTENT_URI, null, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
long dataId = cursor.getLong(
cursor.getColumnIndex(Data._ID));
// 更新这个号码
ContentValues values = new ContentValues();
values.put(Phone.NUMBER, "13800138000");
values.put(Phone.TYPE, Phone.TYPE_MOBILE);
Uri dataUri = ContentUris.withAppendedId(
Data.CONTENT_URI, dataId);
getContentResolver().update(dataUri, values, null, null);
}
这里有个细节:更新号码时,最好把TYPE也带上。为什么?因为用户可能同时改了号码类型,比如从「工作」改成「手机」。你不传TYPE,系统会保留旧值,但用户界面可能显示不对。我在项目里踩过这个坑,用户投诉说「我明明改了类型,怎么还是显示旧的」。
7.3 更新邮箱
邮箱的更新逻辑跟电话号码几乎一样。只是MIMETYPE换成了Email.CONTENT_ITEM_TYPE。但有一个区别:邮箱可以没有,也可以有多个。所以更新前,你得想清楚——是更新现有的,还是新增一个?
我建议的做法是:先查用户有没有这个类型的邮箱。如果有,就更新;如果没有,就插入。别一股脑全删了再插,那样会丢失数据。
// 更新邮箱
ContentValues values = new ContentValues();
values.put(Email.ADDRESS, "newemail@example.com");
values.put(Email.TYPE, Email.TYPE_HOME);
String where = Data.RAW_CONTACT_ID + "=? AND " +
Data.MIMETYPE + "=? AND " +
Email.TYPE + "=?";
String[] whereArgs = new String[]{
String.valueOf(rawContactId),
Email.CONTENT_ITEM_TYPE,
String.valueOf(Email.TYPE_HOME)
};
getContentResolver().update(
Data.CONTENT_URI, values, where, whereArgs);
你看,这里我加了TYPE的筛选条件。这样只更新「家庭」类型的邮箱,不会动「工作」邮箱。嗯,这个细节很重要——用户可能有多个邮箱,你不能一把梭全改了。
7.4 ContentProvider事务与同步
好,重点来了。如果你要同时更新名字、电话、邮箱,你会怎么做?一条一条更新?那不行。万一更新到一半手机没电了,数据就乱了。这时候就需要事务。
Android的ContactsProvider支持批量操作,通过ContentProviderOperation来实现。说白了,就是把多个操作打包成一个事务,要么全成功,要么全失败。
ContentProviderOperation构建操作列表,然后通过ContentResolver.applyBatch()一次性提交。
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// 1. 更新显示名称
ops.add(ContentProviderOperation.newUpdate(
ContentUris.withAppendedId(
RawContacts.CONTENT_URI, rawContactId))
.withValue(RawContacts.DISPLAY_NAME, "张三")
.build());
// 2. 更新电话号码
ops.add(ContentProviderOperation.newUpdate(
Data.CONTENT_URI)
.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
Data.MIMETYPE + "=?",
new String[]{String.valueOf(rawContactId),
Phone.CONTENT_ITEM_TYPE})
.withValue(Phone.NUMBER, "13900001111")
.build());
// 3. 更新邮箱
ops.add(ContentProviderOperation.newUpdate(
Data.CONTENT_URI)
.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
Data.MIMETYPE + "=?",
new String[]{String.valueOf(rawContactId),
Email.CONTENT_ITEM_TYPE})
.withValue(Email.ADDRESS, "zhangsan@test.com")
.build());
// 一次性提交
ContentProviderResult[] results =
getContentResolver().applyBatch(
ContactsContract.AUTHORITY, ops);
这段代码看着长,但逻辑很清晰。三个操作打包成一个事务。任何一个失败,整个事务回滚。数据不会处于「半更新」状态。
applyBatch()返回一个ContentProviderResult[]数组,每个元素对应一个操作的结果。你可以遍历它,检查每个操作是否成功。
说到同步,这里有个概念要搞清楚:事务是保证数据一致性,同步是保证多设备数据一致。你更新了联系人,系统会自动触发同步适配器(SyncAdapter),把改动同步到服务器。但如果你用了事务,同步会在事务提交后才触发。所以不用担心同步乱掉。
我曾经在一个项目里,因为没用事务,导致用户同时修改联系人和拨打电话时,数据出现不一致。后来改成applyBatch,问题就解决了。说白了,事务就是给你的数据操作上了一把锁,防止并发捣乱。
7.5 知识体系总览
下面这张图,把咱们今天讲的内容串起来了。你可以看到,更新操作的核心就是三个步骤:定位数据、构建操作、提交事务。
你看,整个流程其实不复杂。关键就两点:一是用ContentProviderOperation构建操作,二是用applyBatch提交。这两步做好了,数据一致性就有保障。
applyBatch()。它可能耗时较长,建议在子线程或AsyncTask中执行。否则,用户界面会卡住,体验很差。
好了,关于更新联系人,咱们就聊这么多。记住:能用事务就别单条更新,能精确匹配就别模糊查询。这两个原则,能帮你避开90%的坑。