产品资讯

联系人选择工具:隐私至上的联系人分享功能

阅读用时:4 分钟
查看 Roxanna Aliabadi Walker 的个人资料
Roxanna Aliabadi Walker 产品经理

隐私保护和用户控制仍然是 Android 体验的核心。正如照片选择器让媒体分享变得安全且易于实现一样,我们现在也将同样的隐私保护、简单性和出色用户体验带到了联系人选择功能中。

联系人隐私保护新标准

过去,需要访问特定用户通讯录的应用依赖于广泛的 READ_CONTACTS 权限。虽然这种方法可行,但往往会向应用授予超出必要范围的数据。Android 17 中引入的新 Android 联系人选择工具改变了这一现状,它提供了一个标准化、安全且可搜索的联系人选择界面。

此功能允许用户仅向应用授予对所选特定联系人的访问权限,这符合 Android 致力于提高数据透明度和最大限度减少权限占用的承诺。

picker.png
selection.png

工作方式

开发者可以使用 Intent.ACTION_PICK_CONTACTS intent 集成联系人选择工具。此更新后的 API 提供了多项强大的功能:

  • 精细数据请求:应用可以确切指定所需的字段(例如电话号码或邮箱),而无需接收整个联系人记录。
  • 支持多选:选择工具支持单选和多选联系人,让开发者能够更灵活地实现群组邀请等功能。
  • 选择限制:开发者可以自定义用户一次可选择的联系人数量上限。
  • 临时访问权限:选择后,系统会返回一个会话 URI,该 URI 提供对所请求数据的临时读取权限,从而确保访问权限不会持续超过必要的时间。
  • 访问其他用户个人资料: 使用此新 intent 时,界面将允许用户从其他用户个人资料(例如工作资料、克隆资料或私密空间)中选择内容。
  • 优化性能: 联系人选择工具会返回一个 Uri,以便进行集体结果查询,从而无需像 ACTION_PICK 那样单独查询每个联系人 Uri。这种高效性通过利用单个 Binder 事务进一步减少了系统开销。

向后兼容性和实现

对于搭载 Android 17 或更高版本的设备,系统会自动将指定联系人数据类型的旧版 ACTION_PICK intent 升级到更安全的新接口。不过,为了充分利用多选等高级功能,我们建议开发者更新其实现代码,并利用 ContentResolver 查询返回的会话 URI。


集成联系人选择工具若要集成联系人选择工具,开发者需使用 ACTION_PICK_CONTACTS intent。以下代码示例演示了如何启动选择器并请求特定数据字段,例如邮箱和电话号码。

// State to hold the list of selected contacts
var contacts by remember { mutableStateOf<List>(emptyList()) }
// 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
    coroutine.launch {
        contacts = processContactPickerResultUri(resultUri, context)
    }
}
}
// 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 {
putExtra(EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 5)
putStringArrayListExtra(
EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
requestedFields
)
putExtra(EXTRA_PICK_CONTACTS_MATCH_ALL_DATA_FIELDS, false)
}
// Launch the picker
pickContact.launch(pickContactIntent)

用户做出选择后,应用会查询返回的会话 URI 以提取请求的联系信息,从而处理结果。

// Data class representing a parsed Contact with selected details
data class Contact(val id: String, val name: String, val email: String?, val phone: 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._ID,
        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)
    )

    val results = mutableListOf<Contact>()

    // Note: The Contact Picker Session Uri doesn't support custom selection & selectionArgs.
    context.contentResolver.query(sessionUri, projection, null, null, null)?.use { cursor ->
        // Get the column indices for our requested projection
        val contactIdIdx = cursor.getColumnIndex(ContactsContract.Contacts._ID)
        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 contactId = cursor.getString(contactIdIdx)
            val mimeType = cursor.getString(mimeTypeIdx)
            val name = cursor.getString(nameIdx) ?: ""
            val data1 = cursor.getString(data1Idx) ?: ""

            // Determine if the current row represents an email or a phone number
            val email = if (mimeType == Email.CONTENT_ITEM_TYPE) data1 else null
            val phone = if (mimeType == Phone.CONTENT_ITEM_TYPE) data1 else null

            // Add the parsed contact to our results list
            results.add(Contact(contactId, name, email, phone))
        }
    }

    return@withContext results
}

点击此处查看完整文档。

开发者最佳实践

为了提供最佳用户体验并保持高安全标准,我们建议您采取以下措施:

  • 数据最少化:仅请求应用所需的特定数据字段(例如邮箱)。
  • 立即持久保存:由于会话 URI 访问权限是临时的,因此请立即持久保存所选数据。
继续阅读