联系人选择器

Android 联系人选择器是一个标准化的可浏览界面,用户可以通过该界面与您的应用分享联系人。该选择器适用于搭载 Android 17(API 级别 37)或更高版本的设备,可提供一种注重隐私保护的替代方案,以取代范围广泛的 READ_CONTACTS 权限。您的应用不会请求访问用户的整个地址簿,而是指定所需的数据字段(例如电话号码或电子邮件地址),然后用户选择要分享的特定联系人。这样一来,您的应用就只能读取所选数据,从而确保精细控制,同时提供一致的用户体验,并具备内置的搜索、个人资料切换和多选功能,而无需构建或维护界面。

集成联系人选择工具

如需集成联系人选择工具,请使用 Intent.ACTION_PICK_CONTACTS intent。此 intent 会启动选择器,并将所选联系人返回给您的应用。

与旧版 ACTION_PICK 不同,借助联系人选择工具,您可以同时指定应用所需的多个数据字段。您可以使用 Intent.EXTRA_REQUESTED_DATA_FIELDS 来实现此目的,并传递 ContactsContract.CommonDataKinds 中定义的 MIME 类型的 ArrayList<String>

常见的 MIME 类型包括:

  • ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
  • ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE
  • ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE

启动选择器

使用 registerForActivityResultStartActivityForResult 合约启动选择器。您可以配置 intent 以允许单选或多选。

// Launcher for the Contact Picker intent
val pickContact = rememberLauncherForActivityResult(StartActivityForResult()) {
    if (it.resultCode == Activity.RESULT_OK) {
        val resultUri = it.data?.data ?: return@rememberLauncherForActivityResult

        // Process the result URI in a background thread to fetch all selected contacts
        coroutine.launch {
            contacts = processContactPickerResultUri(resultUri, context)
        }
    }
}

选择模式

联系人选择器的界面会根据所请求的数据字段进行调整。根据这些要求,用户可以选择整个联系人记录(当需要多个字段时),也可以从联系人信息中选择特定的数据项。

联系人选择工具的不同界面模式
图 1. 联系人选择工具界面会根据所请求的数据字段(单个联系人、多个联系人和多个电话号码选择)进行调整。

选择单个联系人

在此示例中,应用仅请求电话号码。选择器将过滤列表,仅显示包含电话号码的联系人,并允许用户选择特定号码。

// Define the specific contact data fields you need
val requestedFields = arrayListOf(
    Email.CONTENT_ITEM_TYPE,
    Phone.CONTENT_ITEM_TYPE,
)

// Set up the intent for the Contact Picker
val pickContactIntent = Intent(ACTION_PICK_CONTACTS).apply {
    putStringArrayListExtra(
        EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
        requestedFields
    )
}

// Launch the picker
pickContact.launch(pickContactIntent)

选择多个联系人

如需启用多选,请添加 Intent.EXTRA_ALLOW_MULTIPLE extra。您可以选择性地限制用户可选择的商品数量。

val requestedFields = arrayListOf(
    Email.CONTENT_ITEM_TYPE,
    Phone.CONTENT_ITEM_TYPE,
)

// Set up the intent for the Contact Picker
val pickContactIntent = Intent(ACTION_PICK_CONTACTS).apply {
    // Enable multi-select
    putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
    // Set limit of selectable contacts
    putExtra(EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 5)
    // Define the specific contact data fields you need
    putStringArrayListExtra(
        EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
        requestedFields
    )
    // Enable this option to only filter contacts that have all the requested data fields
    putExtra(EXTRA_PICK_CONTACTS_MATCH_ALL_DATA_FIELDS, false)
}

// Launch the picker
pickContact.launch(pickContactIntent)

处理结果

当用户完成选择后,系统会返回 RESULT_OK 和会话 URI。此 URI 授予对所选数据的临时读取权限。

您可以使用标准 ContentResolver 查询此 URI。生成的 Cursor 包含所请求的数据字段,并遵循 ContactsContract.Data 的架构。

// Data class representing a parsed Contact with selected details.
data class Contact(
    val lookupKey: String,
    val name: String,
    val emails: List<String>,
    val phones: List<String>
)

