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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}
3 changes: 2 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down Expand Up @@ -47,6 +47,7 @@ android {
}

dependencies {
//noinspection GradleDependency
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
}
63 changes: 63 additions & 0 deletions android/src/main/kotlin/co/quis/flutter_contacts/Contact.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any>),
@Suppress("UNCHECKED_CAST")
(m["phones"] as List<Map<String, Any>>).map { Phone.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["emails"] as List<Map<String, Any>>).map { Email.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["addresses"] as List<Map<String, Any>>).map { Address.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["organizations"] as List<Map<String, Any>>).map { Organization.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["websites"] as List<Map<String, Any>>).map { Website.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["socialMedias"] as List<Map<String, Any>>).map { SocialMedia.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["events"] as List<Map<String, Any?>>).map { Event.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["notes"] as List<Map<String, Any>>).map { Note.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["accounts"] as List<Map<String, Any>>).map { Account.fromMap(it) },
@Suppress("UNCHECKED_CAST")
(m["groups"] as List<Map<String, Any>>).map { Group.fromMap(it) }
)
}
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,10 @@ class FlutterContacts {
// Maps contact ID to its index in `contacts`.
var index = mutableMapOf<String, Int>()

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str
if (editResult != null) {
// Result is of the form:
// content://com.android.contacts/contacts/lookup/<hash>/<id>
val id = intent?.getData()?.getLastPathSegment()
val id = intent?.data?.lastPathSegment
editResult!!.success(id)
editResult = null
}
FlutterContacts.REQUEST_CODE_PICK ->
if (pickResult != null) {
// Result is of the form:
// content://com.android.contacts/contacts/lookup/<hash>/<id>
val id = intent?.getData()?.getLastPathSegment()
val id = intent?.data?.lastPathSegment
pickResult!!.success(id)
pickResult = null
}
Expand Down Expand Up @@ -250,7 +250,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str
coroutineScope.launch(Dispatchers.IO) {
val args = call.arguments as List<Any>
val group = args[0] as Map<String, Any>
val insertedGroup: Map<String, Any?>? =
val insertedGroup: Map<String, Any?> =
FlutterContacts.insertGroup(resolver!!, group)
coroutineScope.launch(Dispatchers.Main) {
result.success(insertedGroup)
Expand All @@ -261,7 +261,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str
coroutineScope.launch(Dispatchers.IO) {
val args = call.arguments as List<Any>
val group = args[0] as Map<String, Any>
val updatedGroup: Map<String, Any?>? =
val updatedGroup: Map<String, Any?> =
FlutterContacts.updateGroup(resolver!!, group)
coroutineScope.launch(Dispatchers.Main) {
result.success(updatedGroup)
Expand Down Expand Up @@ -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<Any>
val args = call.arguments as List<Any>
val contact = args.getOrNull(0)?.let { it as? Map<String, Any?> } ?: run {
null
}
Expand All @@ -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
Expand All @@ -336,7 +333,7 @@ class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.Str
// Result can be of two forms:
// content://com.android.contacts/<lookup_key>/<raw_id>
// content://com.android.contacts/raw_contacts/<raw_id>
val segments = intent.getData()?.getPathSegments()
val segments = intent.data?.pathSegments
if (segments == null || segments.size < 2) {
return null
}
Expand Down
52 changes: 41 additions & 11 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ class _FlutterContactsExampleState extends State<FlutterContactsExample> {
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);
}
}
Expand All @@ -39,10 +44,14 @@ class _FlutterContactsExampleState extends State<FlutterContactsExample> {
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!)));
}));
Expand All @@ -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)'}'),
],
),
));
}
2 changes: 1 addition & 1 deletion example_full/ios/Podfile
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion example_full/lib/pages/contact_list_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class _ContactListPageState extends State<ContactListPage>
Future<void> _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();
Expand Down