連絡先選択ツール

Android 連絡先選択ツールは、ユーザーがアプリと連絡先を共有するための標準化されたブラウザブルなインターフェースです。Android 17(API レベル 37)以降を搭載したデバイスで利用できるこの選択ツールは、広範な READ_CONTACTS 権限に代わるプライバシーの保護に配慮した選択肢を提供します。アプリは、ユーザーのアドレス帳全体へのアクセスをリクエストするのではなく、電話番号やメールアドレスなど、必要なデータ フィールドを指定します。ユーザーは、共有する特定の連絡先を選択します。これにより、アプリは選択したデータへの読み取りアクセス権のみを取得できます。UI を構築または維持することなく、組み込みの検索、プロファイルの切り替え、複数選択機能を使用して、一貫したユーザー エクスペリエンスを提供しながら、きめ細かい制御を実現できます。

連絡先ピッカーを統合する

連絡先ピッカー(TBC)を統合するには、Intent.ACTION_PICK_CONTACTS インテントを使用します。このインテントは選択ツールを起動し、選択した連絡先をアプリに返します。

以前の ACTION_PICK とは異なり、連絡先ピッカー(TBC)では、アプリが必要とする複数のデータ フィールドを同時に指定できます。これを行うには、 Intent.EXTRA_REQUESTED_DATA_FIELDS を使用して、MIME タイプの ArrayList<String> を渡します。ContactsContract.CommonDataKinds

一般的な MIME タイプは次のとおりです。

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

選択ツールを起動する

StartActivityForResult コントラクトで registerForActivityResult を使用して、選択ツールを起動します。インテントを構成して、単一選択または複数選択を許可できます。

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

選択モード

連絡先選択ツールの UI は、リクエストされたデータ フィールドに応じて調整されます。 これらの要件に応じて、複数のフィールドが必要な場合は連絡先レコード全体を選択するか、連絡先情報内の特定のデータアイテムを選択できます。

連絡先ピッカー(TBC)のさまざまな UI モード
図 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 エクストラを追加します。必要に応じて、ユーザーが選択できるアイテム数を制限できます。

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 により、選択したデータへの一時的な読み取りアクセス権が付与されます。

この URI は、標準の ContentResolver でクエリできます。結果の 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 インテントは、新しい連絡先ピッカー(TBC) インターフェースを使用するように自動的にアップグレードされます。

アプリですでに ACTION_PICK を使用している場合は、新しい UI を受け取るためにコードを変更する必要はありません。ただし、連絡先データをクエリするための単一の Uri の受信、個人用プロファイルと仕事用プロファイルの切り替え、複数のデータ フィールド リクエストなどの新機能を利用するには、Intent.ACTION_PICK_CONTACTS または新しいインテント エクストラを使用するように実装を更新する必要があります。

古いターゲット SDK でのテスト

アプリが低い SDK バージョンをターゲットとしている場合でも、EXTRA_USE_SYSTEM_CONTACTS_PICKER ブール値エクストラを ACTION_PICK インテントに追加することで、Android 17 以降を搭載したデバイスで新しい選択ツールの動作をテストできます。

ベスト プラクティス

  • 必要なものだけをリクエストする: アプリで SMS を送信する必要がある場合は、 Phone.CONTENT_ITEM_TYPE をリクエストします。選択ツールは、電話番号のない連絡先を自動的に除外するため、ユーザーにとってよりクリーンな UI になります。
  • 連絡先ごとに複数のデータエントリを管理する: 個々の連絡先には、さまざまなメールアドレスや電話番号が含まれていることがよくあります。これらがユーザーに明確かつ直感的に表示されるようにするには、ContactsContract.Contacts.LOOKUP_KEY を使用してグループ化することをおすすめします。さらに、各エントリの特定のラベル(仕事用や個人用など)を取得して、アプリのインターフェース内でよりきめ細かい選択オプションを提供できます。
  • データをすぐに永続化する: セッション URI は一時的な読み取り 権限を付与します。後で(アプリのプロセスが強制終了した後)この連絡先情報にアクセスする必要がある場合は、アプリで連絡先データを永続化する必要があります。
  • アカウント データに依存しない: ユーザーのプライバシーを保護し、 フィンガープリントを防止するため、アカウント固有のメタデータは結果から削除されます。