15、联系人备份与恢复:导出vCard格式、解析vCard文件、备份到本地文件、从云端恢复

说到联系人备份,我脑子里第一个蹦出来的就是vCard。这玩意儿在Android系统里简直就是联系人的"通用语言"。你想想看,不管是iPhone、华为还是小米,只要导出vCard文件,大家都能互相认。今天我们就来彻底搞懂它。

15.1 vCard格式到底是什么?

vCard,全称是Virtual Card File,说白了就是联系人的"身份证"文件。它的标准格式是.vcf。我刚开始做Android开发时,以为这格式很复杂,后来发现其实就是一堆键值对。

一个典型的vCard长这样:

BEGIN:VCARD
VERSION:3.0
FN:张三
TEL;TYPE=CELL:13800138000
EMAIL:zhangsan@example.com
END:VCARD

嗯,是不是很简单?每个字段都有特定含义:

字段 含义 示例
BEGIN/END 标记vCard的开始和结束 BEGIN:VCARD
VERSION 版本号,常用3.0或4.0 VERSION:3.0
FN 显示名称 FN:张三
TEL 电话号码,可带类型 TEL;TYPE=CELL:13800138000
EMAIL 电子邮箱 EMAIL:zhangsan@example.com
我的小经验:vCard 3.0是目前兼容性最好的版本。4.0虽然功能更多,但有些老手机不认。我个人习惯统一用3.0。

15.2 导出vCard:把联系人变成文件

导出vCard,其实就是把系统联系人数据库里的数据,按照vCard格式拼成字符串。Android系统提供了一个很方便的类——ContactsContract.Contacts,配合ContentResolver就能搞定。

来看看核心代码:

public void exportToVCard(Context context, List<Long> contactIds, File outputFile) {
    try {
        ContentResolver resolver = context.getContentResolver();
        StringBuilder vCardBuilder = new StringBuilder();

        for (Long contactId : contactIds) {
            Uri contactUri = ContentUris.withAppendedId(
                ContactsContract.Contacts.CONTENT_URI, contactId);
            
            // 查询联系人数据
            Cursor cursor = resolver.query(contactUri, null, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                String displayName = cursor.getString(
                    cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
                String phoneNumber = cursor.getString(
                    cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER));
                
                // 拼装vCard格式
                vCardBuilder.append("BEGIN:VCARD\n");
                vCardBuilder.append("VERSION:3.0\n");
                vCardBuilder.append("FN:").append(displayName).append("\n");
                
                if (phoneNumber != null) {
                    vCardBuilder.append("TEL;TYPE=CELL:").append(phoneNumber).append("\n");
                }
                
                vCardBuilder.append("END:VCARD\n");
                cursor.close();
            }
        }

        // 写入文件
        FileOutputStream fos = new FileOutputStream(outputFile);
        fos.write(vCardBuilder.toString().getBytes("UTF-8"));
        fos.close();
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}
注意:上面的代码是简化版。实际项目中,一个联系人可能有多个电话、多个邮箱。我曾经踩过坑——只导出了第一个电话,用户恢复后发现少了好几个号码,被投诉了。所以一定要遍历所有数据。

15.3 解析vCard:把文件变回联系人

解析vCard,说白了就是逆向操作。把.vcf文件读出来,一行一行地解析,然后插入到系统联系人数据库。

Android官方推荐用vCard库来解析,但如果你不想引入额外依赖,自己写解析器也不难:

public List<ContactInfo> parseVCard(File vcfFile) {
    List<ContactInfo> contacts = new ArrayList<>();
    
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(new FileInputStream(vcfFile), "UTF-8"));
        
        String line;
        ContactInfo currentContact = null;
        
        while ((line = reader.readLine()) != null) {
            if (line.startsWith("BEGIN:VCARD")) {
                currentContact = new ContactInfo();
            } else if (line.startsWith("FN:")) {
                if (currentContact != null) {
                    currentContact.name = line.substring(3);
                }
            } else if (line.startsWith("TEL")) {
                if (currentContact != null) {
                    // 处理TEL;TYPE=CELL:13800138000 这种格式
                    String[] parts = line.split(":");
                    if (parts.length >= 2) {
                        currentContact.phone = parts[1];
                    }
                }
            } else if (line.startsWith("END:VCARD")) {
                if (currentContact != null) {
                    contacts.add(currentContact);
                    currentContact = null;
                }
            }
        }
        
        reader.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
    
    return contacts;
}
避坑指南:我曾经遇到过一个奇葩问题——用户从iPhone导出的vCard文件,编码是GBK的,直接按UTF-8读全是乱码。所以解析时最好先检测文件编码,或者让用户选择编码格式。

15.4 备份到本地文件

备份到本地,其实就是把vCard文件存到手机存储里。这里有两个选择:

  • 应用私有目录:context.getFilesDir(),其他应用看不到,安全但不好分享
  • 公共下载目录:Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),用户可以直接在文件管理器里找到

我个人建议用公共目录,因为用户备份的目的就是为了以后能恢复,放在私有目录里万一卸载应用就全没了。

public void backupToLocal(Context context, List<Long> contactIds) {
    // 生成文件名,带上时间戳避免覆盖
    String fileName = "contacts_backup_" + 
        new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
            .format(new Date()) + ".vcf";
    
    File backupDir = Environment.getExternalStoragePublicDirectory(
        Environment.DIRECTORY_DOWNLOADS);
    
    if (!backupDir.exists()) {
        backupDir.mkdirs();
    }
    
    File backupFile = new File(backupDir, fileName);
    exportToVCard(context, contactIds, backupFile);
    
    // 通知系统文件已创建
    Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    intent.setData(Uri.fromFile(backupFile));
    context.sendBroadcast(intent);
}

15.5 从云端恢复

从云端恢复,流程其实和本地恢复差不多,只是多了一步下载。我一般这样设计:

  1. 用户点击"从云端恢复"
  2. 从服务器获取备份文件列表(按时间排序)
  3. 用户选择要恢复的备份
  4. 下载vCard文件到临时目录
  5. 解析vCard并插入联系人数据库
  6. 删除临时文件

这里有个关键点——插入联系人时,一定要用ContentResolver.applyBatch()批量操作,而不是一条一条插入。为什么呢?因为每条插入都会触发一次数据库事务,几百个联系人下来,速度慢得让人抓狂。

public void restoreFromCloud(Context context, String downloadUrl) {
    try {
        // 1. 下载文件
        File tempFile = new File(context.getCacheDir(), "temp_restore.vcf");
        downloadFile(downloadUrl, tempFile);
        
        // 2. 解析vCard
        List<ContactInfo> contacts = parseVCard(tempFile);
        
        // 3. 批量插入联系人
        ArrayList<ContentProviderOperation> operations = new ArrayList<>();
        
        for (ContactInfo contact : contacts) {
            // 插入RawContacts
            int rawContactIndex = operations.size();
            operations.add(ContentProviderOperation.newInsert(
                RawContacts.CONTENT_URI)
                .withValue(RawContacts.ACCOUNT_TYPE, null)
                .withValue(RawContacts.ACCOUNT_NAME, null)
                .build());
            
            // 插入姓名
            operations.add(ContentProviderOperation.newInsert(
                Data.CONTENT_URI)
                .withValueBackReference(Data.RAW_CONTACT_ID, rawContactIndex)
                .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
                .withValue(StructuredName.DISPLAY_NAME, contact.name)
                .build());
            
            // 插入电话
            if (contact.phone != null) {
                operations.add(ContentProviderOperation.newInsert(
                    Data.CONTENT_URI)
                    .withValueBackReference(Data.RAW_CONTACT_ID, rawContactIndex)
                    .withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
                    .withValue(Phone.NUMBER, contact.phone)
                    .withValue(Phone.TYPE, Phone.TYPE_MOBILE)
                    .build());
            }
        }
        
        // 执行批量操作
        context.getContentResolver().applyBatch(
            ContactsContract.AUTHORITY, operations);
        
        // 4. 清理临时文件
        tempFile.delete();
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}
我的建议:恢复前最好先让用户确认是否要覆盖现有联系人。我见过有人恢复备份后,发现联系人变成了双份,气得直跺脚。给用户一个"清空现有联系人再恢复"的选项,会贴心很多。

15.6 本章知识体系

下面这张图,把整个备份恢复的流程串起来了:

联系人备份与恢复核心流程 系统联系人数据库 vCard文件(.vcf) 云端备份文件 导出vCard格式 解析vCard文件 下载/上传文件 备份到本地文件 从本地文件恢复 从云端恢复 联系人数据安全无忧 备份方向 恢复方向 双向

嗯,整个流程其实就三个核心动作:导出、解析、存储。你想想看,只要把这三点搞明白了,不管是本地备份还是云端恢复,都是换汤不换药。

最后说一句:联系人数据对用户来说太重要了。我见过有人丢了手机,哭得比丢了钱包还伤心。所以备份功能一定要做得稳、做得快、做得安全。别让用户在你这里再哭一次。
公众号:蓝海资料掘金营,微信deep3321