Selektor kontaktów

Selektor kontaktów na Androidzie to standardowy interfejs przeglądania, który umożliwia użytkownikom udostępnianie kontaktów w aplikacji. Jest on dostępny na urządzeniach z Androidem 17 (interfejs API na poziomie 37) lub nowszym i stanowi alternatywę dla szerokiego uprawnienia READ_CONTACTS, która zapewnia ochronę prywatności. Zamiast prosić o dostęp do całej książki adresowej użytkownika, aplikacja określa potrzebne pola danych, takie jak numery telefonów lub adresy e-mail, a użytkownik wybiera konkretne kontakty do udostępnienia. Dzięki temu aplikacja uzyskuje dostęp do odczytu tylko wybranych danych, co zapewnia szczegółową kontrolę i spójne wrażenia użytkowników dzięki wbudowanym funkcjom wyszukiwania, przełączania profili i wielokrotnego wyboru bez konieczności tworzenia interfejsu użytkownika ani zarządzania nim.

Integracja selektora kontaktów

Aby zintegrować selektor kontaktów, użyj intencji ContactsPickerSessionContract.ACTION_PICK_CONTACTS. Uruchamia ona selektor i zwraca wybrane kontakty do aplikacji.

W przeciwieństwie do starszej intencji ACTION_PICK selektor kontaktów umożliwia jednoczesne określenie wielu pól danych wymaganych przez aplikację. Możesz to zrobić za pomocą funkcji ContactsPickerSessionContract.EXTRA_REQUESTED_DATA_FIELDS, przekazując ArrayList<String> typów MIME zdefiniowanych w ContactsContract.CommonDataKinds.

Typowe typy MIME to:

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

Uruchamianie selektora

Użyj registerForActivityResult z umową StartActivityForResult, aby uruchomić selektor. Możesz skonfigurować intencję tak, aby zezwalała na pojedynczy lub wielokrotny wybór.

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

Tryb wyboru

Interfejs selektora kontaktów dostosowuje się do żądanych pól danych. W zależności od tych wymagań użytkownicy mogą wybrać cały rekord kontaktu, jeśli potrzebnych jest wiele pól, lub wybrać konkretne elementy danych z informacji o kontakcie.

Różne tryby interfejsu selektora kontaktów
Rysunek 1. Interfejs selektora kontaktów dostosowuje się do żądanych pól danych (wybieranie pojedynczego kontaktu, wielu kontaktów i wielu numerów telefonów).

Wybieranie pojedynczego kontaktu

W tym przykładzie aplikacja prosi tylko o numery telefonów. Selektor odfiltruje listę, aby wyświetlić tylko kontakty z numerami telefonów, i umożliwi użytkownikowi wybranie konkretnego numeru.

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

Wybieranie wielu kontaktów

Aby włączyć wielokrotny wybór, dodaj Intent.EXTRA_ALLOW_MULTIPLE. Możesz opcjonalnie ograniczyć liczbę elementów, które użytkownik może wybrać.

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)

Obsługa wyników

Gdy użytkownik dokona wyboru, system zwraca wartość RESULT_OK i adres URI sesji. Ten URI przyznaje tymczasowy dostęp do odczytu wybranych danych.

Możesz wysyłać zapytania do tego identyfikatora URI za pomocą standardowego ContentResolver. Wynikowy plik Cursor zawiera żądane pola danych i jest zgodny ze schematem pliku 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()
}

Zgodność wsteczna

W przypadku aplikacji kierowanych na Androida 17 (API na poziomie 37) lub nowszego system automatycznie uaktualnia istniejącą intencję Intent.ACTION_PICK, aby korzystać z nowego interfejsu selektora kontaktów.

Jeśli Twoja aplikacja korzysta już z ACTION_PICK, nie musisz zmieniać kodu, aby otrzymać nowy interfejs. Aby jednak korzystać z nowych funkcji, takich jak wysyłanie pojedynczego żądania Uri w celu wysłania zapytania o dane kontaktowe, przełączanie się między profilami osobistym i służbowym czy wysyłanie żądań dotyczących wielu pól danych, musisz zaktualizować implementację, aby używać ContactsPickerSessionContract.ACTION_PICK_CONTACTS lub nowych dodatków do intencji.

Testowanie na starszych docelowych pakietach SDK

Możesz przetestować nowe działanie selektora na urządzeniach z Androidem 17 i nowszym, nawet jeśli Twoja aplikacja jest kierowana na starszą wersję pakietu SDK. Wystarczy, że dodasz do intencji ACTION_PICK dodatkową wartość logiczną EXTRA_USE_SYSTEM_CONTACTS_PICKER.

Sprawdzone metody

  • Proś tylko o to, czego potrzebujesz: jeśli Twoja aplikacja musi tylko wysyłać SMS-y, poproś o Phone.CONTENT_ITEM_TYPE. Selektor automatycznie odfiltruje kontakty, które nie mają numerów telefonów, co zapewni użytkownikowi przejrzysty interfejs.
  • Zarządzanie wieloma wpisami danych dla jednego kontaktu: poszczególne kontakty często zawierają różne adresy e-mail lub numery telefonów. Aby zapewnić użytkownikowi jasne i intuicyjne wyświetlanie tych informacji, zalecamy grupowanie ich za pomocą symbolu ContactsContract.Contacts.LOOKUP_KEY. Możesz też pobierać konkretne etykiety dla każdego wpisu (np. służbowy lub osobisty), aby oferować bardziej szczegółowe opcje wyboru w interfejsie aplikacji.
  • Natychmiastowe utrwalanie danych: identyfikator URI sesji przyznaje tymczasowe uprawnienia do odczytu. Jeśli chcesz później uzyskać dostęp do tych informacji kontaktowych (po zakończeniu procesu aplikacji), musisz zapisać dane kontaktowe.
  • Nie polegaj na danych konta: aby chronić prywatność użytkowników i zapobiegać tworzeniu odcisków cyfrowych, z wyników usuwane są metadane dotyczące konta.