5、联系人详情展示:查询RawContacts表、获取电话号码(Data表)、获取邮箱地址、多数据类型合并展示

好,咱们接着往下走。上一章我们把联系人列表搞定了,现在点进去看详情,对吧?

说实话,联系人详情这块,是Android通讯录开发里最容易踩坑的地方。为什么?因为数据太散了。一个联系人的名字、电话、邮箱、地址、头像……全部分散在不同的表里。你想想看,要是没理清楚,代码写出来就是一坨浆糊。

我个人习惯是,先把数据模型想清楚,再动手写代码。别急着敲键盘,先画图。

5.1 数据到底存在哪?

咱们先回顾一下Contacts Provider的核心表结构。上一章我提过,但这里得再深入一点。

表名 作用 关键字段
ContactsContract.Contacts 聚合后的联系人(一个联系人对应一行) _ID, DISPLAY_NAME, PHOTO_URI
ContactsContract.RawContacts 原始联系人数据(来自不同账户) _ID, CONTACT_ID, ACCOUNT_NAME, ACCOUNT_TYPE
ContactsContract.Data 具体数据行(电话、邮箱、地址等) _ID, RAW_CONTACT_ID, MIMETYPE, DATA1~DATA15

嗯,这里要注意:一个联系人可能对应多个RawContacts。比如你手机里同步了Google账号和微信账号,同一个联系人可能有两个RawContacts行。而每个RawContacts下面,又挂着一堆Data行。

所以,查询详情的基本思路是:

  1. 根据联系人ID,查出所有关联的RawContacts
  2. 根据RawContacts的ID,查出所有关联的Data行
  3. 根据Data行的MIMETYPE,区分出电话、邮箱、地址等

核心原则:不要直接查Contacts表拿电话!电话只存在Data表里。我见过有新手直接去Contacts表里翻PHONE_NUMBER字段,结果发现是空的——因为那个字段早就废弃了。

5.2 查询RawContacts表

先写个查询RawContacts的代码。假设我们已经有了联系人ID(就是Contacts表里的_ID)。

