diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..0ca4d0be --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 0e7e899c..0bbbf2fe 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.3.1' + classpath 'com.android.tools.build:gradle:8.3.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -47,6 +47,7 @@ android { } dependencies { + //noinspection GradleDependency implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } } diff --git a/android/src/main/kotlin/co/quis/flutter_contacts/Contact.kt b/android/src/main/kotlin/co/quis/flutter_contacts/Contact.kt index b3df5fc3..efb49063 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/Contact.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/Contact.kt @@ -38,16 +38,27 @@ data class Contact( m["thumbnail"] as? ByteArray, m["photo"] as? ByteArray, m["isStarred"] as Boolean, + @Suppress("UNCHECKED_CAST") Name.fromMap(m["name"] as Map), + @Suppress("UNCHECKED_CAST") (m["phones"] as List>).map { Phone.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["emails"] as List>).map { Email.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["addresses"] as List>).map { Address.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["organizations"] as List>).map { Organization.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["websites"] as List>).map { Website.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["socialMedias"] as List>).map { SocialMedia.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["events"] as List>).map { Event.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["notes"] as List>).map { Note.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["accounts"] as List>).map { Account.fromMap(it) }, + @Suppress("UNCHECKED_CAST") (m["groups"] as List>).map { Group.fromMap(it) } ) } @@ -71,4 +82,56 @@ data class Contact( "accounts" to accounts.map { it.toMap() }, "groups" to groups.map { it.toMap() } ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Contact + + if (id != other.id) return false + if (displayName != other.displayName) return false + if (thumbnail != null) { + if (other.thumbnail == null) return false + if (!thumbnail.contentEquals(other.thumbnail)) return false + } else if (other.thumbnail != null) return false + if (photo != null) { + if (other.photo == null) return false + if (!photo.contentEquals(other.photo)) return false + } else if (other.photo != null) return false + if (isStarred != other.isStarred) return false + if (name != other.name) return false + if (phones != other.phones) return false + if (emails != other.emails) return false + if (addresses != other.addresses) return false + if (organizations != other.organizations) return false + if (websites != other.websites) return false + if (socialMedias != other.socialMedias) return false + if (events != other.events) return false + if (notes != other.notes) return false + if (accounts != other.accounts) return false + if (groups != other.groups) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + displayName.hashCode() + result = 31 * result + (thumbnail?.contentHashCode() ?: 0) + result = 31 * result + (photo?.contentHashCode() ?: 0) + result = 31 * result + isStarred.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + phones.hashCode() + result = 31 * result + emails.hashCode() + result = 31 * result + addresses.hashCode() + result = 31 * result + organizations.hashCode() + result = 31 * result + websites.hashCode() + result = 31 * result + socialMedias.hashCode() + result = 31 * result + events.hashCode() + result = 31 * result + notes.hashCode() + result = 31 * result + accounts.hashCode() + result = 31 * result + groups.hashCode() + return result + } } diff --git a/android/src/main/kotlin/co/quis/flutter_contacts/ContactChangeObserver.kt b/android/src/main/kotlin/co/quis/flutter_contacts/ContactChangeObserver.kt index 642028cb..e6d41432 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/ContactChangeObserver.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/ContactChangeObserver.kt @@ -4,12 +4,10 @@ import android.database.ContentObserver import android.os.Handler import io.flutter.plugin.common.EventChannel -class ContactChangeObserver : ContentObserver { - val _sink: EventChannel.EventSink - constructor(handler: Handler, sink: EventChannel.EventSink) : super(handler) { - this._sink = sink - } +class ContactChangeObserver(handler: Handler, sink: EventChannel.EventSink) : + ContentObserver(handler) { + val _sink: EventChannel.EventSink = sink override fun onChange(selfChange: Boolean) { _sink?.success(selfChange) diff --git a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt index 9a95833f..2c117d49 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt @@ -213,7 +213,10 @@ class FlutterContacts { // Maps contact ID to its index in `contacts`. var index = mutableMapOf() - fun getString(col: String): String = cursor.getString(cursor.getColumnIndex(col)) ?: "" + fun getString(col: String): String { + val index = cursor.getColumnIndex(col) + return if (index >= 0) cursor.getString(index) ?: "" else "" + } fun getInt(col: String): Int = cursor.getInt(cursor.getColumnIndex(col)) ?: 0 fun getBool(col: String): Boolean = getInt(col) == 1 diff --git a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt index 82915f0b..b539ac1e 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContactsPlugin.kt @@ -91,7 +91,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str if (editResult != null) { // Result is of the form: // content://com.android.contacts/contacts/lookup// - val id = intent?.getData()?.getLastPathSegment() + val id = intent?.data?.lastPathSegment editResult!!.success(id) editResult = null } @@ -99,7 +99,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str if (pickResult != null) { // Result is of the form: // content://com.android.contacts/contacts/lookup// - val id = intent?.getData()?.getLastPathSegment() + val id = intent?.data?.lastPathSegment pickResult!!.success(id) pickResult = null } @@ -250,7 +250,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str coroutineScope.launch(Dispatchers.IO) { val args = call.arguments as List val group = args[0] as Map - val insertedGroup: Map? = + val insertedGroup: Map = FlutterContacts.insertGroup(resolver!!, group) coroutineScope.launch(Dispatchers.Main) { result.success(insertedGroup) @@ -261,7 +261,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str coroutineScope.launch(Dispatchers.IO) { val args = call.arguments as List val group = args[0] as Map - val updatedGroup: Map? = + val updatedGroup: Map = FlutterContacts.updateGroup(resolver!!, group) coroutineScope.launch(Dispatchers.Main) { result.success(updatedGroup) @@ -302,7 +302,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str // Opens external contact app to insert a new contact. "openExternalInsert" -> coroutineScope.launch(Dispatchers.IO) { - var args = call.arguments as List + val args = call.arguments as List val contact = args.getOrNull(0)?.let { it as? Map } ?: run { null } @@ -318,10 +318,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str return null } - val uri = intent.getData()?.getPath() - if (uri == null) { - return null - } + val uri = intent.data?.path ?: return null val hasContactsReadPermission = ContextCompat.checkSelfPermission( context!!, Manifest.permission.READ_CONTACTS @@ -336,7 +333,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str // Result can be of two forms: // content://com.android.contacts// // content://com.android.contacts/raw_contacts/ - val segments = intent.getData()?.getPathSegments() + val segments = intent.data?.pathSegments if (segments == null || segments.size < 2) { return null } diff --git a/example/lib/main.dart b/example/lib/main.dart index bfe19eec..a96bd063 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,7 +22,12 @@ class _FlutterContactsExampleState extends State { if (!await FlutterContacts.requestPermission(readonly: true)) { setState(() => _permissionDenied = true); } else { - final contacts = await FlutterContacts.getContacts(); + final contacts = await FlutterContacts.getContacts( + withProperties: true, + withPhoto: true, + withThumbnail: true, + sorted: true + ); setState(() => _contacts = contacts); } } @@ -39,10 +44,14 @@ class _FlutterContactsExampleState extends State { return ListView.builder( itemCount: _contacts!.length, itemBuilder: (context, i) => ListTile( + leading: _contacts![i].thumbnail != null + ? CircleAvatar(backgroundImage: MemoryImage(_contacts![i].thumbnail!)) + : CircleAvatar(child: Text(_contacts![i].displayName[0])), title: Text(_contacts![i].displayName), + subtitle: Text( + _contacts![i].phones.isNotEmpty ? _contacts![i].phones.first.number : ''), onTap: () async { - final fullContact = - await FlutterContacts.getContact(_contacts![i].id); + final fullContact = await FlutterContacts.getContact(_contacts![i].id); await Navigator.of(context).push( MaterialPageRoute(builder: (_) => ContactPage(fullContact!))); })); @@ -56,12 +65,33 @@ class ContactPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: Text(contact.displayName)), - body: Column(children: [ - Text('First name: ${contact.name.first}'), - Text('Last name: ${contact.name.last}'), - Text( - 'Phone number: ${contact.phones.isNotEmpty ? contact.phones.first.number : '(none)'}'), - Text( - 'Email address: ${contact.emails.isNotEmpty ? contact.emails.first.address : '(none)'}'), - ])); + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (contact.photo != null) + Center( + child: Container( + width: 100, + height: 100, + margin: EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: MemoryImage(contact.photo!), + fit: BoxFit.cover, + ), + ), + ), + ), + Text('First name: ${contact.name.first}'), + Text('Last name: ${contact.name.last}'), + Text( + 'Phone number: ${contact.phones.isNotEmpty ? contact.phones.first.number : '(none)'}'), + Text( + 'Email address: ${contact.emails.isNotEmpty ? contact.emails.first.address : '(none)'}'), + ], + ), + )); } diff --git a/example_full/ios/Podfile b/example_full/ios/Podfile index 279576f3..10f3c9b4 100644 --- a/example_full/ios/Podfile +++ b/example_full/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example_full/lib/pages/contact_list_page.dart b/example_full/lib/pages/contact_list_page.dart index b11bba47..c3a6647e 100644 --- a/example_full/lib/pages/contact_list_page.dart +++ b/example_full/lib/pages/contact_list_page.dart @@ -114,7 +114,7 @@ class _ContactListPageState extends State Future _handleOverflowSelected(String value) async { switch (value) { case 'Groups': - Navigator.of(context).pushNamed('/groups'); + await Navigator.of(context).pushNamed('/groups'); break; case 'Insert external': final contact = await FlutterContacts.openExternalInsert();