diff --git a/android/build.gradle b/android/build.gradle index 0e7e899c..89d07a23 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdk 34 + compileSdk 36 namespace 'co.quis.flutter_contacts' diff --git a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt index 9a95833f..f3941049 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt @@ -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 @@ -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 @@ -436,28 +439,36 @@ class FlutterContacts { fun insert( resolver: ContentResolver, + context: Context, contactMap: Map ): Map? { val ops = mutableListOf() 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) @@ -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 { + // 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) + } } } diff --git a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt index 82915f0b..fb1b4fd0 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt @@ -207,7 +207,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str val args = call.arguments as List val contact = args[0] as Map val insertedContact: Map? = - FlutterContacts.insert(resolver!!, contact) + FlutterContacts.insert(resolver!!, context!!, contact) coroutineScope.launch(Dispatchers.Main) { if (insertedContact != null) { result.success(insertedContact)