プロダクト ニュース

連絡先ピッカー: プライバシーを重視した連絡先の共有

所要時間: 4 分
Roxanna Aliabadi Walker
プロダクト マネージャー

プライバシーとユーザーによる管理は、Android エクスペリエンスの中核をなすものです。写真選択ツールによってメディアの共有が安全かつ簡単に実装できるようになったのと同様に、連絡先選択ツールによって連絡先の選択に同レベルのプライバシー、シンプルさ、優れたユーザー エクスペリエンスがもたらされます。

連絡先のプライバシーに関する新しい標準

これまで、特定のユーザーの連絡先へのアクセスを必要とするアプリケーションは、広範な READ_CONTACTS 権限に依存していました。この方法は機能しますが、アプリに必要以上のデータへのアクセス権を付与してしまうことがよくありました。Android 17 で導入された新しい Android 連絡先選択ツールは、連絡先選択のための標準化された安全な検索可能なインターフェースを提供することで、この状況を変えます。

この機能により、ユーザーは選択した特定の連絡先のみへのアクセス権をアプリに付与できます。これは、データの透明性と権限のフットプリントの最小化に対する Android の取り組みに沿ったものです。

picker.png
selection.png

仕組み

デベロッパーは、Intent.ACTION_PICK_CONTACTS インテントを使用して連絡先ピッカー(TBC)を統合できます。この更新された API には、次のような強力な機能がいくつかあります。

  • 詳細なデータ リクエスト: アプリは、連絡先レコード全体を受け取るのではなく、電話番号やメールアドレスなど、必要なフィールドを正確に指定できます。
  • 複数選択のサポート: 選択ツールは、単一の連絡先と複数の連絡先の両方の選択をサポートしているため、デベロッパーはグループ招待などの機能をより柔軟に実装できます。
  • 選択数の上限: デベロッパーは、ユーザーが一度に選択できる連絡先の数にカスタムの上限を設定できます。
  • 一時的なアクセス: 選択すると、システムはリクエストされたデータへの一時的な読み取りアクセスを提供するセッション URI を返します。これにより、アクセスが必要以上に長く続くことはありません。
  • 他のプロファイルへのアクセス: この新しいインテントを使用すると、インターフェースで、仕事用プロファイル、クローン プロファイル、プライベート スペースなど、他のユーザー プロファイルからコンテンツを選択できます。
  • パフォーマンスの最適化:  連絡先ピッカー(TBC)は、結果をまとめてクエリできる単一の URI を返します。これにより、個々の連絡先 URI を個別にクエリする必要がなくなります。これは ACTION_PICK で必要とされていました。この効率化により、単一の Binder トランザクションを使用することで、システム オーバーヘッドがさらに削減されます。

下位互換性と実装

Android 17 以降を搭載したデバイスの場合、連絡先データ型を指定する従来の ACTION_PICK インテントは、より安全な新しいインターフェースに自動的にアップグレードされます。ただし、複数選択などの高度な機能を最大限に活用するには、デベロッパーは実装コードを更新し、ContentResolver を使用して返されたセッション URI をクエリすることをおすすめします。


連絡先ピッカーを統合する連絡先ピッカーを統合するには、デベロッパーは ACTION_PICK_CONTACTS インテントを使用します。以下は、選択ツールを起動して、メールや電話番号などの特定のデータ フィールドをリクエストする方法を示すコード例です。

  // 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 のアクセスは一時的なため、選択したデータをすぐに永続化します。
作成者:

続きを読む