Selector de contactos

El Selector de contactos de Android es una interfaz estandarizada y navegable para que los usuarios compartan contactos con tu app. Disponible en dispositivos con Android 17 (nivel de API 37) o versiones posteriores, el selector ofrece una alternativa que preserva la privacidad al permiso amplio READ_CONTACTS. En lugar de solicitar acceso a toda la libreta de direcciones del usuario, tu app especifica los campos de datos que necesita, como números de teléfono o direcciones de correo electrónico, y el usuario selecciona contactos específicos para compartir. Esto le otorga a tu app acceso de lectura solo a los datos seleccionados, lo que garantiza un control detallado y, al mismo tiempo, proporciona una experiencia del usuario coherente con capacidades integradas de búsqueda, cambio de perfil y selección múltiple sin tener que compilar ni mantener la IU.

Integra el Selector de contactos

Para integrar el Selector de contactos, usa el intent Intent.ACTION_PICK_CONTACTS. Este intent inicia el selector y muestra los contactos seleccionados en tu app.

A diferencia del ACTION_PICK heredado, el Selector de contactos te permite especificar varios campos de datos que requiere tu app al mismo tiempo. Para ello, usa Intent.EXTRA_REQUESTED_DATA_FIELDS, pasando un ArrayList<String> de tipos MIME definidos en ContactsContract.CommonDataKinds.

Entre los tipos MIME comunes, se incluyen los siguientes:

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

Inicia el selector

Usa registerForActivityResult con el contrato StartActivityForResult para iniciar el selector. Puedes configurar el intent para permitir selecciones únicas o múltiples.

// 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 selección

La IU del Selector de contactos se ajusta según los campos de datos solicitados. Según estos requisitos, los usuarios pueden elegir un registro de contacto completo cuando se necesitan varios campos o seleccionar elementos de datos específicos dentro de la información de un contacto.

Los diferentes modos de IU del selector de contactos
Figure 1. La interfaz del Selector de contactos se adapta a los campos de datos solicitados (selección de un solo contacto, varios contactos y varios números de teléfono).

Selecciona un solo contacto

En este ejemplo, la app solo solicita números de teléfono. El selector filtrará la lista para mostrar solo los contactos con números de teléfono y permitirá que el usuario seleccione un 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 {
    putStringArrayListExtra(
        EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
        requestedFields
    )
}

// Launch the picker
pickContact.launch(pickContactIntent)

Selecciona varios contactos

Para habilitar la selección múltiple, agrega el extra Intent.EXTRA_ALLOW_MULTIPLE. De manera opcional, puedes limitar la cantidad de elementos que puede seleccionar un usuario.

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)

Controla los resultados

Cuando el usuario completa la selección, el sistema muestra un RESULT_OK y un URI de sesión. Este URI otorga acceso de lectura temporal a los datos seleccionados.

Puedes consultar este URI con un ContentResolver estándar. El Cursor resultante contiene los campos de datos solicitados y sigue el 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()
}

Retrocompatibilidad

En el caso de las apps orientadas a Android 17 (nivel de API 37) y versiones posteriores, el sistema actualiza automáticamente el intent Intent.ACTION_PICK existente para usar la nueva interfaz del Selector de contactos.

Si tu app ya usa ACTION_PICK, no es necesario que cambies el código para recibir la nueva IU. Sin embargo, para aprovechar las funciones nuevas, como recibir un solo Uri para consultar datos de contacto, cambiar entre perfiles personales y laborales o varias solicitudes de campos de datos, debes actualizar tu implementación para usar Intent.ACTION_PICK_CONTACTS o los nuevos extras de intent.

Pruebas en SDKs de destino anteriores

Puedes probar el nuevo comportamiento del selector en dispositivos con Android 17 y versiones posteriores, incluso si tu app está orientada a una versión inferior del SDK. Para ello, agrega el extra booleano EXTRA_USE_SYSTEM_CONTACTS_PICKER a tu intent ACTION_PICK.

Prácticas recomendadas

  • Solicita solo lo que necesitas: Si tu app solo necesita enviar un SMS, solicita Phone.CONTENT_ITEM_TYPE. El selector filtrará automáticamente los contactos que no tengan números de teléfono, lo que generará una IU más limpia para el usuario.
  • Administra varias entradas de datos por contacto: Los contactos individuales suelen contener varias direcciones de correo electrónico o números de teléfono. Para garantizar que se presenten de forma clara e intuitiva para el usuario, se recomienda agruparlos con ContactsContract.Contacts.LOOKUP_KEY. Además, puedes recuperar etiquetas específicas para cada entrada (como laboral o personal) para ofrecer opciones de selección más detalladas dentro de la interfaz de tu app.
  • Haz que los datos persistan de inmediato: El URI de sesión otorga permiso de lectura temporal Si necesitas acceder a esta información de contacto más adelante (después de que se finalice el proceso de tu app), tu app debe hacer que los datos de contacto persistan.
  • No dependas de los datos de la cuenta: Para proteger la privacidad del usuario y evitar la huella digital, se quitan los metadatos específicos de la cuenta de los resultados.