diff --git a/analysis_options.yaml b/analysis_options.yaml index 072fab2..a5744c1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,194 +1,4 @@ -include: package:pedantic/analysis_options.yaml +include: package:flutter_lints/flutter.yaml -analyzer: - exclude: - - "bin/cache/**" - # the following two are relative to the stocks example and the flutter package respectively - # see https://github.com/dart-lang/sdk/issues/28463 - - "lib/l10n/*.p.dart" - - ".idea/" - - ".direnv/**/*.dart" - - ".direnv/*.dart" - - "current/**/*.dart" - - "current/*/*.dart" - - "fastlane" - - "test_driver/**" - strong-mode: - implicit-casts: false - - errors: - throw_in_finally: warning - annotate_overrides: warning - library_names: warning - # treat missing required parameters as a warning (not a hint) - missing_required_param: warning - - # treat missing returns as a warning (not a hint) - missing_return: warning - # allow having TODOs in the code - todo: ignore - prefer_single_quotes: ignore - # Ignore analyzer hints for updating pubspecs when using Future or - # Stream and not importing dart:async - # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore - unnecessary_this: ignore - use_function_type_syntax_for_parameters: ignore - omit_local_variable_types: ignore - use_to_and_as_if_applicable: warning - always_declare_return_types: warning -linter: - rules: - # these rules are documented on and in the same order as - # the Dart Lint rules page to make maintenance easier - # https://github.com/dart-lang/linter/blob/master/example/all.yaml - # - always_declare_return_types - # - always_put_control_body_on_new_line - # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 - - always_require_non_null_named_parameters - # - annotate_overrides - # - avoid_annotating_with_dynamic # conflicts with always_specify_types - # - avoid_as - - avoid_bool_literals_in_conditional_expressions - # - avoid_catches_without_on_clauses # we do this commonly - # - avoid_catching_errors # we do this commonly - # - avoid_classes_with_only_static_members - # - avoid_double_and_int_checks # only useful when targeting JS runtime - # - avoid_empty_else - # - avoid_field_initializers_in_const_classes - # - avoid_function_literals_in_foreach_calls - # - avoid_implementing_value_types # not yet tested - - avoid_init_to_null - # - avoid_js_rounded_ints # only useful when targeting JS runtime - - avoid_null_checks_in_equality_operators - # - avoid_positional_boolean_parameters # not yet tested - # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - # - avoid_returning_null # there are plenty of valid reasons to return null - # - avoid_returning_null_for_future # not yet tested - - avoid_returning_null_for_void - # - avoid_returning_this # there are plenty of valid reasons to return this - # - avoid_setters_without_getters # not yet tested - # - avoid_shadowing_type_parameters # not yet tested - # - avoid_single_cascade_in_expression_statements # not yet tested - - avoid_slow_async_io - - avoid_types_as_parameter_names - # - avoid_types_on_closure_parameters # conflicts with always_specify_types - - avoid_unused_constructor_parameters - - avoid_void_async - - await_only_futures - - camel_case_types - - cancel_subscriptions - # - cascade_invocations # not yet tested - # - close_sinks # not reliable enough - # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 - # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 - - control_flow_in_finally - # - curly_braces_in_flow_control_structures # not yet tested - # - diagnostic_describe_all_properties # not yet tested - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - # - file_names # not yet tested - - flutter_style_todos - - hash_and_equals - - implementation_imports - # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 - - iterable_contains_unrelated_type - # - join_return_with_assignment # not yet tested - - library_names - - library_prefixes - # - lines_longer_than_80_chars # not yet tested - - list_remove_unrelated_type - # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 - - no_adjacent_strings_in_list - - no_duplicate_case_values - - non_constant_identifier_names - # - null_closures # not yet tested - # - omit_local_variable_types # opposite of always_specify_types - # - one_member_abstracts # too many false positives - # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - # - parameter_assignments # we do this commonly - - prefer_adjacent_string_concatenation - #- prefer_asserts_in_initializer_lists - # - prefer_asserts_with_message # not yet tested - - prefer_collection_literals - - prefer_conditional_assignment - # - prefer_const_constructors - # - prefer_const_constructors_in_immutables - - prefer_const_declarations - #- prefer_const_literals_to_create_immutables - # - prefer_constructors_over_static_methods # not yet tested - - prefer_contains - # - prefer_double_quotes # opposite of prefer_single_quotes - #- prefer_equal_for_default_values - # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods - # - prefer_final_fields - # - prefer_final_in_for_each # not yet tested - # - prefer_final_locals - # - prefer_for_elements_to_map_fromIterable # not yet tested - - prefer_foreach - # - prefer_function_declarations_over_variables # not yet tested - # - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - # - prefer_int_literals # not yet tested - # - prefer_interpolation_to_compose_strings # not yet tested - - prefer_is_empty - - prefer_is_not_empty - - prefer_iterable_whereType - # - prefer_mixin # https://github.com/dart-lang/language/issues/32 - # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932 - # - prefer_single_quotes - - prefer_spread_collections - # - prefer_typing_uninitialized_variables - - prefer_void_to_null - # - provide_deprecation_message # not yet tested - # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml - - recursive_getters - - slash_for_doc_comments - # - sort_child_properties_last # not yet tested - # - sort_constructors_first - - sort_pub_dependencies - # - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - # - type_annotate_public_apis # subset of always_specify_types - - type_init_formals - # - unawaited_futures # too many false positives - # - unnecessary_await_in_return # not yet tested - - unnecessary_brace_in_string_interps - # - unnecessary_const - - unnecessary_getters_setters - # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 - # - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_in_if_null_operators - - unnecessary_overrides - # - unnecessary_parenthesis - - unnecessary_statements - # - unnecessary_this - - unrelated_type_equality_checks - # - unsafe_html # not yet tested - - use_full_hex_values_for_flutter_colors - # - use_function_type_syntax_for_parameters # not yet tested - - use_rethrow_when_possible - # - use_setters_to_change_properties # not yet tested - # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 - # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - - valid_regexps - # - void_checks # not yet tested - - use_to_and_as_if_applicable - - prefer_const_constructors_in_immutables - - unnecessary_new - - annotate_overrides - - always_declare_return_types +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/build.gradle b/android/build.gradle index 631ba35..60009d9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,40 +2,49 @@ group 'co.sunnyapp.flutter_contact' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '2.3.20' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:9.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } -rootProject.allprojects { +allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + namespace = 'co.sunnyapp.flutter_contact' + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } sourceSets { main.java.srcDirs += 'src/main/kotlin' } + defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + minSdk = 24 } - lintOptions { - disable 'InvalidPackage' +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } @@ -43,5 +52,4 @@ dependencies { // Joda-Time provides a quality replacement for the Java date and time classes. //Joda-Time is the de facto standard date and time library for Java. From Java SE 8 onwards, users are asked to migrate to java.time (JSR-310). implementation("joda-time:joda-time:2.10.6") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 000f970..a2f47b6 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - + diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/BaseFlutterContactPlugin.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/BaseFlutterContactPlugin.kt new file mode 100644 index 0000000..c153ce0 --- /dev/null +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/BaseFlutterContactPlugin.kt @@ -0,0 +1,358 @@ +@file:Suppress("UNCHECKED_CAST", "NewApi", "MemberVisibilityCanBePrivate", "UnnecessaryVariable") + +package co.sunnyapp.flutter_contact + +import android.annotation.TargetApi +import android.content.ContentProviderOperation +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.os.Build +import android.os.Handler +import android.provider.ContactsContract +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry +import java.util.* + +/** + * Base class for the flutter_contact plugin. There are two modes for this plugin: aggregate and raw. + * + * Aggregate deals with linked contacts, where there will be one record per group of linked contacts + * Raw deals with the individual contacts. + */ +abstract class BaseFlutterContactPlugin : ContactExtensions, EventChannel.StreamHandler { + lateinit var context: Context + lateinit var contactForms: BaseFlutterContactForms + + var methodChannel: MethodChannel? = null + var eventChannel: EventChannel? = null + + abstract override val mode: ContactMode + + override val resolver: ContentResolver + get() = context.contentResolver + + abstract fun initInstance(applicationContext: Context, messenger: BinaryMessenger) + + fun unInitInstance() { + methodChannel?.setMethodCallHandler(null) + methodChannel = null + eventChannel?.setStreamHandler(null) + eventChannel = null + } + + @TargetApi(Build.VERSION_CODES.ECLAIR) + protected fun getContacts(query: String?, withThumbnails: Boolean, photoHighResolution: Boolean, + sortBy: String? = null, + phoneQuery: Boolean?, offset: Int?, limit: Int?): StructList { + + val contacts = resolver.queryContacts(query, sortBy) + ?.toContactList(mode, limit ?: 30, offset ?: 0) + ?.onEach { contact -> + if (withThumbnails || photoHighResolution) { + contact.setAvatarDataForContactIfAvailable(photoHighResolution) + } + } ?: emptyList() + + return contacts.map { it.toMap() } + } + + protected fun getTotalContacts(query: String?, phoneQuery: Boolean?): Int { + return resolver.queryContacts(query, null, forCount = true) + ?.count ?: 0 + } + + protected fun getContactImage(identifier: Any?): ByteArray? { + return getAvatarDataForContactIfAvailable(contactKeyOf(mode, identifier) ?: return null) + } + + fun getContact(identifier: ContactKeys, withThumbnails: Boolean, photoHighResolution: Boolean): Struct { + val contact = getContactRecord(identifier, withThumbnails, photoHighResolution) + return contact.toMap() + } + + fun getContactRecord(identifier: ContactKeys, withThumbnails: Boolean, + photoHighResolution: Boolean): Contact { + resolver + .findContactById(identifier) + ?.use { cursor -> + val contactList = cursor.toContactList(mode, 1, 0) + val contact = contactList + .firstOrNull() + ?: methodError("getContact", "notFound", + "Expected a single result for contact ${identifier}, " + + "but instead found ${contactList.size}") + + if (withThumbnails || photoHighResolution) { + contact.setAvatarDataForContactIfAvailable(photoHighResolution) + } + return contact + } + ?: methodError("getContact", "notFound", "Expected a single result for contact $identifier") + } + + + protected fun addContact(contact: Contact): Struct { + val ops = arrayListOf() + + // "Using default value to save the contact. [getDefaultAccountForNewContacts()](https://developer.android.com/reference/android/provider/ContactsContract.RawContacts.DefaultAccount#getDefaultAccountForNewContacts(android.content.ContentResolver) requires API 36". + ops += ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) + .withValues(ContentValues()) + .build() + + + ops += 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.GIVEN_NAME, contact.givenName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.familyName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contact.prefix) + .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contact.suffix).build() + + + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contact.note) + .build() + + + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company) + .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contact.jobTitle).withYieldAllowed(true) + .build() + + //Phones + for (phone in contact.phones) { + ops += 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.value) + .withTypeAndLabel(ItemType.phone, phone.label) + .build() + } + + //Emails + for (email in contact.emails) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.value) + .withTypeAndLabel(ItemType.email, email.label) + .build() + } + + //Postal addresses + for (address in contact.postalAddresses) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + .withTypeAndLabel(ItemType.address, address.label) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.street) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.city) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.region) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.country) + .build() + } + + for (date in contact.dates) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) + .withTypeAndLabel(ItemType.event, date.label) + .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, date.toDateValue()) + .build() + } + + for (url in contact.urls) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE) + .withTypeAndLabel(ItemType.url, url.label) + .withValue(ContactsContract.CommonDataKinds.Website.URL, url.value) + .build() + } + + + val saveResult = resolver.applyBatch(ContactsContract.AUTHORITY, ops) + val rawContactId = saveResult.first().uri?.lastPathSegment?.toLong() + ?: pluginError("invalidId", "Expected a valid id") + + // In UNIFIED mode, getContact looks up by aggregate contact ID (Contacts._ID), not raw + // contact ID. The insert returns the raw contact ID; resolve it to the aggregate ID so + // the lookup succeeds (especially after last-delete + add or when contacts are synced). + val contactIdForLookup = when (mode) { + ContactMode.UNIFIED -> resolver.query( + ContactsContract.RawContacts.CONTENT_URI, + arrayOf(ContactsContract.RawContacts.CONTACT_ID), + "${ContactsContract.RawContacts._ID}=?", + arrayOf(rawContactId.toString()), + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + val idx = cursor.getColumnIndex(ContactsContract.RawContacts.CONTACT_ID) + if (idx >= 0) cursor.getLong(idx) else rawContactId + } else rawContactId + } ?: rawContactId + ContactMode.SINGLE -> rawContactId + } + + return getContact(ContactKeys(mode, contactIdForLookup), withThumbnails = true, photoHighResolution = true) + } + + protected fun deleteContact(contact: Contact): Boolean { + val ops = ArrayList() + val contactUri = contact.keys!!.contactUri + ops.add(ContentProviderOperation.newDelete(contactUri) + .build()) + return try { + resolver.applyBatch(ContactsContract.AUTHORITY, ops) + true + } catch (e: Exception) { + false + } + } + + protected fun updateContact(contact: Contact): Struct { + val ops = arrayListOf() + + ops += listOf(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE + ).map { + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + arrayOf(contact.identifier?.toString(), it)) + .build() + + } + + val names = listOfNotNull(contact.givenName, contact.familyName) + val displayName = when { + names.isEmpty() -> contact.displayName + else -> names.joinToString(" ") + } + // Update data (name) + ops += ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) + .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", + arrayOf(contact.identifier?.toString(), ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.givenName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.familyName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contact.prefix) + .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contact.suffix) + .build() + + // Insert data back into contact + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) + .withValue(ContactsContract.CommonDataKinds.Organization.TYPE, ContactsContract.CommonDataKinds.Organization.TYPE_WORK) + .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company) + .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contact.jobTitle) + .build() + + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) + .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contact.note) + .build() + + for (phone in contact.phones) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone.value) + .withTypeAndLabel(ItemType.phone, phone.label) + .build() + } + + for (email in contact.emails) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) + .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.value) + .withTypeAndLabel(ItemType.email, email.label) + .build() + } + + for (address in contact.postalAddresses) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) + .withTypeAndLabel(ItemType.address, address.label) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.street) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.city) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.region) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.country) + .build() + } + + + + for (date in contact.dates) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) + .withTypeAndLabel(ItemType.event, date.label) + .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, date.toDateValue()) + .build() + } + + for (url in contact.urls) { + ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) + .withTypeAndLabel(ItemType.url, url.label) + .withValue(ContactsContract.CommonDataKinds.Website.URL, url.value) + .build() + } + + resolver.applyBatch(ContactsContract.AUTHORITY, ops) + val updated = getContact(contact.keys + ?: pluginError("invalidInput", "Updated contact should have an id"), + withThumbnails = true, + photoHighResolution = true) + return updated + } + + var contentObserver: ContactsContentObserver? = null + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + if (events != null) { + try { + val contentObserver = ContactsContentObserver(events, Handler(context.mainLooper)) + resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contentObserver) + this.contentObserver = contentObserver + } catch (e: SecurityException) { + events.error("invalidPermissions", "No permissions for event. Try" + + "starting the listener after you've requested permissions", null) + events.endOfStream() + } + } + } + + override fun onCancel(arguments: Any?) { + + when (val observer = contentObserver) { + null -> { + } + else -> { + resolver.unregisterContentObserver(observer) + observer.close() + this.contentObserver = null + } + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/BaseFlutterContactsFormPlugin.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/BaseFlutterContactsFormPlugin.kt new file mode 100644 index 0000000..f30acdd --- /dev/null +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/BaseFlutterContactsFormPlugin.kt @@ -0,0 +1,261 @@ +@file:Suppress("NAME_SHADOWING") + +package co.sunnyapp.flutter_contact + +import android.content.ContentValues +import android.content.Intent +import android.provider.ContactsContract +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry + +/** + * Base class that facilitates edit/add contacts. + * + * There will be two modes. + * for embedding v1 - FlutterContactFormsPluginOld which handles the starting of the intent with registrar. + * for embedding v2 - FlutterContactFormsPlugin which handles the starting of the intent with ActivityPluginBinding. + */ +abstract class BaseFlutterContactForms(private val plugin: BaseFlutterContactPlugin) : PluginRegistry.ActivityResultListener { + companion object { + const val REQUEST_OPEN_CONTACT_FORM = 52941 + const val REQUEST_OPEN_EXISTING_CONTACT = 52942 + const val REQUEST_OPEN_CONTACT_PICKER = 52943 + const val REQUEST_INSERT_OR_UPDATE_CONTACT = 52944 + } + + var result: MethodChannel.Result? = null + private set + + abstract fun startIntent(intent: Intent, request: Int) + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean { + val success = when (requestCode) { + REQUEST_OPEN_EXISTING_CONTACT, REQUEST_OPEN_CONTACT_FORM, REQUEST_INSERT_OR_UPDATE_CONTACT -> when (val uri = intent?.data) { + null -> { + result?.success(mapOf("success" to false, "code" to ErrorCodes.FORM_OPERATION_CANCELED)) + false + } + else -> when (val contactIdString = uri.lastPathSegment) { + null -> { + result?.success(mapOf("success" to false, "code" to ErrorCodes.FORM_OPERATION_CANCELED)) + false + } + else -> { + try { + result?.success(mapOf( + "success" to true, + "contact" to plugin.getContact(ContactKeys(plugin.mode, contactIdString.toLong()), + withThumbnails = false, photoHighResolution = false))) + } catch (e: MethodCallException) { + result?.success(mapOf("success" to false, "code" to e.code)) + } + true + } + } + } + REQUEST_OPEN_CONTACT_PICKER -> when (val uri = intent?.data) { + null -> { + result?.success(mapOf("success" to false, "code" to ErrorCodes.PICKER_OPERATION_CANCELED)) + false + } + else -> when (val contactIdString = uri.lastPathSegment) { + null -> { + result?.success(mapOf("success" to false, "code" to ErrorCodes.PICKER_OPERATION_CANCELED)) + false + } + else -> { + try { + result?.success(mapOf( + "success" to true, + "contact" to plugin.getContact(ContactKeys(plugin.mode, contactIdString.toLong()), + withThumbnails = false, photoHighResolution = false))) + } catch (e: MethodCallException) { + result?.success(mapOf("success" to false, "code" to e.code)) + } + true + } + } + } + else -> { + result?.success(ErrorCodes.FORM_COULD_NOT_BE_OPENED) + false + } + } + result = null + return success + } + + fun openContactEditForm(result: MethodChannel.Result, contactId: ContactKeys) { + this.result = result + try { + val lookupUri = contactId.mode.lookupUri(plugin.resolver, contactId) +// val contact = plugin.getContactRecord(contactId, withThumbnails = false, photoHighResolution = false) + val intent = Intent(Intent.ACTION_EDIT) + intent.setDataAndTypeAndNormalize(lookupUri, ContactsContract.Contacts.CONTENT_ITEM_TYPE) + intent.putExtra("finishActivityOnSaveCompleted", true) + startIntent(intent, REQUEST_OPEN_EXISTING_CONTACT) + } catch (e: MethodCallException) { + result.error(e.code, "Error with ${e.method}: ${e.error}", e.error) + this.result = null + } catch (e: Exception) { + result.error(ErrorCodes.UNKNOWN_ERROR, "Unable to open form", e.toString()) + this.result = null + } + } + + fun openContactInsertForm(result: MethodChannel.Result, mode: ContactMode, contact: Contact) { + try { + this.result = result + val intent = Intent(Intent.ACTION_INSERT, mode.contentUri) + contact.applyToIntent(mode, intent) + intent.putExtra("finishActivityOnSaveCompleted", true) + startIntent(intent, REQUEST_OPEN_CONTACT_FORM) + } catch (e: MethodCallException) { + result.error(e.code, "Error with ${e.method}: ${e.error}", e.error) + this.result = null + } catch (e: Exception) { + result.error(ErrorCodes.UNKNOWN_ERROR, "Problem opening contact form", "$e") + this.result = null + } + } + + fun openContactPicker(result: MethodChannel.Result, mode: ContactMode) { + try { + this.result = result + val intent = Intent(Intent.ACTION_PICK) + intent.type = ContactsContract.Contacts.CONTENT_TYPE + startIntent(intent, REQUEST_OPEN_CONTACT_PICKER) + } catch (e: MethodCallException) { + result.error(e.code, "Error with ${e.method}: ${e.error}", e.error) + this.result = null + } catch (e: Exception) { + result.error(ErrorCodes.UNKNOWN_ERROR, "Problem opening contact picker", "$e") + this.result = null + } + } + + fun insertOrUpdateContactViaPicker(result: MethodChannel.Result, mode: ContactMode, contact: Contact) { + try { + this.result = result + val intentInsertEdit = Intent(Intent.ACTION_INSERT_OR_EDIT) + intentInsertEdit.type = ContactsContract.Contacts.CONTENT_ITEM_TYPE + contact.applyToIntent(mode, intentInsertEdit) + intentInsertEdit.putExtra("finishActivityOnSaveCompleted", true) + startIntent(intentInsertEdit, REQUEST_INSERT_OR_UPDATE_CONTACT) + } catch (e: MethodCallException) { + result.error(e.code, "Error with ${e.method}: ${e.error}", e.error) + this.result = null + } catch (e: Exception) { + result.error(ErrorCodes.UNKNOWN_ERROR, "Problem opening contact picker", "$e") + this.result = null + } + } +} + +/// For now, only copies basic properties +fun Contact.applyToIntent(mode: ContactMode, intent: Intent) { + val contact = this + + val inboundData = ArrayList() + + inboundData += contentValues(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.givenName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.familyName) + .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contact.prefix) + .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contact.suffix) + + + inboundData += contentValues(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contact.note) + + + inboundData += contentValues(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company) + .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contact.jobTitle) + + //Phones + for (phone in contact.phones) { + inboundData += contentValues(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone.value) + .withTypeAndLabel(ItemType.phone, phone.label) + } + + //Emails + for (email in contact.emails) { + inboundData += contentValues(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.value) + .withTypeAndLabel(ItemType.email, email.label) + } + + //Postal addresses + for (address in contact.postalAddresses) { + inboundData += contentValues(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) + .withTypeAndLabel(ItemType.address, address.label) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.street) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.city) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.region) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) + .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.country) + } + + + for (date in contact.dates) { + inboundData += contentValues(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) + .withTypeAndLabel(ItemType.event, date.label) + .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, date.toDateValue()) + + } + + for (url in contact.urls) { + inboundData += contentValues(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE) + .withTypeAndLabel(ItemType.url, url.label) + .withValue(ContactsContract.CommonDataKinds.Website.URL, url.value) + } + + + + intent.apply { + putExtra(ContactsContract.Intents.Insert.EMAIL, emails.firstOrNull()?.value) + putExtra(ContactsContract.Intents.Insert.PHONE, phones.firstOrNull()?.value) + putExtra(ContactsContract.Intents.Insert.NAME, listOfNotNull(contact.givenName, contact.familyName).let { + when { + it.isNotEmpty() -> it + else -> listOfNotNull(contact.displayName) + } + }.joinToString(" ")) + putExtra(ContactsContract.Intents.Insert.COMPANY, company) + putExtra(ContactsContract.Intents.Insert.NOTES, note) + putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, inboundData) + } + +} + +fun ContentValues.withValue(key: String, value: String?) = apply { + val value = value ?: return@apply + put(key, value) +} + +fun ContentValues.withValue(key: String, value: Int?) = apply { + val value = value ?: return@apply + put(key, value) +} + +fun ContentValues.withTypeAndLabel(type: ItemType, labelString: String?) = apply { + val label = labelString ?: return@apply + return when (val typeInt = type.calculateTypeInt(label)) { + type.otherType -> withValue(type.typeField, typeInt) + .withValue(type.labelField, label) + else -> withValue(type.typeField, typeInt) + } + +} + +fun contentValues(mimeType: String, vararg values: Pair) = ContentValues().apply { + put(ContactsContract.Data.MIMETYPE, mimeType) + for ((k, v) in values) { + if (v != null) { + put(k, v) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactFormsPlugin.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactFormsPlugin.kt index 5da049a..6282aed 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactFormsPlugin.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactFormsPlugin.kt @@ -1,205 +1,34 @@ -@file:Suppress("NAME_SHADOWING") - package co.sunnyapp.flutter_contact -import android.content.ContentProviderOperation -import android.content.ContentValues +import android.content.Context import android.content.Intent -import android.provider.ContactsContract -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry - -/// Class that facilitates edit/add contacts -class FlutterContactForms(private val plugin: FlutterContactPlugin, private val registrar: PluginRegistry.Registrar) : PluginRegistry.ActivityResultListener { - init { - registrar.addActivityResultListener(this) - } +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding - var result: Result? = null - override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean { - val success = when (requestCode) { - REQUEST_OPEN_EXISTING_CONTACT, REQUEST_OPEN_CONTACT_FORM -> when (val uri = intent?.data) { - null -> { - result?.success(mapOf("success" to false, "code" to ErrorCodes.FORM_OPERATION_CANCELED)) - false - } - else -> when (val contactIdString = uri.lastPathSegment) { - null -> { - result?.success(mapOf("success" to false, "code" to ErrorCodes.FORM_OPERATION_CANCELED)) - false - } - else -> { - result?.success(mapOf( - "success" to true, - "contact" to plugin.getContact(ContactKeys(plugin.mode, contactIdString.toLong()), - withThumbnails = false, photoHighResolution = false))) - true - } - } - } - else -> { - result?.success(ErrorCodes.FORM_COULD_NOT_BE_OPENED) - false - } - } - result = null - return success - } - fun openContactEditForm(result: Result, contactId: ContactKeys) { - this.result = result - try { - val lookupUri = contactId.mode.lookupUri(plugin.resolver, contactId) -// val contact = plugin.getContactRecord(contactId, withThumbnails = false, photoHighResolution = false) - val intent = Intent(Intent.ACTION_EDIT) - intent.setDataAndTypeAndNormalize(lookupUri, ContactsContract.Contacts.CONTENT_ITEM_TYPE) - intent.putExtra("finishActivityOnSaveCompleted", true) - startIntent(intent, REQUEST_OPEN_EXISTING_CONTACT) - } catch (e: MethodCallException) { - result.error(e.code, "Error with ${e.method}: ${e.error}", e.error) - this.result = null - } catch (e: Exception) { - result.error(ErrorCodes.UNKNOWN_ERROR, "Unable to open form", e.toString()) - this.result = null - } - } +/// Class that facilitates edit/add contacts for v2 embedding. +class FlutterContactForms(plugin: BaseFlutterContactPlugin, private val context: Context) : BaseFlutterContactForms(plugin) { + private var activityPluginBinding: ActivityPluginBinding? = null - fun openContactInsertForm(result: Result, mode: ContactMode, contact: Contact) { - try { - this.result = result - val intent = Intent(Intent.ACTION_INSERT, mode.contentUri) - contact.applyToIntent(mode, intent) - intent.putExtra("finishActivityOnSaveCompleted", true) - startIntent(intent, REQUEST_OPEN_CONTACT_FORM) - } catch (e: MethodCallException) { - result.error(e.code, "Error with ${e.method}: ${e.error}", e.error) - this.result = null - } catch (e: Exception) { - result.error(ErrorCodes.UNKNOWN_ERROR, "Problem opening contact form", "$e") - this.result = null - } + fun bindToActivity(activityPluginBinding: ActivityPluginBinding) { + this.activityPluginBinding = activityPluginBinding + activityPluginBinding.addActivityResultListener(this) } - private fun startIntent(intent: Intent, request: Int) { - if (registrar.activity() != null) { - registrar.activity().startActivityForResult(intent, request) - } else { - registrar.context().startActivity(intent) - } - } - - - companion object { - const val REQUEST_OPEN_CONTACT_FORM = 52941 - const val REQUEST_OPEN_EXISTING_CONTACT = 52942 - } - -} - -/// For now, only copies basic properties -fun Contact.applyToIntent(mode: ContactMode, intent: Intent) { - val contact = this - - val inboundData = ArrayList() - - inboundData += contentValues(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.givenName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.familyName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contact.prefix) - .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contact.suffix) - - - inboundData += contentValues(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contact.note) - - - inboundData += contentValues(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company) - .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contact.jobTitle) - - //Phones - for (phone in contact.phones) { - inboundData += contentValues(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone.value) - .withTypeAndLabel(ItemType.phone, phone.label) + fun unbindActivity() { + this.activityPluginBinding?.removeActivityResultListener(this) + this.activityPluginBinding = null } - //Emails - for (email in contact.emails) { - inboundData += contentValues(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.value) - .withTypeAndLabel(ItemType.email, email.label) - } - - //Postal addresses - for (address in contact.postalAddresses) { - inboundData += contentValues(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) - .withTypeAndLabel(ItemType.address, address.label) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.street) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.city) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.region) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.country) - } - - - for (date in contact.dates) { - inboundData += contentValues(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withTypeAndLabel(ItemType.event, date.label) - .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, date.toDateValue()) - - } - - for (url in contact.urls) { - inboundData += contentValues(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE) - .withTypeAndLabel(ItemType.url, url.label) - .withValue(ContactsContract.CommonDataKinds.Website.URL, url.value) - } - - - - intent.apply { - putExtra(ContactsContract.Intents.Insert.EMAIL, emails.firstOrNull()?.value) - putExtra(ContactsContract.Intents.Insert.PHONE, phones.firstOrNull()?.value) - putExtra(ContactsContract.Intents.Insert.NAME, listOfNotNull(contact.givenName, contact.familyName).let { - when { - it.isNotEmpty() -> it - else -> listOfNotNull(contact.displayName) + override fun startIntent(intent: Intent, request: Int) { + if (this.activityPluginBinding != null) { + try { + this.activityPluginBinding!!.activity.startActivityForResult(intent, request) + } catch (e: Exception) { + e.printStackTrace() + result?.success(ErrorCodes.FORM_COULD_NOT_BE_OPENED) } - }.joinToString(" ")) - putExtra(ContactsContract.Intents.Insert.COMPANY, company) - putExtra(ContactsContract.Intents.Insert.NOTES, note) - putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, inboundData) - } - -} - -fun ContentValues.withValue(key: String, value: String?) = apply { - val value = value ?: return@apply - put(key, value) -} - -fun ContentValues.withValue(key: String, value: Int?) = apply { - val value = value ?: return@apply - put(key, value) -} - -fun ContentValues.withTypeAndLabel(type: ItemType, labelString: String?) = apply { - val label = labelString ?: return@apply - return when (val typeInt = type.calculateTypeInt(label)) { - type.otherType -> withValue(type.typeField, typeInt) - .withValue(type.labelField, label) - else -> withValue(type.typeField, typeInt) - } - -} - -fun contentValues(mimeType: String, vararg values: Pair) = ContentValues().apply { - put(ContactsContract.Data.MIMETYPE, mimeType) - for ((k, v) in values) { - if (v != null) { - put(k, v) + } else { + context.startActivity(intent) } } } diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactPlugin.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactPlugin.kt index 98858f4..914327d 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactPlugin.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterContactPlugin.kt @@ -1,336 +1,83 @@ -@file:Suppress("UNCHECKED_CAST", "NewApi", "MemberVisibilityCanBePrivate", "UnnecessaryVariable") - package co.sunnyapp.flutter_contact -import android.annotation.TargetApi -import android.content.ContentProviderOperation -import android.content.ContentResolver import android.content.Context -import android.os.Build -import android.os.Handler -import android.provider.ContactsContract -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.PluginRegistry.Registrar -import java.util.* +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.BinaryMessenger /** - * Base class for the flutter_contact plugin. There are two modes for this plugin: aggregate and raw. + * class for the flutter_contact plugin. There are two modes for this plugin: aggregate and raw. * * Aggregate deals with linked contacts, where there will be one record per group of linked contacts - * Raw deals with the individual contacts + * Raw deals with the individual contacts. + * + * This class will register the method calls from both modes (aggregate and raw). */ -abstract class FlutterContactPlugin() : ContactExtensions, EventChannel.StreamHandler { - - abstract val registrar: Registrar - abstract override val mode: ContactMode - - abstract val contactForms: FlutterContactForms - - override val resolver: ContentResolver - get() = registrar.context().contentResolver - - protected val context: Context get() = registrar.context() - - @TargetApi(Build.VERSION_CODES.ECLAIR) - protected fun getContacts(query: String?, withThumbnails: Boolean, photoHighResolution: Boolean, - sortBy: String? = null, - phoneQuery: Boolean?, offset: Int?, limit: Int?): StructList { +open class FlutterContactPlugin : FlutterPlugin, ActivityAware { + private var rawPlugin: FlutterRawContactPlugin? = null + private var unifiedPlugin: FlutterAggregateContactPlugin? = null - val contacts = resolver.queryContacts(query, sortBy) - ?.toContactList(mode, limit ?: 30, offset ?: 0) - ?.onEach { contact -> - if (withThumbnails || photoHighResolution) { - contact.setAvatarDataForContactIfAvailable(photoHighResolution) - } - } ?: emptyList() - - return contacts.map { it.toMap() } - } - - protected fun getTotalContacts(query: String?, phoneQuery: Boolean?): Int { - return resolver.queryContacts(query, null, forCount = true) - ?.count ?: 0 + // --- FlutterPlugin implementation --- + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + initInstances(binding.applicationContext, binding.binaryMessenger) } - protected fun getContactImage(identifier: Any?): ByteArray? { - return getAvatarDataForContactIfAvailable(contactKeyOf(mode, identifier) ?: return null) + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + unInitInstances() } - fun getContact(identifier: ContactKeys, withThumbnails: Boolean, photoHighResolution: Boolean): Struct { - val contact = getContactRecord(identifier, withThumbnails, photoHighResolution) - return contact.toMap() + // --- ActivityAware implementation --- + override fun onDetachedFromActivity() { + unbindActivity(rawPlugin?.contactForms) + unbindActivity(unifiedPlugin?.contactForms) } - fun getContactRecord(identifier: ContactKeys, withThumbnails: Boolean, - photoHighResolution: Boolean): Contact { - context.contentResolver - .findContactById(identifier) - ?.use { cursor -> - val contactList = cursor.toContactList(mode, 1, 0) - val contact = contactList - .firstOrNull() - ?: methodError("getContact", "notFound", - "Expected a single result for contact ${identifier}, " + - "but instead found ${contactList?.size ?: 0}") - - if (withThumbnails || photoHighResolution) { - contact.setAvatarDataForContactIfAvailable(photoHighResolution) - } - return contact - } - ?: methodError("getContact", "notFound", "Expected a single result for contact $identifier") + override fun onDetachedFromActivityForConfigChanges() { + unbindActivity(rawPlugin?.contactForms) + unbindActivity(unifiedPlugin?.contactForms) } - - protected fun addContact(contact: Contact): Struct { - val ops = arrayListOf() - - ops += ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) - .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) - .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) - .build() - - - ops += 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.GIVEN_NAME, contact.givenName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.familyName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contact.prefix) - .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contact.suffix).build() - - - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contact.note) - .build() - - - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company) - .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contact.jobTitle).withYieldAllowed(true) - .build() - - //Phones - for (phone in contact.phones) { - ops += 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.value) - .withTypeAndLabel(ItemType.phone, phone.label) - .build() - } - - //Emails - for (email in contact.emails) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.value) - .withTypeAndLabel(ItemType.email, email.label) - .build() - } - - //Postal addresses - for (address in contact.postalAddresses) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) - .withTypeAndLabel(ItemType.address, address.label) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.street) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.city) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.region) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.country) - .build() - } - - for (date in contact.dates) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withTypeAndLabel(ItemType.event, date.label) - .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, date.toDateValue()) - .build() - } - - for (url in contact.urls) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE) - .withTypeAndLabel(ItemType.url, url.label) - .withValue(ContactsContract.CommonDataKinds.Website.URL, url.value) - .build() - } - - - val saveResult = context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) - val contactId = saveResult.first().uri?.lastPathSegment?.toLong() - ?: pluginError("invalidId", "Expected a valid id") - - return getContact(ContactKeys(contactId), withThumbnails = true, photoHighResolution = true) + override fun onReattachedToActivityForConfigChanges(@NonNull binding: ActivityPluginBinding) { + bindToActivity(binding, rawPlugin?.contactForms) + bindToActivity(binding, unifiedPlugin?.contactForms) } - protected fun deleteContact(contact: Contact): Boolean { - val ops = ArrayList() - val contactUri = contact.keys!!.contactUri - ops.add(ContentProviderOperation.newDelete(contactUri) - .build()) - return try { - context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) - true - } catch (e: Exception) { - false - } + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + bindToActivity(binding, rawPlugin?.contactForms) + bindToActivity(binding, unifiedPlugin?.contactForms) } - protected fun updateContact(contact: Contact): Struct { - val ops = arrayListOf() - - ops += listOf(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE - ).map { - ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) - .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - arrayOf(contact.identifier?.toString(), it)) - .build() + private fun initInstances(applicationContext: Context, binaryMessenger: BinaryMessenger) { + rawPlugin = FlutterRawContactPlugin() + rawPlugin!!.initInstance(applicationContext, binaryMessenger) - } - - val names = listOfNotNull(contact.givenName, contact.familyName) - val displayName = when { - names.isEmpty() -> contact.displayName - else -> names.joinToString(" ") - } - // Update data (name) - ops += ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) - .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?", - arrayOf(contact.identifier?.toString(), ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) - .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.givenName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.middleName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.familyName) - .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contact.prefix) - .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contact.suffix) - .build() - - // Insert data back into contact - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) - .withValue(ContactsContract.CommonDataKinds.Organization.TYPE, ContactsContract.CommonDataKinds.Organization.TYPE_WORK) - .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contact.company) - .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contact.jobTitle) - .build() - - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) - .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contact.note) - .build() - - for (phone in contact.phones) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) - .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone.value) - .withTypeAndLabel(ItemType.phone, phone.label) - .build() - } - - for (email in contact.emails) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) - .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.value) - .withTypeAndLabel(ItemType.email, email.label) - .build() - } - - for (address in contact.postalAddresses) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) - .withTypeAndLabel(ItemType.address, address.label) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.street) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.city) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.region) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.postcode) - .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.country) - .build() - } - - - - for (date in contact.dates) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) - .withTypeAndLabel(ItemType.event, date.label) - .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, date.toDateValue()) - .build() - } - - for (url in contact.urls) { - ops += ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, contact.identifier?.toString()) - .withTypeAndLabel(ItemType.url, url.label) - .withValue(ContactsContract.CommonDataKinds.Website.URL, url.value) - .build() - } - - context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) - val updated = getContact(contact.keys - ?: pluginError("invalidInput", "Updated contact should have an id"), - withThumbnails = true, - photoHighResolution = true) - return updated + unifiedPlugin = FlutterAggregateContactPlugin() + unifiedPlugin!!.initInstance(applicationContext, binaryMessenger) } - companion object { - @JvmStatic - fun registerWith(registrar: Registrar) { - FlutterAggregateContactPlugin.registerWith(registrar) - FlutterRawContactPlugin.registerWith(registrar) - } + private fun unInitInstances() { + rawPlugin?.unInitInstance() + unifiedPlugin?.unInitInstance() } - var contentObserver: ContactsContentObserver? = null - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - if (events != null) { - try { - val contentObserver = ContactsContentObserver(events, Handler(context.mainLooper)) - context.contentResolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contentObserver) - this.contentObserver = contentObserver - } catch (e: SecurityException) { - events.error("invalidPermissions", "No permissions for event. Try" + - "starting the listener after you've requested permissions", null) - events.endOfStream() - } + /** + * bind the binding to the activity + */ + private fun bindToActivity(binding: ActivityPluginBinding, contactForms: BaseFlutterContactForms?) { + if (contactForms is FlutterContactForms) { + contactForms.bindToActivity(binding) } } - override fun onCancel(arguments: Any?) { - - when (val observer = contentObserver) { - null -> { - } - else -> { - context.contentResolver.unregisterContentObserver(observer) - observer.close() - this.contentObserver = null - } + /** + * unbind the binding from activity + */ + private fun unbindActivity(contactForms: BaseFlutterContactForms?) { + if (contactForms is FlutterContactForms) { + contactForms.unbindActivity() } } } diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterSingleContactPlugin.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterSingleContactPlugin.kt index efcf717..8173ab6 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterSingleContactPlugin.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterSingleContactPlugin.kt @@ -3,14 +3,12 @@ package co.sunnyapp.flutter_contact +import android.content.Context import android.os.Build import androidx.annotation.RequiresApi -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.* import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar const val flutterRawContactsChannelName = "github.com/sunnyapp/flutter_single_contact" @@ -19,11 +17,9 @@ const val flutterRawContactsEventName = "github.com/sunnyapp/flutter_single_cont /** * Instance of plugin that deals with non-unified single contacts */ -class FlutterRawContactPlugin(override val registrar: Registrar) : FlutterContactPlugin(), - MethodCallHandler, EventChannel.StreamHandler { +class FlutterRawContactPlugin : BaseFlutterContactPlugin(), MethodCallHandler { override val mode = ContactMode.SINGLE - override val contactForms = FlutterContactForms(this, registrar) @RequiresApi(Build.VERSION_CODES.O) override fun onMethodCall(call: MethodCall, result: Result) { @@ -88,6 +84,15 @@ class FlutterRawContactPlugin(override val registrar: Registrar) : FlutterContac ?: emptyMap()) contactForms.openContactInsertForm(result, mode, contactFromArgs) } + "openContactPicker" -> { + contactForms.openContactPicker(result, mode) + } + "insertOrUpdateContactViaPicker" -> { + val contactFromArgs = Contact.fromMap(mode, call.arguments as? Map + ?: emptyMap()) + println(contactFromArgs.toMap()) + contactForms.insertOrUpdateContactViaPicker(result, mode, contactFromArgs) + } else -> result.notImplemented() } @@ -96,14 +101,13 @@ class FlutterRawContactPlugin(override val registrar: Registrar) : FlutterContac } } - companion object { - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), flutterRawContactsChannelName) - val events = EventChannel(registrar.messenger(), flutterRawContactsEventName) - val plugin = FlutterRawContactPlugin(registrar) - channel.setMethodCallHandler(plugin) - events.setStreamHandler(plugin) - } + override fun initInstance(applicationContext: Context, messenger: BinaryMessenger) { + methodChannel = MethodChannel(messenger, flutterRawContactsChannelName) + eventChannel = EventChannel(messenger, flutterRawContactsEventName) + methodChannel!!.setMethodCallHandler(this) + eventChannel!!.setStreamHandler(this) + context = applicationContext + // initializing the instance with v2 embedding + contactForms = FlutterContactForms(this, context) } } diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterUnifiedContactPlugin.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterUnifiedContactPlugin.kt index 5750e8a..e5f66af 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterUnifiedContactPlugin.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/FlutterUnifiedContactPlugin.kt @@ -3,14 +3,12 @@ package co.sunnyapp.flutter_contact +import android.content.Context import android.os.Build import androidx.annotation.RequiresApi -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.* import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar const val flutterContactsChannelName = "github.com/sunnyapp/flutter_unified_contact" @@ -19,10 +17,7 @@ const val flutterContactsEventName = "github.com/sunnyapp/flutter_unified_contac /** * The variant that operates on Aggregate contacts vs raw contacts */ -class FlutterAggregateContactPlugin(override val registrar: Registrar) : FlutterContactPlugin(), - MethodCallHandler { - - override val contactForms = FlutterContactForms(this, registrar) +class FlutterAggregateContactPlugin : BaseFlutterContactPlugin(), MethodCallHandler { override val mode = ContactMode.UNIFIED @@ -50,7 +45,8 @@ class FlutterAggregateContactPlugin(override val registrar: Registrar) : Flutter } "getContact" -> asyncTask(result) { this.getContact( - identifier = contactKeyOf(mode, call.argument("identifier")) ?: badParameter("getContact", "identifier"), + identifier = contactKeyOf(mode, call.argument("identifier")) + ?: badParameter("getContact", "identifier"), withThumbnails = call.argument("withThumbnails") == true, photoHighResolution = call.argument("photoHighResolution") == true) } @@ -77,7 +73,8 @@ class FlutterAggregateContactPlugin(override val registrar: Registrar) : Flutter when (val contactId = call.argument("identifier")) { null -> result.error(ErrorCodes.INVALID_PARAMETER, "Missing parameter: identifier", null) else -> { - val keys = contactKeyOf(mode, contactId) ?: badParameter("openEditForm", "identifier") + val keys = contactKeyOf(mode, contactId) + ?: badParameter("openEditForm", "identifier") contactForms.openContactEditForm(result, keys) } } @@ -87,6 +84,15 @@ class FlutterAggregateContactPlugin(override val registrar: Registrar) : Flutter ?: emptyMap()) contactForms.openContactInsertForm(result, mode, contactFromArgs) } + "openContactPicker" -> { + contactForms.openContactPicker(result, mode) + } + "insertOrUpdateContactViaPicker" -> { + val contactFromArgs = Contact.fromMap(mode, call.arguments as? Map + ?: emptyMap()) + println(contactFromArgs.toMap()) + contactForms.insertOrUpdateContactViaPicker(result, mode, contactFromArgs) + } else -> result.notImplemented() } @@ -95,16 +101,13 @@ class FlutterAggregateContactPlugin(override val registrar: Registrar) : Flutter } } - companion object { - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), flutterContactsChannelName) - val events = EventChannel(registrar.messenger(), flutterContactsEventName) - val plugin = FlutterAggregateContactPlugin(registrar) - channel.setMethodCallHandler(plugin) - events.setStreamHandler(plugin) - - } + override fun initInstance(applicationContext: Context, messenger: BinaryMessenger) { + methodChannel = MethodChannel(messenger, flutterContactsChannelName) + eventChannel = EventChannel(messenger, flutterContactsEventName) + methodChannel!!.setMethodCallHandler(this) + eventChannel!!.setStreamHandler(this) + context = applicationContext + // initializing the instance with v2 embedding + contactForms = FlutterContactForms(this, context) } } - diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/Item.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/Item.kt index 46dc652..18506c2 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_contact/Item.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/Item.kt @@ -1,9 +1,7 @@ @file:Suppress("MoveVariableDeclarationIntoWhen") -@file:SuppressLint("DefaultLocale") package co.sunnyapp.flutter_contact -import android.annotation.SuppressLint import android.content.ContentProviderOperation import android.database.Cursor import android.provider.ContactsContract.CommonDataKinds.* @@ -24,7 +22,7 @@ sealed class ItemType(val otherType: Int, val labelField: String, val typeField: val type = cursor.getInt(cursor.getColumnIndex(typeField)) val fromTypeInt = if (type == otherType) null else calculateTypeValue(type) val fromLabelField = cursor.string(labelField) - return fromTypeInt ?: fromLabelField?.toLowerCase() ?: "other" + return fromTypeInt ?: fromLabelField?.lowercase() ?: "other" } companion object { @@ -38,7 +36,7 @@ sealed class ItemType(val otherType: Int, val labelField: String, val typeField: class EmailType : ItemType(otherType = Email.TYPE_CUSTOM, labelField = Email.LABEL, typeField = Email.TYPE) { override fun calculateTypeInt(type: String?): Int { - return when (type?.toLowerCase()) { + return when (type?.lowercase()) { "home" -> Email.TYPE_HOME "work" -> Email.TYPE_WORK "mobile" -> Email.TYPE_MOBILE @@ -55,7 +53,7 @@ class EmailType : ItemType(otherType = Email.TYPE_CUSTOM, labelField = Email.LAB } class PhoneType : ItemType(otherType = Phone.TYPE_CUSTOM, labelField = Phone.LABEL, typeField = Phone.TYPE) { - override fun calculateTypeInt(type: String?) = when (type?.toLowerCase()) { + override fun calculateTypeInt(type: String?) = when (type?.lowercase()) { "home" -> Phone.TYPE_HOME "work" -> Phone.TYPE_WORK "mobile" -> Phone.TYPE_MOBILE @@ -81,7 +79,7 @@ class PhoneType : ItemType(otherType = Phone.TYPE_CUSTOM, labelField = Phone.LAB } class UrlType : ItemType(otherType = Website.TYPE_CUSTOM, labelField = Website.LABEL, typeField = Website.TYPE) { - override fun calculateTypeInt(type: String?) = when (type?.toLowerCase()) { + override fun calculateTypeInt(type: String?) = when (type?.lowercase()) { "work" -> Website.TYPE_WORK "blog" -> Website.TYPE_BLOG "home" -> Website.TYPE_HOME @@ -105,7 +103,7 @@ class UrlType : ItemType(otherType = Website.TYPE_CUSTOM, labelField = Website.L } class EventType : ItemType(otherType = Event.TYPE_CUSTOM, labelField = Event.LABEL, typeField = Event.TYPE) { - override fun calculateTypeInt(type: String?) = when (type?.toLowerCase()) { + override fun calculateTypeInt(type: String?) = when (type?.lowercase()) { "anniversary" -> Event.TYPE_ANNIVERSARY "birthday" -> Event.TYPE_ANNIVERSARY else -> Event.TYPE_CUSTOM @@ -119,7 +117,7 @@ class EventType : ItemType(otherType = Event.TYPE_CUSTOM, labelField = Event.LAB } class AddressType : ItemType(otherType = StructuredPostal.TYPE_CUSTOM, labelField = StructuredPostal.LABEL, typeField = StructuredPostal.TYPE) { - override fun calculateTypeInt(type: String?) = when (type?.toLowerCase()) { + override fun calculateTypeInt(type: String?) = when (type?.lowercase()) { "home" -> StructuredPostal.TYPE_HOME "work" -> StructuredPostal.TYPE_WORK else -> StructuredPostal.TYPE_CUSTOM diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/contact-from-cursor-ext.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/contact-from-cursor-ext.kt index 942586b..2f62213 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_contact/contact-from-cursor-ext.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/contact-from-cursor-ext.kt @@ -131,6 +131,11 @@ interface ContactExtensions { } } + // set the display name as primary and outside the mime type check. + // if the raw contact has no name then there will be no mime type about name data, + // so display name will be null. + contact.displayName = cursor.string(Contacts.DISPLAY_NAME) + //NAMES when (mimeType) { CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { @@ -144,7 +149,6 @@ interface ContactExtensions { ?: cursor.string(CommonDataKinds.StructuredName.PREFIX) contact.suffix = contact.suffix ?: cursor.string(CommonDataKinds.StructuredName.SUFFIX) - contact.displayName = contact.displayName ?: cursor.string(mode.nameRef) } CommonDataKinds.Note.CONTENT_ITEM_TYPE -> contact.note = cursor.string(CommonDataKinds.Note.NOTE) CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_contact/shared-extensions.kt b/android/src/main/kotlin/co/sunnyapp/flutter_contact/shared-extensions.kt index d3f5b5e..c0da32f 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_contact/shared-extensions.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_contact/shared-extensions.kt @@ -38,6 +38,7 @@ class ContactsContentObserver(private val sink: EventChannel.EventSink, handler: object ErrorCodes { const val FORM_OPERATION_CANCELED = "formOperationCancelled" const val FORM_COULD_NOT_BE_OPENED = "formCouldNotBeOpened" + const val PICKER_OPERATION_CANCELED = "pickerOperationCancelled" const val NOT_FOUND = "notFound" const val UNKNOWN_ERROR = "unknownError" const val INVALID_PARAMETER = "invalidParameter" diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f7..4f8d4d2 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 252d9ec..a850680 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -37,5 +37,60 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + # You can enable the permissions needed here. For example to enable camera + # permission, just remove the `#` character in front so it looks like this: + # + # ## dart: PermissionGroup.camera + # 'PERMISSION_CAMERA=1' + # + # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.calendar + # 'PERMISSION_EVENTS=1', + + ## dart: PermissionGroup.reminders + # 'PERMISSION_REMINDERS=1', + + ## dart: PermissionGroup.contacts + 'PERMISSION_CONTACTS=1', + + ## dart: PermissionGroup.camera + # 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.microphone + # 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.speech + # 'PERMISSION_SPEECH_RECOGNIZER=1', + + ## dart: PermissionGroup.photos + # 'PERMISSION_PHOTOS=1', + + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + # 'PERMISSION_LOCATION=1', + + ## dart: PermissionGroup.notification + # 'PERMISSION_NOTIFICATIONS=1', + + ## dart: PermissionGroup.mediaLibrary + # 'PERMISSION_MEDIA_LIBRARY=1', + + ## dart: PermissionGroup.sensors + # 'PERMISSION_SENSORS=1', + + ## dart: PermissionGroup.bluetooth + # 'PERMISSION_BLUETOOTH=1', + + ## dart: PermissionGroup.appTrackingTransparency + # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', + + ## dart: PermissionGroup.criticalAlerts + # 'PERMISSION_CRITICAL_ALERTS=1' + ] + end end -end +end \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore index e96ef60..585d056 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -24,6 +24,8 @@ Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* +.build/ +.swiftpm/ # Exceptions to above rules. !default.mode1v3 diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/ios/Classes/FlutterContactPlugin.h b/ios/Classes/FlutterContactPlugin.h deleted file mode 100644 index 1ad6ff6..0000000 --- a/ios/Classes/FlutterContactPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface FlutterContactPlugin : NSObject -@end diff --git a/ios/Classes/FlutterContactPlugin.m b/ios/Classes/FlutterContactPlugin.m deleted file mode 100644 index b9df0d9..0000000 --- a/ios/Classes/FlutterContactPlugin.m +++ /dev/null @@ -1,12 +0,0 @@ -#import "FlutterContactPlugin.h" -#import - -@implementation FlutterContactPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - if (@available(iOS 9.0, *)) { - [SwiftFlutterContactPlugin registerWithRegistrar:registrar]; - } else { - // Fallback on earlier versions - } -} -@end diff --git a/ios/flutter_contact.podspec b/ios/flutter_contact.podspec index 0af7913..7f91e2d 100644 --- a/ios/flutter_contact.podspec +++ b/ios/flutter_contact.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |s| s.name = 'flutter_contact' s.version = '0.0.1' - s.swift_version = '4.2' + s.swift_version = '5.0' s.summary = 'A plugin for managing contacts across platforms.' s.description = <<-DESC A new flutter plugin project. @@ -14,11 +14,10 @@ A new flutter plugin project. s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.source_files = 'flutter_contact/Sources/flutter_contact/**/*.swift' s.dependency 'Flutter' - s.platform = :ios, '9.0' + s.platform = :ios, '13.0' - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end diff --git a/ios/flutter_contact/Package.swift b/ios/flutter_contact/Package.swift new file mode 100644 index 0000000..edc3625 --- /dev/null +++ b/ios/flutter_contact/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "flutter_contact", + platforms: [ + .iOS("13.0") + ], + products: [ + .library(name: "flutter-contact", targets: ["flutter_contact"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], + targets: [ + .target( + name: "flutter_contact", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework") + ] + ) + ] +) diff --git a/ios/Classes/ContactEventHandler.swift b/ios/flutter_contact/Sources/flutter_contact/ContactEventHandler.swift similarity index 100% rename from ios/Classes/ContactEventHandler.swift rename to ios/flutter_contact/Sources/flutter_contact/ContactEventHandler.swift diff --git a/ios/Classes/ContactFormsHandler.swift b/ios/flutter_contact/Sources/flutter_contact/ContactFormsHandler.swift similarity index 67% rename from ios/Classes/ContactFormsHandler.swift rename to ios/flutter_contact/Sources/flutter_contact/ContactFormsHandler.swift index 43eb249..6f8ea6c 100644 --- a/ios/Classes/ContactFormsHandler.swift +++ b/ios/flutter_contact/Sources/flutter_contact/ContactFormsHandler.swift @@ -13,7 +13,7 @@ import Flutter var flutterResult: FlutterResult? = nil @available(iOS 9.0, *) -extension SwiftFlutterContactPlugin: CNContactViewControllerDelegate { +extension SwiftFlutterContactPlugin: CNContactViewControllerDelegate, CNContactPickerDelegate { func openContactInsertForm(result: @escaping FlutterResult, contact: CNContact?=nil) -> [String:Any]? { @@ -76,6 +76,62 @@ extension SwiftFlutterContactPlugin: CNContactViewControllerDelegate { message: "Error opening form: \(error.localizedDescription)") } } + + func openContactPicker(result: @escaping FlutterResult) { + flutterResult = result + DispatchQueue.main.async { + let contactPicker = CNContactPickerViewController() + contactPicker.delegate = self + var rvc = UIApplication.shared.keyWindow?.rootViewController + while let nextView = rvc?.presentedViewController { + rvc = nextView + } + rvc?.present(contactPicker, animated:true, completion: nil) + } + } + + func insertOrUpdateContactViaPicker(result: @escaping FlutterResult, contact: CNContact?=nil) -> [String:Any]? { + flutterResult = result + let contact = contact ?? CNMutableContact() + DispatchQueue.main.async { + let cnvc = CNContactViewController(forUnknownContact:contact) + cnvc.delegate = self + cnvc.contactStore = CNContactStore() +// cnvc.displayedPropertyKeys = [CNContactPhoneNumbersKey] + cnvc.allowsActions = false + cnvc.allowsEditing = false + cnvc.edgesForExtendedLayout = [] + cnvc.view.layoutIfNeeded() + let navigationController = UINavigationController .init(rootViewController: cnvc) + if #available(iOS 13.0, *) { + // use the feature available in iOS 13 or later + navigationController.navigationBar.backgroundColor = .systemBackground + } else { + navigationController.navigationBar.backgroundColor = .white + } + var rvc = UIApplication.shared.keyWindow?.rootViewController + while let nextView = rvc?.presentedViewController { + rvc = nextView + } + rvc?.present(navigationController, animated: true, completion: nil) + } + return nil + } + + //MARK:- CNContactPickerDelegate Method + public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { + if let result = flutterResult { + result(contactResult(self.mode, contact: contact)) + flutterResult = nil + } + } + + public func contactPickerDidCancel(_ picker: CNContactPickerViewController) { + if let result = flutterResult { + result(contactFailResult(code: ErrorCodes.formOperationCancelled.description)) + flutterResult = nil + } + } public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) { viewController.dismiss(animated: true, completion: nil) @@ -109,12 +165,12 @@ extension SwiftFlutterContactPlugin: CNContactViewControllerDelegate { } func contactFailResult(code: String)-> [String:Any] { - return ["successful": false, "code": code] + return ["success": false, "code": code] } @available(iOS 9.0, *) func contactResult(_ mode: ContactMode, contact:CNContact)-> [String:Any] { - return ["successful": true, "contact": contact.toDictionary(mode)] + return ["success": true, "contact": contact.toDictionary(mode)] } diff --git a/ios/Classes/FlutterExtensions.swift b/ios/flutter_contact/Sources/flutter_contact/FlutterExtensions.swift similarity index 98% rename from ios/Classes/FlutterExtensions.swift rename to ios/flutter_contact/Sources/flutter_contact/FlutterExtensions.swift index b0f41d1..03d1eb5 100644 --- a/ios/Classes/FlutterExtensions.swift +++ b/ios/flutter_contact/Sources/flutter_contact/FlutterExtensions.swift @@ -197,18 +197,21 @@ extension DComponents { } extension DateComponents: DComponents { + // As per crash reports on iOS 17, the fields have nil value. + // Using "!" crashes the app. + // So returning "0" as default value, if nil. var year: Int { - get { self.value(for: .year)! } + get { self.value(for: .year) ?? 0 } set(value) { self.setValue(value, for: .year) } } var month: Int { - get { self.value(for: .month)! } + get { self.value(for: .month) ?? 0 } set(value) { self.setValue(value, for: .month) } } var day: Int { - get { self.value(for: .day)! } + get { self.value(for: .day) ?? 0 } set(value) { self.setValue(value, for: .day) } } } diff --git a/ios/Classes/SwiftFlutterContactPlugin.swift b/ios/flutter_contact/Sources/flutter_contact/SwiftFlutterContactPlugin.swift similarity index 97% rename from ios/Classes/SwiftFlutterContactPlugin.swift rename to ios/flutter_contact/Sources/flutter_contact/SwiftFlutterContactPlugin.swift index f0d7686..9a26876 100644 --- a/ios/Classes/SwiftFlutterContactPlugin.swift +++ b/ios/flutter_contact/Sources/flutter_contact/SwiftFlutterContactPlugin.swift @@ -134,7 +134,20 @@ public class SwiftFlutterContactPlugin: NSObject, FlutterPlugin { contact = mutable } let _ = self.openContactInsertForm(result: result, contact: contact) - + + case "openContactPicker": + let _ = self.openContactPicker(result: result); + + case "insertOrUpdateContactViaPicker": + let contactData:[String:Any?]? = call.arguments as? [String:Any?] + var contact:CNContact? = nil + if let contactData = contactData { + let mutable = CNMutableContact() + mutable.takeFromDictionary(contactData) + contact = mutable + } + let _ = self.insertOrUpdateContactViaPicker(result: result, contact: contact) + default: result(FlutterMethodNotImplemented) } diff --git a/lib/base_contacts.dart b/lib/base_contacts.dart index a7a5963..c8b2f2f 100644 --- a/lib/base_contacts.dart +++ b/lib/base_contacts.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:equatable/equatable.dart'; import 'package:flutter/services.dart'; @@ -20,10 +19,15 @@ const _kphotoHighResolution = "photoHighResolution"; abstract class FormsContract { /// Opens a native edit form for the contact with [identifier]. This can be a /// simple string, but you can also provide a [ContactKeys] instance - Future openContactEditForm(identifier); + Future openContactEditForm(dynamic identifier); /// Opens a native insert form with [data] preloaded Future openContactInsertForm(Contact data); + + /// opens a native contact picker + Future openContactPicker(); + + Future insertOrUpdateContactViaPicker(Contact data); } abstract class ContactsContract implements FormsContract { @@ -79,7 +83,7 @@ const kphoneQuery = 'phoneQuery'; const kids = 'ids'; PageGenerator _defaultPageGenerator( - ContactService _service, + ContactService service, String? query, bool? phoneQuery, bool withThumbnails, @@ -87,10 +91,10 @@ PageGenerator _defaultPageGenerator( bool withUnifyInfo, ContactSortOrder? sortBy) => (int limit, int offset) async { - final List page = await (_service.channel.invokeMethod('getContacts', { + final List page = await (service.channel.invokeMethod('getContacts', { kquery: query, klimit: limit, - ksortBy: sortBy?._value ?? _service.defaultSort._value, + ksortBy: sortBy?._value ?? service.defaultSort._value, koffset: offset, kphoneQuery: phoneQuery, kwithUnifyInfo: withUnifyInfo, @@ -101,14 +105,12 @@ PageGenerator _defaultPageGenerator( return [ ...page .whereType() - .map((_) => Contact.fromMap(_, _service.mode)) + .map((item) => Contact.fromMap(item, service.mode)) ]; }; class ContactFormService extends ContactService implements FormsContract { - ContactFormService( - MethodChannel channel, EventChannel events, ContactMode mode) - : super(channel, events, mode); + ContactFormService(super.channel, super.events, super.mode); } class ContactService implements ContactsContract { @@ -148,6 +150,34 @@ class ContactService implements ContactsContract { } } + @override + Future openContactPicker() async { + final map = await channel.invokeMethod('openContactPicker'); + if (map["success"] == true) { + final contact = Contact.of(map["contact"] ?? {}, mode); + _log.info("picked contact: ${contact?.identifier}"); + return contact; + } else { + _log.info("Contact was not picked: ${map["code"] ?? 'unknown'}"); + return null; + } + } + + @override + Future insertOrUpdateContactViaPicker(Contact data) async { + final map = await channel.invokeMethod( + 'insertOrUpdateContactViaPicker', data.toMap()); + if (map["success"] == true) { + final contact = Contact.of(map["contact"] ?? {}, mode); + _log.info("inserted or updated contact: ${contact?.identifier}"); + return contact; + } else { + _log.info( + "Contact was not inserted or updated: ${map["code"] ?? 'unknown'}"); + return null; + } + } + /// Fetches all contacts, or when specified, the contacts with a name /// matching [query]. The stream terminates once the items are all iterated. @override @@ -236,10 +266,10 @@ class ContactService implements ContactsContract { {bool withThumbnails = true, bool withHiResPhoto = true, bool withUnifyInfo = true}) async { - final _keys = ContactKeys.of(mode, identifier); + final keys = ContactKeys.of(mode, identifier); final fromChannel = await channel.invokeMethod('getContact', { - _kidentifier: _keys.toMap(), + _kidentifier: keys.toMap(), _kwithThumbnails: withThumbnails, kwithUnifyInfo: withUnifyInfo, _kphotoHighResolution: withHiResPhoto, @@ -296,7 +326,7 @@ class ContactService implements ContactsContract { event = ContactsChangedEvent(); break; default: - print("Unable to determine type"); + _log.warning("Unable to determine type"); event = UnknownContactEvent(dyn); break; } diff --git a/lib/contact.dart b/lib/contact.dart index 4154b08..7d895e2 100644 --- a/lib/contact.dart +++ b/lib/contact.dart @@ -14,7 +14,7 @@ final flutterContactLog = Logger('flutterContact'); enum ContactMode { single, unified } -ContactMode? contactModeOf(dyn) { +ContactMode? contactModeOf(dynamic dyn) { if (dyn == null) return null; switch (dyn.toString()) { case 'single': @@ -77,9 +77,6 @@ class ContactKeys extends Equatable { unifiedContactId: identifier ?? unifiedContactId, otherKeys: otherKeys, ); - - default: - return (throw "This can't happen"); } } @@ -128,10 +125,10 @@ class ContactKeys extends Equatable { Map toMap() { // ignore: unnecessary_cast return { - 'identifier': this.identifier, - 'singleContactId': this.singleContactId, - 'unifiedContactId': this.unifiedContactId, - 'otherKeys': this.otherKeys, + 'identifier': identifier, + 'singleContactId': singleContactId, + 'unifiedContactId': unifiedContactId, + 'otherKeys': otherKeys, } as Map; } @@ -212,9 +209,9 @@ class Contact { if (avatar != null) return avatar; if (keys?.unifiedContactId == keys?.singleContactId) { - return UnifiedContacts.getContactImage(this.identifier); + return UnifiedContacts.getContactImage(identifier); } else { - return SingleContacts.getContactImage(this.identifier); + return SingleContacts.getContactImage(identifier); } } @@ -270,12 +267,12 @@ class Contact { bool get hasAvatar => avatar?.isNotEmpty == true; String initials() { - return ((this.givenName?.isNotEmpty == true ? this.givenName![0] : "") + - (this.familyName?.isNotEmpty == true ? this.familyName![0] : "")) + return ((givenName?.isNotEmpty == true ? givenName![0] : "") + + (familyName?.isNotEmpty == true ? familyName![0] : "")) .toUpperCase(); } - static Contact? of(final dyn, ContactMode mode) { + static Contact? of(final dynamic dyn, ContactMode mode) { if (dyn == null) { return null; } else if (dyn is Contact) { @@ -285,7 +282,7 @@ class Contact { } } - factory Contact.fromMap(final dyn, ContactMode? mode) { + factory Contact.fromMap(final dynamic dyn, ContactMode? mode) { mode ??= contactModeOf(dyn["mode"])!; return Contact( identifier: dyn[_kidentifier] as String?, @@ -323,35 +320,32 @@ class Contact { ); } - Map toMap() { - final _ = _contactToMap(this); - return _; - } + Map toMap() => _contactToMap(this); /// The [+] operator fills in this contact's empty fields with the fields from [other] Contact operator +(Contact other) => Contact( - keys: this.keys ?? other.keys, - identifier: this.identifier ?? other.identifier, - displayName: this.displayName ?? other.displayName, - givenName: this.givenName ?? other.givenName, - middleName: this.middleName ?? other.middleName, - prefix: this.prefix ?? other.prefix, - lastModified: this.lastModified ?? other.lastModified, - suffix: this.suffix ?? other.suffix, - familyName: this.familyName ?? other.familyName, - company: this.company ?? other.company, - jobTitle: this.jobTitle ?? other.jobTitle, - linkedContactIds: this.linkedContactIds + other.linkedContactIds, - note: this.note ?? other.note, - emails: {...this.emails, ...other.emails}.toList(), + keys: keys ?? other.keys, + identifier: identifier ?? other.identifier, + displayName: displayName ?? other.displayName, + givenName: givenName ?? other.givenName, + middleName: middleName ?? other.middleName, + prefix: prefix ?? other.prefix, + lastModified: lastModified ?? other.lastModified, + suffix: suffix ?? other.suffix, + familyName: familyName ?? other.familyName, + company: company ?? other.company, + jobTitle: jobTitle ?? other.jobTitle, + linkedContactIds: linkedContactIds + other.linkedContactIds, + note: note ?? other.note, + emails: {...emails, ...other.emails}.toList(), socialProfiles: - {...this.socialProfiles, ...other.socialProfiles}.toList(), - dates: {...this.dates, ...other.dates}.toList(), - urls: {...this.urls, ...other.urls}.toList(), - phones: {...this.phones, ...other.phones}.toList(), + {...socialProfiles, ...other.socialProfiles}.toList(), + dates: {...dates, ...other.dates}.toList(), + urls: {...urls, ...other.urls}.toList(), + phones: {...phones, ...other.phones}.toList(), postalAddresses: - {...this.postalAddresses, ...other.postalAddresses}.toList(), - avatar: this.avatar ?? other.avatar); + {...postalAddresses, ...other.postalAddresses}.toList(), + avatar: avatar ?? other.avatar); /// Removes duplicates from the collections. Duplicates are defined as having the exact same value Contact removeDuplicates() { @@ -362,26 +356,26 @@ class Contact { @override bool operator ==(Object other) { return other is Contact && - this.keys == other.keys && - this.identifier == other.identifier && - this.company == other.company && - this.displayName == other.displayName && - this.givenName == other.givenName && - this.familyName == other.familyName && - this.jobTitle == other.jobTitle && - this.middleName == other.middleName && - this.note == other.note && - this.prefix == other.prefix && - this.suffix == other.suffix && - this.lastModified == other.lastModified && - DeepCollectionEquality.unordered().equals(this.phones, other.phones) && + keys == other.keys && + identifier == other.identifier && + company == other.company && + displayName == other.displayName && + givenName == other.givenName && + familyName == other.familyName && + jobTitle == other.jobTitle && + middleName == other.middleName && + note == other.note && + prefix == other.prefix && + suffix == other.suffix && + lastModified == other.lastModified && + DeepCollectionEquality.unordered().equals(phones, other.phones) && DeepCollectionEquality.unordered() - .equals(this.socialProfiles, other.socialProfiles) && - DeepCollectionEquality.unordered().equals(this.urls, other.urls) && - DeepCollectionEquality.unordered().equals(this.dates, other.dates) && - DeepCollectionEquality.unordered().equals(this.emails, other.emails) && + .equals(socialProfiles, other.socialProfiles) && + DeepCollectionEquality.unordered().equals(urls, other.urls) && + DeepCollectionEquality.unordered().equals(dates, other.dates) && + DeepCollectionEquality.unordered().equals(emails, other.emails) && DeepCollectionEquality.unordered() - .equals(this.postalAddresses, other.postalAddresses); + .equals(postalAddresses, other.postalAddresses); } @override @@ -411,7 +405,7 @@ class ContactDate { this.date, }) : assert(value != null || date != null); - static ContactDate? fromMap(final dyn) { + static ContactDate? fromMap(final dynamic dyn) { if (dyn is Map) { if (dyn[_kdate] == null && dyn[_kvalue] == null) { flutterContactLog.warning( @@ -469,7 +463,7 @@ class PostalAddress extends Equatable { String? label, street, city, postcode, region, country; - static PostalAddress? fromMap(final dyn) { + static PostalAddress? fromMap(final dynamic dyn) { if (dyn is Map) { return PostalAddress( label: dyn[_klabel] as String?, @@ -486,12 +480,12 @@ class PostalAddress extends Equatable { @override List get props => [ - this.label, - this.street, - this.city, - this.country, - this.region, - this.postcode, + label, + street, + city, + country, + region, + postcode, ]; } @@ -503,7 +497,7 @@ class Item extends Equatable { String? label, value; - static Item? fromMap(final dyn) { + static Item? fromMap(final dynamic dyn) { if (dyn is Map) { return Item( value: dyn["value"] as String?, @@ -524,9 +518,9 @@ class Item extends Equatable { class PhoneNumber extends Item { final String _unformattedNumber; - PhoneNumber({String? label, String? number}) + PhoneNumber({super.label, String? number}) : _unformattedNumber = _sanitizer(number), - super(label: label, value: number); + super(value: number); @override String get equalsValue { @@ -562,7 +556,7 @@ extension ItemListsToMap on Iterable { } } -Iterable _iterableKey(map, String key) { +Iterable _iterableKey(dynamic map, String key) { if (map == null) return []; return map[key] as Iterable? ?? []; } @@ -651,7 +645,7 @@ bool _isNumeric(String? str) { return double.tryParse(str) != null; } -DateTime? parseDateTime(final dyn) { +DateTime? parseDateTime(final dynamic dyn) { if (dyn is DateTime) return dyn; if (dyn == null) return null; return DateTime.tryParse(dyn.toString()); @@ -691,7 +685,7 @@ const _kcountry = "country"; extension FlexiDateToMap on FlexiDate { Map? toDateMap() { - if (this is FlexiDateData && this.isValid) { + if (this is FlexiDateData && isValid) { return (this as FlexiDateData).toMap(); } else { return null; diff --git a/lib/date_components_compat.dart b/lib/date_components_compat.dart index ea91704..ee2d82b 100644 --- a/lib/date_components_compat.dart +++ b/lib/date_components_compat.dart @@ -3,47 +3,48 @@ import 'package:logging/logging.dart'; final _log = Logger("dateComponents"); -/// @deprecated: Use FlexiDate instead. -@deprecated +const _deprecationMessage = 'Use FlexiDate instead.'; + +@Deprecated(_deprecationMessage) class DateComponents extends FlexiDateData { - @deprecated - DateComponents({int? day, int? month = 1, int? year}) - : super(day: day, month: month, year: year); + @Deprecated(_deprecationMessage) + DateComponents({super.day, super.month, super.year}); - @deprecated + @Deprecated(_deprecationMessage) static DateComponents fromDateTime(DateTime dateTime) => DateComponents( day: dateTime.day, month: dateTime.month, year: dateTime.year); - @deprecated + @Deprecated(_deprecationMessage) factory DateComponents.now() => DateComponents.fromDateTime(DateTime.now()); - @deprecated + @Deprecated(_deprecationMessage) DateComponents.fromMap(Map toParse) : super( day: _tryParseInt(toParse[kday]), month: _tryParseInt(toParse[kmonth]), year: _tryParseInt(toParse[kyear])); - @deprecated + @Deprecated(_deprecationMessage) static DateComponents? _fromFlexiDate(FlexiDate? flexiDate) { if (flexiDate == null) return null; return DateComponents( day: flexiDate.day, month: flexiDate.month, year: flexiDate.year); } - @deprecated - static DateComponents? tryFrom(input) => + @Deprecated(_deprecationMessage) + static DateComponents? tryFrom(dynamic input) => _fromFlexiDate(FlexiDate.tryFrom(input)); - @deprecated - static DateComponents from(input) => _fromFlexiDate(FlexiDate.from(input))!; + @Deprecated(_deprecationMessage) + static DateComponents from(dynamic input) => + _fromFlexiDate(FlexiDate.from(input))!; - @deprecated + @Deprecated(_deprecationMessage) DateComponents copy() { return DateComponents(day: day, month: month, year: year); } - @deprecated + @Deprecated(_deprecationMessage) static DateComponents? tryParse(String input) { try { return _fromFlexiDate(FlexiDate.parse(input))!; @@ -53,16 +54,16 @@ class DateComponents extends FlexiDateData { } } - @deprecated + @Deprecated(_deprecationMessage) static DateComponents parse(String toParse) => _fromFlexiDate(FlexiDate.parse(toParse))!; - @deprecated - static DateComponents? fromJson(json) => + @Deprecated(_deprecationMessage) + static DateComponents? fromJson(dynamic json) => _fromFlexiDate(FlexiDate.fromJson(json)); } -int? _tryParseInt(dyn) { +int? _tryParseInt(dynamic dyn) { if (dyn == null) return null; return int.tryParse("$dyn"); } diff --git a/lib/flutter_contact.dart b/lib/flutter_contact.dart index ddbc68f..b4a7acb 100644 --- a/lib/flutter_contact.dart +++ b/lib/flutter_contact.dart @@ -1,4 +1,4 @@ -library flutter_contact; +library; export 'base_contacts.dart'; export 'contact.dart'; diff --git a/lib/group.dart b/lib/group.dart index 80d04e9..6418675 100644 --- a/lib/group.dart +++ b/lib/group.dart @@ -15,7 +15,7 @@ class Group extends Equatable { final String? identifier, name, description; final Set contacts; - Group.fromMap(dyn) + Group.fromMap(dynamic dyn) : this( identifier: dyn[_kidentifier] as String?, name: dyn[_kname] as String?, diff --git a/lib/paging_iterable.dart b/lib/paging_iterable.dart index 0255085..e97a5c1 100644 --- a/lib/paging_iterable.dart +++ b/lib/paging_iterable.dart @@ -18,8 +18,8 @@ class PagingList { required final FutureOr length}) : _length = length { if (length is Future) { - length.then((_resolved) { - _length = _resolved; + length.then((resolved) { + _length = resolved; }); } } @@ -73,10 +73,10 @@ class PagingList { final nextPage = pageGenerator!(bufferSize, (bufferSize * _page++)); if (nextPage is Future>) { _pageFuture = nextPage; - return nextPage.then((_nextList) { + return nextPage.then((nextList) { _pageFuture = null; - _pageList = _nextList; - _currIter = _nextList.iterator; + _pageList = nextList; + _currIter = nextList.iterator; return _currIter!.moveNext() == true; }); } else { @@ -88,8 +88,8 @@ class PagingList { } FutureOr moveNext() { - final _next = _currIter?.moveNext() == true; - if (_next == true) return true; + final next = _currIter?.moveNext() == true; + if (next == true) return true; return moveNextPage(); } @@ -130,13 +130,13 @@ class PagingStream extends Stream { } FutureOr moveNext() { - final _next = _currPage?.moveNext() == true; - if (_next == true) return true; + final next = _currPage?.moveNext() == true; + if (next == true) return true; final nextPage = pageGenerator(bufferSize, (bufferSize * _page++)); if (nextPage is Future>) { - return nextPage.then((_next) { - _currPage = _next.iterator; + return nextPage.then((next) { + _currPage = next.iterator; return _currPage!.moveNext(); }); } else { @@ -153,7 +153,7 @@ class PagingStream extends Stream { _controller ??= StreamController( onListen: () => _resume(), onCancel: () { -// if (!_controller.isClosed) _controller.close(); + if (!_controller!.isClosed) _controller!.close(); }, onPause: () => _isPaused = true, onResume: () => _resume(), diff --git a/pubspec.lock b/pubspec.lock index e8fe818..2e9b91c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,91 +5,120 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" source: hosted - version: "2.8.1" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "5bbf32bc9e518d41ec49718e2931cd4527292c9b0c6d2dffcf7fe6b9a8a8cf72" + url: "https://pub.dev" source: hosted version: "2.1.0" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.4.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.19.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: "8be10341257b613566fdc9fd073c46f7c032ed329b1c732bda17aca29f2366c8" + url: "https://pub.dev" source: hosted version: "3.0.0" dartx: dependency: transitive description: name: dartx - url: "https://pub.dartlang.org" + sha256: "4bc881203319f1d88dc6bf011a98e247b92424d1cb3f846b4a4d998412915776" + url: "https://pub.dev" source: hosted version: "0.7.1" dartxx: dependency: "direct main" description: name: dartxx - url: "https://pub.dartlang.org" + sha256: fe9a209395b16be4ad98f9170d4e98b810b91c81b3f83ce6b13f94f7c3c73191 + url: "https://pub.dev" source: hosted version: "0.5.3+2" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c6094fd1efad3046334a9c40bee022147e55c25401ccd89b94e373e3edadd375 + url: "https://pub.dev" source: hosted version: "2.0.3" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flexidate: dependency: "direct main" description: - name: flexidate - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "7931be0c4ee95d5349dccc2f135874e67c9e3530" + resolved-ref: "7931be0c4ee95d5349dccc2f135874e67c9e3530" + url: "https://github.com/agendaboa/flexidate-0.8.0-3" + source: git version: "0.8.0+3" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -99,161 +128,223 @@ packages: dependency: transitive description: name: inflection3 - url: "https://pub.dartlang.org" + sha256: cb54689bcfe3c0168a6979ee3191df584dcef4f870c5ef41c798bcb35ff5aca3 + url: "https://pub.dev" source: hosted version: "0.5.3+2" intl: - dependency: "direct main" + dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + url: "https://pub.dev" source: hosted version: "0.6.3" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" logging: dependency: "direct main" description: name: logging - url: "https://pub.dartlang.org" + sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + url: "https://pub.dev" source: hosted version: "1.0.2" logging_config: dependency: "direct main" description: name: logging_config - url: "https://pub.dartlang.org" + sha256: dcee5b076587954fbe0fd9fbf1e0faf889fc6c3ca4e468f9d3e4b46413fc7f2a + url: "https://pub.dev" source: hosted version: "0.6.7+7" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" source: hosted - version: "0.12.10" + version: "0.13.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.18.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" source: hosted - version: "1.8.0" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "1.9.1" quiver: dependency: "direct main" description: name: quiver - url: "https://pub.dartlang.org" + sha256: "616b691d1c8f5c53b7b39ce3542f6a25308d7900bf689d0210e72a644a10387e" + url: "https://pub.dev" source: hosted version: "3.0.1+1" recase: dependency: transitive description: name: recase - url: "https://pub.dartlang.org" + sha256: "7aec9b9f498cba65ed969eda51ea3d86a77bbd633d876b57d9db7d9f94fc6ca5" + url: "https://pub.dev" source: hosted version: "4.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: d5f89a9e52b36240a80282b3dc0667dd36e53459717bb17b8fb102d30496606a + url: "https://pub.dev" source: hosted version: "1.8.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: dd11571b8a03f7cadcf91ec26a77e02bfbd6bbba2a512924d3116646b4198fc4 + url: "https://pub.dev" source: hosted version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a88162591b02c1f3a3db3af8ce1ea2b374bd75a7bb8d5e353bcfbdc79d719830 + url: "https://pub.dev" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.7.11" time: dependency: transitive description: name: time - url: "https://pub.dartlang.org" + sha256: "21535c177cafcad30b50c941726d7bf6e506ffe4b4901e179ecf8a046c5ae0ce" + url: "https://pub.dev" source: hosted version: "2.0.0" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.4" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" uuid: dependency: "direct main" description: name: uuid - url: "https://pub.dartlang.org" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "4.6.0" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.10.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index c3ee1d4..f7f3231 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,30 +3,35 @@ version: 0.9.1+7 homepage: https://github.com/SunnyApp/flutter_contact description: A Flutter plugin to retrieve, create and save contacts and contact-related events on Android and iOS devices. maintainer: Eric Martineau -environment: - sdk: '>=2.14.0 <3.0.0' - flutter: '>=1.10.0' -dependencies: +publish_to: 'none' +environment: + sdk: ^3.12.0 + flutter: '>=3.44.0' +dependencies: collection: ^1.15.0 dartxx: ^0.5.3+2 equatable: ^2.0.3 - flexidate: ^0.8.0+3 - intl: ^0.17.0 + flexidate: + git: + url: https://github.com/agendaboa/flexidate-0.8.0-3 + ref: 7931be0c4ee95d5349dccc2f135874e67c9e3530 + flutter: + sdk: flutter logging: ^1.0.2 logging_config: ^0.6.7+7 quiver: ^3.0.1+1 - uuid: ^3.0.5 - flutter: - sdk: flutter -dev_dependencies: - pedantic: ^1.11.1 - flutter_test: + uuid: ^4.2.1 + +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: sdk: flutter -flutter: - plugin: - platforms: - android: + +flutter: + plugin: + platforms: + android: package: co.sunnyapp.flutter_contact pluginClass: FlutterContactPlugin - ios: - pluginClass: FlutterContactPlugin + ios: + pluginClass: SwiftFlutterContactPlugin