2 * Copyright (C) 2011 Collabora Ltd.
4 * This library is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 2.1 of the License, or
7 * (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library. If not, see <http://www.gnu.org/licenses/>.
18 * Travis Reitter <travis.reitter@collabora.co.uk>
19 * Marco Barisione <marco.barisione@collabora.co.uk>
20 * Raul Gutierrez Segales <raul.gutierrez.segales@collabora.co.uk>
30 * A persona subclass which represents a single EDS contact.
32 public class Edsf.Persona : Folks.Persona,
50 /* The following 4 definitions are used by the tests */
52 * vCard field names for telephone numbers.
56 public static const string[] phone_fields = {
57 "assistant_phone", "business_phone", "business_phone_2", "callback_phone",
58 "car_phone", "company_phone", "home_phone", "home_phone_2", "isdn_phone",
59 "mobile_phone", "other_phone", "primary_phone"
62 * vCard field names for postal addresses.
66 public static const string[] address_fields = {
67 "address_home", "address_other", "address_work"
70 * vCard field names for e-mail addresses.
74 public static const string[] email_fields = {
75 "email_1", "email_2", "email_3", "email_4"
80 * vCard field names for miscellaneous URIs.
84 public static const string[] url_properties = {
85 "blog_url", "fburl", "homepage_url", "video_url"
88 /* Some types of URLs are represented in EDS using custom vCard fields rather
89 * than the X-URIS field. Here are mappings between the custom vCard field
90 * names which EDS uses, and the TYPE values which folks uses which map to
92 private struct UrlTypeMapping
94 string vcard_field_name;
98 internal static const UrlTypeMapping[] _url_properties =
100 { "homepage_url", UrlFieldDetails.PARAM_TYPE_HOME_PAGE },
101 { "blog_url", UrlFieldDetails.PARAM_TYPE_BLOG },
102 { "fburl", "x-free-busy" },
103 { "video_url", "x-video" }
107 * The vCard attribute used to specify a Contact's gender
110 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
112 * Note that the above document is a draft and the gender property
113 * is still considered experimental, hence the "X-" prefix in the
114 * attribute name. So this might change.
118 public static const string gender_attribute_name = "X-GENDER";
121 * The value used to define the male gender for the
122 * X-GENDER vCard property.
125 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
129 public static const string gender_male = "M";
132 * The value used to define the female gender for the
133 * X-GENDER vCard property.
136 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
140 public static const string gender_female = "F";
142 private const string[] _linkable_properties = { "im-addresses",
144 "web-service-addresses" };
146 private HashSet<PhoneFieldDetails> _phone_numbers;
147 private Set<PhoneFieldDetails> _phone_numbers_ro;
148 private HashSet<EmailFieldDetails> _email_addresses;
149 private Set<EmailFieldDetails> _email_addresses_ro;
150 private HashSet<NoteFieldDetails> _notes;
151 private Set<NoteFieldDetails> _notes_ro;
152 private static HashTable<string, E.ContactField>? _im_eds_map = null;
154 private HashSet<PostalAddressFieldDetails> _postal_addresses;
155 private Set<PostalAddressFieldDetails> _postal_addresses_ro;
157 private HashSet<string> _local_ids;
158 private Set<string> _local_ids_ro;
160 private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
162 private bool _is_favourite;
164 private E.Contact _contact; /* should be set on construct */
167 * The e-d-s contact represented by this Persona
169 public E.Contact contact
171 get { return this._contact; }
172 construct { this._contact = value; }
178 [CCode (notify = false)]
179 public MultiMap<string, WebServiceFieldDetails> web_service_addresses
181 get { return this._web_service_addresses; }
182 set { this.change_web_service_addresses.begin (value); }
190 public async void change_web_service_addresses (
191 MultiMap<string, WebServiceFieldDetails> web_service_addresses)
194 yield ((Edsf.PersonaStore) this.store)._set_web_service_addresses (this,
195 web_service_addresses);
199 * IDs used to link {@link Edsf.Persona}s.
201 [CCode (notify = false)]
202 public Set<string> local_ids
206 if (this._local_ids.contains (this.iid) == false)
208 this._local_ids.add (this.iid);
210 return this._local_ids_ro;
212 set { this.change_local_ids.begin (value); }
220 public async void change_local_ids (Set<string> local_ids)
223 yield ((Edsf.PersonaStore) this.store)._set_local_ids (this, local_ids);
227 * The postal addresses of the contact.
229 * A list of postal addresses associated to the contact.
233 [CCode (notify = false)]
234 public Set<PostalAddressFieldDetails> postal_addresses
236 get { return this._postal_addresses_ro; }
237 set { this.change_postal_addresses.begin (value); }
245 public async void change_postal_addresses (
246 Set<PostalAddressFieldDetails> postal_addresses) throws PropertyError
248 yield ((Edsf.PersonaStore) this.store)._set_postal_addresses (this,
257 [CCode (notify = false)]
258 public Set<PhoneFieldDetails> phone_numbers
260 get { return this._phone_numbers_ro; }
261 set { this.change_phone_numbers.begin (value); }
269 public async void change_phone_numbers (
270 Set<PhoneFieldDetails> phone_numbers) throws PropertyError
272 yield ((Edsf.PersonaStore) this.store)._set_phones (this, phone_numbers);
280 [CCode (notify = false)]
281 public Set<EmailFieldDetails> email_addresses
283 get { return this._email_addresses_ro; }
284 set { this.change_email_addresses.begin (value); }
292 public async void change_email_addresses (
293 Set<EmailFieldDetails> email_addresses) throws PropertyError
295 yield ((Edsf.PersonaStore) this.store)._set_emails (this,
304 [CCode (notify = false)]
305 public Set<NoteFieldDetails> notes
307 get { return this._notes_ro; }
308 set { this.change_notes.begin (value); }
316 public async void change_notes (Set<NoteFieldDetails> notes)
319 yield ((Edsf.PersonaStore) this.store)._set_notes (this, notes);
327 public override string[] linkable_properties
329 get { return Persona._linkable_properties; }
337 public override string[] writeable_properties
339 get { return this.store.always_writeable_properties; }
342 private LoadableIcon? _avatar = null;
344 * An avatar for the Persona.
346 * See {@link Folks.AvatarDetails.avatar}.
350 [CCode (notify = false)]
351 public LoadableIcon? avatar
353 get { return this._avatar; }
354 set { this.change_avatar.begin (value); }
362 public async void change_avatar (LoadableIcon? avatar) throws PropertyError
364 yield ((Edsf.PersonaStore) this.store)._set_avatar (this, avatar);
367 private StructuredName? _structured_name = null;
373 [CCode (notify = false)]
374 public StructuredName? structured_name
376 get { return this._structured_name; }
377 set { this.change_structured_name.begin (value); }
385 public async void change_structured_name (StructuredName? structured_name)
388 yield ((Edsf.PersonaStore) this.store)._set_structured_name (this,
393 * The e-d-s contact uid
397 public string contact_id { get; construct; }
399 private string _full_name = "";
405 [CCode (notify = false)]
406 public string full_name
408 get { return this._full_name; }
409 set { this.change_full_name.begin (value); }
417 public async void change_full_name (string full_name) throws PropertyError
419 yield ((Edsf.PersonaStore) this.store)._set_full_name (this, full_name);
422 private string _nickname = "";
428 [CCode (notify = false)]
429 public string nickname
431 get { return this._nickname; }
432 set { this.change_nickname.begin (value); }
440 public async void change_nickname (string nickname) throws PropertyError
442 yield ((Edsf.PersonaStore) this.store)._set_nickname (this, nickname);
445 private Gender _gender;
451 [CCode (notify = false)]
454 get { return this._gender; }
455 set { this.change_gender.begin (value); }
463 public async void change_gender (Gender gender) throws PropertyError
465 yield ((Edsf.PersonaStore) this.store)._set_gender (this, gender);
468 private HashSet<UrlFieldDetails> _urls;
469 private Set<UrlFieldDetails> _urls_ro;
475 [CCode (notify = false)]
476 public Set<UrlFieldDetails> urls
478 get { return this._urls_ro; }
479 set { this.change_urls.begin (value); }
487 public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
489 yield ((Edsf.PersonaStore) this.store)._set_urls (this, urls);
492 private HashMultiMap<string, ImFieldDetails> _im_addresses =
493 new HashMultiMap<string, ImFieldDetails> (null, null,
494 (GLib.HashFunc) ImFieldDetails.hash,
495 (GLib.EqualFunc) ImFieldDetails.equal);
502 [CCode (notify = false)]
503 public MultiMap<string, ImFieldDetails> im_addresses
505 get { return this._im_addresses; }
506 set { this.change_im_addresses.begin (value); }
514 public async void change_im_addresses (
515 MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
517 yield ((Edsf.PersonaStore) this.store)._set_im_fds (this, im_addresses);
520 private HashSet<string> _groups;
521 private Set<string> _groups_ro;
528 [CCode (notify = false)]
529 public Set<string> groups
531 get { return this._groups_ro; }
532 set { this.change_groups.begin (value); }
540 public async void change_group (string group, bool is_member)
544 if ((is_member == true && this._groups.contains (group) == true) ||
545 (is_member == false && this._groups.contains (group) == false))
550 /* Replace the current set of groups with a modified one. */
551 var new_groups = new HashSet<string> ();
552 foreach (var category_name in this._groups)
554 new_groups.add (category_name);
557 if (is_member == false)
559 new_groups.remove (group);
563 new_groups.add (group);
566 yield this.change_groups (new_groups);
574 public async void change_groups (Set<string> groups) throws PropertyError
576 yield ((Edsf.PersonaStore) this.store)._set_groups (this, groups);
582 * e-d-s has no equivalent field, so this is unsupported.
586 [CCode (notify = false)]
587 public string? calendar_event_id
589 get { return null; } /* unsupported */
590 set { this.change_calendar_event_id.begin (value); } /* not writeable */
593 /* We cache the timezone we use for converting birthdays to UTC since creating
594 * it requires mmapping /etc/localtime, which means lots of syscalls. */
595 private static TimeZone _local_time_zone = new TimeZone.local ();
597 private DateTime? _birthday = null;
603 [CCode (notify = false)]
604 public DateTime? birthday
606 get { return this._birthday; }
607 set { this.change_birthday.begin (value); }
615 public async void change_birthday (DateTime? bday)
618 yield ((Edsf.PersonaStore) this.store)._set_birthday (this,
622 private HashSet<RoleFieldDetails> _roles;
623 private Set<RoleFieldDetails> _roles_ro;
630 [CCode (notify = false)]
631 public Set<RoleFieldDetails> roles
633 get { return this._roles_ro; }
634 set { this.change_roles.begin (value); }
642 public async void change_roles (Set<RoleFieldDetails> roles)
645 yield ((Edsf.PersonaStore) this.store)._set_roles (this, roles);
649 * Whether this contact is a user-defined favourite.
653 [CCode (notify = false)]
654 public bool is_favourite
656 get { return this._is_favourite; }
657 set { this.change_is_favourite.begin (value); }
665 public async void change_is_favourite (bool is_favourite) throws PropertyError
667 if (this._is_favourite == is_favourite)
672 yield ((Edsf.PersonaStore) this.store)._set_is_favourite (this,
676 private HashSet<string> _anti_links;
677 private Set<string> _anti_links_ro;
684 [CCode (notify = false)]
685 public Set<string> anti_links
687 get { return this._anti_links_ro; }
688 set { this.change_anti_links.begin (value); }
696 public async void change_anti_links (Set<string> anti_links)
699 yield ((Edsf.PersonaStore) this.store)._set_anti_links (this, anti_links);
702 private bool _in_google_personal_group;
705 * Whether this contact is in the “My Contacts” section of the user’s address
706 * book, rather than the “Other” section.
710 [CCode (notify = false)]
711 public bool in_google_personal_group
713 get { return this._in_google_personal_group; }
719 * @param store_id the {@link PersonaStore.id}
720 * @param contact the Contact
721 * @return a valid IID
725 internal static string build_iid_from_contact (string store_id,
729 Edsf.Persona._get_property_from_contact<string> (contact, "id");
730 return Edsf.Persona.build_iid (store_id, (!) (contact_id ?? ""));
736 * @param store_id the {@link PersonaStore.id}
737 * @param contact_id the id belonging to the Contact
738 * @return a valid IID
742 internal static string build_iid (string store_id, string contact_id)
744 return "%s:%s".printf (store_id, contact_id);
749 * Create a new persona.
751 * Create a new persona for the {@link PersonaStore} `store`, representing
752 * the EDS contact given by `contact`.
754 * @param store the store which will contain the persona
755 * @param contact the EDS contact being represented by the persona
759 public Persona (PersonaStore store, E.Contact contact)
762 Edsf.Persona._get_property_from_contact<string> (contact, "id");
763 var contact_id = (!) (_contact_id ?? "");
765 var uid = Folks.Persona.build_uid (BACKEND_NAME, store.id, contact_id);
766 var iid = Edsf.Persona.build_iid (store.id, contact_id);
767 var is_user = BookClient.is_self (contact);
769 Edsf.Persona._get_property_from_contact<string> (contact,
771 var full_name = (!) (_full_name ?? "");
773 Object (display_id: full_name,
778 contact_id: contact_id,
784 debug ("Creating new Edsf.Persona with IID '%s'", this.iid);
786 this._gender = Gender.UNSPECIFIED;
787 this._phone_numbers = new HashSet<PhoneFieldDetails> (
788 (GLib.HashFunc) PhoneFieldDetails.hash,
789 (GLib.EqualFunc) PhoneFieldDetails.equal);
790 this._phone_numbers_ro = this._phone_numbers.read_only_view;
791 this._email_addresses = new HashSet<EmailFieldDetails> (
792 (GLib.HashFunc) EmailFieldDetails.hash,
793 (GLib.EqualFunc) EmailFieldDetails.equal);
794 this._email_addresses_ro = this._email_addresses.read_only_view;
795 this._notes = new HashSet<NoteFieldDetails> (
796 (GLib.HashFunc) NoteFieldDetails.hash,
797 (GLib.EqualFunc) NoteFieldDetails.equal);
798 this._notes_ro = this._notes.read_only_view;
799 this._urls = new HashSet<UrlFieldDetails> (
800 (GLib.HashFunc) UrlFieldDetails.hash,
801 (GLib.EqualFunc) UrlFieldDetails.equal);
802 this._urls_ro = this._urls.read_only_view;
803 this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
804 (GLib.HashFunc) PostalAddressFieldDetails.hash,
805 (GLib.EqualFunc) PostalAddressFieldDetails.equal);
806 this._postal_addresses_ro = this._postal_addresses.read_only_view;
807 this._local_ids = new HashSet<string> ();
808 this._local_ids_ro = this._local_ids.read_only_view;
809 this._web_service_addresses =
810 new HashMultiMap<string, WebServiceFieldDetails> (
812 (GLib.HashFunc) WebServiceFieldDetails.hash,
813 (GLib.EqualFunc) WebServiceFieldDetails.equal);
814 this._groups = new HashSet<string> ();
815 this._groups_ro = this._groups.read_only_view;
816 this._roles = new HashSet<RoleFieldDetails> (
817 (GLib.HashFunc) RoleFieldDetails.hash,
818 (GLib.EqualFunc) RoleFieldDetails.equal);
819 this._roles_ro = this._roles.read_only_view;
820 this._anti_links = new HashSet<string> ();
821 this._anti_links_ro = this._anti_links.read_only_view;
823 this._update (this._contact);
831 public override void linkable_property_to_links (string prop_name,
832 Folks.Persona.LinkablePropertyCallback callback)
834 if (prop_name == "im-addresses")
836 foreach (var protocol in this._im_addresses.get_keys ())
838 var im_fds = this._im_addresses.get (protocol);
840 foreach (var im_fd in im_fds)
841 callback (protocol + ":" + im_fd.value);
844 else if (prop_name == "local-ids")
846 /* Note: we need to use this.local_ids and not this._local_ids,
847 * otherwise this can have a different behaviour depending
848 * on the state of the current Persona depending on whether
849 * this.local_ids was called before or not. */
850 foreach (var id in this.local_ids)
855 else if (prop_name == "web-service-addresses")
857 foreach (var web_service in this.web_service_addresses.get_keys ())
859 var web_service_addresses =
860 this._web_service_addresses.get (web_service);
862 foreach (var ws_fd in web_service_addresses)
863 callback (web_service + ":" + ws_fd.value);
869 base.linkable_property_to_links (prop_name, callback);
875 debug ("Destroying Edsf.Persona '%s': %p", this.uid, this);
879 * Update attribs of the persona.
881 internal void _update (E.Contact updated_contact)
883 this.freeze_notify ();
885 /* We get a new E.Contact instance from EDS containing all the updates,
886 * so replace our existing contact with it. */
887 this._contact = updated_contact;
888 this.notify_property ("contact");
890 this._update_names ();
891 this._update_avatar ();
892 this._update_urls ();
893 this._update_phones ();
894 this._update_addresses ();
895 this._update_emails ();
897 /* Note: because we assume certain e-mail addresses
898 * (@gmail, @msn, etc) to also be IM IDs we /must/
899 * update the latter after we've taken care of the former.
901 this._update_im_addresses ();
903 this._update_groups ();
904 this._update_notes ();
905 this._update_local_ids ();
906 this._update_web_services_addresses ();
907 this._update_gender ();
908 this._update_birthday ();
909 this._update_roles ();
910 this._update_favourite ();
911 this._update_anti_links ();
916 private void _update_params (AbstractFieldDetails details,
917 E.VCardAttribute attr)
919 foreach (unowned E.VCardAttributeParam param in attr.get_params ())
921 string param_name = param.get_name ().down ();
922 foreach (unowned string param_value in param.get_values ())
924 if (param_name == AbstractFieldDetails.PARAM_TYPE)
926 details.add_parameter (param_name, param_value.down ());
930 details.add_parameter (param_name, param_value);
936 private void _update_gender ()
938 var gender = Gender.UNSPECIFIED;
940 this.contact.get_attribute (Edsf.Persona.gender_attribute_name);
942 if (gender_attr != null)
944 var val = ((!) gender_attr).get_value ();
947 switch (((!) val).up ())
949 case Edsf.Persona.gender_male:
950 gender = Gender.MALE;
952 case Edsf.Persona.gender_female:
953 gender = Gender.FEMALE;
956 /* Unspecified, as above */
962 if (this._gender != gender)
964 this._gender = gender;
965 this.notify_property ("gender");
969 private void _update_birthday ()
971 var _bday = this._get_property<E.ContactDate> ("birth_date");
975 var bday = (!) _bday;
977 /* Since e-d-s stores birthdays as a plain date, we take the
978 * given date in local time and convert it to UTC as mandated
979 * by the BirthdayDetails interface.
980 * We cache the timezone since creating it requires mmapping
981 * /etc/localtime, which means lots of syscalls. */
982 var d = new DateTime (Persona._local_time_zone,
983 (int) bday.year, (int) bday.month, (int) bday.day, 0, 0, 0.0);
984 if (this._birthday == null ||
985 (this._birthday != null &&
986 !((!) this._birthday).equal (d.to_utc ())))
988 this._birthday = d.to_utc ();
989 this.notify_property ("birthday");
994 if (this._birthday != null)
996 this._birthday = null;
997 this.notify_property ("birthday");
1002 private void _update_roles ()
1004 var new_roles = new HashSet<RoleFieldDetails> (
1005 (GLib.HashFunc) RoleFieldDetails.hash,
1006 (GLib.EqualFunc) RoleFieldDetails.equal);
1008 var default_role_fd = this._get_default_role ();
1009 if (default_role_fd != null)
1011 new_roles.add ((!) default_role_fd);
1014 var vcard = (E.VCard) this.contact;
1015 foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1017 if (attr.get_name () != "X-ROLES")
1020 var val = attr.get_value ();
1021 if (val == null || (!) val == "")
1026 var role = new Role ("", "");
1027 role.role = (!) val;
1028 var role_fd = new RoleFieldDetails (role);
1030 foreach (unowned E.VCardAttributeParam param in
1033 string param_name = param.get_name ().down ();
1035 if (param_name == "organisation_name")
1037 foreach (unowned string param_value in
1038 param.get_values ())
1040 role.organisation_name = param_value;
1044 else if (param_name == "title")
1046 foreach (unowned string param_value in
1047 param.get_values ())
1049 role.title = param_value;
1055 foreach (unowned string param_value in
1056 param.get_values ())
1058 role_fd.add_parameter (param_name, param_value);
1063 new_roles.add (role_fd);
1066 if (!Folks.Internal.equal_sets<RoleFieldDetails> (new_roles, this._roles))
1068 this._roles = new_roles;
1069 this._roles_ro = new_roles.read_only_view;
1070 this.notify_property ("roles");
1074 private RoleFieldDetails? _get_default_role ()
1076 RoleFieldDetails? _default_role = null;
1078 var org = this._get_property<string> ("org");
1079 var org_unit = this._get_property<string> ("org_unit");
1080 var office = this._get_property<string> ("office");
1081 var title = this._get_property<string> ("title");
1082 var role = this._get_property<string> ("role");
1083 var manager = this._get_property<string> ("manager");
1084 var assistant = this._get_property<string> ("assistant");
1094 var new_role = new Role (title, org);
1095 if (role != null && (!) role != "")
1096 new_role.role = (!) role;
1098 /* Check if it's non-empty. */
1099 if (!new_role.is_empty ())
1101 var default_role = new RoleFieldDetails (new_role);
1103 if (org_unit != null && org_unit != "")
1104 default_role.set_parameter ("org_unit", (!) org_unit);
1106 if (office != null && office != "")
1107 default_role.set_parameter ("office", (!) office);
1109 if (manager != null && manager != "")
1110 default_role.set_parameter ("manager", (!) manager);
1112 if (assistant != null && manager != "")
1113 default_role.set_parameter ("assistant", (!) assistant);
1115 _default_role = default_role;
1119 return _default_role;
1122 private void _update_web_services_addresses ()
1124 var new_services = new HashMultiMap<string, WebServiceFieldDetails> (
1126 (GLib.HashFunc) WebServiceFieldDetails.hash,
1127 (GLib.EqualFunc) WebServiceFieldDetails.equal);
1129 var services = this.contact.get_attribute ("X-FOLKS-WEB-SERVICES-IDS");
1130 if (services != null)
1132 foreach (var service in ((!) services).get_params ())
1134 var service_name = service.get_name ().down ();
1135 foreach (var service_id in service.get_values ())
1137 if (service_id == "")
1142 new_services.set (service_name,
1143 new WebServiceFieldDetails (service_id));
1148 if (!Utils.multi_map_str_afd_equal (new_services,
1149 this._web_service_addresses))
1151 this._web_service_addresses = new_services;
1152 this.notify_property ("web-service-addresses");
1156 private void _update_emails ()
1158 var new_email_addresses = new HashSet<EmailFieldDetails> (
1159 (GLib.HashFunc) EmailFieldDetails.hash,
1160 (GLib.EqualFunc) EmailFieldDetails.equal);
1162 var attrs = this.contact.get_attributes (E.ContactField.EMAIL);
1163 foreach (var attr in attrs)
1165 var val = attr.get_value ();
1166 if (val == null || (!) val == "")
1171 var email_fd = new EmailFieldDetails ((!) val);
1172 this._update_params (email_fd, attr);
1173 new_email_addresses.add (email_fd);
1176 if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
1177 this._email_addresses))
1179 this._email_addresses = new_email_addresses;
1180 this._email_addresses_ro = new_email_addresses.read_only_view;
1181 this.notify_property ("email-addresses");
1185 private void _update_notes ()
1187 var new_notes = new HashSet<NoteFieldDetails> (
1188 (GLib.HashFunc) NoteFieldDetails.hash,
1189 (GLib.EqualFunc) NoteFieldDetails.equal);
1191 var n = this._get_property<string> ("note");
1192 if (n != null && n != "")
1194 var note = new NoteFieldDetails ((!) n);
1195 new_notes.add (note);
1198 if (!Folks.Internal.equal_sets<NoteFieldDetails> (new_notes, this._notes))
1200 this._notes = new_notes;
1201 this._notes_ro = this._notes.read_only_view;
1202 this.notify_property ("notes");
1206 private void _update_names ()
1208 var _full_name = this._get_property<string> ("full_name");
1210 if (_full_name == null)
1215 var full_name = (!) _full_name;
1217 if (this._full_name != full_name)
1219 this._full_name = full_name;
1220 this.notify_property ("full-name");
1223 var _nickname = this._get_property<string> ("nickname");
1225 if (_nickname == null)
1230 var nickname = (!) _nickname;
1232 if (this._nickname != nickname)
1234 this._nickname = nickname;
1235 this.notify_property ("nickname");
1238 StructuredName? structured_name = null;
1239 var _cn = this._get_property<E.ContactName> ("name");
1244 string family_name = cn.family;
1245 string given_name = cn.given;
1246 string additional_names = cn.additional;
1247 string prefixes = cn.prefixes;
1248 string suffixes = cn.suffixes;
1249 structured_name = new StructuredName (family_name, given_name,
1250 additional_names, prefixes,
1254 if (structured_name != null && !((!) structured_name).is_empty ())
1256 this._structured_name = (!) structured_name;
1257 this.notify_property ("structured-name");
1259 else if (this._structured_name != null)
1261 this._structured_name = null;
1262 this.notify_property ("structured-name");
1266 private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? _p)
1277 case ContactPhotoType.URI:
1278 var uri = p.get_uri ();
1284 return new FileIcon (File.new_for_uri ((!) uri));
1285 case ContactPhotoType.INLINED:
1286 var data = p.get_inlined ();
1287 var mime_type = p.get_mime_type ();
1288 if (data == null || mime_type == null)
1293 return new Edsf.MemoryIcon ((!) mime_type, (!) data);
1299 private void _update_avatar ()
1301 var p = this._get_property<E.ContactPhoto> ("photo");
1303 var cache = AvatarCache.dup ();
1305 // Convert the ContactPhoto to a LoadableIcon and store or update it.
1306 var new_avatar = this._contact_photo_to_loadable_icon (p);
1308 if (this._avatar != null && new_avatar == null)
1310 // Remove the old cached avatar, ignoring errors.
1311 cache.remove_avatar.begin (this.uid, (obj, res) =>
1315 cache.remove_avatar.end (res);
1317 catch (GLib.Error e1) {}
1319 this._avatar = null;
1320 this.notify_property ("avatar");
1323 else if ((this._avatar == null && new_avatar != null) ||
1324 (this._avatar != null && new_avatar != null &&
1325 ((!) this._avatar).equal (new_avatar) == false))
1327 /* Store the new avatar in the cache. new_avatar is guaranteed to be
1329 cache.store_avatar.begin (this.uid, (!) new_avatar, (obj, res) =>
1333 cache.store_avatar.end (res);
1334 this._avatar = new_avatar;
1335 this.notify_property ("avatar");
1337 catch (GLib.Error e2)
1339 warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
1340 this.uid, e2.message);
1341 new_avatar = null; /* failure */
1347 private void _update_urls ()
1349 var new_urls = new HashSet<UrlFieldDetails> ();
1351 /* First we get the standard Evo urls.. */
1352 foreach (var mapping in Persona._url_properties)
1354 var url_property = mapping.vcard_field_name;
1355 var folks_type = mapping.folks_type;
1357 var u = this._get_property<string> (url_property);
1358 if (u != null && u != "")
1360 var fd_u = new UrlFieldDetails ((!) u);
1361 fd_u.set_parameter (AbstractFieldDetails.PARAM_TYPE, folks_type);
1362 new_urls.add (fd_u);
1366 /* Now we go for extra URLs */
1367 var vcard = (E.VCard) this.contact;
1368 foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1370 if (attr.get_name () == "X-URIS")
1372 var val = attr.get_value ();
1373 if (val == null || (!) val == "")
1378 var url_fd = new UrlFieldDetails ((!) val);
1379 this._update_params (url_fd, attr);
1380 new_urls.add (url_fd);
1384 if (!Utils.set_afd_equal (new_urls, this._urls))
1386 this._urls = new_urls;
1387 this._urls_ro = new_urls.read_only_view;
1388 this.notify_property ("urls");
1392 private void _update_im_addresses ()
1394 var im_eds_map = Persona._get_im_eds_map ();
1395 var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (null,
1396 null, (GLib.HashFunc) ImFieldDetails.hash,
1397 (GLib.EqualFunc) ImFieldDetails.equal);
1399 foreach (var im_proto in im_eds_map.get_keys ())
1401 var addresses = this.contact.get_attributes (
1402 im_eds_map.lookup (im_proto));
1403 foreach (var attr in addresses)
1407 var addr = attr.get_value ();
1408 if (addr == null || (!) addr == "")
1413 string normalised_addr =
1414 (owned) ImDetails.normalise_im_address ((!) addr, im_proto);
1416 if (normalised_addr == "")
1421 var im_fd = new ImFieldDetails (normalised_addr);
1422 new_im_addresses.set (im_proto, im_fd);
1424 catch (Folks.ImDetailsError e)
1427 "Problem when trying to normalise address: %s\n",
1433 /* We consider some e-mail addresses to be IM IDs too. This
1434 * is pretty much a hack to make sure e-d-s contacts are
1435 * automatically linked with their corresponding Telepathy
1436 * Persona. As an undesired side effect we might end up having
1437 * IM addresses that aren't actually used as such (i.e.: people
1438 * who don't actually use GMail or MSN addresses for IM).
1442 foreach (var email in this.email_addresses)
1444 var _proto = this._im_proto_from_addr (email.value);
1447 var proto = (!) _proto;
1449 /* Has this already been added? */
1451 Collection<ImFieldDetails>? current_im_addrs =
1452 new_im_addresses.get (proto);
1453 if (current_im_addrs != null)
1455 foreach (var cur_im in (!) current_im_addrs)
1457 if (cur_im.value == email.value)
1470 string normalised_addr =
1471 (owned) ImDetails.normalise_im_address (email.value, proto);
1472 var im_fd = new ImFieldDetails (normalised_addr);
1473 new_im_addresses.set (proto, im_fd);
1475 catch (Folks.ImDetailsError e)
1478 "Problem when trying to normalise address: %s\n",
1484 if (!Utils.multi_map_str_afd_equal (new_im_addresses,
1485 this._im_addresses))
1487 this._im_addresses = new_im_addresses;
1488 this.notify_property ("im-addresses");
1492 private void _update_groups ()
1494 var category_names =
1495 this._contact.get<GLib.List<string>> (E.ContactField.CATEGORY_LIST);
1496 var new_categories = new HashSet<string> ();
1497 var added_categories = new LinkedList<string> ();
1499 foreach (var category_name in category_names)
1501 /* Skip the “Starred in Android” group for Google personas; we handle
1503 if (((Edsf.PersonaStore) store)._is_google_contacts_address_book () &&
1504 category_name == Edsf.PersonaStore.android_favourite_group_name)
1509 new_categories.add (category_name);
1511 /* Is this a new category? */
1512 if (!this._groups.contains (category_name))
1514 added_categories.add (category_name);
1518 /* Work out which categories have been removed. */
1519 var removed_categories = new LinkedList<string> ();
1521 foreach (var category_name in this._groups)
1523 /* Skip the “Starred in Android” group for Google personas; we handle
1525 if (((Edsf.PersonaStore) store)._is_google_contacts_address_book () &&
1526 category_name == Edsf.PersonaStore.android_favourite_group_name)
1531 if (!new_categories.contains (category_name))
1533 removed_categories.add (category_name);
1537 /* Check our new set of system groups if this is a Google address book. */
1538 var store = (Edsf.PersonaStore) this.store;
1539 var in_google_personal_group = false;
1541 if (store._is_google_contacts_address_book ())
1543 var vcard = (E.VCard) this.contact;
1544 unowned E.VCardAttribute? attr =
1545 vcard.get_attribute ("X-GOOGLE-SYSTEM-GROUP-IDS");
1548 unowned GLib.List<string> vals = attr.get_values ();
1550 /* If we're in the GDATA_CONTACTS_GROUP_CONTACTS group, then
1551 * we're in the user's "My Contacts" address book, as opposed
1552 * to their "Other" address book. */
1553 foreach (var system_group_id in vals)
1555 if (system_group_id == "Contacts")
1557 in_google_personal_group = true;
1564 /* Check whether our favourite status needs updating. */
1565 var old_is_favourite = this._is_favourite;
1567 if (store._is_google_contacts_address_book ())
1569 this._is_favourite = false;
1571 foreach (var category_name in category_names)
1573 /* We link the “Starred in Android” group to Google Contacts
1574 * address books. See: bgo#661490. */
1575 if (category_name ==
1576 Edsf.PersonaStore.android_favourite_group_name)
1578 this._is_favourite = true;
1583 /* Notify if anything's changed. */
1584 this.freeze_notify ();
1586 if (added_categories.size != 0 || removed_categories.size != 0)
1588 this.notify_property ("groups");
1590 if (this._is_favourite != old_is_favourite)
1592 this.notify_property ("is-favourite");
1594 if (in_google_personal_group != this._in_google_personal_group)
1596 this._in_google_personal_group = in_google_personal_group;
1597 this.notify_property ("in-google-personal-group");
1600 this.thaw_notify ();
1604 * build a table of im protocols / im protocol aliases
1606 internal static HashTable<string, E.ContactField> _get_im_eds_map ()
1608 HashTable<string, E.ContactField> retval;
1610 lock (Edsf.Persona._im_eds_map)
1612 if (Edsf.Persona._im_eds_map == null)
1615 new HashTable<string, E.ContactField> (str_hash, str_equal);
1617 table.insert ("aim", ContactField.IM_AIM);
1618 table.insert ("yahoo", ContactField.IM_YAHOO);
1619 table.insert ("groupwise", ContactField.IM_GROUPWISE);
1620 table.insert ("jabber", ContactField.IM_JABBER);
1621 table.insert ("msn", ContactField.IM_MSN);
1622 table.insert ("icq", ContactField.IM_ICQ);
1623 table.insert ("gadugadu", ContactField.IM_GADUGADU);
1624 table.insert ("skype", ContactField.IM_SKYPE);
1626 Edsf.Persona._im_eds_map = table;
1629 retval = (!) Edsf.Persona._im_eds_map;
1635 private void _update_phones ()
1637 var new_phone_numbers = new HashSet<PhoneFieldDetails> (
1638 (GLib.HashFunc) PhoneFieldDetails.hash,
1639 (GLib.EqualFunc) PhoneFieldDetails.equal);
1641 var attrs = this.contact.get_attributes (E.ContactField.TEL);
1642 foreach (var attr in attrs)
1644 var val = attr.get_value ();
1645 if (val == null || (!) val == "")
1650 var phone_fd = new PhoneFieldDetails ((!) val);
1651 this._update_params (phone_fd, attr);
1652 new_phone_numbers.add (phone_fd);
1655 if (!Folks.Internal.equal_sets<PhoneFieldDetails> (new_phone_numbers,
1656 this._phone_numbers))
1658 this._phone_numbers = new_phone_numbers;
1659 this._phone_numbers_ro = new_phone_numbers.read_only_view;
1660 this.notify_property ("phone-numbers");
1664 private PostalAddress _postal_address_from_attribute (E.VCardAttribute attr)
1666 unowned GLib.List<string>? values = attr.get_values();
1667 unowned GLib.List<string>? l = values;
1669 var address_format = "";
1675 var postal_code = "";
1680 po_box = ((!) l).data;
1685 extension = ((!) l).data;
1690 street = ((!) l).data;
1695 locality = ((!) l).data;
1700 region = ((!) l).data;
1705 postal_code = ((!) l).data;
1710 country = ((!) l).data;
1714 return new PostalAddress (po_box, extension, street,
1715 locality, region, postal_code, country,
1716 address_format, null);
1720 * TODO: we should check if addresses corresponding to different types
1721 * are the same and if so instantiate only one PostalAddress
1722 * (with the given types).
1724 private void _update_addresses ()
1726 var new_postal_addresses = new HashSet<PostalAddressFieldDetails> (
1727 (GLib.HashFunc) PhoneFieldDetails.hash,
1728 (GLib.EqualFunc) PhoneFieldDetails.equal);
1730 var attrs = this.contact.get_attributes (E.ContactField.ADDRESS);
1731 foreach (unowned E.VCardAttribute attr in attrs)
1733 var address = this._postal_address_from_attribute (attr);
1734 if (address.is_empty ())
1739 var pa_fd = new PostalAddressFieldDetails (address);
1740 this._update_params (pa_fd, attr);
1741 new_postal_addresses.add (pa_fd);
1744 if (!Folks.Internal.equal_sets<PostalAddressFieldDetails> (
1745 new_postal_addresses,
1746 this._postal_addresses))
1748 this._postal_addresses = new_postal_addresses;
1749 this._postal_addresses_ro = new_postal_addresses.read_only_view;
1750 this.notify_property ("phone-numbers");
1753 this.notify_property ("postal-addresses");
1756 private void _update_local_ids ()
1758 var new_local_ids = new HashSet<string> ();
1760 var ids = this.contact.get_attribute ("X-FOLKS-CONTACTS-IDS");
1763 unowned GLib.List<string> ids_v = ((!) ids).get_values ();
1765 foreach (var local_id in ids_v)
1769 new_local_ids.add (local_id);
1774 /* Make sure it includes our local id */
1775 new_local_ids.add (this.iid);
1777 if (!Folks.Internal.equal_sets<string> (new_local_ids, this.local_ids))
1779 this._local_ids = new_local_ids;
1780 this._local_ids_ro = this._local_ids.read_only_view;
1781 this.notify_property ("local-ids");
1785 private void _update_favourite ()
1787 bool is_fav = false;
1789 var fav = this.contact.get_attribute ("X-FOLKS-FAVOURITE");
1792 var val = ((!) fav).get_value ();
1793 if (val != null && ((!) val).down () == "true")
1799 if (is_fav != this._is_favourite)
1801 this._is_favourite = is_fav;
1802 this.notify_property ("is-favourite");
1806 private void _update_anti_links ()
1808 var new_anti_links = new HashSet<string> ();
1810 var vcard = (E.VCard) this.contact;
1811 foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1813 if (attr.get_name () != Edsf.PersonaStore.anti_links_attribute_name)
1818 var val = attr.get_value ();
1819 if (val == null || (!) val == "")
1824 new_anti_links.add ((!) val);
1827 if (!Folks.Internal.equal_sets<string> (new_anti_links, this._anti_links))
1829 this._anti_links = new_anti_links;
1830 this._anti_links_ro = new_anti_links.read_only_view;
1831 this.notify_property ("anti-links");
1835 internal static T? _get_property_from_contact<T> (E.Contact contact,
1838 T? prop_value = null;
1839 prop_value = contact.get<T> (E.Contact.field_id (prop_name));
1843 private T? _get_property<T> (string prop_name)
1845 return Edsf.Persona._get_property_from_contact<T> (this.contact,
1849 private string? _im_proto_from_addr (string addr)
1851 if (addr.index_of ("@") == -1)
1854 var tokens = addr.split ("@", 2);
1856 if (tokens.length != 2)
1859 var domain = tokens[1];
1860 if (domain.index_of (".") == -1)
1863 tokens = domain.split (".", 2);
1865 if (tokens.length != 2)
1870 if (domain == "msn" ||
1871 domain == "hotmail" ||
1874 else if (domain == "gmail" ||
1875 domain == "googlemail")
1877 else if (domain == "yahoo")