← All posts

APP UPDATES & COMPATIBILITY

Google Play Contacts Permission Policy: How to Use the Android Contact Picker

Last reviewed against Google Play's published policy documentation. Policy scope and deadlines can change; the Policy status and declaration prompts in your own Play Console are authoritative for your app.

Contacts are the most quietly invasive data on a phone. An app with broad contact access can read every name, number and email a user has saved — including details about people who never installed the app and never agreed to anything.

Google Play therefore treats contact access as a restricted permission with a documented minimum-scope alternative. If your app requests READ_CONTACTS, two questions decide your work: can the Android Contact Picker do the job, and what does Play Console expect if it cannot?

Quick answer: what the Contacts Permission Policy requires

The short version

  • Scoped by target SDK. Apps targeting Android 17 (API level 37) or higher may request READ_CONTACTS only if the Contact Picker cannot provide core functionality.
  • Requesting it means declaring it. Apps in scope must submit a Play Console declaration.
  • The picker shares only what the user selects — specific contacts, specific fields, temporary access.
  • “We built our own picker” is not a justification. Google says a custom picking experience does not qualify.
  • Private and enterprise device management apps are exempt.
  • Restricted, not removed. The permission remains available to apps that pass access review.

What the Contacts Permission Policy actually says

Apps that target Android 17 or higher (API level 37+) 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 the permission must submit a Play Console declaration to demonstrate access needs for Contacts and why Contact Picker would not suffice.

  • Target SDK decides scope, not the device OS. An app targeting an older API level falls outside this restriction, even on a newer device.
  • “Core functionality” is the test — not “nicer with contacts”. The declaration makes you name the feature and justify why the picker cannot deliver it.
  • It is a restriction, not a ban. The permission stays available to apps that pass access review.

A separate obligation runs alongside it: apps may not disclose or publish non-public contacts data unless the contacts themselves authorise it. That sits next to the Data Safety section you already maintain. For timing, use the Policy Deadlines page and the status Play Console shows for your app, which also documents a 30-day automatic self-extension — not a date quoted in a blog post, including this one.

What does READ_CONTACTS allow an app to do?

It is a runtime permission granting read access to the Contacts Provider — Android’s central contact store, organised around ContactsContract.Contacts (people), ContactsContract.RawContacts (a person’s data from one account) and ContactsContract.Data (phone numbers, emails and other details).

Granted, an app can query all of it: enumerate the entire address book, read every field, and retain what it reads. Consent is a single dialog for the whole dataset — which is the mismatch. Most features that ask for contacts need one contact, once.

Broad access with READ_CONTACTS

  • Every saved contact
  • People who never installed your app
  • One consent for everything
  • Persists until revoked
  • Requires policy justification

Selected access with the Contact Picker

  • Only the contacts the user taps
  • Only the fields you requested
  • Consent is the selection
  • Temporary session access
  • No READ_CONTACTS needed

Android Contact Picker vs. READ_CONTACTS

The picker is not a smaller version of the permission. It is a different model: a system UI that returns a snapshot of what the user chose.

Question Android Contact Picker READ_CONTACTS What to verify
Data shared Selected contacts and fields only. Whatever the app queries from the whole book. Which fields the feature needs.
Consent Implicit in selection; no runtime prompt. Permission dialog, then persistent access. Whether per-selection consent works.
Duration Temporary read access via a session URI. Until the user revokes it. That you persist data in time.
Minimum Android version Android 17 (API 37). Not backported. Available across supported versions. Your minSdk fallback path.
Policy burden The named minimum-scope alternative. Declaration and access review when targeting API 37+. Whether you meet the core-functionality test.
Good fit for Sharing, inviting, referring, choosing who to message or pay. Contact management, call history, dialers, backup, autocomplete, friend matching. That it cannot work per-selection.

When the Contact Picker is the right answer

Google’s policy page names cases where broad access is not permissible: sharing a file with a contact, collaborating with one, inviting or referring someone to join a service, or choosing a contact to transact with. If your feature appears here, the picker is the intended path — the user knows exactly which person they mean.

Which contact-access approach fits?

  1. 1 Does the user pick a specific person to share, invite, message or pay? Use the picker
  2. 2 Can the feature work from a per-selection snapshot? Use the picker
  3. 3 Does it need the whole book — listing, editing, backup or matching every contact? Declare
  4. 4 Is the app a private or enterprise device management app? Exempt

Order matters: a custom in-app picker does not qualify as a reason to keep contacts access, so “we already built our own” does not move you from step 2 to step 3.

When might broader contact access be justified?

The declaration lists the use cases Google recognises for READ_CONTACTS. All share one property: the feature cannot function from a single selection.

  • Contact management, CRM and backup/restore — showing, editing, organising or preserving all contacts.
  • Dialing, SMS, in-call UI and call history — showing who is being called or texted, and which contact a call belongs to.
  • Call screening — identifying, blocking or filtering calls and messages, which needs saved contacts.
  • Accessibility features, personal assistants and keyboards — assistive tools, commands such as “email them”, and auto-complete that needs the full set of names.
  • Friend matching — identifying which contacts already use the platform.