// Helper function to query the content resolver with the URI returned by the Contact Picker.
// Parses the cursor to extract contact details such as name, email, and phone number.
private suspend fun processContactPickerResultUri(
    sessionUri: Uri,
    context: Context
): List<Contact> = withContext(Dispatchers.IO) {
    // Define the columns we want to retrieve from the ContactPicker ContentProvider
    val projection = arrayOf(
        ContactsContract.Contacts.LOOKUP_KEY,
        ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
        ContactsContract.Data.MIMETYPE, // Type of data (e.g., email or phone)
        ContactsContract.Data.DATA1, // The actual data (Phone number / Email string)
    )

    // We use `LOOKUP_KEY` as a unique ID to aggregate all contact info related to a same person
    val contactsMap = mutableMapOf<String, Contact>()

    // Note: The Contact Picker Session Uri doesn't support custom selection & selectionArgs.
    // We query the URI directly to get the results chosen by the user.
    context.contentResolver.query(sessionUri, projection, null, null, null)?.use { cursor ->
        // Get the column indices for our requested projection
        val lookupKeyIdx = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)
        val mimeTypeIdx = cursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
        val nameIdx = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
        val data1Idx = cursor.getColumnIndex(ContactsContract.Data.DATA1)

        while (cursor.moveToNext()) {
            val lookupKey = cursor.getString(lookupKeyIdx)
            val mimeType = cursor.getString(mimeTypeIdx)
            val name = cursor.getString(nameIdx) ?: ""
            val data1 = cursor.getString(data1Idx) ?: ""

            val email = if (mimeType == Email.CONTENT_ITEM_TYPE) data1 else null
            val phone = if (mimeType == Phone.CONTENT_ITEM_TYPE) data1 else null

            val existingContact = contactsMap[lookupKey]
            if (existingContact != null) {
                contactsMap[lookupKey] = existingContact.copy(
                    emails = if (email != null) existingContact.emails + email else existingContact.emails,
                    phones = if (phone != null) existingContact.phones + phone else existingContact.phones
                )
            } else {
                contactsMap[lookupKey] = Contact(
                    lookupKey = lookupKey,
                    name = name,
                    emails = if (email != null) listOf(email) else emptyList(),
                    phones = if (phone != null) listOf(phone) else emptyList()
                )
            }
        }
    }

    return@withContext contactsMap.values.toList()
}

向后兼容性

对于以 Android 17(API 级别 37)及更高版本为目标平台的应用,系统会自动升级现有的 Intent.ACTION_PICK intent 以使用新的“联系人选择工具”界面。

如果您的应用已使用 ACTION_PICK,则无需更改代码即可接收新界面。不过,如需使用新功能(例如接收单个 Uri 来查询联系人数据、在个人资料和工作资料之间切换或发出多个数据字段请求),您必须更新实现以使用 Intent.ACTION_PICK_CONTACTS 或新的 intent extra。

在旧版目标 SDK 上进行测试

即使您的应用以较低的 SDK 版本为目标平台,您也可以通过向 ACTION_PICK intent 添加 EXTRA_USE_SYSTEM_CONTACTS_PICKER 布尔值 extra 来在搭载 Android 17 及更高版本的设备上测试新的选择器行为。

最佳做法

  • 仅请求所需权限:如果您的应用只需要发送短信,请请求 Phone.CONTENT_ITEM_TYPE。选择器会自动过滤掉没有电话号码的联系人,从而为用户提供更简洁的界面。
  • 管理每个联系人的多条数据条目:单个联系人通常包含多个电子邮件地址或电话号码。为确保这些内容以清晰直观的方式呈现给用户,建议使用 ContactsContract.Contacts.LOOKUP_KEY 对其进行分组。此外,您还可以检索每个条目的特定标签(例如工作或个人),以便在应用界面中提供更精细的选择选项。
  • 立即持久保留数据:会话 URI 授予临时读取权限。如果您需要在应用进程被终止后访问此联系信息,则应用必须持久保存联系人数据。
  • 请勿依赖账号数据:为保护用户隐私并防止指纹识别,系统会从结果中移除账号特定的元数据。