Replace READ_CONTACTS With Android 17's Contact Picker

Play Console starts flagging contacts permission issues on 27 October, and the policy applies from 27 January 2027. How to move to Android 17's Contact Picker, with code.

Aditya BansalAditya BansalAuthor30 October 20264 min read 22 views
Replace READ_CONTACTS With Android 17's Contact Picker
In this article▾
  1. What the policy requires
  2. Sort your contacts usage first
  3. The Contact Picker in practice
  4. Launching it
  5. Reading the result
  6. Older Android versions
  7. Testing before you raise your target SDK
  8. A five-step migration checklist

If your app asks for READ_CONTACTS so users can invite a friend or pick a phone number, Google Play is about to ask you why. From 27 October 2026, the Play Console runs new pre-review checks that flag potential contacts and location permission issues before you submit. The policy itself takes effect on 27 January 2027 for apps targeting Android 17 (API level 37) or higher. Some earlier coverage put the deadline at 28 October 2026; Play's own policy page, checked on 22 October, lists 27 January 2027. Either way, the work is the same: move to Android 17's Contact Picker. Here is what the policy asks and the code to do it.

What the policy requires

From Google's policy preview: apps targeting Android 17 or higher "may only request the READ_CONTACTS permission if the Android Contact Picker is not sufficient for your app to provide core functionality". Apps that still request it must submit a Play Console declaration explaining why the picker would not suffice.

The Android Developers blog post from April is blunter about the common case. If you ask for contacts for sharing or inviting, update your code to use the picker and remove READ_CONTACTS entirely when targeting Android 17 and above. If you need full, ongoing access to the contact list, you file a declaration.

Location gets the same treatment. For one-time, user-initiated precise location, such as finding nearby stores, apps targeting Android 17 must use the new location button with the onlyForLocationButton manifest flag. Persistent precise location needs a declaration showing why the button or coarse location is not enough.

Sort your contacts usage first

Search your codebase for READ_CONTACTS and every place you query ContactsContract. Put each use in one of two buckets:

  • User picks someone: invite a friend, split a bill, send to a number, choose an emergency contact. These belong in the picker.

  • App needs the whole list continuously: a contacts manager, a backup app. These are declaration territory.

Most consumer apps will find everything in the first bucket. If you are unsure about a flow, ask whether the user is choosing specific people. If they are, the picker is the intended tool.

The Contact Picker in practice

The picker is documented on the Android 17 Contact Picker page. You launch an intent, the user chooses contacts in a system UI, and you get back a session URI that grants temporary read access to only the fields you asked for. The snippets below are adapted from the official samples; the constants live in ContactsPickerSessionContract.

Launching it

val pickContacts = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val sessionUri = result.data?.data ?: return@registerForActivityResult
        lifecycleScope.launch {
            val picked = readPickedPhones(sessionUri)
            saveInvites(picked)   // persist now: access is temporary
        }
    }
}

fun openPicker() {
    val intent = Intent(ContactsPickerSessionContract.ACTION_PICK_CONTACTS).apply {
        putExtra(ContactsPickerSessionContract.EXTRA_USE_SYSTEM_CONTACTS_PICKER, true)
        putStringArrayListExtra(
            ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
            arrayListOf(Phone.CONTENT_ITEM_TYPE)   // ask only for what you need
        )
        putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        putExtra(ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 5)
    }
    pickContacts.launch(intent)
}

Reading the result

suspend fun readPickedPhones(sessionUri: Uri): List<Pair<String, String>> =
    withContext(Dispatchers.IO) {
        val projection = arrayOf(
            ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
            ContactsContract.Data.MIMETYPE,
            ContactsContract.Data.DATA1,
        )
        val out = mutableListOf<Pair<String, String>>()
        // Session URIs reject custom selection/selectionArgs: pass null
        contentResolver.query(sessionUri, projection, null, null, null)?.use { c ->
            val nameIdx = c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
            val mimeIdx = c.getColumnIndex(ContactsContract.Data.MIMETYPE)
            val dataIdx = c.getColumnIndex(ContactsContract.Data.DATA1)
            while (c.moveToNext()) {
                if (c.getString(mimeIdx) == Phone.CONTENT_ITEM_TYPE) {
                    out += (c.getString(nameIdx) ?: "") to (c.getString(dataIdx) ?: "")
                }
            }
        }
        out
    }

Three details from the docs that are easy to miss:

  • Passing a selection or selectionArgs to the session URI raises an exception.

  • Read access through the session URI is temporary, so store what you need straight away.

  • Account-specific metadata is stripped from results. Do not build logic that depends on which account a contact came from.

Older Android versions

The picker exists on devices running Android 17 and higher. The docs do not describe a fallback for older devices, so handle it yourself with a version check and the classic single-contact picker:

val intent = if (Build.VERSION.SDK_INT >= 37) {
    Intent(ContactsPickerSessionContract.ACTION_PICK_CONTACTS).apply {
        // extras as above
    }
} else {
    Intent(Intent.ACTION_PICK, Phone.CONTENT_URI)
}

One useful bonus: for apps targeting API 37 or higher, the system automatically upgrades an existing Intent.ACTION_PICK for contact data to the new picker UI. If you already use ACTION_PICK, you get the new interface without changes, but you need the new action and extras for multi-select and multiple fields.

Testing before you raise your target SDK

You do not have to retarget to API 37 just to try the picker. The docs note that you can add EXTRA_USE_SYSTEM_CONTACTS_PICKER to an existing ACTION_PICK intent to see the new picker on an Android 17 device, even if your app targets a lower SDK. That lets you test the flow, and your QA team's expectations, before the retargeting work lands.

While you are there, trim the fields you request. The docs' own example of good practice: if you only need to send an SMS, request only Phone.CONTENT_ITEM_TYPE. Fewer fields means a clearer picker for the user and less data for you to protect.

A five-step migration checklist

  1. Find every READ_CONTACTS use and sort it into the two buckets above.

  2. Replace bucket-one flows with the picker, requesting only the fields each flow needs.

  3. Remove READ_CONTACTS from the manifest, and check that no library adds it back through manifest merging.

  4. Test on an Android 17 device or emulator and on an older device for the fallback.

  5. After 27 October, upload to an internal track and read what the Play Console pre-review check flags before you go to production.

The trade-off is real but small. You lose silent access to the whole address book, which most users never wanted to give you. You gain a permission prompt fewer, a picker users already understand, and one less reason for a review to stall in January.

Keep reading

More from Aditya Bansal

More from Aditya Bansal

More in Software Development

Have a story of your own?

Publisha is free to start. Write with AI that keeps your voice, and publish in a click.