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账号里的同名联系人也变了。嗯,这里要注意——如果你只想改本地数据,最好加上账号类型的筛选条件。

⚠️ 注意: 更新RawContacts时,务必确认ACCOUNT_TYPEACCOUNT_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 ContentProviderOperation ContentProviderOperation applyBatch() 事务提交 要么全部成功,要么全部回滚

你看,整个流程其实不复杂。关键就两点:一是用ContentProviderOperation构建操作,二是用applyBatch提交。这两步做好了,数据一致性就有保障。

⚠️ 重要提醒: 不要在UI线程调用applyBatch()。它可能耗时较长,建议在子线程或AsyncTask中执行。否则,用户界面会卡住,体验很差。

好了,关于更新联系人,咱们就聊这么多。记住:能用事务就别单条更新,能精确匹配就别模糊查询。这两个原则,能帮你避开90%的坑。

公众号:蓝海资料掘金营,微信deep3321