// 查询RawContacts
public List<Long> queryRawContactIds(Context context, long contactId) {
    List<Long> rawContactIds = new ArrayList<>();
    Uri uri = RawContacts.CONTENT_URI;
    String selection = RawContacts.CONTACT_ID + " = ?";
    String[] selectionArgs = new String[]{String.valueOf(contactId)};
    
    Cursor cursor = null;
    try {
        cursor = context.getContentResolver().query(
            uri,
            new String[]{RawContacts._ID},
            selection,
            selectionArgs,
            null
        );
        if (cursor != null) {
            while (cursor.moveToNext()) {
                rawContactIds.add(cursor.getLong(0));
            }
        }
    } catch (Exception e) {
        // 处理异常
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return rawContactIds;
}

这段代码很简单,对吧?但我在项目中遇到过一个问题:有些RawContacts是被标记为删除的。如果你不加上 RawContacts.DELETED + " = 0" 这个条件,可能会查到一堆脏数据。

避坑指南:我曾经在一个企业通讯录项目里,因为没过滤DELETED字段,导致详情页显示了一堆空数据。排查了半天才发现是同步过程中产生的废弃RawContacts没清理干净。所以,建议查询时加上 RawContacts.DELETED + " = 0"

5.3 获取电话号码(Data表)

拿到RawContacts的ID列表后,下一步就是查Data表。电话的MIMETYPE是 Phone.CONTENT_ITEM_TYPE,也就是 "vnd.android.cursor.item/phone_v2"

// 查询电话号码
public List<String> queryPhoneNumbers(Context context, List<Long> rawContactIds) {
    List<String> phoneNumbers = new ArrayList<>();
    if (rawContactIds == null || rawContactIds.isEmpty()) {
        return phoneNumbers;
    }
    
    // 构建IN查询条件
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < rawContactIds.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append("?");
    }
    String placeholders = sb.toString();
    
    Uri uri = Data.CONTENT_URI;
    String selection = Data.RAW_CONTACT_ID + " IN (" + placeholders + ") AND "
            + Data.MIMETYPE + " = ?";
    String[] selectionArgs = new String[rawContactIds.size() + 1];
    for (int i = 0; i < rawContactIds.size(); i++) {
        selectionArgs[i] = String.valueOf(rawContactIds.get(i));
    }
    selectionArgs[rawContactIds.size()] = Phone.CONTENT_ITEM_TYPE;
    
    Cursor cursor = null;
    try {
        cursor = context.getContentResolver().query(
            uri,
            new String[]{Phone.NUMBER, Phone.TYPE, Phone.LABEL},
            selection,
            selectionArgs,
            null
        );
        if (cursor != null) {
            while (cursor.moveToNext()) {
                String number = cursor.getString(0);
                int type = cursor.getInt(1);
                String label = cursor.getString(2);
                // 这里可以处理号码类型:HOME、MOBILE、WORK等
                phoneNumbers.add(number);
            }
        }
    } catch (Exception e) {
        // 处理异常
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return phoneNumbers;
}

这里有个小技巧:查询Data表时,尽量只查你需要的列。别用 null 代替投影列,那样会拉回所有15个DATA字段,性能差很多。你想想看,Data表可能有几十万行数据,全查出来内存就炸了。

5.4 获取邮箱地址

邮箱的查询逻辑和电话几乎一模一样,只是MIMETYPE不同。邮箱的MIMETYPE是 Email.CONTENT_ITEM_TYPE,也就是 "vnd.android.cursor.item/email_v2"

// 查询邮箱地址
public List<String> queryEmails(Context context, List<Long> rawContactIds) {
    List<String> emails = new ArrayList<>();
    if (rawContactIds == null || rawContactIds.isEmpty()) {
        return emails;
    }
    
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < rawContactIds.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append("?");
    }
    String placeholders = sb.toString();
    
    Uri uri = Data.CONTENT_URI;
    String selection = Data.RAW_CONTACT_ID + " IN (" + placeholders + ") AND "
            + Data.MIMETYPE + " = ?";
    String[] selectionArgs = new String[rawContactIds.size() + 1];
    for (int i = 0; i < rawContactIds.size(); i++) {
        selectionArgs[i] = String.valueOf(rawContactIds.get(i));
    }
    selectionArgs[rawContactIds.size()] = Email.CONTENT_ITEM_TYPE;
    
    Cursor cursor = null;
    try {
        cursor = context.getContentResolver().query(
            uri,
            new String[]{Email.ADDRESS, Email.TYPE, Email.LABEL},
            selection,
            selectionArgs,
            null
        );
        if (cursor != null) {
            while (cursor.moveToNext()) {
                String address = cursor.getString(0);
                emails.add(address);
            }
        }
    } catch (Exception e) {
        // 处理异常
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return emails;
}

嗯,代码确实很像。但别急着复制粘贴,咱们得想想怎么优化。

5.5 多数据类型合并展示

在实际项目中,你不会只查电话和邮箱。可能还有地址、IM账号、网站、生日……如果每个类型都写一个查询方法,那代码就太冗余了。

我个人习惯是:一次查询,按MIMETYPE分类。这样只需要一次数据库查询,然后自己在内存里做分类。

// 一次查询所有Data,按MIMETYPE分类
public Map<String, List<ContentValues>> queryAllData(Context context, List<Long> rawContactIds) {
    Map<String, List<ContentValues>> dataMap = new HashMap<>();
    if (rawContactIds == null || rawContactIds.isEmpty()) {
        return dataMap;
    }
    
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < rawContactIds.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append("?");
    }
    String placeholders = sb.toString();
    
    Uri uri = Data.CONTENT_URI;
    String selection = Data.RAW_CONTACT_ID + " IN (" + placeholders + ")";
    String[] selectionArgs = new String[rawContactIds.size()];
    for (int i = 0; i < rawContactIds.size(); i++) {
        selectionArgs[i] = String.valueOf(rawContactIds.get(i));
    }
    
    Cursor cursor = null;
    try {
        cursor = context.getContentResolver().query(
            uri,
            null,  // 这里可以指定需要的列,但为了演示先查全部
            selection,
            selectionArgs,
            null
        );
        if (cursor != null) {
            while (cursor.moveToNext()) {
                ContentValues values = new ContentValues();
                // 把需要的字段都取出来
                String mimeType = cursor.getString(cursor.getColumnIndex(Data.MIMETYPE));
                // 根据MIMETYPE存到不同的列表里
                List<ContentValues> list = dataMap.get(mimeType);
                if (list == null) {
                    list = new ArrayList<>();
                    dataMap.put(mimeType, list);
                }
                // 这里只存关键数据,实际项目中可以封装成对象
                values.put("data1", cursor.getString(cursor.getColumnIndex("data1")));
                values.put("data2", cursor.getString(cursor.getColumnIndex("data2")));
                values.put("data3", cursor.getString(cursor.getColumnIndex("data3")));
                list.add(values);
            }
        }
    } catch (Exception e) {
        // 处理异常
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return dataMap;
}

拿到这个Map之后,展示就简单了。根据MIMETYPE去Map里取对应的列表,然后渲染到UI上。

小提示:Data表的data1~data15字段含义取决于MIMETYPE。比如电话类型下,data1是号码,data2是类型(HOME/MOBILE等),data3是自定义标签。邮箱类型下,data1是地址,data2是类型。所以,一定要先判断MIMETYPE,再解析data字段

5.6 核心流程梳理

说了这么多,咱们用一张图把整个流程串起来。这样你脑子里就有个清晰的脉络了。

联系人详情查询流程 1. 获取联系人ID 2. 查询RawContacts表 过滤 DELETED = 0 3. 查询Data表 一次查询,按MIMETYPE分类 4. 按MIMETYPE分类处理 电话 (phone_v2) 邮箱 (email_v2) 地址 (postal-address_v2) 其他类型 5. 合并展示到UI

这张图把整个流程分成了5步。说白了,核心就是:先查RawContacts拿到ID列表,再查Data表拿到所有数据,最后按类型分类展示

5.7 合并展示的UI设计

最后聊聊UI展示。我个人建议用RecyclerView来实现,每个数据类型作为一个Section。比如:

  • 电话区域:显示所有电话号码,每个号码旁边带类型标签(家庭、工作、手机等)
  • 邮箱区域:显示所有邮箱地址,同样带类型标签
  • 地址区域:如果有地址数据,也展示出来

每个Section之间用分割线隔开,或者用不同的背景色区分。这样用户一眼就能看清楚。

经验之谈:我在做国际版通讯录时,发现有些联系人有十几个电话号码。如果全部平铺展示,界面会很长。后来我改成默认只显示前3个,点击"展开"再显示全部。这个交互细节,用户反馈很好。

好了,联系人详情展示的核心逻辑就这些。记住:一次查询Data表,按MIMETYPE分类,比多次查询高效得多。代码写起来也清爽。


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