Two deserve a second look. Google’s list also includes “user-initiated device contact selection”, but the not-permissible list covers that behaviour and the FAQ confirms custom picking does not justify access. And friend matching is legitimate only when matching truly runs against the whole book; inviting one friend is a picker feature.

Test your justification out loud. If the reason reduces to “it is easier than building the picker flow”, that is the answer review exists to reject.

How to check your app’s contact requirements in Play Console

Do not infer obligations from an article, including this one. Play Console reports them per app and per track, across three surfaces:

  1. Policy status — the requirements applying to the app, including restricted-permission items and their status.
  2. Pre-review checks — flags potential contacts and location policy issues before you submit a release.
  3. The declaration prompt — apps carrying READ_CONTACTS are prompted to declare or remove the permission and use the picker.

Scope is per app version and per track, not per account: an app can be clean on one track and out of date on another. And because the policy is scoped by target SDK, check the effective target in the uploaded bundle, not a Gradle file on an unshipped branch — the target API level guide explains how Play Console reports those values.

Reviewing and submitting a Contacts Permission declaration

The declaration is two questions, and both need preparation.

1. Name the user-facing feature

Select the use case matching the feature that needs broad access, or “Other” if none applies. The reviewer expects the selection to match what the app actually does; if two features use contacts, describe the full need once.

2. Explain why the Contact Picker is insufficient

This answer decides the outcome, and it must be technical rather than persuasive. Weak answers describe inconvenience; strong answers describe a capability gap: which behaviour the feature depends on that the picker cannot provide, why the result cannot be rebuilt from a user selection, and what breaks if broad access is removed. Keep it checkable — a reviewer can test a claim about behaviour, not a claim about effort.

Before you submit

  • Confirm the store listing describes the feature needing contacts; the policy ties permission use to functionality promoted in your listing.
  • Confirm Data Safety reflects what you collect, share and retain.
  • Check READ_CONTACTS is absent from builds where the feature does not exist, including test tracks.
  • Agree what happens if the declaration is declined.

For the general workflow and how a declined review affects a release, see the sensitive permissions declaration guide.

How to plan the technical migration

The picker exists only on Android 17 and later, so migration is a design question before it is a code question. Decide what the feature does on older devices and make that explicit.

  • It returns a session URI in the result intent’s data field. Query it through a normal ContentResolver; rows follow the ContactsContract.Data schema.
  • Access is temporary. Persist the fields you need immediately.
  • You choose the fields by requesting MIME types such as Phone.CONTENT_ITEM_TYPE or Email.CONTENT_ITEM_TYPE.
  • Selection limits are bounded: default 50, maximum 100; exceeding it throws IllegalArgumentException.
  • Account metadata is stripped to prevent fingerprinting, and custom selection arguments are not supported on the session URI.
Kotlin — launch the Contact Picker and read the result
private val pickContacts =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        val data = result.data
        if (result.resultCode != Activity.RESULT_OK || data?.data == null) {
            return@registerForActivityResult   // cancelled or empty selection
        }

        lifecycleScope.launch(Dispatchers.IO) {
            val picked = queryPickedContacts(data.data!!)
            withContext(Dispatchers.Main) { renderPickedContacts(picked) }
        }
    }

private fun launchContactPicker() {
    val requestedFields = arrayListOf(
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
        ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE,
    )

    val intent = Intent(ContactsPickerSessionContract.ACTION_PICK_CONTACTS).apply {
        putStringArrayListExtra(
            ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
            requestedFields,
        )
        putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        putExtra(ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 5)
        putExtra(ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_MATCH_ALL_DATA_FIELDS, false)
    }

    pickContacts.launch(intent)
}

private fun queryPickedContacts(sessionUri: Uri): List<PickedContact> {
    val projection = arrayOf(
        ContactsContract.Contacts.LOOKUP_KEY,
        ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
        ContactsContract.Data.MIMETYPE,
        ContactsContract.Data.DATA1,
    )

    val byLookupKey = linkedMapOf<String, PickedContact>()

    contentResolver.query(sessionUri, projection, null, null, null)?.use { c ->
        val keyIdx = c.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)
        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()) {
            val key = c.getString(keyIdx) ?: continue
            val name = c.getString(nameIdx).orEmpty()
            val value = c.getString(dataIdx).orEmpty()
            val contact = byLookupKey[key] ?: PickedContact(key, name, emptyList(), emptyList())

            byLookupKey[key] = when (c.getString(mimeIdx)) {
                ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
                    contact.copy(phones = contact.phones + value)
                ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
                    contact.copy(emails = contact.emails + value)
                else -> contact
            }
        }
    }

    return byLookupKey.values.toList()
}

