Seletor de contatos

O Seletor de Contatos do Android é uma interface padronizada e navegável para que os usuários compartilhem contatos com seu app. Disponível em dispositivos com Android 17 (nível 37 da API) ou mais recente, o seletor oferece uma alternativa que preserva a privacidade à permissão ampla READ_CONTACTS. Em vez de solicitar acesso a toda a lista de endereços do usuário, o app especifica os campos de dados necessários, como números de telefone ou endereços de e-mail, e o usuário seleciona contatos específicos para compartilhar. Isso concede ao app acesso de leitura apenas aos dados selecionados, garantindo um controle granular e oferecendo uma experiência do usuário consistente com recursos integrados de pesquisa, troca de perfil na Pesquisa e seleção múltipla sem precisar criar ou manter a interface.

Integrar o seletor de contatos

Para integrar o seletor de contatos, use a intent ContactsPickerSessionContract.ACTION_PICK_CONTACTS. Essa intent inicia o seletor e retorna os contatos selecionados para o app.

Ao contrário do ACTION_PICK legado, o seletor de contatos permite especificar vários campos de dados que o app exige ao mesmo tempo. Para isso, use ContactsPickerSessionContract.EXTRA_REQUESTED_DATA_FIELDS, transmitindo um ArrayList<String> de tipos MIME definidos em ContactsContract.CommonDataKinds.

Os tipos MIME comuns incluem:

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

Iniciar o seletor

Use registerForActivityResult com o contrato StartActivityForResult para iniciar o seletor. É possível configurar a intent para permitir seleções únicas ou múltiplas.

// 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)
        }
    }
}

Modo de seleção

A interface do Seletor de Contatos se ajusta de acordo com os campos de dados solicitados. Dependendo desses requisitos, os usuários podem escolher um registro de contato inteiro quando vários campos são necessários ou selecionar itens de dados específicos nas informações de um contato.

Os diferentes modos de interface do Seletor de Contatos
Figura 1. A interface do seletor de contatos se adapta aos campos de dados solicitados (seleção de um contato, vários contatos e vários números de telefone).

Selecionar um único contato

Neste exemplo, o app solicita apenas números de telefone. O seletor vai filtrar a lista para mostrar apenas os contatos com números de telefone e permitir que o usuário selecione um número específico.

// 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_USE_SYSTEM_CONTACTS_PICKER, true)
    putStringArrayListExtra(
        EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
        requestedFields
    )
}

// Launch the picker
pickContact.launch(pickContactIntent)

Selecionar vários contatos

Para ativar a seleção múltipla, adicione o extra Intent.EXTRA_ALLOW_MULTIPLE. Você pode limitar o número de itens que um usuário pode selecionar.

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_USE_SYSTEM_CONTACTS_PICKER, true)
    // 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)

Processar os resultados

Quando o usuário conclui a seleção, o sistema retorna um RESULT_OK e um URI de sessão. Esse URI concede acesso temporário de leitura aos dados selecionados.

É possível consultar esse URI usando um ContentResolver padrão. O Cursor resultante contém os campos de dados solicitados e segue o esquema de 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()
}

Compatibilidade com versões anteriores

Para apps destinados ao Android 17 (nível 37 da API) e versões mais recentes, o sistema atualiza automaticamente a intent Intent.ACTION_PICK para usar a nova interface do seletor de contatos.

Se o app já usa ACTION_PICK, não é necessário mudar o código para receber a nova interface. No entanto, para aproveitar os novos recursos, como receber um único Uri para consultar dados de contato, alternar entre perfis pessoais e de trabalho ou várias solicitações de campos de dados, é necessário atualizar sua implementação para usar ContactsPickerSessionContract.ACTION_PICK_CONTACTS ou os novos extras de intent.

Como testar em SDKs de destino mais antigos

É possível testar o novo comportamento do seletor em dispositivos com o Android 17 e versões mais recentes, mesmo que o app seja destinado a uma versão anterior do SDK, adicionando o booleano extra EXTRA_USE_SYSTEM_CONTACTS_PICKER à intent ACTION_PICK.

Práticas recomendadas

  • Solicite apenas o que for necessário: se o app só precisa enviar um SMS, solicite Phone.CONTENT_ITEM_TYPE. O seletor vai filtrar automaticamente os contatos que não têm números de telefone, resultando em uma interface mais limpa para o usuário.
  • Gerenciamento de várias entradas de dados por contato: os contatos individuais geralmente contêm vários endereços de e-mail ou números de telefone. Para garantir que elas sejam apresentadas de forma clara e intuitiva para o usuário, recomendamos agrupá-las usando ContactsContract.Contacts.LOOKUP_KEY. Além disso, é possível recuperar rótulos específicos para cada entrada (como trabalho ou pessoal) para oferecer opções de seleção mais granulares na interface do app.
  • Persistir dados imediatamente: o URI da sessão concede permissão de leitura temporária. Se você precisar acessar esses dados de contato mais tarde (depois que o processo do app for encerrado), o app precisará manter os dados de contato.
  • Não dependa dos dados da conta: para proteger a privacidade do usuário e evitar impressões digitais, os metadados específicos da conta são removidos dos resultados.