Android联系人导入导出技术文档
目录
- [1. 概述](#1. 概述)
- [2. Android联系人API](#2. Android联系人API)
- [3. 数据结构设计](#3. 数据结构设计)
- [4. 核心功能实现](#4. 核心功能实现)
- [5. 导入导出原理](#5. 导入导出原理)
- [6. 使用示例](#6. 使用示例)
- [7. 技术要点](#7. 技术要点)
- [8. 完整源码](#8. 完整源码)
1. 概述
1.1 功能特性
- ✅ 联系人查询(全部、按ID、搜索)
- ✅ 联系人导出(JSON格式、vCard格式)
- ✅ 联系人导入(JSON格式、vCard格式)
- ✅ 联系人管理(创建、删除)
- ✅ 联系人数据统计
1.2 技术栈
- 语言: Kotlin
- SDK: Android API 24+ (Android 7.0+)
- 序列化: kotlinx-serialization
- 存储格式: JSON、vCard
1.3 权限要求
xml
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
2. Android联系人API
2.1 核心类说明
ContactsContract
Android联系人数据的核心提供者,定义了所有联系人相关的数据表和字段。
主要数据表:
ContactsContract.Contacts- 联系人主表ContactsContract.Data- 联系人详细数据表ContactsContract.RawContacts- 原始联系人表
重要URI常量:
kotlin
ContactsContract.Contacts.CONTENT_URI // 联系人列表URI
ContactsContract.Data.CONTENT_URI // 联系人数据URI
ContactsContract.RawContacts.CONTENT_URI // 原始联系人URI
ContactsContract.CommonDataKinds
联系人的不同数据类型,每个类型都有对应的MIME类型和URI。
常用数据类型:
kotlin
// 电话号码
ContactsContract.CommonDataKinds.Phone
- CONTENT_URI: 查询电话号码
- MIMETYPE: "vnd.android.cursor.item/phone_v2"
// 邮箱
ContactsContract.CommonDataKinds.Email
- CONTENT_URI: 查询邮箱
- MIMETYPE: "vnd.android.cursor.item/email_v2"
// 组织信息
ContactsContract.CommonDataKinds.Organization
- CONTENT_ITEM_TYPE: "vnd.android.cursor.item/organization"
- 注意: 没有CONTENT_URI,需要使用Data.CONTENT_URI + MIMETYPE筛选
// 备注
ContactsContract.CommonDataKinds.Note
- CONTENT_ITEM_TYPE: "vnd.android.cursor.item/note"
- 注意: 没有CONTENT_URI,需要使用Data.CONTENT_URI + MIMETYPE筛选
// 地址
ContactsContract.CommonDataKinds.StructuredPostal
- CONTENT_URI: 查询地址
- MIMETYPE: "vnd.android.cursor.item/postal-address_v2"
2.2 查询方式
方式1:直接查询子表
适用于Phone、Email等有专门CONTENT_URI的类型。
kotlin
val cursor = contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID + " = ?",
arrayOf(contactId),
null
)
方式2:通过Data表查询
适用于Organization、Note等没有专门CONTENT_URI的类型。
kotlin
val cursor = contentResolver.query(
ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID + " = ? AND " +
ContactsContract.Data.MIMETYPE + " = ?",
arrayOf(contactId, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE),
null
)
3. 数据结构设计
3.1 联系人数据类
kotlin
@Serializable
data class Contact(
val id: String? = null, // 联系人ID(导出时包含,导入时忽略)
val displayName: String = "", // 显示名称
val phoneNumbers: List<PhoneNumber> = emptyList(), // 电话号码列表
val emails: List<Email> = emptyList(), // 邮箱列表
val organizations: List<Organization> = emptyList(), // 组织信息列表
val note: String = "", // 备注
val addresses: List<Address> = emptyList() // 地址列表
)
3.2 子数据类
电话号码
kotlin
@Serializable
data class PhoneNumber(
val number: String, // 电话号码
val type: String, // 类型:home/mobile/work/fax_home/fax_work/pager/other
val label: String? = null, // 自定义标签(当type为custom时使用)
val isPrimary: Boolean = false // 是否为主号码
)
邮箱
kotlin
@Serializable
data class Email(
val address: String, // 邮箱地址
val type: String, // 类型:home/work/mobile/other
val label: String? = null, // 自定义标签
val isPrimary: Boolean = false // 是否为主邮箱
)
组织信息
kotlin
@Serializable
data class Organization(
val company: String, // 公司名称
val title: String, // 职位
val type: String // 类型:work/other
)
地址
kotlin
@Serializable
data class Address(
val formattedAddress: String, // 格式化地址
val type: String, // 类型:home/work/other
val label: String? = null // 自定义标签
)
3.3 数据关系图
Contact (联系人)
├── displayName (显示名称)
├── phoneNumbers[] (电话号码列表)
│ ├── number (号码)
│ ├── type (类型)
│ ├── label (标签)
│ └── isPrimary (是否主号码)
├── emails[] (邮箱列表)
│ ├── address (邮箱地址)
│ ├── type (类型)
│ ├── label (标签)
│ └── isPrimary (是否主邮箱)
├── organizations[] (组织信息)
│ ├── company (公司)
│ ├── title (职位)
│ └── type (类型)
├── addresses[] (地址列表)
│ ├── formattedAddress (地址)
│ ├── type (类型)
│ └── label (标签)
└── note (备注)
4. 核心功能实现
4.1 查询联系人
获取所有联系人
kotlin
fun getAllContacts(limit: Int = -1): List<Contact> {
val contacts = mutableListOf<Contact>()
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME
)
val selection = if (limit > 0) "${ContactsContract.Contacts._ID} <= ?" else null
val selectionArgs = if (limit > 0) arrayOf(limit.toString()) else null
val cursor = contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
ContactsContract.Contacts.DISPLAY_NAME + " ASC"
)
cursor?.use {
while (it.moveToNext()) {
val contactId = it.getString(0)
val displayName = it.getString(1) ?: ""
contacts.add(
Contact(
id = contactId,
displayName = displayName,
phoneNumbers = getPhoneNumbers(contactId),
emails = getEmails(contactId),
organizations = getOrganizations(contactId),
note = getNote(contactId),
addresses = getAddresses(contactId)
)
)
}
}
return contacts
}
实现要点:
- 使用
cursor?.use {}确保资源自动释放 - 通过
ContactsContract.Contacts._ID获取联系人ID - 通过联系人ID查询子表获取详细信息
根据ID查询联系人
kotlin
fun getContactById(contactId: String): Contact? {
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME
)
val cursor = contentResolver.query(
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId.toLong()),
projection,
null,
null,
null
)
cursor?.use {
if (it.moveToFirst()) {
val displayName = it.getString(1) ?: ""
return Contact(
id = contactId,
displayName = displayName,
phoneNumbers = getPhoneNumbers(contactId),
emails = getEmails(contactId),
organizations = getOrganizations(contactId),
note = getNote(contactId),
addresses = getAddresses(contactId)
)
}
}
return null
}
搜索联系人
kotlin
fun searchContacts(query: String): List<Contact> {
val contacts = mutableListOf<Contact>()
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME
)
val selection = "${ContactsContract.Contacts.DISPLAY_NAME} LIKE ?"
val selectionArgs = arrayOf("%$query%")
val cursor = contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
ContactsContract.Contacts.DISPLAY_NAME + " ASC"
)
cursor?.use {
while (it.moveToNext()) {
val contactId = it.getString(0)
val displayName = it.getString(1) ?: ""
contacts.add(
Contact(
id = contactId,
displayName = displayName,
phoneNumbers = getPhoneNumbers(contactId),
emails = getEmails(contactId),
organizations = getOrganizations(contactId),
note = getNote(contactId),
addresses = getAddresses(contactId)
)
)
}
}
return contacts
}
4.2 获取联系人详细信息
获取电话号码
kotlin
private fun getPhoneNumbers(contactId: String): List<PhoneNumber> {
val phoneNumbers = mutableListOf<PhoneNumber>()
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.LABEL,
ContactsContract.CommonDataKinds.Phone.IS_PRIMARY
)
val cursor = contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID + " = ?",
arrayOf(contactId),
null
)
cursor?.use {
while (it.moveToNext()) {
phoneNumbers.add(
PhoneNumber(
number = it.getString(0) ?: "",
type = getPhoneTypeLabel(it.getInt(1)),
label = it.getString(2),
isPrimary = it.getInt(3) == 1
)
)
}
}
return phoneNumbers
}
获取邮箱
kotlin
private fun getEmails(contactId: String): List<Email> {
val emails = mutableListOf<Email>()
val projection = arrayOf(
ContactsContract.CommonDataKinds.Email.ADDRESS,
ContactsContract.CommonDataKinds.Email.TYPE,
ContactsContract.CommonDataKinds.Email.LABEL,
ContactsContract.CommonDataKinds.Email.IS_PRIMARY
)
val cursor = contentResolver.query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID + " = ?",
arrayOf(contactId),
null
)
cursor?.use {
while (it.moveToNext()) {
emails.add(
Email(
address = it.getString(0) ?: "",
type = getEmailTypeLabel(it.getInt(1)),
label = it.getString(2),
isPrimary = it.getInt(3) == 1
)
)
}
}
return emails
}
获取组织信息(关键实现)
kotlin
private fun getOrganizations(contactId: String): List<Organization> {
val organizations = mutableListOf<Organization>()
val projection = arrayOf(
ContactsContract.CommonDataKinds.Organization.COMPANY,
ContactsContract.CommonDataKinds.Organization.TITLE,
ContactsContract.CommonDataKinds.Organization.TYPE
)
// 关键:使用Data.CONTENT_URI并通过MIMETYPE筛选
val cursor = contentResolver.query(
ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID + " = ? AND " +
ContactsContract.Data.MIMETYPE + " = ?",
arrayOf(
contactId,
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE
),
null
)
cursor?.use {
while (it.moveToNext()) {
organizations.add(
Organization(
company = it.getString(0) ?: "",
title = it.getString(1) ?: "",
type = getOrganizationTypeLabel(it.getInt(2))
)
)
}
}
return organizations
}
关键技术点:
- Organization和Note没有专门的CONTENT_URI
- 必须使用
ContactsContract.Data.CONTENT_URI - 通过MIMETYPE筛选数据类型
CONTENT_ITEM_TYPE常量提供MIME类型字符串
获取备注
kotlin
private fun getNote(contactId: String): String {
val projection = arrayOf(ContactsContract.CommonDataKinds.Note.NOTE)
val cursor = contentResolver.query(
ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID + " = ? AND " +
ContactsContract.Data.MIMETYPE + " = ?",
arrayOf(
contactId,
ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE
),
null
)
cursor?.use {
if (it.moveToFirst()) {
return it.getString(0) ?: ""
}
}
return ""
}
获取地址
kotlin
private fun getAddresses(contactId: String): List<Address> {
val addresses = mutableListOf<Address>()
val projection = arrayOf(
ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS,
ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
ContactsContract.CommonDataKinds.StructuredPostal.LABEL
)
val cursor = contentResolver.query(
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID + " = ?",
arrayOf(contactId),
null
)
cursor?.use {
while (it.moveToNext()) {
addresses.add(
Address(
formattedAddress = it.getString(0) ?: "",
type = getAddressTypeLabel(it.getInt(1)),
label = it.getString(2)
)
)
}
}
return addresses
}
4.3 导出联系人
导出为JSON
kotlin
fun exportContactsToJson(outputFile: File): Result<Int> {
return try {
val contacts = getAllContacts()
val jsonString = json.encodeToString(contacts)
FileOutputStream(outputFile).use { fos ->
fos.write(jsonString.toByteArray(Charsets.UTF_8))
}
Result.success(contacts.size)
} catch (e: Exception) {
Log.e(TAG, "导出联系人失败: ${e.message}")
Result.failure(e)
}
}
实现流程:
- 查询所有联系人
- 使用kotlinx.serialization序列化为JSON字符串
- 写入文件
导出为vCard
kotlin
fun exportContactsToVCard(outputFile: File): Result<Int> {
return try {
val contacts = getAllContacts()
FileOutputStream(outputFile).use { fos ->
contacts.forEach { contact ->
val vCard = convertToVCard(contact)
fos.write(vCard.toByteArray(Charsets.UTF_8))
}
}
Result.success(contacts.size)
} catch (e: Exception) {
Log.e(TAG, "导出联系人失败: ${e.message}")
Result.failure(e)
}
}
private fun convertToVCard(contact: Contact): String {
val vCard = StringBuilder()
vCard.appendLine("BEGIN:VCARD")
vCard.appendLine("VERSION:3.0")
vCard.appendLine("FN:${escapeVCardValue(contact.displayName)}")
// 添加电话号码
contact.phoneNumbers.forEach { phone ->
vCard.appendLine("TEL;TYPE=${phone.type.uppercase()}:${phone.number}")
}
// 添加邮箱
contact.emails.forEach { email ->
vCard.appendLine("EMAIL;TYPE=${email.type.lowercase()}:${email.address}")
}
// 添加组织
contact.organizations.forEach { org ->
vCard.appendLine("ORG:${escapeVCardValue(org.company)}")
vCard.appendLine("TITLE:${escapeVCardValue(org.title)}")
}
// 添加地址
contact.addresses.forEach { address ->
vCard.appendLine("ADR;TYPE=${address.type.uppercase()}:${escapeVCardValue(address.formattedAddress)}")
}
// 添加备注
if (contact.note.isNotEmpty()) {
vCard.appendLine("NOTE:${escapeVCardValue(contact.note)}")
}
vCard.appendLine("END:VCARD")
vCard.appendLine()
return vCard.toString()
}
vCard格式说明:
BEGIN:VCARD # 开始标记
VERSION:3.0 # 版本
FN:张三 # 完整姓名
TEL;TYPE=MOBILE:13800138000 # 电话号码
EMAIL;TYPE=home:zhangsan@example.com # 邮箱
ORG:某某科技有限公司 # 组织
TITLE:软件工程师 # 职位
ADR;TYPE=HOME:北京市朝阳区某某街道123号 # 地址
NOTE:这是备注 # 备注
END:VCARD # 结束标记
4.4 导入联系人
从JSON导入
kotlin
fun importContactsFromJson(inputFile: File): Result<Int> {
return try {
val jsonString = inputFile.readText(Charsets.UTF_8)
val contacts = json.decodeFromString<List<Contact>>(jsonString)
var importedCount = 0
contacts.forEach { contact ->
if (insertContact(contact)) {
importedCount++
}
}
Result.success(importedCount)
} catch (e: Exception) {
Log.e(TAG, "导入联系人失败: ${e.message}")
Result.failure(e)
}
}
实现流程:
- 读取JSON文件
- 反序列化为Contact对象列表
- 逐个插入联系人
- 统计导入数量
插入联系人
kotlin
private fun insertContact(contact: Contact): Boolean {
return try {
val operations = ArrayList<ContentProviderOperation>()
// 1. 创建原始联系人
operations.add(
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build()
)
// 2. 添加姓名
operations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
)
.withValue(
ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
contact.displayName
)
.build()
)
// 3. 添加电话号码
contact.phoneNumbers.forEach { phoneNumber ->
operations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber.number)
.withValue(
ContactsContract.CommonDataKinds.Phone.TYPE,
getPhoneTypeFromLabel(phoneNumber.type)
)
.withValue(
ContactsContract.CommonDataKinds.Phone.IS_PRIMARY,
if (phoneNumber.isPrimary) 1 else 0
)
.build()
)
}
// 4. 添加邮箱
contact.emails.forEach { email ->
operations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.address)
.withValue(
ContactsContract.CommonDataKinds.Email.TYPE,
getEmailTypeFromLabel(email.type)
)
.withValue(
ContactsContract.CommonDataKinds.Email.IS_PRIMARY,
if (email.isPrimary) 1 else 0
)
.build()
)
}
// 5. 添加组织信息
contact.organizations.forEach { org ->
operations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, org.company)
.withValue(ContactsContract.CommonDataKinds.Organization.TITLE, org.title)
.withValue(
ContactsContract.CommonDataKinds.Organization.TYPE,
getOrganizationTypeFromLabel(org.type)
)
.build()
)
}
// 6. 添加备注
if (contact.note.isNotEmpty()) {
operations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.Note.NOTE, contact.note)
.build()
)
}
// 7. 添加地址
contact.addresses.forEach { address ->
operations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE
)
.withValue(
ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS,
address.formattedAddress
)
.withValue(
ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
getAddressTypeFromLabel(address.type)
)
.build()
)
}
// 批量执行操作
contentResolver.applyBatch(ContactsContract.AUTHORITY, operations)
true
} catch (e: Exception) {
Log.e(TAG, "插入联系人失败: ${e.message}")
false
}
}
关键实现要点:
-
批量操作模式
- 使用
ContentProviderOperation构建操作列表 - 使用
applyBatch()批量执行,提高性能 - 所有操作要么全部成功,要么全部回滚
- 使用
-
账户设置
kotlin.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)- 设置为null表示创建本地联系人
- 不会与Google账户等同步
-
关联引用
kotlin.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)- 引用第一个操作创建的联系人ID
- 参数0表示引用operations列表中的第0个操作
-
MIME类型设置
- 每个Data操作都需要设置正确的MIMETYPE
- 使用
CONTENT_ITEM_TYPE常量获取MIME类型字符串
从vCard导入
kotlin
fun importContactsFromVCard(inputFile: File): Result<Int> {
return try {
val vCards = parseVCardFile(inputFile)
var importedCount = 0
vCards.forEach { vCard ->
if (insertVCard(vCard)) {
importedCount++
}
}
Result.success(importedCount)
} catch (e: Exception) {
Log.e(TAG, "导入联系人失败: ${e.message}")
Result.failure(e)
}
}
解析vCard文件
kotlin
private fun parseVCardFile(file: File): List<Map<String, Any>> {
val vCards = mutableListOf<Map<String, Any>>()
val content = file.readText(Charsets.UTF_8)
val lines = content.lines()
var currentVCard = mutableMapOf<String, Any>()
var inVCard = false
lines.forEach { line ->
when {
line.trim().startsWith("BEGIN:VCARD") -> {
inVCard = true
currentVCard = mutableMapOf()
}
line.trim().startsWith("END:VCARD") -> {
if (inVCard && currentVCard.isNotEmpty()) {
vCards.add(currentVCard.toMap())
}
inVCard = false
}
inVCard && line.isNotEmpty() -> {
parseVCardLine(line, currentVCard)
}
}
}
return vCards
}
private fun parseVCardLine(line: String, vCard: MutableMap<String, Any>) {
val parts = line.split(":")
if (parts.size >= 2) {
val key = parts[0].trim()
val value = unescapeVCardValue(parts.drop(1).joinToString(":").trim())
when {
key.startsWith("FN") -> vCard["displayName"] = value
key.startsWith("TEL") -> {
val phones = vCard["phoneNumbers"] as? MutableList<String> ?: mutableListOf()
phones.add(value)
vCard["phoneNumbers"] = phones
}
key.startsWith("EMAIL") -> {
val emails = vCard["emails"] as? MutableList<String> ?: mutableListOf()
emails.add(value)
vCard["emails"] = emails
}
key.startsWith("ORG") -> vCard["company"] = value
key.startsWith("TITLE") -> vCard["title"] = value
key.startsWith("ADR") -> vCard["address"] = value
key.startsWith("NOTE") -> vCard["note"] = value
}
}
}
4.5 删除联系人
kotlin
fun deleteContact(contactId: String): Boolean {
return try {
val uri = ContentUris.withAppendedId(
ContactsContract.Contacts.CONTENT_URI,
contactId.toLong()
)
val rowsDeleted = contentResolver.delete(uri, null, null)
rowsDeleted > 0
} catch (e: Exception) {
Log.e(TAG, "删除联系人失败: ${e.message}")
false
}
}
4.6 联系人数量统计
kotlin
fun getContactsCount(): Int {
val cursor = contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
arrayOf(ContactsContract.Contacts._ID),
null,
null,
null
)
return cursor?.use { it.count } ?: 0
}
5. 导入导出原理
5.1 导出原理
JSON导出流程
1. 查询Contacts表获取联系人基本信息
└─ 获取 contactId, displayName
2. 根据contactId查询各子表获取详细信息
├─ Phone表 → 电话号码
├─ Email表 → 邮箱
├─ Organization (通过Data表) → 组织信息
├─ Note (通过Data表) → 备注
└─ StructuredPostal表 → 地址
3. 构建Contact对象
└─ 整合所有数据
4. 序列化为JSON字符串
└─ kotlinx.serialization.encodeToString()
5. 写入文件
└─ FileOutputStream.write()
vCard导出流程
1. 获取Contact对象
└─ 同JSON导出步骤1-3
2. 转换为vCard格式字符串
├─ BEGIN:VCARD
├─ VERSION:3.0
├─ FN:姓名
├─ TEL;TYPE=类型:号码
├─ EMAIL;TYPE=类型:邮箱
├─ ORG:公司
├─ TITLE:职位
├─ ADR;TYPE=类型:地址
├─ NOTE:备注
└─ END:VCARD
3. 写入文件
└─ FileOutputStream.write()
5.2 导入原理
JSON导入流程
1. 读取JSON文件
└─ File.readText()
2. 反序列化为Contact对象列表
└─ kotlinx.serialization.decodeFromString()
3. 逐个插入联系人
├─ 创建RawContact
├─ 插入姓名
├─ 插入电话号码
├─ 插入邮箱
├─ 插入组织信息
├─ 插入备注
├─ 插入地址
└─ 批量执行操作
4. 统计导入数量
└─ 成功插入的联系人数量
vCard导入流程
1. 读取vCard文件
└─ File.readText()
2. 解析vCard内容
├─ 按行分割
├─ 识别BEGIN:VCARD和END:VCARD
├─ 解析每个字段
└─ 构建临时Map结构
3. 转换为Contact对象
└─ 从Map构建Contact
4. 插入联系人
└─ 同JSON导入步骤3
5. 统计导入数量
└─ 成功插入的联系人数量
5.3 数据一致性保证
事务处理
kotlin
contentResolver.applyBatch(ContactsContract.AUTHORITY, operations)
- 所有操作在一个事务中执行
- 任何操作失败则全部回滚
- 保证数据完整性
关联引用机制
kotlin
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
- 引用之前创建的联系人ID
- 避免硬编码ID
- 支持批量创建多个联系人
6. 使用示例
6.1 基本查询
kotlin
// 创建工具实例
val contactsUtils = ContactsUtils(context)
// 获取所有联系人
val allContacts = contactsUtils.getAllContacts()
allContacts.forEach { contact ->
println("姓名: ${contact.displayName}")
println("电话: ${contact.phoneNumbers.joinToString { it.number }}")
println("邮箱: ${contact.emails.joinToString { it.address }}")
}
// 根据ID查询
val contact = contactsUtils.getContactById("123")
contact?.let {
println("找到联系人: ${it.displayName}")
}
// 搜索联系人
val results = contactsUtils.searchContacts("张")
println("找到 ${results.size} 个联系人")
// 获取联系人数量
val count = contactsUtils.getContactsCount()
println("共有 $count 个联系人")
6.2 导出操作
kotlin
// 导出为JSON
val jsonFile = File(context.getExternalFilesDir(null), "contacts.json")
contactsUtils.exportContactsToJson(jsonFile)
.onSuccess { count ->
println("成功导出 $count 个联系人到JSON")
}
.onFailure { error ->
println("导出失败: ${error.message}")
}
// 导出为vCard
val vcfFile = File(context.getExternalFilesDir(null), "contacts.vcf")
contactsUtils.exportContactsToVCard(vcfFile)
.onSuccess { count ->
println("成功导出 $count 个联系人到vCard")
}
.onFailure { error ->
println("导出失败: ${error.message}")
}
6.3 导入操作
kotlin
// 从JSON导入
val jsonFile = File(context.getExternalFilesDir(null), "contacts.json")
contactsUtils.importContactsFromJson(jsonFile)
.onSuccess { count ->
println("成功导入 $count 个联系人")
}
.onFailure { error ->
println("导入失败: ${error.message}")
}
// 从vCard导入
val vcfFile = File(context.getExternalFilesDir(null), "contacts.vcf")
contactsUtils.importContactsFromVCard(vcfFile)
.onSuccess { count ->
println("成功导入 $count 个联系人")
}
.onFailure { error ->
println("导入失败: ${error.message}")
}
6.4 备份与恢复
kotlin
// 备份联系人
fun backupContacts(context: Context): Boolean {
val contactsUtils = ContactsUtils(context)
val backupFile = File(context.getExternalFilesDir(null),
"contacts_backup_${System.currentTimeMillis()}.vcf")
return contactsUtils.exportContactsToVCard(backupFile).isSuccess
}
// 恢复联系人
fun restoreContacts(context: Context, backupFile: File): Boolean {
val contactsUtils = ContactsUtils(context)
return contactsUtils.importContactsFromVCard(backupFile).isSuccess
}
6.5 批量操作
kotlin
// 批量导入联系人
fun batchImport(context: Context, contacts: List<ContactsUtils.Contact>) {
val contactsUtils = ContactsUtils(context)
var successCount = 0
var failCount = 0
contacts.forEach { contact ->
// 创建临时JSON文件
val tempFile = File.createTempFile("temp_contact", ".json")
val json = Json { ignoreUnknownKeys = true }
tempFile.writeText(json.encodeToString(listOf(contact)))
// 导入
if (contactsUtils.importContactsFromJson(tempFile).isSuccess) {
successCount++
} else {
failCount++
}
tempFile.delete()
}
println("批量导入完成: 成功 $successCount, 失败 $failCount")
}
7. 技术要点
7.1 权限处理
运行时权限请求
kotlin
// 检查权限
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// 请求权限
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS),
REQUEST_CONTACTS_PERMISSION
)
}
// 处理权限结果
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
if (requestCode == REQUEST_CONTACTS_PERMISSION) {
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (allGranted) {
// 权限已获取,可以进行操作
} else {
// 权限被拒绝
}
}
}
7.2 类型转换
电话类型映射
kotlin
private fun getPhoneTypeLabel(type: Int): String {
return when (type) {
ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> "home"
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> "mobile"
ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> "work"
ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME -> "fax_home"
ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK -> "fax_work"
ContactsContract.CommonDataKinds.Phone.TYPE_PAGER -> "pager"
ContactsContract.CommonDataKinds.Phone.TYPE_OTHER -> "other"
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM -> "custom"
else -> "other"
}
}
private fun getPhoneTypeFromLabel(label: String): Int {
return when (label.lowercase()) {
"home" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
"mobile" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
"work" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
"fax_home" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME
"fax_work" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK
"pager" -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER
"other" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER
else -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER
}
}
7.3 性能优化
使用Cursor.use自动释放资源
kotlin
// 好的做法 - 自动释放
cursor?.use {
while (it.moveToNext()) {
// 处理数据
}
}
// 不好的做法 - 可能泄漏
val cursor = contentResolver.query(...)
while (cursor.moveToNext()) {
// 处理数据
}
cursor.close() // 容易忘记关闭
批量操作提高性能
kotlin
// 使用ContentProviderOperation批量操作
val operations = ArrayList<ContentProviderOperation>()
operations.add(operation1)
operations.add(operation2)
// ... 更多操作
contentResolver.applyBatch(ContactsContract.AUTHORITY, operations)
// 而不是逐个插入
contentResolver.insert(uri, values1)
contentResolver.insert(uri, values2)
7.4 异常处理
完善的异常处理
kotlin
fun exportContactsToJson(outputFile: File): Result<Int> {
return try {
// 1. 检查权限
if (!hasContactsPermission()) {
return Result.failure(SecurityException("没有联系人权限"))
}
// 2. 检查存储空间
if (!hasEnoughStorage()) {
return Result.failure(IOException("存储空间不足"))
}
// 3. 执行导出
val contacts = getAllContacts()
val jsonString = json.encodeToString(contacts)
FileOutputStream(outputFile).use { fos ->
fos.write(jsonString.toByteArray(Charsets.UTF_8))
}
Result.success(contacts.size)
} catch (e: SecurityException) {
Log.e(TAG, "权限异常: ${e.message}")
Result.failure(e)
} catch (e: IOException) {
Log.e(TAG, "IO异常: ${e.message}")
Result.failure(e)
} catch (e: Exception) {
Log.e(TAG, "未知异常: ${e.message}")
Result.failure(e)
}
}
7.5 数据验证
导入数据验证
kotlin
private fun validateContact(contact: Contact): Boolean {
// 验证必填字段
if (contact.displayName.isBlank()) {
Log.w(TAG, "联系人为空")
return false
}
// 验证电话号码格式
contact.phoneNumbers.forEach { phone ->
if (phone.number.isBlank()) {
Log.w(TAG, "电话号码为空")
return false
}
if (!isValidPhoneNumber(phone.number)) {
Log.w(TAG, "电话号码格式无效: ${phone.number}")
return false
}
}
// 验证邮箱格式
contact.emails.forEach { email ->
if (!isValidEmail(email.address)) {
Log.w(TAG, "邮箱格式无效: ${email.address}")
return false
}
}
return true
}
private fun isValidPhoneNumber(phone: String): Boolean {
val phoneRegex = Regex("^1[3-9]\\d{9}$")
return phoneRegex.matches(phone.replace(Regex("[\\s-]"), ""))
}
private fun isValidEmail(email: String): Boolean {
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
return emailRegex.matches(email)
}
8. 完整源码
8.1 ContactsUtils.kt
参见项目文件:app/src/main/java/com/zh/systemtest/utils/ContactsUtils.kt
8.2 ContactsExample.kt
参见项目文件:app/src/main/java/com/zh/systemtest/utils/ContactsExample.kt
8.3 AndroidManifest.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 联系人相关权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SystemTest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
8.4 依赖配置
build.gradle.kts
kotlin
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.jetbrains.kotlin.plugin.serialization)
}
dependencies {
// ... 其他依赖
implementation(libs.kotlinx.serialization.json)
}
libs.versions.toml
toml
[versions]
kotlinxSerialization = "1.6.0"
[libraries]
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
[plugins]
jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
附录
A. 常见问题
Q1: 如何处理大量联系人导入?
A: 使用批量操作和异步处理,避免阻塞主线程。考虑使用分页或进度显示。
Q2: vCard格式有哪些版本?
A: 主要有vCard 2.1和3.0版本,本实现使用vCard 3.0格式。
Q3: 导入时如何避免重复联系人?
A: 可以根据姓名+电话号码进行去重,或者检查是否已存在相同联系人。
Q4: 如何处理联系人头像?
A: 联系人头像存储在ContactsContract.Data表中,MIMETYPE为ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE。四
文档版本 : 1.0
最后更新 : 2026-04-18
作者 : zh
维护: 持续更新中...