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
维护: 持续更新中...

相关推荐
zh_xuan2 小时前
把Android Library 上传到github并在工程中引用该远程仓库
android·github·远程仓库
诸神黄昏EX2 小时前
Android Google MADA
android
盖丽男2 小时前
使用 GitHub Actions 自动打包 Android APK
android·github
小林望北2 小时前
Kotlin 协程:StateFlow 与 SharedFlow 深度解析
android·开发语言·kotlin
alexhilton11 小时前
Compose中的CameraX二维码扫描器
android·kotlin·android jetpack
eric*168814 小时前
Android15 enableEdgeToEdge 全面屏沉浸式体验
android·edgetoedge
小智社群16 小时前
小米安卓真机ADB对硬件操作
android·adb
嗷o嗷o16 小时前
Android BLE 为什么连上了却收不到数据
android
pengyu16 小时前
【Kotlin 协程修仙录 · 炼气境 · 后阶】 | 划定疆域:CoroutineScope 与 Android 生命周期的绑定艺术
android·kotlin