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:zhangsan@example.com |
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;
}
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 从云端恢复
从云端恢复,流程其实和本地恢复差不多,只是多了一步下载。我一般这样设计:
- 用户点击"从云端恢复"
- 从服务器获取备份文件列表(按时间排序)
- 用户选择要恢复的备份
- 下载vCard文件到临时目录
- 解析vCard并插入联系人数据库
- 删除临时文件
这里有个关键点——插入联系人时,一定要用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 本章知识体系
下面这张图,把整个备份恢复的流程串起来了:
嗯,整个流程其实就三个核心动作:导出、解析、存储。你想想看,只要把这三点搞明白了,不管是本地备份还是云端恢复,都是换汤不换药。