Devices below Android 17

The picker is not backported, so this flow cannot be the only path with a lower minSdk. Two documented behaviours help: for apps targeting Android 17 or higher, the system automatically upgrades Intent.ACTION_PICK to the new interface; and adding the EXTRA_USE_SYSTEM_CONTACTS_PICKER boolean extra to an ACTION_PICK intent lets you test the picker on Android 17 devices while targeting a lower SDK.

If your app must retain READ_CONTACTS for older devices while targeting API 37, that belongs in the declaration — the policy applies to what your app requests, not to each user’s OS version. Confirm the scope in your own Play Console before assuming a fallback exempts you.

Testing checklist

Test these on the oldest supported version and on Android 17 or later.

What to test before you ship

The left column exercises the picker. The right column proves the app stopped reading the whole address book.

Selection and results

  • Select one contact; confirm the expected fields arrive.
  • Select several; confirm each maps to the intended record.
  • Handle a contact with multiple numbers or emails by grouping on lookup key.
  • Handle a contact missing a requested field; search by display name.
  • Hit the selection limit and confirm it holds; select from a work or cloned profile if supported.

Cancellation, permissions and scope

  • Cancel the picker; confirm nothing is written or cleared.
  • Force an empty selection; confirm no crash or stale data.
  • Kill the process after selection; confirm nothing depends on the session URI.
  • Confirm no READ_CONTACTS dialog appears in the picker flow.
  • Confirm shipping builds no longer declare the permission, across every track and version.
  • Confirm sensible behaviour below Android 17.

For version and device matrices, see version compatibility testing and compatibility testing across devices.

Common mistakes and how to troubleshoot them

Problem Likely cause What to do
Picker intent does not resolve Device runs below Android 17. Add a version check and a documented fallback.
Empty results after a valid selection Wrong columns queried, or MIME types requested but never parsed. Read MIMETYPE and DATA1; request only what you parse.
Data vanishes later Session access is temporary, not a permanent reference. Persist the fields at selection time.
Declaration declined Use case does not match the feature, or the justification is generic. Rewrite with a checkable capability gap; ship the picker version if none exists.
One track still blocks a release An older bundle on another track still declares the permission. Audit every version and track, not just production.

Privacy, disclosure and data minimisation

Permission compliance is the floor, not the whole obligation. Contacts are personal and sensitive user data under Google Play’s User Data policy, and the contacts policy adds a specific constraint: you may not disclose or publish non-public contacts data unless the contacts themselves authorise it.

  • Collect only what the feature needs. Need a phone number? Do not request postal addresses because they were available.
  • Keep selection local unless there is a reason not to. Uploading contacts changes the risk profile and must appear in Data Safety.
  • Explain the request in context, not in a launch-time sweep, and honour a decline — manual entry is a reasonable alternative.
  • Apply normal retention and deletion rules to contacts you were given.

Frequently asked questions

Does every app have to remove READ_CONTACTS?

No. For apps targeting Android 17 (API level 37) or higher it is available only where the Contact Picker cannot provide core functionality, and those apps must declare. Apps targeting older API levels are outside this specific restriction.

Is READ_CONTACTS being deprecated?

No. It remains available for apps that pass access review. It is restricted, not removed.

What is the difference between the Contact Picker and the Contacts Provider?

The Contacts Provider is the device’s underlying contact data store. The picker is a system interface that hands your app a specific selection without granting access to the provider at large.

Does a custom contact picker satisfy the policy?

No. Google’s FAQ states a custom contact picking experience does not qualify as a reason to retain access.

Does the picker work on older Android versions?

No. It is available on Android 17 and later and is not backported. Apps targeting Android 17 or higher have legacy ACTION_PICK intents automatically upgraded on those devices.

How many contacts can a user select?

Enable multi-selection with Intent.EXTRA_ALLOW_MULTIPLE. The default limit is 50 and the documented maximum is 100.

Are private or enterprise apps exempt?

Yes — Google states private and enterprise device management apps are exempt from this policy requirement.

When does the policy take effect?

Google publishes the effective date on its Policy Deadlines page and shows the applicable status in Play Console, including a 30-day automatic self-extension. Use your Console rather than a third-party summary.

Can I keep the permission only for older devices?

The policy is scoped by target SDK rather than device OS, so a version-scoped permission can still require a declaration when you target API 37 or higher. Verify against your app’s policy status before relying on that design.

Conclusion: prefer selection, justify access

If a user is choosing who to contact, show a picker and take back only what they chose. If your app genuinely needs the whole address book — to manage it, back it up, screen calls or complete names — expect to explain that in Play Console and have it reviewed.

Three steps now: check the policy status for every app version and track, confirm what target SDK each live release declares, and map each contacts feature to either the picker or a specific use case.

Official sources

Ready for your 12 testers?

Stop reading — start testing. Real people on real devices, 14 days, zero chasing.

WhatsApp