From d4c687a8becddf2d86319558c0771463c6e8944a Mon Sep 17 00:00:00 2001 From: SoftWyer Date: Wed, 25 Oct 2023 13:35:38 +0100 Subject: [PATCH 1/6] Full example supports NNBD --- android/build.gradle | 4 +- example_full/android/app/build.gradle | 6 +- example_full/android/build.gradle | 6 +- example_full/lib/pages/contact_list_page.dart | 6 +- example_full/lib/pages/contact_page.dart | 58 +++++++++------- example_full/lib/pages/edit_contact_page.dart | 26 +++---- .../pages/form_components/address_form.dart | 68 ++++++++++--------- .../lib/pages/form_components/email_form.dart | 28 ++++---- .../lib/pages/form_components/event_form.dart | 30 ++++---- .../lib/pages/form_components/name_form.dart | 22 +++--- .../lib/pages/form_components/note_form.dart | 16 ++--- .../form_components/organization_form.dart | 20 +++--- .../lib/pages/form_components/phone_form.dart | 24 ++++--- .../form_components/social_media_form.dart | 29 ++++---- .../pages/form_components/website_form.dart | 28 ++++---- example_full/lib/pages/groups_page.dart | 12 ++-- example_full/lib/util/avatar.dart | 2 +- example_full/pubspec.yaml | 6 +- 18 files changed, 203 insertions(+), 188 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 952d21c6..94326b78 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,6 @@ buildscript { ext.kotlin_coroutines_version = '1.6.4' repositories { google() - jcenter() mavenCentral() } @@ -19,7 +18,6 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() mavenCentral() } } @@ -28,7 +26,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 34 if (!project.hasProperty("namespace")) { namespace 'co.quis.flutter_contacts' diff --git a/example_full/android/app/build.gradle b/example_full/android/app/build.gradle index 1e305d9c..f724da0c 100644 --- a/example_full/android/app/build.gradle +++ b/example_full/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,8 +39,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "co.quis.flutter_contacts_example" - minSdkVersion 16 - targetSdkVersion 31 + minSdkVersion flutter.minSdkVersion + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example_full/android/build.gradle b/example_full/android/build.gradle index 9610d8f2..8f73307b 100644 --- a/example_full/android/build.gradle +++ b/example_full/android/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '1.7.21' repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example_full/lib/pages/contact_list_page.dart b/example_full/lib/pages/contact_list_page.dart index 9f46c425..15cea807 100644 --- a/example_full/lib/pages/contact_list_page.dart +++ b/example_full/lib/pages/contact_list_page.dart @@ -12,7 +12,7 @@ class ContactListPage extends StatefulWidget { class _ContactListPageState extends State with AfterLayoutMixin { - List _contacts; + List _contacts = []; bool _permissionDenied = false; @override @@ -33,7 +33,7 @@ class _ContactListPageState extends State Future _fetchContacts() async { if (!await FlutterContacts.requestPermission()) { setState(() { - _contacts = null; + _contacts.clear(); _permissionDenied = true; }); return; @@ -50,7 +50,7 @@ class _ContactListPageState extends State Future _refetchContacts() async { // First load all contacts without photo - await _loadContacts(false); + // await _loadContacts(false); // Next with photo await _loadContacts(true); diff --git a/example_full/lib/pages/contact_page.dart b/example_full/lib/pages/contact_page.dart index 001d6e0d..077ef8bc 100644 --- a/example_full/lib/pages/contact_page.dart +++ b/example_full/lib/pages/contact_page.dart @@ -11,11 +11,11 @@ class ContactPage extends StatefulWidget { class _ContactPageState extends State with AfterLayoutMixin { - Contact _contact; + Contact? _contact; @override void afterFirstLayout(BuildContext context) { - final contact = ModalRoute.of(context).settings.arguments as Contact; + final contact = ModalRoute.of(context)?.settings.arguments as Contact?; setState(() { _contact = contact; }); @@ -30,17 +30,19 @@ class _ContactPageState extends State await _fetchContactWith(highRes: true); } - Future _fetchContactWith({@required bool highRes}) async { - final contact = await FlutterContacts.getContact( - _contact.id, - withThumbnail: !highRes, - withPhoto: highRes, - withGroups: true, - withAccounts: true, - ); - setState(() { - _contact = contact; - }); + Future _fetchContactWith({required bool highRes}) async { + if (_contact != null) { + final contact = await FlutterContacts.getContact( + _contact!.id, + withThumbnail: !highRes, + withPhoto: highRes, + withGroups: true, + withAccounts: true, + ); + setState(() { + _contact = contact; + }); + } } @override @@ -55,8 +57,8 @@ class _ContactPageState extends State await showDialog( context: context, builder: (_) => AlertDialog( - content: Text(prettyJson( - _contact.toJson(withPhoto: false, withThumbnail: false))), + content: Text(prettyJson(_contact?.toJson( + withPhoto: false, withThumbnail: false))), ), ); }, @@ -68,7 +70,9 @@ class _ContactPageState extends State context: context, builder: (_) => AlertDialog( content: Text( - _contact.toVCard(withPhoto: false, includeDate: true)), + _contact?.toVCard(withPhoto: false, includeDate: true) ?? + '', + ), ), ); }, @@ -101,8 +105,8 @@ class _ContactPageState extends State ); } - Widget _body(Contact contact) { - if (_contact?.name == null) { + Widget _body(Contact? contact) { + if (contact == null) { return Center(child: CircularProgressIndicator()); } return SingleChildScrollView( @@ -266,7 +270,7 @@ class _ContactPageState extends State Card _makeCard( String title, List fields, List Function(dynamic) mapper) { var elements = []; - fields?.forEach((field) => elements.addAll(mapper(field))); + fields.forEach((field) => elements.addAll(mapper(field))); return Card( child: Padding( padding: EdgeInsets.all(8), @@ -282,13 +286,15 @@ class _ContactPageState extends State } Future _handleOverflowSelected(String choice) async { - if (choice == 'Delete contact') { - await _contact.delete(); - Navigator.of(context).pop(); - } else if (choice == 'External view') { - await FlutterContacts.openExternalView(_contact.id); - } else if (choice == 'External edit') { - await FlutterContacts.openExternalEdit(_contact.id); + if (_contact != null) { + if (choice == 'Delete contact') { + await _contact!.delete(); + Navigator.of(context).pop(); + } else if (choice == 'External view') { + await FlutterContacts.openExternalView(_contact!.id); + } else if (choice == 'External edit') { + await FlutterContacts.openExternalEdit(_contact!.id); + } } } } diff --git a/example_full/lib/pages/edit_contact_page.dart b/example_full/lib/pages/edit_contact_page.dart index 01b72932..bb6db46e 100644 --- a/example_full/lib/pages/edit_contact_page.dart +++ b/example_full/lib/pages/edit_contact_page.dart @@ -23,14 +23,14 @@ class _EditContactPageState extends State with AfterLayoutMixin { var _contact = Contact(); bool _isEdit = false; - void Function() _onUpdate; + void Function()? _onUpdate; final _imagePicker = ImagePicker(); @override void afterFirstLayout(BuildContext context) { final args = - ModalRoute.of(context).settings.arguments as Map; + ModalRoute.of(context)?.settings.arguments as Map?; if (args != null) { setState(() { _contact = args['contact']; @@ -78,7 +78,7 @@ class _EditContactPageState extends State } else { await _contact.insert(); } - if (_onUpdate != null) _onUpdate(); + if (_onUpdate != null) _onUpdate!(); Navigator.of(context).pop(); }, ), @@ -117,7 +117,7 @@ class _EditContactPageState extends State ]; Future _pickPhoto() async { - final photo = await _imagePicker.getImage(source: ImageSource.camera); + final photo = await _imagePicker.pickImage(source: ImageSource.camera); if (photo != null) { final bytes = await photo.readAsBytes(); setState(() { @@ -150,9 +150,9 @@ class _EditContactPageState extends State Card _fieldCard( String fieldName, List fields, - /* void | Future */ Function() addField, + Function()? addField, Widget Function(int, dynamic) formWidget, - void Function() clearAllFields, { + void Function()? clearAllFields, { bool createAsync = false, }) { var forms = [ @@ -162,12 +162,12 @@ class _EditContactPageState extends State void Function() onPressed; if (createAsync) { onPressed = () async { - await addField(); + await addField?.call(); setState(() {}); }; } else { onPressed = () => setState(() { - addField(); + addField?.call(); }); } var buttons = []; @@ -301,7 +301,7 @@ class _EditContactPageState extends State () => _contact.socialMedias = [], ); - Future _selectDate(BuildContext context) async => showDatePicker( + Future _selectDate(BuildContext context) async => showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(1900), @@ -368,19 +368,19 @@ class _EditContactPageState extends State SizedBox(width: 24.0), Checkbox( value: _contact.isStarred, - onChanged: (bool isStarred) => - setState(() => _contact.isStarred = isStarred), + onChanged: (isStarred) => + setState(() => _contact.isStarred = isStarred ?? false), ), ], ), ); - Future _promptGroup({@required List exclude}) async { + Future _promptGroup({required List exclude}) async { final excludeIds = exclude.map((x) => x.id).toSet(); final groups = (await FlutterContacts.getGroups()) .where((g) => !excludeIds.contains(g.id)) .toList(); - Group selectedGroup; + Group? selectedGroup; await showDialog( context: context, builder: (BuildContext ctx) => AlertDialog( diff --git a/example_full/lib/pages/form_components/address_form.dart b/example_full/lib/pages/form_components/address_form.dart index e89bc9d6..939c9e6c 100644 --- a/example_full/lib/pages/form_components/address_form.dart +++ b/example_full/lib/pages/form_components/address_form.dart @@ -8,9 +8,9 @@ class AddressForm extends StatefulWidget { AddressForm( this.address, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -21,19 +21,19 @@ class _AddressFormState extends State { final _formKey = GlobalKey(); static final _validLabels = AddressLabel.values; - TextEditingController _addressController; - AddressLabel _label; - TextEditingController _customLabelController; - TextEditingController _streetController; - TextEditingController _poboxController; - TextEditingController _neighborhoodController; - TextEditingController _cityController; - TextEditingController _stateController; - TextEditingController _postalCodeController; - TextEditingController _countryController; - TextEditingController _isoCountryController; - TextEditingController _subAdminAreaController; - TextEditingController _subLocalityController; + late TextEditingController _addressController; + AddressLabel? _label; + late TextEditingController _customLabelController; + late TextEditingController _streetController; + late TextEditingController _poboxController; + late TextEditingController _neighborhoodController; + late TextEditingController _cityController; + late TextEditingController _stateController; + late TextEditingController _postalCodeController; + late TextEditingController _countryController; + late TextEditingController _isoCountryController; + late TextEditingController _subAdminAreaController; + late TextEditingController _subLocalityController; @override void initState() { @@ -60,23 +60,25 @@ class _AddressFormState extends State { } void _onChanged() { - final address = Address( - _addressController.text, - label: _label, - customLabel: - _label == AddressLabel.custom ? _customLabelController.text : '', - street: _streetController.text, - pobox: _poboxController.text, - neighborhood: _neighborhoodController.text, - city: _cityController.text, - state: _stateController.text, - postalCode: _postalCodeController.text, - country: _countryController.text, - isoCountry: _isoCountryController.text, - subAdminArea: _subAdminAreaController.text, - subLocality: _subLocalityController.text, - ); - widget.onUpdate(address); + if (_label != null) { + final address = Address( + _addressController.text, + label: _label!, + customLabel: + _label == AddressLabel.custom ? _customLabelController.text : '', + street: _streetController.text, + pobox: _poboxController.text, + neighborhood: _neighborhoodController.text, + city: _cityController.text, + state: _stateController.text, + postalCode: _postalCodeController.text, + country: _countryController.text, + isoCountry: _isoCountryController.text, + subAdminArea: _subAdminAreaController.text, + subLocality: _subLocalityController.text, + ); + widget.onUpdate(address); + } } @override diff --git a/example_full/lib/pages/form_components/email_form.dart b/example_full/lib/pages/form_components/email_form.dart index 5f68700d..55670bc9 100644 --- a/example_full/lib/pages/form_components/email_form.dart +++ b/example_full/lib/pages/form_components/email_form.dart @@ -8,9 +8,9 @@ class EmailForm extends StatefulWidget { EmailForm( this.email, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -21,9 +21,9 @@ class _EmailFormState extends State { final _formKey = GlobalKey(); static final _validLabels = EmailLabel.values; - TextEditingController _addressController; - EmailLabel _label; - TextEditingController _customLabelController; + late TextEditingController _addressController; + EmailLabel? _label; + late TextEditingController _customLabelController; @override void initState() { @@ -35,13 +35,15 @@ class _EmailFormState extends State { } void _onChanged() { - final email = Email( - _addressController.text, - label: _label, - customLabel: - _label == EmailLabel.custom ? _customLabelController.text : '', - ); - widget.onUpdate(email); + if (_label != null) { + final email = Email( + _addressController.text, + label: _label!, + customLabel: + _label == EmailLabel.custom ? _customLabelController.text : '', + ); + widget.onUpdate(email); + } } @override diff --git a/example_full/lib/pages/form_components/event_form.dart b/example_full/lib/pages/form_components/event_form.dart index 109ef830..8deb970c 100644 --- a/example_full/lib/pages/form_components/event_form.dart +++ b/example_full/lib/pages/form_components/event_form.dart @@ -8,9 +8,9 @@ class EventForm extends StatefulWidget { EventForm( this.event, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -21,16 +21,16 @@ class _EventFormState extends State { final _formKey = GlobalKey(); static final _validLabels = EventLabel.values; - TextEditingController _dateController; - EventLabel _label; - TextEditingController _customLabelController; - int _year; - int _month; - int _day; - bool _noYear; + late TextEditingController _dateController; + EventLabel? _label; + late TextEditingController _customLabelController; + int? _year; + int? _month; + int? _day; + bool? _noYear; String _formatDate() => - '${_noYear ? '--' : _year.toString().padLeft(4, '0')}/' + '${_noYear ?? false ? '--' : _year.toString().padLeft(4, '0')}/' '${_month.toString().padLeft(2, '0')}/' '${_day.toString().padLeft(2, '0')}'; @@ -49,10 +49,10 @@ class _EventFormState extends State { void _onChanged() { final event = Event( - year: _noYear ? null : _year, - month: _month, - day: _day, - label: _label, + year: _noYear ?? false ? null : _year, + month: _month ?? 0, + day: _day ?? 0, + label: _label ?? EventLabel.other, customLabel: _label == EventLabel.custom ? _customLabelController.text : '', ); diff --git a/example_full/lib/pages/form_components/name_form.dart b/example_full/lib/pages/form_components/name_form.dart index e4b5be8a..ccba5854 100644 --- a/example_full/lib/pages/form_components/name_form.dart +++ b/example_full/lib/pages/form_components/name_form.dart @@ -7,8 +7,8 @@ class NameForm extends StatefulWidget { NameForm( this.name, { - @required this.onUpdate, - Key key, + required this.onUpdate, + Key? key, }) : super(key: key); @override @@ -18,15 +18,15 @@ class NameForm extends StatefulWidget { class _NameFormState extends State { final _formKey = GlobalKey(); - TextEditingController _firstController; - TextEditingController _lastController; - TextEditingController _middleController; - TextEditingController _prefixController; - TextEditingController _suffixController; - TextEditingController _nicknameController; - TextEditingController _firstPhoneticController; - TextEditingController _lastPhoneticController; - TextEditingController _middlePhoneticController; + late TextEditingController _firstController; + late TextEditingController _lastController; + late TextEditingController _middleController; + late TextEditingController _prefixController; + late TextEditingController _suffixController; + late TextEditingController _nicknameController; + late TextEditingController _firstPhoneticController; + late TextEditingController _lastPhoneticController; + late TextEditingController _middlePhoneticController; @override void initState() { diff --git a/example_full/lib/pages/form_components/note_form.dart b/example_full/lib/pages/form_components/note_form.dart index cbbaa29f..ae396081 100644 --- a/example_full/lib/pages/form_components/note_form.dart +++ b/example_full/lib/pages/form_components/note_form.dart @@ -3,14 +3,14 @@ import 'package:flutter_contacts/properties/note.dart'; class NoteForm extends StatefulWidget { final Note note; - final void Function(Note) onUpdate; - final void Function() onDelete; + final void Function(Note)? onUpdate; + final void Function()? onDelete; NoteForm( this.note, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -20,7 +20,7 @@ class NoteForm extends StatefulWidget { class _NoteFormState extends State { final _formKey = GlobalKey(); - TextEditingController _noteController; + late TextEditingController _noteController; @override void initState() { @@ -32,7 +32,7 @@ class _NoteFormState extends State { final note = Note( _noteController.text, ); - widget.onUpdate(note); + widget.onUpdate?.call(note); } @override @@ -41,7 +41,7 @@ class _NoteFormState extends State { trailing: PopupMenuButton( itemBuilder: (context) => [PopupMenuItem(value: 'Delete', child: Text('Delete'))], - onSelected: (_) => widget.onDelete(), + onSelected: (_) => widget.onDelete?.call(), ), subtitle: Padding( padding: const EdgeInsets.all(8.0), diff --git a/example_full/lib/pages/form_components/organization_form.dart b/example_full/lib/pages/form_components/organization_form.dart index 1ec3e16b..7b0e31c4 100644 --- a/example_full/lib/pages/form_components/organization_form.dart +++ b/example_full/lib/pages/form_components/organization_form.dart @@ -8,9 +8,9 @@ class OrganizationForm extends StatefulWidget { OrganizationForm( this.organization, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -20,13 +20,13 @@ class OrganizationForm extends StatefulWidget { class _OrganizationFormState extends State { final _formKey = GlobalKey(); - TextEditingController _companyController; - TextEditingController _titleController; - TextEditingController _departmentController; - TextEditingController _jobDescriptionController; - TextEditingController _symbolController; - TextEditingController _phoneticNameController; - TextEditingController _officeLocationController; + late TextEditingController _companyController; + late TextEditingController _titleController; + late TextEditingController _departmentController; + late TextEditingController _jobDescriptionController; + late TextEditingController _symbolController; + late TextEditingController _phoneticNameController; + late TextEditingController _officeLocationController; @override void initState() { diff --git a/example_full/lib/pages/form_components/phone_form.dart b/example_full/lib/pages/form_components/phone_form.dart index f22b4b9f..9c59f0c9 100644 --- a/example_full/lib/pages/form_components/phone_form.dart +++ b/example_full/lib/pages/form_components/phone_form.dart @@ -8,9 +8,9 @@ class PhoneForm extends StatefulWidget { PhoneForm( this.phone, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -21,9 +21,9 @@ class _PhoneFormState extends State { final _formKey = GlobalKey(); static final _validLabels = PhoneLabel.values; - TextEditingController _numberController; - PhoneLabel _label; - TextEditingController _customLabelController; + late TextEditingController _numberController; + PhoneLabel? _label; + late TextEditingController _customLabelController; @override void initState() { @@ -35,11 +35,13 @@ class _PhoneFormState extends State { } void _onChanged() { - final phone = Phone(_numberController.text, - label: _label, - customLabel: - _label == PhoneLabel.custom ? _customLabelController.text : ''); - widget.onUpdate(phone); + if (_label != null) { + final phone = Phone(_numberController.text, + label: _label!, + customLabel: + _label == PhoneLabel.custom ? _customLabelController.text : ''); + widget.onUpdate(phone); + } } @override diff --git a/example_full/lib/pages/form_components/social_media_form.dart b/example_full/lib/pages/form_components/social_media_form.dart index 9c0065cc..06455eec 100644 --- a/example_full/lib/pages/form_components/social_media_form.dart +++ b/example_full/lib/pages/form_components/social_media_form.dart @@ -8,9 +8,9 @@ class SocialMediaForm extends StatefulWidget { SocialMediaForm( this.socialMedia, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -21,9 +21,9 @@ class _SocialMediaFormState extends State { final _formKey = GlobalKey(); static final _validLabels = SocialMediaLabel.values; - TextEditingController _userNameController; - SocialMediaLabel _label; - TextEditingController _customLabelController; + late TextEditingController _userNameController; + SocialMediaLabel? _label; + late TextEditingController _customLabelController; @override void initState() { @@ -36,13 +36,16 @@ class _SocialMediaFormState extends State { } void _onChanged() { - final socialMedia = SocialMedia( - _userNameController.text, - label: _label, - customLabel: - _label == SocialMediaLabel.custom ? _customLabelController.text : '', - ); - widget.onUpdate(socialMedia); + if (_label != null) { + final socialMedia = SocialMedia( + _userNameController.text, + label: _label!, + customLabel: _label == SocialMediaLabel.custom + ? _customLabelController.text + : '', + ); + widget.onUpdate(socialMedia); + } } @override diff --git a/example_full/lib/pages/form_components/website_form.dart b/example_full/lib/pages/form_components/website_form.dart index 7ebd997b..39083b5d 100644 --- a/example_full/lib/pages/form_components/website_form.dart +++ b/example_full/lib/pages/form_components/website_form.dart @@ -8,9 +8,9 @@ class WebsiteForm extends StatefulWidget { WebsiteForm( this.website, { - @required this.onUpdate, - @required this.onDelete, - Key key, + required this.onUpdate, + required this.onDelete, + Key? key, }) : super(key: key); @override @@ -21,9 +21,9 @@ class _WebsiteFormState extends State { final _formKey = GlobalKey(); static final _validLabels = WebsiteLabel.values; - TextEditingController _urlController; - WebsiteLabel _label; - TextEditingController _customLabelController; + late TextEditingController _urlController; + WebsiteLabel? _label; + late TextEditingController _customLabelController; @override void initState() { @@ -35,13 +35,15 @@ class _WebsiteFormState extends State { } void _onChanged() { - final website = Website( - _urlController.text, - label: _label, - customLabel: - _label == WebsiteLabel.custom ? _customLabelController.text : '', - ); - widget.onUpdate(website); + if (_label != null) { + final website = Website( + _urlController.text, + label: _label!, + customLabel: + _label == WebsiteLabel.custom ? _customLabelController.text : '', + ); + widget.onUpdate(website); + } } @override diff --git a/example_full/lib/pages/groups_page.dart b/example_full/lib/pages/groups_page.dart index f37ffeed..1f36da72 100644 --- a/example_full/lib/pages/groups_page.dart +++ b/example_full/lib/pages/groups_page.dart @@ -10,7 +10,7 @@ class GroupsPage extends StatefulWidget { class _GroupsPageState extends State with AfterLayoutMixin { - List _groups; + List _groups = []; @override void afterFirstLayout(BuildContext context) { @@ -35,7 +35,7 @@ class _GroupsPageState extends State ); Widget _body() { - if (_groups == null) { + if (_groups.isEmpty) { return Center(child: CircularProgressIndicator()); } return ListView.builder( @@ -64,8 +64,8 @@ class _GroupsPageState extends State Future _newGroup() async { final name = await prompt(context); - if (name.isNotEmpty) { - final group = await FlutterContacts.insertGroup(Group('', name)); + if (name?.isNotEmpty == true) { + final group = await FlutterContacts.insertGroup(Group('', name!)); print('Inserted group $group'); await _fetchGroups(); } @@ -73,9 +73,9 @@ class _GroupsPageState extends State Future _renameGroup(Group group) async { final name = await prompt(context, initialValue: group.name); - if (name.isNotEmpty) { + if (name?.isNotEmpty == true) { final updatedGroup = - await FlutterContacts.updateGroup(Group(group.id, name)); + await FlutterContacts.updateGroup(Group(group.id, name!)); print('Updated group $updatedGroup'); await _fetchGroups(); } diff --git a/example_full/lib/util/avatar.dart b/example_full/lib/util/avatar.dart index 6d821db7..35fdc8f6 100644 --- a/example_full/lib/util/avatar.dart +++ b/example_full/lib/util/avatar.dart @@ -5,7 +5,7 @@ Widget avatar(Contact contact, [double radius = 48.0, IconData defaultIcon = Icons.person]) { if (contact.photoOrThumbnail != null) { return CircleAvatar( - backgroundImage: MemoryImage(contact.photoOrThumbnail), + backgroundImage: MemoryImage(contact.photoOrThumbnail!), radius: radius, ); } diff --git a/example_full/pubspec.yaml b/example_full/pubspec.yaml index 4f309f78..ebcef572 100644 --- a/example_full/pubspec.yaml +++ b/example_full/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the flutter_contacts plugin. publish_to: "none" environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -11,8 +11,8 @@ dependencies: after_layout: ^1.0.7+2 flutter_contacts: path: ../ - image_picker: ^0.6.7+22 - pretty_json: ^1.1.0 + image_picker: ^1.0.4 + pretty_json: ^2.0.0 prompt_dialog: ^1.0.9 dev_dependencies: From d1a12048007202b44225ff11087c5990b8b32eb5 Mon Sep 17 00:00:00 2001 From: SoftWyer Date: Wed, 25 Oct 2023 14:57:27 +0100 Subject: [PATCH 2/6] Fix vCard parsing/unfolding on Windows --- lib/vcard.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/vcard.dart b/lib/vcard.dart index 67e127e4..6eeb0baf 100644 --- a/lib/vcard.dart +++ b/lib/vcard.dart @@ -44,10 +44,10 @@ class VCardParser { String unfold(String s) => s // https://tools.ietf.org/html/rfc2425#section-5.8.1 - .replaceAll(RegExp(r'\n[ \t]'), '') + .replaceAll(RegExp(r'(\r\n|\n)[ \t]'), '') // Quoted-encoded contents are sometimes split on multiple lines ending and // starting with '='. - .replaceAll(RegExp(r'=\n='), '='); + .replaceAll(RegExp(r'=(\r\n|\n)='), '='); void parse(String content, Contact contact) { var lines = encode(unfold(content)).split('\n').map((String x) => x.trim()); From e8155504a8911b29d51eb4f3e77e65166fb18340 Mon Sep 17 00:00:00 2001 From: SoftWyer Date: Thu, 26 Oct 2023 10:13:15 +0100 Subject: [PATCH 3/6] Add relations for Android --- android/build.gradle | 4 +- .../co/quis/flutter_contacts/Contact.kt | 4 + .../quis/flutter_contacts/FlutterContacts.kt | 77 ++++++- .../flutter_contacts/properties/Relation.kt | 23 ++ example/android/app/build.gradle | 2 +- example/android/build.gradle | 2 +- example_full/android/app/build.gradle | 4 +- example_full/lib/main.dart | 7 +- example_full/lib/pages/contact_list_page.dart | 2 +- example_full/lib/pages/contact_page.dart | 17 +- example_full/lib/pages/edit_contact_page.dart | 16 ++ .../pages/form_components/relation_form.dart | 100 +++++++++ lib/contact.dart | 16 +- lib/properties/relation.dart | 198 ++++++++++++++++++ lib/vcard.dart | 33 +++ .../vcards/android-default-contact-app.json | 5 + .../vcards/android-default-contact-app.vcf | 4 + test/testdata/vcards/bvcard.com.json | 1 + test/testdata/vcards/ios.json | 1 + test/testdata/vcards/macos.json | 1 + .../vcards/qr-code-generator.com.json | 1 + test/testdata/vcards/vcardmaker.com.json | 1 + test/testdata/vcards/whatsapp.json | 1 + 23 files changed, 507 insertions(+), 13 deletions(-) create mode 100644 android/src/main/kotlin/co/quis/flutter_contacts/properties/Relation.kt create mode 100644 example_full/lib/pages/form_components/relation_form.dart create mode 100644 lib/properties/relation.dart diff --git a/android/build.gradle b/android/build.gradle index 94326b78..dd3824c6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 34 + compileSdkVersion 31 if (!project.hasProperty("namespace")) { namespace 'co.quis.flutter_contacts' @@ -49,4 +49,4 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0") -} \ No newline at end of file +} 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..da77ae9a 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/Contact.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/Contact.kt @@ -9,6 +9,7 @@ import co.quis.flutter_contacts.properties.Name import co.quis.flutter_contacts.properties.Note import co.quis.flutter_contacts.properties.Organization import co.quis.flutter_contacts.properties.Phone +import co.quis.flutter_contacts.properties.Relation import co.quis.flutter_contacts.properties.SocialMedia import co.quis.flutter_contacts.properties.Website @@ -26,6 +27,7 @@ data class Contact( var websites: List = listOf(), var socialMedias: List = listOf(), var events: List = listOf(), + var relations: List = listOf(), var notes: List = listOf(), var accounts: List = listOf(), var groups: List = listOf() @@ -46,6 +48,7 @@ data class Contact( (m["websites"] as List>).map { Website.fromMap(it) }, (m["socialMedias"] as List>).map { SocialMedia.fromMap(it) }, (m["events"] as List>).map { Event.fromMap(it) }, + (m["relations"] as List>).map { Relation.fromMap(it) }, (m["notes"] as List>).map { Note.fromMap(it) }, (m["accounts"] as List>).map { Account.fromMap(it) }, (m["groups"] as List>).map { Group.fromMap(it) } @@ -67,6 +70,7 @@ data class Contact( "websites" to websites.map { it.toMap() }, "socialMedias" to socialMedias.map { it.toMap() }, "events" to events.map { it.toMap() }, + "relations" to relations.map { it.toMap() }, "notes" to notes.map { it.toMap() }, "accounts" to accounts.map { it.toMap() }, "groups" to groups.map { it.toMap() } 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 33a4f96e..e3b0110a 100644 --- a/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt +++ b/android/src/main/kotlin/co/quis/flutter_contacts/FlutterContacts.kt @@ -20,6 +20,7 @@ import android.provider.ContactsContract.CommonDataKinds.Note import android.provider.ContactsContract.CommonDataKinds.Organization import android.provider.ContactsContract.CommonDataKinds.Phone import android.provider.ContactsContract.CommonDataKinds.Photo +import android.provider.ContactsContract.CommonDataKinds.Relation import android.provider.ContactsContract.CommonDataKinds.StructuredName import android.provider.ContactsContract.CommonDataKinds.StructuredPostal import android.provider.ContactsContract.CommonDataKinds.Website @@ -39,6 +40,7 @@ import co.quis.flutter_contacts.properties.Name as PName import co.quis.flutter_contacts.properties.Note as PNote import co.quis.flutter_contacts.properties.Organization as POrganization import co.quis.flutter_contacts.properties.Phone as PPhone +import co.quis.flutter_contacts.properties.Relation as PRelation import co.quis.flutter_contacts.properties.SocialMedia as PSocialMedia import co.quis.flutter_contacts.properties.Website as PWebsite @@ -127,6 +129,8 @@ class FlutterContacts { Event.START_DATE, Event.TYPE, Event.LABEL, + Relation.TYPE, + Relation.LABEL, Note.NOTE ) ) @@ -382,6 +386,17 @@ class FlutterContacts { contact.events += event } } + Relation.CONTENT_ITEM_TYPE -> { + val label: String = getRelationLabel(cursor) + val customLabel: String = + if (label == "custom") getRelationCustomLabel(cursor) else "" + val relation = PRelation( + getString(Relation.NAME), + label, + customLabel + ) + contact.relations += relation + } Note.CONTENT_ITEM_TYPE -> { val note: String = getString(Note.NOTE) // It seems that every contact has an empty note by default; @@ -504,7 +519,7 @@ class FlutterContacts { ops.add( ContentProviderOperation.newDelete(Data.CONTENT_URI) .withSelection( - "${RawContacts.CONTACT_ID}=? and ${Data.MIMETYPE} in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "${RawContacts.CONTACT_ID}=? and ${Data.MIMETYPE} in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", arrayOf( contactId, StructuredName.CONTENT_ITEM_TYPE, @@ -516,6 +531,7 @@ class FlutterContacts { Website.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, + Relation.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE ) ) @@ -996,6 +1012,54 @@ class FlutterContacts { } } + private fun getRelationLabel(cursor: Cursor): String { + val type = cursor.getInt(cursor.getColumnIndex(Relation.TYPE)) + return when (type) { + Relation.TYPE_CUSTOM -> "custom" + Relation.TYPE_ASSISTANT -> "assistant" + Relation.TYPE_BROTHER -> "brother" + Relation.TYPE_CHILD -> "child" + Relation.TYPE_DOMESTIC_PARTNER -> "domestic-partner" + Relation.TYPE_FATHER -> "father" + Relation.TYPE_FRIEND -> "friend" + Relation.TYPE_MANAGER -> "manager" + Relation.TYPE_MOTHER -> "mother" + Relation.TYPE_PARENT -> "parent" + Relation.TYPE_PARTNER -> "partner" + Relation.TYPE_REFERRED_BY -> "referred-by" + Relation.TYPE_RELATIVE -> "relative" + Relation.TYPE_SISTER -> "sister" + Relation.TYPE_SPOUSE -> "spouse" + else -> "relation" + } + } + + private fun getRelationCustomLabel(cursor: Cursor): String { + return cursor.getString(cursor.getColumnIndex(Relation.LABEL)) ?: "" + } + + private data class RelationLabelPair(val label: Int, val customLabel: String) + private fun getRelationLabelInv(label: String, customLabel: String): RelationLabelPair { + return when (label) { + "custom" -> RelationLabelPair(Relation.TYPE_CUSTOM, customLabel) + "assistant" -> RelationLabelPair(Relation.TYPE_ASSISTANT, "") + "brother" -> RelationLabelPair(Relation.TYPE_BROTHER, "") + "child" -> RelationLabelPair(Relation.TYPE_CHILD, "") + "domestic-partner" -> RelationLabelPair(Relation.TYPE_DOMESTIC_PARTNER, "") + "father" -> RelationLabelPair(Relation.TYPE_FATHER, "") + "friend" -> RelationLabelPair(Relation.TYPE_FRIEND, "") + "manager" -> RelationLabelPair(Relation.TYPE_MANAGER, "") + "mother" -> RelationLabelPair(Relation.TYPE_MOTHER, "") + "parent" -> RelationLabelPair(Relation.TYPE_PARENT, "") + "partner" -> RelationLabelPair(Relation.TYPE_PARTNER, "") + "referred-by" -> RelationLabelPair(Relation.TYPE_REFERRED_BY, "") + "relative" -> RelationLabelPair(Relation.TYPE_RELATIVE, "") + "sister" -> RelationLabelPair(Relation.TYPE_SISTER, "") + "spouse" -> RelationLabelPair(Relation.TYPE_SPOUSE, "") + else -> RelationLabelPair(Email.TYPE_CUSTOM, label) + } + } + private fun buildOpsForContact( contact: Contact, ops: MutableList, @@ -1131,6 +1195,17 @@ class FlutterContacts { .build() ) } + for ((i, relation) in contact.relations.withIndex()) { + val labelPair: RelationLabelPair = getRelationLabelInv(relation.label, relation.customLabel) + ops.add( + newInsert() + .withValue(Data.MIMETYPE, Relation.CONTENT_ITEM_TYPE) + .withValue(Relation.NAME, emptyToNull(relation.name)) + .withValue(Relation.TYPE, labelPair.label) + .withValue(Relation.LABEL, emptyToNull(labelPair.customLabel)) + .build() + ) + } for (note in contact.notes) { if (!note.note.isEmpty()) { ops.add( diff --git a/android/src/main/kotlin/co/quis/flutter_contacts/properties/Relation.kt b/android/src/main/kotlin/co/quis/flutter_contacts/properties/Relation.kt new file mode 100644 index 00000000..94992e68 --- /dev/null +++ b/android/src/main/kotlin/co/quis/flutter_contacts/properties/Relation.kt @@ -0,0 +1,23 @@ +package co.quis.flutter_contacts.properties + +data class Relation( + var name: String, + // one of: assistant, brother, child, daughter, domestic-partner, father, friend, manager, + // mother, other, parent, partner, referred-by, relative, sister, son, spouse, custom + var label: String = "relative", + var customLabel: String = "" +) { + companion object { + fun fromMap(m: Map): Relation = Relation( + m["name"] as String, + m["label"] as String, + m["customLabel"] as String + ) + } + + fun toMap(): Map = mapOf( + "name" to name, + "label" to label, + "customLabel" to customLabel + ) +} diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 0804894f..60a77b82 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -39,7 +39,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "co.quis.flutter_contacts_example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/build.gradle b/example/android/build.gradle index 9610d8f2..c0d45437 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example_full/android/app/build.gradle b/example_full/android/app/build.gradle index f724da0c..980243d3 100644 --- a/example_full/android/app/build.gradle +++ b/example_full/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 34 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -40,7 +40,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "co.quis.flutter_contacts_example" minSdkVersion flutter.minSdkVersion - targetSdkVersion 34 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example_full/lib/main.dart b/example_full/lib/main.dart index cd52275f..31c5b3f0 100644 --- a/example_full/lib/main.dart +++ b/example_full/lib/main.dart @@ -1,11 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_contacts/config.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; import 'pages/contact_list_page.dart'; import 'pages/contact_page.dart'; import 'pages/edit_contact_page.dart'; import 'pages/groups_page.dart'; -void main() => runApp(FlutterContactsExample()); +void main() { + FlutterContacts.config.vCardVersion = VCardVersion.v4; + runApp(FlutterContactsExample()); +} class FlutterContactsExample extends StatelessWidget { @override diff --git a/example_full/lib/pages/contact_list_page.dart b/example_full/lib/pages/contact_list_page.dart index 15cea807..c94e933c 100644 --- a/example_full/lib/pages/contact_list_page.dart +++ b/example_full/lib/pages/contact_list_page.dart @@ -91,7 +91,7 @@ class _ContactListPageState extends State if (_permissionDenied) { return Center(child: Text('Permission denied')); } - if (_contacts == null) { + if (_contacts.isEmpty) { return Center(child: CircularProgressIndicator()); } return ListView.builder( diff --git a/example_full/lib/pages/contact_page.dart b/example_full/lib/pages/contact_page.dart index 077ef8bc..6ded0a54 100644 --- a/example_full/lib/pages/contact_page.dart +++ b/example_full/lib/pages/contact_page.dart @@ -57,8 +57,10 @@ class _ContactPageState extends State await showDialog( context: context, builder: (_) => AlertDialog( - content: Text(prettyJson(_contact?.toJson( - withPhoto: false, withThumbnail: false))), + content: SingleChildScrollView( + child: Text(prettyJson(_contact?.toJson( + withPhoto: false, withThumbnail: false))), + ), ), ); }, @@ -218,6 +220,15 @@ class _ContactPageState extends State Text('Label: ${x.label}'), Text('Custom label: ${x.customLabel}'), ]), + _makeCard( + 'Relations', + contact.relations, + (x) => [ + Divider(), + Text('Name: ${x.name}'), + Text('Label: ${x.label}'), + Text('Custom label: ${x.customLabel}'), + ]), _makeCard( 'Notes', contact.notes, @@ -257,7 +268,7 @@ class _ContactPageState extends State } String _formatDate(Event e) => - '${e.year?.toString()?.padLeft(4, '0') ?? '--'}/' + '${e.year?.toString().padLeft(4, '0') ?? '--'}/' '${e.month.toString().padLeft(2, '0')}/' '${e.day.toString().padLeft(2, '0')}'; diff --git a/example_full/lib/pages/edit_contact_page.dart b/example_full/lib/pages/edit_contact_page.dart index bb6db46e..3dc9d83c 100644 --- a/example_full/lib/pages/edit_contact_page.dart +++ b/example_full/lib/pages/edit_contact_page.dart @@ -1,6 +1,7 @@ import 'package:after_layout/after_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:flutter_contacts/properties/relation.dart'; import 'package:flutter_contacts_example/pages/form_components/address_form.dart'; import 'package:flutter_contacts_example/pages/form_components/email_form.dart'; import 'package:flutter_contacts_example/pages/form_components/event_form.dart'; @@ -8,6 +9,7 @@ import 'package:flutter_contacts_example/pages/form_components/name_form.dart'; import 'package:flutter_contacts_example/pages/form_components/note_form.dart'; import 'package:flutter_contacts_example/pages/form_components/organization_form.dart'; import 'package:flutter_contacts_example/pages/form_components/phone_form.dart'; +import 'package:flutter_contacts_example/pages/form_components/relation_form.dart'; import 'package:flutter_contacts_example/pages/form_components/social_media_form.dart'; import 'package:flutter_contacts_example/pages/form_components/website_form.dart'; import 'package:flutter_contacts_example/util/avatar.dart'; @@ -112,6 +114,7 @@ class _EditContactPageState extends State _websiteCard(), _socialMediaCard(), _eventCard(), + _relationCard(), _noteCard(), _groupCard(), ]; @@ -327,6 +330,19 @@ class _EditContactPageState extends State createAsync: true, ); + Card _relationCard() => _fieldCard( + 'Relations', + _contact.relations, + () => _contact.relations = _contact.relations + [Relation('')], + (int i, dynamic e) => RelationForm( + e, + onUpdate: (relation) => _contact.relations[i] = relation, + onDelete: () => setState(() => _contact.relations.removeAt(i)), + key: UniqueKey(), + ), + () => _contact.relations = [], + ); + Card _noteCard() => _fieldCard( 'Notes', _contact.notes, diff --git a/example_full/lib/pages/form_components/relation_form.dart b/example_full/lib/pages/form_components/relation_form.dart new file mode 100644 index 00000000..ec4fbc18 --- /dev/null +++ b/example_full/lib/pages/form_components/relation_form.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/properties/relation.dart'; + +class RelationForm extends StatefulWidget { + final Relation relation; + final void Function(Relation) onUpdate; + final void Function() onDelete; + + RelationForm( + this.relation, { + required this.onUpdate, + required this.onDelete, + Key? key, + }) : super(key: key); + + @override + _RelationFormState createState() => _RelationFormState(); +} + +class _RelationFormState extends State { + final _formKey = GlobalKey(); + static final _validLabels = RelationLabel.values; + + late TextEditingController _nameController; + RelationLabel? _label; + late TextEditingController _customLabelController; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.relation.name); + _label = widget.relation.label; + _customLabelController = + TextEditingController(text: widget.relation.customLabel); + } + + void _onChanged() { + if (_label != null) { + final email = Relation( + _nameController.text, + label: _label!, + customLabel: + _label == RelationLabel.custom ? _customLabelController.text : '', + ); + widget.onUpdate(email); + } + } + + @override + Widget build(BuildContext context) { + return ListTile( + trailing: PopupMenuButton( + itemBuilder: (context) => + [PopupMenuItem(value: 'Delete', child: Text('Delete'))], + onSelected: (_) => widget.onDelete(), + ), + subtitle: Padding( + padding: const EdgeInsets.all(8.0), + child: Form( + key: _formKey, + onChanged: _onChanged, + child: Column( + children: [ + TextFormField( + controller: _nameController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration(hintText: 'Name'), + ), + DropdownButtonFormField( + isExpanded: true, // to avoid overflow + items: _validLabels + .map((e) => DropdownMenuItem( + value: e, child: Text(e.toString()))) + .toList(), + value: _label, + onChanged: (label) { + setState(() { + _label = label; + }); + // Unfortunately, the form's `onChanged` gets triggered before + // the dropdown's `onChanged`, so it doesn't update the + // contact when updating the dropdown, and we need to do it + // explicitly here. + _onChanged(); + }, + ), + _label == RelationLabel.custom + ? TextFormField( + controller: _customLabelController, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration(hintText: 'Custom label'), + ) + : Container(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/contact.dart b/lib/contact.dart index 03ad4c8b..9e871a2b 100644 --- a/lib/contact.dart +++ b/lib/contact.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_contacts/config.dart'; +import 'package:flutter_contacts/properties/relation.dart'; import 'package:flutter_contacts/vcard.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; @@ -111,6 +112,9 @@ class Contact { /// Notes. List notes; + /// Relations. + List relations; + /// Raw accounts (Android only). List accounts; @@ -143,6 +147,7 @@ class Contact { List? websites, List? socialMedias, List? events, + List? relations, List? notes, List? accounts, List? groups, @@ -154,6 +159,7 @@ class Contact { websites = websites ?? [], socialMedias = socialMedias ?? [], events = events ?? [], + relations = relations ?? [], notes = notes ?? [], accounts = accounts ?? [], groups = groups ?? []; @@ -186,6 +192,9 @@ class Contact { events: ((json['events'] as List?) ?? []) .map((x) => Event.fromJson(Map.from(x))) .toList(), + relations: ((json['relations'] as List?) ?? []) + .map((x) => Relation.fromJson(Map.from(x))) + .toList(), notes: ((json['notes'] as List?) ?? []) .map((x) => Note.fromJson(Map.from(x))) .toList(), @@ -215,6 +224,7 @@ class Contact { 'websites': websites.map((x) => x.toJson()).toList(), 'socialMedias': socialMedias.map((x) => x.toJson()).toList(), 'events': events.map((x) => x.toJson()).toList(), + 'relations': relations.map((x) => x.toJson()).toList(), 'notes': notes.map((x) => x.toJson()).toList(), 'accounts': accounts.map((x) => x.toJson()).toList(), 'groups': groups.map((x) => x.toJson()).toList(), @@ -235,6 +245,7 @@ class Contact { _listHashCode(websites) ^ _listHashCode(socialMedias) ^ _listHashCode(events) ^ + _listHashCode(relations) ^ _listHashCode(notes); @override @@ -253,6 +264,7 @@ class Contact { _listEqual(o.websites, websites) && _listEqual(o.socialMedias, socialMedias) && _listEqual(o.events, events) && + _listEqual(o.relations, relations) && _listEqual(o.notes, notes); @override @@ -261,7 +273,7 @@ class Contact { 'photo=$photo, isStarred=$isStarred, name=$name, phones=$phones, ' 'emails=$emails, addresses=$addresses, organizations=$organizations, ' 'websites=$websites, socialMedias=$socialMedias, events=$events, ' - 'notes=$notes, accounts=$accounts, groups=$groups)'; + 'relations=$relations, notes=$notes, accounts=$accounts, groups=$groups)'; /// Inserts the contact into the database. Future insert() => FlutterContacts.insertContact(this); @@ -332,6 +344,7 @@ class Contact { websites.map((x) => x.toVCard()).expand((x) => x), socialMedias.map((x) => x.toVCard()).expand((x) => x), events.map((x) => x.toVCard()).expand((x) => x), + relations.map((x) => x.toVCard()).expand((x) => x), notes.map((x) => x.toVCard()).expand((x) => x), ].expand((x) => x)); lines.add('END:VCARD'); @@ -361,6 +374,7 @@ class Contact { websites = _depuplicateProperty(websites); socialMedias = _depuplicateProperty(socialMedias); events = _depuplicateProperty(events); + relations = _depuplicateProperty(relations); notes = _depuplicateProperty(notes); } diff --git a/lib/properties/relation.dart b/lib/properties/relation.dart new file mode 100644 index 00000000..421463a3 --- /dev/null +++ b/lib/properties/relation.dart @@ -0,0 +1,198 @@ +import 'package:flutter_contacts/config.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:flutter_contacts/vcard.dart'; + +/// Labeled Relation. +class Relation { + /// Relation name. + String name; + + /// Label (default [RelationLabel.relative]). + RelationLabel label; + + /// Custom label, if [label] is [RelationLabel.custom]. + String customLabel; + + Relation( + this.name, { + this.label = RelationLabel.relative, + this.customLabel = '', + }); + + factory Relation.fromJson(Map json) => Relation( + (json['name'] as String?) ?? '', + label: _stringToRelationLabel[json['label'] as String? ?? ''] ?? + RelationLabel.assistant, + customLabel: (json['customLabel'] as String?) ?? '', + ); + + Map toJson() => { + 'name': name, + 'label': _RelationLabelToString[label], + 'customLabel': customLabel, + }; + + @override + int get hashCode => name.hashCode ^ label.hashCode ^ customLabel.hashCode; + + @override + bool operator ==(Object o) => + o is Relation && + o.name == name && + o.label == label && + o.customLabel == customLabel; + + @override + String toString() => + 'Relation(name=$name, label=$label, customLabel=$customLabel'; + + List toVCard() { + // Related (V4): https://datatracker.ietf.org/doc/html/rfc6350#section-6.6.6 + // Relationship types: https://gmpg.org/xfn/11 also https://datatracker.ietf.org/doc/html/rfc6350#section-10.3.4 + + if (FlutterContacts.config.vCardVersion != VCardVersion.v4) { + // Not supported in v3 + return []; + } + + var s = 'RELATED'; + + switch (label) { + case RelationLabel.assistant: + s += ';TYPE=co-worker'; + break; + case RelationLabel.brother: + s += ';TYPE=sibling'; + break; + case RelationLabel.child: + s += ';TYPE=child'; + break; + case RelationLabel.daughter: + s += ';TYPE=child'; + break; + case RelationLabel.domesticPartner: + s += ';TYPE=spouse'; + break; + case RelationLabel.father: + s += ';TYPE=parent'; + break; + case RelationLabel.friend: + s += ';TYPE=friend'; + break; + case RelationLabel.manager: + s += ';TYPE=co-worker'; + break; + case RelationLabel.mother: + s += ';TYPE=parent'; + break; + case RelationLabel.parent: + s += ';TYPE=parent'; + break; + case RelationLabel.partner: + s += ';TYPE=spouse'; + break; + case RelationLabel.relative: + s += ';TYPE=kin'; + break; + case RelationLabel.sister: + s += ';TYPE=sibling'; + break; + case RelationLabel.son: + s += ';TYPE=child'; + break; + case RelationLabel.spouse: + s += ';TYPE=spouse'; + break; + default: + } + s += ':${vCardEncode(name)}'; + return [s]; + } +} + +/// Relation labels. +/// +/// | Label | Android | iOS | +/// |----------|:-------:|:---:| +/// | assistant| ✔ | ✔ | +/// | brother | ⨯ | ✔ | +/// | child | ✔ | ✔ | +/// | daughter | ⨯ | ✔ | +/// | domestic_ +/// | partner| ✔ | ⨯ | +/// | father | ✔ | ✔ | +/// | friend | ✔ | ✔ | +/// | manager | ✔ | ✔ | +/// | mother | ✔ | ✔ | +/// | other | ⨯ | ✔ | +/// | parent | ✔ | ✔ | +/// | partner | ✔ | ✔ | +/// | referred_ +/// | by | ✔ | ⨯ | +/// | relative | ✔ | ⨯ | +/// | sister | ✔ | ✔ | +/// | son | ⨯ | ✔ | +/// | spouse | ✔ | ✔ | +/// | custom | ✔ | ✔ | +enum RelationLabel { + assistant, + brother, + child, + daughter, + domesticPartner, + father, + friend, + manager, + mother, + other, + parent, + partner, + referredBy, + relative, + sister, + son, + spouse, + custom, +} + +final _RelationLabelToString = { + RelationLabel.assistant: 'assistant', + RelationLabel.brother: 'brother', + RelationLabel.child: 'child', + RelationLabel.daughter: 'daughter', + RelationLabel.domesticPartner: 'domestic-partner', + RelationLabel.father: 'father', + RelationLabel.friend: 'friend', + RelationLabel.manager: 'manager', + RelationLabel.mother: 'mother', + RelationLabel.other: 'other', + RelationLabel.parent: 'parent', + RelationLabel.partner: 'partner', + RelationLabel.referredBy: 'referred-by', + RelationLabel.relative: 'relative', + RelationLabel.sister: 'sister', + RelationLabel.son: 'son', + RelationLabel.spouse: 'spouse', + RelationLabel.custom: 'custom', +}; + +final _stringToRelationLabel = { + 'assistant': RelationLabel.assistant, + 'brother': RelationLabel.brother, + 'child': RelationLabel.child, + 'daughter': RelationLabel.daughter, + 'domestic-partner': RelationLabel.domesticPartner, + 'father': RelationLabel.father, + 'friend': RelationLabel.friend, + 'manager': RelationLabel.manager, + 'mother': RelationLabel.mother, + 'other': RelationLabel.other, + 'parent': RelationLabel.parent, + 'partner': RelationLabel.partner, + 'referred-by': RelationLabel.referredBy, + 'relative': RelationLabel.relative, + 'sister': RelationLabel.sister, + 'son': RelationLabel.son, + 'spouse': RelationLabel.spouse, + 'custom': RelationLabel.custom, +}; diff --git a/lib/vcard.dart b/lib/vcard.dart index 6eeb0baf..36e04a41 100644 --- a/lib/vcard.dart +++ b/lib/vcard.dart @@ -7,6 +7,7 @@ import 'package:flutter_contacts/properties/event.dart'; import 'package:flutter_contacts/properties/note.dart'; import 'package:flutter_contacts/properties/organization.dart'; import 'package:flutter_contacts/properties/phone.dart'; +import 'package:flutter_contacts/properties/relation.dart'; import 'package:flutter_contacts/properties/social_media.dart'; import 'package:flutter_contacts/properties/website.dart'; @@ -163,6 +164,11 @@ class VCardParser { _parseLabel(params, labelOverride, _parseEmailLabel, email); contact.emails.add(email); break; + case 'RELATED': + var relation = Relation(decode(content)); + _parseLabel(params, labelOverride, _parseRelationLabel, relation); + contact.relations.add(relation); + break; case 'ADR': // Format is ADR:;;;; // ;; @@ -511,6 +517,33 @@ void _parseEmailLabel(String label, Email email, bool defaultToCustom) { } } +/// Note that this is not a symmetric mapping +/// with `Relation.toVCard()`, e.g. you could say +/// that a brother is a sibling, but not all +/// siblings are brothers +void _parseRelationLabel( + String label, Relation relation, bool defaultToCustom) { + switch (label.toUpperCase()) { + case 'FRIEND': + relation.label = RelationLabel.friend; + break; + case 'CHILD': + relation.label = RelationLabel.child; + break; + case 'PARENT': + relation.label = RelationLabel.parent; + break; + case 'SPOUSE': + relation.label = RelationLabel.spouse; + break; + default: + if (defaultToCustom) { + relation.label = RelationLabel.custom; + relation.customLabel = label; + } + } +} + void _parseAddressLabel(String label, Address address, bool defaultToCustom) { switch (label.toUpperCase()) { case 'HOME': diff --git a/test/testdata/vcards/android-default-contact-app.json b/test/testdata/vcards/android-default-contact-app.json index 36be33e3..1627d5cf 100644 --- a/test/testdata/vcards/android-default-contact-app.json +++ b/test/testdata/vcards/android-default-contact-app.json @@ -2348,6 +2348,11 @@ "customLabel": "" } ], + "relations": [ + {"name": "Tony Maloney", "label": "friend", "customLabel": ""}, + {"name": "Arnold Scwartz", "label": "relative", "customLabel": ""}, + {"name": "Dr John Jøhñ Smith, Jr.", "label": "relative", "customLabel":""} + ], "notes": [ { "note": "Notes line 1\nNotes line 2\nNotes line 3" diff --git a/test/testdata/vcards/android-default-contact-app.vcf b/test/testdata/vcards/android-default-contact-app.vcf index 7d36d8b0..b4291cab 100644 --- a/test/testdata/vcards/android-default-contact-app.vcf +++ b/test/testdata/vcards/android-default-contact-app.vcf @@ -70,4 +70,8 @@ X-QQ:@qq X-GOOGLE-TALK:@hangouts X-ICQ:@icq X-JABBER:@jabber +RELATED;TYPE=friend;VALUE=text:Tony Maloney +RELATED:Arnold Scwartz +RELATED;TYPE=me;ENCODING=QUOTED-PRINTABLE:=44=72=20=4A=6F=68=6E=20=4A=C3=B8=68=C3=B1=20=53=6D=69=74=68=2C=20=4A= +=72=2E END:VCARD diff --git a/test/testdata/vcards/bvcard.com.json b/test/testdata/vcards/bvcard.com.json index 67764223..9ae4c238 100644 --- a/test/testdata/vcards/bvcard.com.json +++ b/test/testdata/vcards/bvcard.com.json @@ -96,6 +96,7 @@ ], "socialMedias": [], "events": [], + "relations": [], "notes": [], "accounts": [], "groups": [] diff --git a/test/testdata/vcards/ios.json b/test/testdata/vcards/ios.json index 2fca9fd5..0bca2e60 100644 --- a/test/testdata/vcards/ios.json +++ b/test/testdata/vcards/ios.json @@ -766,6 +766,7 @@ "customLabel": "Custom Event Date" } ], + "relations": [], "notes": [], "accounts": [], "groups": [] diff --git a/test/testdata/vcards/macos.json b/test/testdata/vcards/macos.json index 352e745a..ef2a32f1 100644 --- a/test/testdata/vcards/macos.json +++ b/test/testdata/vcards/macos.json @@ -766,6 +766,7 @@ "customLabel": "Custom Event Date" } ], + "relations": [], "notes": [ { "note": "Some notes 1\nSome notes 2" diff --git a/test/testdata/vcards/qr-code-generator.com.json b/test/testdata/vcards/qr-code-generator.com.json index 39fe9b8a..587c643b 100644 --- a/test/testdata/vcards/qr-code-generator.com.json +++ b/test/testdata/vcards/qr-code-generator.com.json @@ -83,6 +83,7 @@ ], "socialMedias": [], "events": [], + "relations": [], "notes": [ { "note": "Some notes line 1 Some notes line 2" diff --git a/test/testdata/vcards/vcardmaker.com.json b/test/testdata/vcards/vcardmaker.com.json index e5ca6170..da8acbaf 100644 --- a/test/testdata/vcards/vcardmaker.com.json +++ b/test/testdata/vcards/vcardmaker.com.json @@ -594,6 +594,7 @@ "customLabel": "" } ], + "relations": [], "notes": [ { "note": "Some notes" diff --git a/test/testdata/vcards/whatsapp.json b/test/testdata/vcards/whatsapp.json index 054fe880..4f180b79 100644 --- a/test/testdata/vcards/whatsapp.json +++ b/test/testdata/vcards/whatsapp.json @@ -2317,6 +2317,7 @@ "customLabel": "" } ], + "relations": [], "notes": [], "accounts": [], "groups": [] From b7ff86a8839daa11578c5595025a86b399850f00 Mon Sep 17 00:00:00 2001 From: Support Date: Thu, 26 Oct 2023 12:25:02 +0100 Subject: [PATCH 4/6] Add iOS implementation --- example_full/ios/Podfile.lock | 12 +-- ios/Classes/Contact.swift | 4 + ios/Classes/SwiftFlutterContactsPlugin.swift | 6 ++ ios/Classes/properties/Relation.swift | 98 ++++++++++++++++++++ 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 ios/Classes/properties/Relation.swift diff --git a/example_full/ios/Podfile.lock b/example_full/ios/Podfile.lock index e0aaf3cf..fcadc415 100644 --- a/example_full/ios/Podfile.lock +++ b/example_full/ios/Podfile.lock @@ -2,27 +2,27 @@ PODS: - Flutter (1.0.0) - flutter_contacts (0.0.1): - Flutter - - image_picker (0.0.1): + - image_picker_ios (0.0.1): - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`) - - image_picker (from `.symlinks/plugins/image_picker/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter flutter_contacts: :path: ".symlinks/plugins/flutter_contacts/ios" - image_picker: - :path: ".symlinks/plugins/image_picker/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_contacts: edb1c5ce76aa433e20e6cb14c615f4c0b66e0983 - image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 -COCOAPODS: 1.11.3 +COCOAPODS: 1.13.0 diff --git a/ios/Classes/Contact.swift b/ios/Classes/Contact.swift index c59e8a91..686a3467 100644 --- a/ios/Classes/Contact.swift +++ b/ios/Classes/Contact.swift @@ -15,6 +15,7 @@ struct Contact { var websites: [Website] = [] var socialMedias: [SocialMedia] = [] var events: [Event] = [] + var relations: [Relation] = [] var notes: [Note] = [] var accounts: [Account] = [] var groups: [Group] = [] @@ -36,6 +37,7 @@ struct Contact { SocialMedia(fromMap: $0) } events = (m["events"] as! [[String: Any?]]).map { Event(fromMap: $0) } + relations = (m["relations"] as! [[String: Any]]).map { Relation(fromMap: $0) } notes = (m["notes"] as! [[String: Any]]).map { Note(fromMap: $0) } accounts = (m["accounts"] as! [[String: Any]]).map { Account(fromMap: $0) } groups = (m["groups"] as! [[String: Any]]).map { Group(fromMap: $0) } @@ -74,6 +76,7 @@ struct Contact { events = [Event(fromContact: c)] } events += c.dates.map { Event(fromDate: $0) } + relations += c.contactRelations.map { Relation(fromRelation: $0) } // Notes need entitlements to be accessed in iOS 13+. // https://stackoverflow.com/questions/57442114/ios-13-cncontacts-no-longer-working-to-retrieve-all-contacts if c.isKeyAvailable(CNContactNoteKey) { @@ -102,6 +105,7 @@ struct Contact { "websites": websites.map { $0.toMap() }, "socialMedias": socialMedias.map { $0.toMap() }, "events": events.map { $0.toMap() }, + "relations": relations.map { $0.toMap() }, "notes": notes.map { $0.toMap() }, "accounts": accounts.map { $0.toMap() }, "groups": groups.map { $0.toMap() }, diff --git a/ios/Classes/SwiftFlutterContactsPlugin.swift b/ios/Classes/SwiftFlutterContactsPlugin.swift index d486b592..0bb0648b 100644 --- a/ios/Classes/SwiftFlutterContactsPlugin.swift +++ b/ios/Classes/SwiftFlutterContactsPlugin.swift @@ -42,6 +42,7 @@ public enum FlutterContacts { CNContactSocialProfilesKey, CNContactInstantMessageAddressesKey, CNContactBirthdayKey, + CNContactRelationsKey, CNContactDatesKey, ] if #available(iOS 10, *) { @@ -230,6 +231,7 @@ public enum FlutterContacts { CNContactSocialProfilesKey, CNContactInstantMessageAddressesKey, CNContactBirthdayKey, + CNContactRelationsKey, CNContactDatesKey, CNContactThumbnailImageDataKey, CNContactImageDataKey, @@ -374,6 +376,7 @@ public enum FlutterContacts { contact.socialProfiles = [] contact.instantMessageAddresses = [] contact.dates = [] + contact.contactRelations = [] contact.birthday = nil if #available(iOS 13, *), !includeNotesOnIos13AndAbove {} else { contact.note = "" @@ -407,6 +410,9 @@ public enum FlutterContacts { (args["events"] as! [[String: Any]]).forEach { Event(fromMap: $0).addTo(contact) } + (args["relations"] as! [[String: Any]]).forEach { + Relation(fromMap: $0).addTo(contact) + } if #available(iOS 13, *), !includeNotesOnIos13AndAbove {} else { if let note = (args["notes"] as! [[String: Any]]).first { Note(fromMap: note).addTo(contact) diff --git a/ios/Classes/properties/Relation.swift b/ios/Classes/properties/Relation.swift new file mode 100644 index 00000000..5a3d3a18 --- /dev/null +++ b/ios/Classes/properties/Relation.swift @@ -0,0 +1,98 @@ +import Contacts + +@available(iOS 9.0, *) +struct Relation { + var name: String + // one of: spouse, partner, daughter, son, child, father, mother, parent, + // brother, sister, friend, assitant, manager + var label: String = "home" + var customLabel: String = "" + + init(fromMap m: [String: Any]) { + name = m["name"] as! String + label = m["label"] as! String + customLabel = m["customLabel"] as! String + } + + init(fromRelation r: CNLabeledValue) { + name = r.value.name + switch r.label { + case CNLabelContactRelationSpouse: + label = "spouse" + case CNLabelContactRelationPartner: + label = "partner" + case CNLabelContactRelationDaughter: + label = "daughter" + case CNLabelContactRelationSon: + label = "son" + case CNLabelContactRelationChild: + label = "child" + case CNLabelContactRelationFather: + label = "father" + case CNLabelContactRelationMother: + label = "mother" + case CNLabelContactRelationParent: + label = "parent" + case CNLabelContactRelationBrother: + label = "brother" + case CNLabelContactRelationSister: + label = "sister" + case CNLabelContactRelationFriend: + label = "friend" + case CNLabelContactRelationAssistant: + label = "assistant" + case CNLabelContactRelationManager: + label = "manager" + default: + label = "custom" + customLabel = r.label ?? "" + } + } + + func toMap() -> [String: Any] { [ + "name": name, + "label": label, + "customLabel": customLabel, + ] + } + + func addTo(_ c: CNMutableContact) { + var labelInv: String + switch label { + case "spouse": + labelInv = CNLabelContactRelationSpouse + case "partner": + labelInv = CNLabelContactRelationPartner + case "daughter": + labelInv = CNLabelContactRelationDaughter + case "son": + labelInv = CNLabelContactRelationSon + case "child": + labelInv = CNLabelContactRelationChild + case "father": + labelInv = CNLabelContactRelationFather + case "mother": + labelInv = CNLabelContactRelationMother + case "parent": + labelInv = CNLabelContactRelationParent + case "brother": + labelInv = CNLabelContactRelationBrother + case "sister": + labelInv = CNLabelContactRelationSister + case "friend": + labelInv = CNLabelContactRelationFriend + case "assistant": + labelInv = CNLabelContactRelationAssistant + case "manager": + labelInv = CNLabelContactRelationManager + default: + labelInv = label + } + c.contactRelations.append( + CNLabeledValue( + label: labelInv, + value: CNContactRelation(name: name) + ) + ) + } +} From 90c77f016e3527198bcf91355574f48e36fa0cba Mon Sep 17 00:00:00 2001 From: SoftWyer Date: Thu, 26 Oct 2023 13:54:22 +0100 Subject: [PATCH 5/6] Add support for X-ABRELATEDNAME --- lib/vcard.dart | 1 + test/testdata/vcards/ios.json | 7 ++++++- test/testdata/vcards/macos.json | 7 ++++++- test/testdata/vcards/whatsapp.json | 13 ++++++++++++- test/testdata/vcards/whatsapp.vcf | 6 +++++- 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/vcard.dart b/lib/vcard.dart index 36e04a41..ddccb4c1 100644 --- a/lib/vcard.dart +++ b/lib/vcard.dart @@ -165,6 +165,7 @@ class VCardParser { contact.emails.add(email); break; case 'RELATED': + case 'X-ABRELATEDNAMES': var relation = Relation(decode(content)); _parseLabel(params, labelOverride, _parseRelationLabel, relation); contact.relations.add(relation); diff --git a/test/testdata/vcards/ios.json b/test/testdata/vcards/ios.json index 0bca2e60..7fdaab8d 100644 --- a/test/testdata/vcards/ios.json +++ b/test/testdata/vcards/ios.json @@ -766,7 +766,12 @@ "customLabel": "Custom Event Date" } ], - "relations": [], + "relations": [ + { + "name": "Mom", + "label": "custom", "customLabel": "Mother" + } + ], "notes": [], "accounts": [], "groups": [] diff --git a/test/testdata/vcards/macos.json b/test/testdata/vcards/macos.json index ef2a32f1..96670ea4 100644 --- a/test/testdata/vcards/macos.json +++ b/test/testdata/vcards/macos.json @@ -766,7 +766,12 @@ "customLabel": "Custom Event Date" } ], - "relations": [], + "relations": [ + { + "name": "Mom", + "label": "custom", "customLabel": "Mother" + } + ], "notes": [ { "note": "Some notes 1\nSome notes 2" diff --git a/test/testdata/vcards/whatsapp.json b/test/testdata/vcards/whatsapp.json index 4f180b79..264e72e0 100644 --- a/test/testdata/vcards/whatsapp.json +++ b/test/testdata/vcards/whatsapp.json @@ -2317,7 +2317,18 @@ "customLabel": "" } ], - "relations": [], + "relations": [ + { + "name": "Curly", + "label": "custom", + "customLabel": "Father" + }, + { + "name": "Carlita", + "label": "spouse", + "customLabel": "" + } + ], "notes": [], "accounts": [], "groups": [] diff --git a/test/testdata/vcards/whatsapp.vcf b/test/testdata/vcards/whatsapp.vcf index 036b47c2..9cfc0045 100644 --- a/test/testdata/vcards/whatsapp.vcf +++ b/test/testdata/vcards/whatsapp.vcf @@ -20,6 +20,10 @@ item6.ADR;type=Home:;;123 Main St;Portland;TN;37148;USA item6.X-ABADR: item7.ADR;type=Work:;;1600 Amphitheatre Pkwy Mountain View CA 94043 USA;;;; item7.X-ABADR:ac +item8.X-ABRELATEDNAMES:Curly +item8.X-ABLabel:_$!!$_ +item9.X-ABRELATEDNAMES:Carlita +item9.X-ABLabel:_$!!$_ BDAY;value=date:1983-02-23 X-JABBER;type=CUSTOM:@jabber X-YAHOO;type=WORK:@yahoo @@ -28,4 +32,4 @@ X-AIM;type=CUSTOM:@aim X-MSN;type=HOME:@WindowsLive X-ICQ;type=CUSTOM:@icq PHOTO;BASE64:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAIQAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAGAAYAMBIgACEQEDEQH/xAAZAAEBAQEBAQAAAAAAAAAAAAACAwEABAn/xAAtEAEAAgIBAgQGAgIDAQAAAAABAhEAITESQQMiUWEEcYGRofATsULhMsHxBf/EABkBAAMBAQEAAAAAAAAAAAAAAAECAwAEBf/EAB4RAQEBAAICAwEAAAAAAAAAAAABEQIhEjEDQVFx/9oADAMBAAIRAxEAPwD5lT6eCFR5kd6/fnhthXQ6ooWt8XrDBKIldMebjfJ9+2SpkMWiJX2+n1z07Xfbns7ZI1GFccc0d3DSwFKPSw01VP72yf8AITVdoMfKVebJuQkdnpibPoulGXn6gDVxZND+1/Wcho6zquj1Pn+cJKTEsoN07D9vDKZfaSt39OcGwNN8z0uxu6ecpfX3tdb7mCUog2PVRexsrD/KdKRENAVzhl7NqsE8Kbqk9Ka9O1c44/DrHzeW9cXu+Px+c83V1ASKO2L+RYbE4FHSYfKM9VlUykPV1XXH19Lcfh+K+HDk5sOb1XrXdvPMeLfhsXe68uv14/OX8P4mMupV6Q/yb+lfSu2UnKLyxGHmnZqtqd/Tj7YOq6Tbehe34zSbDxAUTi639D1wMGMjypbpsyfTnq/wfx/xf/zvG/m+F8fxvhvGBP5fCkxkHpZsyXxHx3xHxsyfxPxHi/ETqh8STJ/OS8SbPd2HG8Gg1y+mTvGb5YXfppV/1WYNxVjZW2s7qLK9V3hJUPHrpwlN5p0HI5q8HVrnTmRn0GwT34zeoJc2e/tg/ja2Mdj2e/K7xRl/HMYXKt2ZOKxkevCXm10NlD/WNDe3oCMemErfVKOcW5QCmQNryVkoMi5f4n037XlfC64jEtgX3vf3yksUiRUrapCvKd8LH/EucTcbPrnWFSrWloqtazBok+WnVvPz/OTtLQlsWgj7GFNt2ve94/ElVVVdtYJeVuznFTrvK/Ps4V2b093nON+nyDMtWth3wA3a23XvilHylLswl3Q/TFGK8BrDx7rNiUW380+WOohVrZry/bMjZY3XpmwLmRWu3GNh4cQUAkx7pusoKHRwdx1T/wBcfnIsllb1LWxKysWYWg9TSsd/6/1jSnCS9QFB23Z65Nq27L7Gqcox6OnfBz+cKdotdv8AWLnYUZeZiXda3gkKW7jxqs1stOKvMlpDYcfPFTotRSytZ3Td749sTva+V4vnMO565mcLRw9t56PhZ+FHxJPiaimg9cgUVZndVJqu+sp8XO/FznOTc/WvpScozWpETekzgQ2Iu9ZO+zu+/bGMnfvuvTNyvlyvKjDJMoy2Xq9c446gCdCe3/X1MESUvEeoF5TK/wDIg01ret+t5opAYi0Ui3vnj9+2ZKB0UG/Ul6OOYTmt+mnWHqJeIr6rapzmvpqEo1TburcEgHjnv3x6ikdU99033zqC70cPvikxKZckW/W86tqUGIPJTVX3zZQqT5hN3WKDAtd69s2rJbTmjMuqCjXOr++d0BcRvfN84dZsVs5iBdmKnqRVN994ehI2SL9f9Y6aqr9InbjDN00KYvSJvqab198r4fTHxTzR6TV3+ftk4x6pNnEt33/ffKQP+IpaL7nv6/8AmPOjwvErw4UdMrbkdYvtk6jpTaXVUGWj0+FFSNrTem9Nm/f+sEvCgRjZbVjF55x6p4jOG/KDJbLiHbJMSSnU2f42VxlIw65eVSfFKW6M6rSba+gluLmp2JSgIWrvkp/e/wBs4ifKx/rHPwmbMSpHev7ycok9/wDGOjjFwuExp2Ux35v/ADDHwmWyNr2MueCoCS6QN/P6fv0zP4ZeHKItlnmjf4szSDiZ4fTFu3daTf7rE+E9FSlsbbfa8X8MpBHp47FcfbF4cWfihse2wP28aQZGAp5RAdAa+Xz9MrDqklxSRa32OPSgzvDOqoMxtoo5o1evdzokJPU2l3K7++v3ePD4/9k= -END:VCARD \ No newline at end of file +END:VCARD From 4bdffa7af1e78274ba5e015e3bb7a52245c01590 Mon Sep 17 00:00:00 2001 From: SoftWyer Date: Thu, 26 Oct 2023 15:12:04 +0100 Subject: [PATCH 6/6] Add support for X-ANDROID-CUSTOM relations --- lib/properties/relation.dart | 3 + lib/vcard.dart | 69 +++++++++++++++++++ .../vcards/android-default-contact-app.json | 4 +- .../vcards/android-default-contact-app.vcf | 2 + 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/lib/properties/relation.dart b/lib/properties/relation.dart index 421463a3..a7842649 100644 --- a/lib/properties/relation.dart +++ b/lib/properties/relation.dart @@ -52,6 +52,9 @@ class Relation { if (FlutterContacts.config.vCardVersion != VCardVersion.v4) { // Not supported in v3 + // TODO could possibly support the ABRELATEDNAMES, e.g. + // item8.X-ABRELATEDNAMES:ABABA + // item8.X-ABLabel:ZZZZZ return []; } diff --git a/lib/vcard.dart b/lib/vcard.dart index ddccb4c1..b76468ce 100644 --- a/lib/vcard.dart +++ b/lib/vcard.dart @@ -293,6 +293,8 @@ class VCardParser { // X-ANDROID-CUSTOM:vnd.android.cursor.item/contact_event;2017-09-23;0;Custom;;;;;;;;;;;; // and nicknames as // X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;Nick;1;;;;;;;;;;;;; + // and relations as + // X-ANDROID-CUSTOM:vnd.android.cursor.item/relation;Monk;0;Not a relation;;;;;;;;;;;; final contentParts = content.split(';'); final n = contentParts.length; if (n < 2) { @@ -316,6 +318,16 @@ class VCardParser { case 'vnd.android.cursor.item/nickname': contact.name.nickname = decode(contentParts[1]); break; + case 'vnd.android.cursor.item/relation': + final name = decode(contentParts[1]); + final labelStr = n >= 3 ? contentParts[2] : ''; + final customLabelStr = n >= 4 ? contentParts[3] : ''; + contact.relations.add(_parseAndroidRelation( + name, + labelStr, + customLabelStr, + )); + break; } break; case 'X-AIM': @@ -630,3 +642,60 @@ void _parseLabel( } } } + +Relation _parseAndroidRelation( + String name, + String labelStr, + String customLabelStr, +) { + late final RelationLabel label; + switch (labelStr) { + case '1': + label = RelationLabel.assistant; + break; + case '2': + label = RelationLabel.brother; + break; + case '3': + label = RelationLabel.child; + break; + case '4': + label = RelationLabel.domesticPartner; + break; + case '5': + label = RelationLabel.father; + break; + case '6': + label = RelationLabel.friend; + break; + case '7': + label = RelationLabel.manager; + break; + case '8': + label = RelationLabel.mother; + break; + case '9': + label = RelationLabel.parent; + break; + case '10': + label = RelationLabel.partner; + break; + case '11': + label = RelationLabel.referredBy; + break; + case '12': + label = RelationLabel.relative; + break; + case '13': + label = RelationLabel.sister; + break; + case '14': + label = RelationLabel.spouse; + break; + default: + label = RelationLabel.custom; + break; + } + + return Relation(name, label: label, customLabel: customLabelStr); +} diff --git a/test/testdata/vcards/android-default-contact-app.json b/test/testdata/vcards/android-default-contact-app.json index 1627d5cf..798a92c5 100644 --- a/test/testdata/vcards/android-default-contact-app.json +++ b/test/testdata/vcards/android-default-contact-app.json @@ -2351,7 +2351,9 @@ "relations": [ {"name": "Tony Maloney", "label": "friend", "customLabel": ""}, {"name": "Arnold Scwartz", "label": "relative", "customLabel": ""}, - {"name": "Dr John Jøhñ Smith, Jr.", "label": "relative", "customLabel":""} + {"name": "Dr John Jøhñ Smith, Jr.", "label": "relative", "customLabel":""}, + {"name": "Monk", "label": "custom", "customLabel": "Not a relation"}, + {"name": "Good Sister", "label": "sister", "customLabel": ""} ], "notes": [ { diff --git a/test/testdata/vcards/android-default-contact-app.vcf b/test/testdata/vcards/android-default-contact-app.vcf index b4291cab..03f17934 100644 --- a/test/testdata/vcards/android-default-contact-app.vcf +++ b/test/testdata/vcards/android-default-contact-app.vcf @@ -74,4 +74,6 @@ RELATED;TYPE=friend;VALUE=text:Tony Maloney RELATED:Arnold Scwartz RELATED;TYPE=me;ENCODING=QUOTED-PRINTABLE:=44=72=20=4A=6F=68=6E=20=4A=C3=B8=68=C3=B1=20=53=6D=69=74=68=2C=20=4A= =72=2E +X-ANDROID-CUSTOM:vnd.android.cursor.item/relation;Monk;0;Not a relation;;;;;;;;;;;; +X-ANDROID-CUSTOM:vnd.android.cursor.item/relation;Good Sister;13;;;;;;;;;;;;; END:VCARD