Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdk 34
compileSdk 36

namespace 'co.quis.flutter_contacts'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package co.quis.flutter_contacts

import android.accounts.AccountManager
import android.app.Activity
import android.content.ContentProviderOperation
import android.content.ContentResolver
Expand All @@ -10,7 +11,9 @@ import android.content.Intent
import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.ContactsContract
import android.util.Log
import android.provider.ContactsContract.CommonDataKinds.Email
import android.provider.ContactsContract.CommonDataKinds.Event
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
Expand Down Expand Up @@ -436,28 +439,36 @@ class FlutterContacts {

fun insert(
resolver: ContentResolver,
context: Context,
contactMap: Map<String, Any?>
): Map<String, Any?>? {
val ops = mutableListOf<ContentProviderOperation>()

val contact = Contact.fromMap(contactMap)

// If no account is provided, create with no account type or account name.
//
// On Android, it is possible the default Contacts app will synchronize it
// with Gmail and add `com.google` account types, seconds after creation, if
// the option is enabled. Other apps may do the same if they have a sync
// option enabled.
// Log the contact being inserted
Log.d("FlutterContacts", "Inserting contact: ${contact.displayName ?: "Unknown"} (ID: ${contact.id})")
Log.d("FlutterContacts", "Full contact data: $contact")

// Handle account creation with proper fallback for cloud account scenarios.
//
// If an account is provided, use it explicitly instead.
// When the default account is set to a cloud account (like Google), Android
// doesn't allow creating contacts with no account or local accounts. In this
// case, we need to use an appropriate cloud account.
if (contact.accounts.isEmpty()) {
// Try to get a suitable default account to avoid the cloud account error
val (accountType, accountName) = getDefaultWritableAccount(context, resolver)

ops.add(
ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_TYPE, null)
.withValue(RawContacts.ACCOUNT_NAME, null)
.withValue(RawContacts.ACCOUNT_TYPE, accountType)
.withValue(RawContacts.ACCOUNT_NAME, accountName)
.build()
)

Log.d("FlutterContacts", "Creating contact with account - Type: $accountType, Name: $accountName")
} else {
// Use the explicitly provided account
ops.add(
ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_TYPE, contact.accounts.first().type)
Expand Down Expand Up @@ -1195,5 +1206,34 @@ class FlutterContacts {
fd.close()
}
}

/**
* Get the system's default cloud account for new contacts, or return null if default account is not cloud.
* Uses the DefaultAccount API (available from Android 8.0 API 26+) when possible,
* falls back to Google account detection on older versions.
*/
private fun getDefaultWritableAccount(context: Context, resolver: ContentResolver): Pair<String?, String?> {
// Use DefaultAccount API if available (Android 8.0+ / API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
runCatching {
val defaultAccountAndState = ContactsContract.RawContacts.DefaultAccount
.getDefaultAccountForNewContacts(resolver)

// Only use cloud accounts to avoid the error
if (defaultAccountAndState.state == ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState.DEFAULT_ACCOUNT_STATE_CLOUD) {
defaultAccountAndState.account?.let { account ->
Log.d("FlutterContacts", "Using system default cloud account: ${account.name} (${account.type})")
return Pair(account.type, account.name)
}
}
}.onFailure { e ->
Log.w("FlutterContacts", "Failed to get system default account: ${e.message}")
}
}

// Final fallback: Use null account (local storage)
Log.d("FlutterContacts", "No cloud account available, using local storage")
return Pair(null, null)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str
val args = call.arguments as List<Any>
val contact = args[0] as Map<String, Any>
val insertedContact: Map<String, Any?>? =
FlutterContacts.insert(resolver!!, contact)
FlutterContacts.insert(resolver!!, context!!, contact)
coroutineScope.launch(Dispatchers.Main) {
if (insertedContact != null) {
result.success(insertedContact)
Expand Down