On October 21, 2026, the Android Developers Blog announced a change that will break a lot of older codebases quietly: apps that target the upcoming Android release can no longer write to contacts directly with WRITE_CONTACTS. Writes are rejected with a SecurityException or permission denial. In their place come three standard intents that need no permission, and a new special app access permission, MANAGE_CONTACTS, for the small set of apps that genuinely need broad access.
The post only calls the target "the upcoming Android release" and places it in 2027, so treat exact API levels as unconfirmed. The migration, though, is something you can start now.
Why Google Is Doing This
WRITE_CONTACTS lets an app change contacts anywhere on the device, in the background, with no user interaction. The blog explains why apps requested it anyway: contact intents used to behave differently per device. Some editors dropped passed-in data such as a photo, some back buttons saved contacts unintentionally, and some "create new contact" options were hard to find. In the new release the intents behave consistently. Manufacturers may restyle the UI, but not the behavior.
The Three Intents
ACTION_INSERTopens a quick half-sheet for a new contact that the user can expand to a full-page editor.ACTION_INSERT_OR_EDITandACTION_EDITlet the user search for and add information to an existing contact.
SHOW_OR_CREATE_CONTACT is not handled by the system editor. Contacts apps may still handle it, but use ACTION_INSERT_OR_EDIT for consistent behavior.
| Extra | Behavior |
|---|---|
Insert.EXTRA_ACCOUNT | Suggests which account to save to. Existed before, standardized in the new release. |
Insert.EXTRA_PHOTO_URI | Pre-fills the contact photo. New in the upcoming release; the editor handles the URI read permission. |
Before and After in Kotlin
The old approach, a batch of ContentProviderOperations, is what stops working once you target the new release:
// Requires android.permission.WRITE_CONTACTS
fun saveContactDirectly(context: Context, name: String, phone: String, account: Account) {
val ops = arrayListOf(
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
.build(),
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)
.build(),
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
.build()
)
context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
}
The replacement is shorter and needs no permission:
fun saveContactWithEditor(
context: Context,
name: String,
phone: String,
account: Account,
photoUri: Uri
) {
val intent = Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI).apply {
putExtra(ContactsContract.Intents.Insert.NAME, name)
putExtra(ContactsContract.Intents.Insert.PHONE, phone)
// Suggest which account the contact is saved to
putExtra(ContactsContract.Intents.Insert.EXTRA_ACCOUNT, account)
// New in the upcoming Android release: pre-fill the contact photo
putExtra(ContactsContract.Intents.Insert.EXTRA_PHOTO_URI, photoUri)
clipData = ClipData.newRawUri("", photoUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(intent)
}
Adding a number to an existing contact follows the same pattern. Let the user pick the target contact, or pass a known contact URI to ACTION_EDIT:
fun addPhoneWithEditor(context: Context, phone: String) {
val intent = Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
putExtra(ContactsContract.Intents.Insert.PHONE, phone)
}
context.startActivity(intent)
}
fun editKnownContact(context: Context, contactUri: Uri, phone: String) {
val intent = Intent(Intent.ACTION_EDIT, contactUri).apply {
putExtra(ContactsContract.Intents.Insert.PHONE, phone)
}
context.startActivity(intent)
}
Gotchas from the post: the editor silently drops Insert.DATA rows whose MIME type is not a standard ContactsContract.CommonDataKinds type. There is no delete intent; users delete contacts in their contacts app, and only MANAGE_CONTACTS apps can delete directly. Because the user finishes the flow in a system UI, you no longer get a synchronous "saved" result to build on.
Reading and Call Actions
Two other replacements round out the story. For reading user-selected contacts, use the Contact Picker, available since Android 17 and requiring no permission. For apps that today write extra rows into the address book to add call or message actions, the post points to the new People Directory (PeopleContract), arriving in Android 17 QPR2. It is a system directory where your app publishes how people can reach each other in your app, without editing the user's contacts.
When You Do Need MANAGE_CONTACTS
MANAGE_CONTACTS is a special app access permission, not a runtime dialog: users enable it under Settings > Special app access > Manage contacts. It covers reading, adding, editing and deleting contacts, accounts and People Directory data, so you don't also need READ_CONTACTS. A custom branded editor screen alone does not qualify.
| App type | Needs it? |
|---|---|
| Saves a contact from a chat, card or order | No, use ACTION_INSERT |
| Adds a number or email to an existing contact | No, use ACTION_INSERT_OR_EDIT / ACTION_EDIT |
| Contact pickers for invite, share or pay | No, use the Contact Picker |
| Call/message actions on contacts | No, use People Directory |
| Dedicated contact manager or dialer replacement | Likely yes |
| Caller ID and spam apps that mass-update entries | Likely yes |
| Background enterprise or cloud contact sync | Likely yes |
| Bulk backup/restore or device-to-device transfer | Likely yes |
| Custom ringtones per contact | Likely yes |
To send users to the right settings screen, the post gives this snippet:
// Opens Settings > Apps > Special app access > Manage contacts
val intent = Intent(Settings.ACTION_MANAGE_CONTACTS, Uri.fromParts("package", packageName, null))
context.startActivity(intent)
On Google Play, using MANAGE_CONTACTS will require a Play Console declaration explaining why the intents are insufficient for your core functionality.
Migration Checklist
- Find every write. Search for
WRITE_CONTACTS,ContactsContract.RawContacts.CONTENT_URI,ContactsContract.Data.CONTENT_URIandapplyBatch(ContactsContract.AUTHORITY, ...). - Replace user-driven writes with
ACTION_INSERT,ACTION_EDITorACTION_INSERT_OR_EDIT. - Stick to standard MIME types in
Insert.DATA, and move call/message actions to People Directory. - Decide on
MANAGE_CONTACTSusing the table above and prepare the Play declaration if you qualify. - Remove
WRITE_CONTACTSfrom the manifest once nothing needs it, or scope it withmaxSdkVersionif older devices still need it. - Test the failure path. Once you target the new release without
MANAGE_CONTACTS, direct writes throw.
Who is affected: only apps targeting the upcoming release or later. Older targets keep their behavior, and the intents also work on older Android versions via the device's contacts app. The standardized editor is available on the new OS regardless of target SDK.
My take: this is the same direction as photo picker and the Android 17 Contact Picker. Permissions that grant ambient, silent access are being swapped for user-visible flows. For most apps the migration is an afternoon of work, and it removes a sensitive permission from your Play listing. Start with the grep, and you will quickly know which group you are in.
Source: A more reliable way to add and edit contacts on Android, Android Developers Blog, October 21, 2026.
No comments yet. Be the first to leave one!