Android联系人导入导出

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
}

实现要点:

  1. 使用 cursor?.use {} 确保资源自动释放
  2. 通过 ContactsContract.Contacts._ID 获取联系人ID
  3. 通过联系人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)
    }
}

实现流程:

  1. 查询所有联系人
  2. 使用kotlinx.serialization序列化为JSON字符串
  3. 写入文件
导出为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)
    }
}

实现流程:

  1. 读取JSON文件
  2. 反序列化为Contact对象列表
  3. 逐个插入联系人
  4. 统计导入数量
插入联系人
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
    }
}

关键实现要点:

  1. 批量操作模式

    • 使用 ContentProviderOperation 构建操作列表
    • 使用 applyBatch() 批量执行,提高性能
    • 所有操作要么全部成功,要么全部回滚
  2. 账户设置

    kotlin 复制代码
    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
    • 设置为null表示创建本地联系人
    • 不会与Google账户等同步
  3. 关联引用

    kotlin 复制代码
    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
    • 引用第一个操作创建的联系人ID
    • 参数0表示引用operations列表中的第0个操作
  4. 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
维护: 持续更新中...

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work3 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work3 天前
ch21 签名、校验与发版
kotlin
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui