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,
49 /* The following 4 definitions are used by the tests */
51 * vCard field names for telephone numbers.
55 public static const string[] phone_fields = {
56 "assistant_phone", "business_phone", "business_phone_2", "callback_phone",
57 "car_phone", "company_phone", "home_phone", "home_phone_2", "isdn_phone",
58 "mobile_phone", "other_phone", "primary_phone"
61 * vCard field names for postal addresses.
65 public static const string[] address_fields = {
66 "address_home", "address_other", "address_work"
69 * vCard field names for e-mail addresses.
73 public static const string[] email_fields = {
74 "email_1", "email_2", "email_3", "email_4"
79 * vCard field names for miscellaneous URIs.
83 public static const string[] url_properties = {
84 "blog_url", "fburl", "homepage_url", "video_url"
87 /* Some types of URLs are represented in EDS using custom vCard fields rather
88 * than the X-URIS field. Here are mappings between the custom vCard field
89 * names which EDS uses, and the TYPE values which folks uses which map to
91 private struct UrlTypeMapping
93 string vcard_field_name;
97 internal static const UrlTypeMapping[] _url_properties =
99 { "homepage_url", UrlFieldDetails.PARAM_TYPE_HOME_PAGE },
100 { "blog_url", UrlFieldDetails.PARAM_TYPE_BLOG },
101 { "fburl", "x-free-busy" },
102 { "video_url", "x-video" }
106 * The vCard attribute used to specify a Contact's gender
109 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
111 * Note that the above document is a draft and the gender property
112 * is still considered experimental, hence the "X-" prefix in the
113 * attribute name. So this might change.
117 public static const string gender_attribute_name = "X-GENDER";
120 * The value used to define the male gender for the
121 * X-GENDER vCard property.
124 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
128 public static const string gender_male = "M";
131 * The value used to define the female gender for the
132 * X-GENDER vCard property.
135 * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
139 public static const string gender_female = "F";
141 private const string[] _linkable_properties = { "im-addresses",
143 "web-service-addresses" };
145 private HashSet<PhoneFieldDetails> _phone_numbers;
146 private Set<PhoneFieldDetails> _phone_numbers_ro;
147 private HashSet<EmailFieldDetails> _email_addresses;
148 private Set<EmailFieldDetails> _email_addresses_ro;
149 private HashSet<NoteFieldDetails> _notes;
150 private Set<NoteFieldDetails> _notes_ro;
151 private static HashTable<string, E.ContactField>? _im_eds_map = null;
153 private HashSet<PostalAddressFieldDetails> _postal_addresses;
154 private Set<PostalAddressFieldDetails> _postal_addresses_ro;
156 private HashSet<string> _local_ids;
157 private Set<string> _local_ids_ro;
159 private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
161 private bool _is_favourite;
163 private E.Contact _contact; /* should be set on construct */
166 * The e-d-s contact represented by this Persona
168 public E.Contact contact
170 get { return this._contact; }
171 construct { this._contact = value; }
177 [CCode (notify = false)]
178 public MultiMap<string, WebServiceFieldDetails> web_service_addresses
180 get { return this._web_service_addresses; }
181 set { this.change_web_service_addresses.begin (value); }
189 public async void change_web_service_addresses (
190 MultiMap<string, WebServiceFieldDetails> web_service_addresses)
193 yield ((Edsf.PersonaStore) this.store)._set_web_service_addresses (this,
194 web_service_addresses);
198 * IDs used to link {@link Edsf.Persona}s.
200 [CCode (notify = false)]
201 public Set<string> local_ids
205 if (this._local_ids.contains (this.iid) == false)
207 this._local_ids.add (this.iid);
209 return this._local_ids_ro;
211 set { this.change_local_ids.begin (value); }
219 public async void change_local_ids (Set<string> local_ids)
222 yield ((Edsf.PersonaStore) this.store)._set_local_ids (this, local_ids);
226 * The postal addresses of the contact.
228 * A list of postal addresses associated to the contact.
232 [CCode (notify = false)]
233 public Set<PostalAddressFieldDetails> postal_addresses
235 get { return this._postal_addresses_ro; }
236 set { this.change_postal_addresses.begin (value); }
244 public async void change_postal_addresses (
245 Set<PostalAddressFieldDetails> postal_addresses) throws PropertyError
247 yield ((Edsf.PersonaStore) this.store)._set_postal_addresses (this,
256 [CCode (notify = false)]
257 public Set<PhoneFieldDetails> phone_numbers
259 get { return this._phone_numbers_ro; }
260 set { this.change_phone_numbers.begin (value); }
268 public async void change_phone_numbers (
269 Set<PhoneFieldDetails> phone_numbers) throws PropertyError
271 yield ((Edsf.PersonaStore) this.store)._set_phones (this, phone_numbers);
279 [CCode (notify = false)]
280 public Set<EmailFieldDetails> email_addresses
282 get { return this._email_addresses_ro; }
283 set { this.change_email_addresses.begin (value); }
291 public async void change_email_addresses (
292 Set<EmailFieldDetails> email_addresses) throws PropertyError
294 yield ((Edsf.PersonaStore) this.store)._set_emails (this,
303 [CCode (notify = false)]
304 public Set<NoteFieldDetails> notes
306 get { return this._notes_ro; }
307 set { this.change_notes.begin (value); }
315 public async void change_notes (Set<NoteFieldDetails> notes)
318 yield ((Edsf.PersonaStore) this.store)._set_notes (this, notes);
326 public override string[] linkable_properties
328 get { return this._linkable_properties; }
336 public override string[] writeable_properties
338 get { return this.store.always_writeable_properties; }
341 private LoadableIcon? _avatar = null;
343 * An avatar for the Persona.
345 * See {@link Folks.AvatarDetails.avatar}.
349 [CCode (notify = false)]
350 public LoadableIcon? avatar
352 get { return this._avatar; }
353 set { this.change_avatar.begin (value); }
361 public async void change_avatar (LoadableIcon? avatar) throws PropertyError
363 yield ((Edsf.PersonaStore) this.store)._set_avatar (this, avatar);
366 private StructuredName? _structured_name = null;
372 [CCode (notify = false)]
373 public StructuredName? structured_name
375 get { return this._structured_name; }
376 set { this.change_structured_name.begin (value); }
384 public async void change_structured_name (StructuredName? structured_name)
387 yield ((Edsf.PersonaStore) this.store)._set_structured_name (this,
392 * The e-d-s contact uid
396 public string contact_id { get; construct; }
398 private string _full_name = "";
404 [CCode (notify = false)]
405 public string full_name
407 get { return this._full_name; }
408 set { this.change_full_name.begin (value); }
416 public async void change_full_name (string full_name) throws PropertyError
418 yield ((Edsf.PersonaStore) this.store)._set_full_name (this, full_name);
421 private string _nickname = "";
427 [CCode (notify = false)]
428 public string nickname
430 get { return this._nickname; }
431 set { this.change_nickname.begin (value); }
439 public async void change_nickname (string nickname) throws PropertyError
441 yield ((Edsf.PersonaStore) this.store)._set_nickname (this, nickname);
444 private Gender _gender;
450 [CCode (notify = false)]
453 get { return this._gender; }
454 set { this.change_gender.begin (value); }
462 public async void change_gender (Gender gender) throws PropertyError
464 yield ((Edsf.PersonaStore) this.store)._set_gender (this, gender);
467 private HashSet<UrlFieldDetails> _urls;
468 private Set<UrlFieldDetails> _urls_ro;
474 [CCode (notify = false)]
475 public Set<UrlFieldDetails> urls
477 get { return this._urls_ro; }
478 set { this.change_urls.begin (value); }
486 public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
488 yield ((Edsf.PersonaStore) this.store)._set_urls (this, urls);
491 private HashMultiMap<string, ImFieldDetails> _im_addresses =
492 new HashMultiMap<string, ImFieldDetails> (null, null,
493 (GLib.HashFunc) ImFieldDetails.hash,
494 (GLib.EqualFunc) ImFieldDetails.equal);
501 [CCode (notify = false)]
502 public MultiMap<string, ImFieldDetails> im_addresses
504 get { return this._im_addresses; }
505 set { this.change_im_addresses.begin (value); }
513 public async void change_im_addresses (
514 MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
516 yield ((Edsf.PersonaStore) this.store)._set_im_fds (this, im_addresses);
519 private HashSet<string> _groups;
520 private Set<string> _groups_ro;
527 [CCode (notify = false)]
528 public Set<string> groups
530 get { return this._groups_ro; }
531 set { this.change_groups.begin (value); }
539 public async void change_group (string group, bool is_member)
543 if ((is_member == true && this._groups.contains (group) == true) ||
544 (is_member == false && this._groups.contains (group) == false))
549 /* Replace the current set of groups with a modified one. */
550 var new_groups = new HashSet<string> ();
551 foreach (var category_name in this._groups)
553 new_groups.add (category_name);
556 if (is_member == false)
558 new_groups.remove (group);
562 new_groups.add (group);
565 yield this.change_groups (new_groups);
573 public async void change_groups (Set<string> groups) throws PropertyError
575 yield ((Edsf.PersonaStore) this.store)._set_groups (this, groups);
581 * e-d-s has no equivalent field, so this is unsupported.
585 [CCode (notify = false)]
586 public string? calendar_event_id
588 get { return null; } /* unsupported */
589 set { this.change_calendar_event_id.begin (value); } /* not writeable */
592 /* We cache the timezone we use for converting birthdays to UTC since creating
593 * it requires mmapping /etc/localtime, which means lots of syscalls. */
594 private static TimeZone _local_time_zone = new TimeZone.local ();
596 private DateTime? _birthday = null;
602 [CCode (notify = false)]
603 public DateTime? birthday
605 get { return this._birthday; }
606 set { this.change_birthday.begin (value); }
614 public async void change_birthday (DateTime? bday)
617 yield ((Edsf.PersonaStore) this.store)._set_birthday (this,
621 private HashSet<RoleFieldDetails> _roles;
622 private Set<RoleFieldDetails> _roles_ro;
629 [CCode (notify = false)]
630 public Set<RoleFieldDetails> roles
632 get { return this._roles_ro; }
633 set { this.change_roles.begin (value); }
641 public async void change_roles (Set<RoleFieldDetails> roles)
644 yield ((Edsf.PersonaStore) this.store)._set_roles (this, roles);
648 * Whether this contact is a user-defined favourite.
652 [CCode (notify = false)]
653 public bool is_favourite
655 get { return this._is_favourite; }
656 set { this.change_is_favourite.begin (value); }
664 public async void change_is_favourite (bool is_favourite) throws PropertyError
666 if (this._is_favourite == is_favourite)
671 yield ((Edsf.PersonaStore) this.store)._set_is_favourite (this,
678 * @param store_id the {@link PersonaStore.id}
679 * @param contact the Contact
680 * @return a valid IID
684 internal static string build_iid_from_contact (string store_id,
688 Edsf.Persona._get_property_from_contact<string> (contact, "id");
689 return Edsf.Persona.build_iid (store_id, (!) (contact_id ?? ""));
695 * @param store_id the {@link PersonaStore.id}
696 * @param contact_id the id belonging to the Contact
697 * @return a valid IID
701 internal static string build_iid (string store_id, string contact_id)
703 return "%s:%s".printf (store_id, contact_id);
708 * Create a new persona.
710 * Create a new persona for the {@link PersonaStore} `store`, representing
711 * the EDS contact given by `contact`.
713 * @param store the store which will contain the persona
714 * @param contact the EDS contact being represented by the persona
718 public Persona (PersonaStore store, E.Contact contact)
721 Edsf.Persona._get_property_from_contact<string> (contact, "id");
722 var contact_id = (!) (_contact_id ?? "");
724 var uid = this.build_uid (BACKEND_NAME, store.id, contact_id);
725 var iid = Edsf.Persona.build_iid (store.id, contact_id);
726 var is_user = BookClient.is_self (contact);
728 Edsf.Persona._get_property_from_contact<string> (contact,
730 var full_name = (!) (_full_name ?? "");
732 Object (display_id: full_name,
737 contact_id: contact_id,
743 debug ("Creating new Edsf.Persona with IID '%s'", this.iid);
745 this._gender = Gender.UNSPECIFIED;
746 this._phone_numbers = new HashSet<PhoneFieldDetails> (
747 (GLib.HashFunc) PhoneFieldDetails.hash,
748 (GLib.EqualFunc) PhoneFieldDetails.equal);
749 this._phone_numbers_ro = this._phone_numbers.read_only_view;
750 this._email_addresses = new HashSet<EmailFieldDetails> (
751 (GLib.HashFunc) EmailFieldDetails.hash,
752 (GLib.EqualFunc) EmailFieldDetails.equal);
753 this._email_addresses_ro = this._email_addresses.read_only_view;
754 this._notes = new HashSet<NoteFieldDetails> (
755 (GLib.HashFunc) NoteFieldDetails.hash,
756 (GLib.EqualFunc) NoteFieldDetails.equal);
757 this._notes_ro = this._notes.read_only_view;
758 this._urls = new HashSet<UrlFieldDetails> (
759 (GLib.HashFunc) UrlFieldDetails.hash,
760 (GLib.EqualFunc) UrlFieldDetails.equal);
761 this._urls_ro = this._urls.read_only_view;
762 this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
763 (GLib.HashFunc) PostalAddressFieldDetails.hash,
764 (GLib.EqualFunc) PostalAddressFieldDetails.equal);
765 this._postal_addresses_ro = this._postal_addresses.read_only_view;
766 this._local_ids = new HashSet<string> ();
767 this._local_ids_ro = this._local_ids.read_only_view;
768 this._web_service_addresses =
769 new HashMultiMap<string, WebServiceFieldDetails> (
771 (GLib.HashFunc) WebServiceFieldDetails.hash,
772 (GLib.EqualFunc) WebServiceFieldDetails.equal);
773 this._groups = new HashSet<string> ();
774 this._groups_ro = this._groups.read_only_view;
775 this._roles = new HashSet<RoleFieldDetails> (
776 (GLib.HashFunc) RoleFieldDetails.hash,
777 (GLib.EqualFunc) RoleFieldDetails.equal);
778 this._roles_ro = this._roles.read_only_view;
780 this._update (this._contact);
788 public override void linkable_property_to_links (string prop_name,
789 Folks.Persona.LinkablePropertyCallback callback)
791 if (prop_name == "im-addresses")
793 foreach (var protocol in this._im_addresses.get_keys ())
795 var im_fds = this._im_addresses.get (protocol);
797 foreach (var im_fd in im_fds)
798 callback (protocol + ":" + im_fd.value);
801 else if (prop_name == "local-ids")
803 /* Note: we need to use this.local_ids and not this._local_ids,
804 * otherwise this can have a different behaviour depending
805 * on the state of the current Persona depending on whether
806 * this.local_ids was called before or not. */
807 foreach (var id in this.local_ids)
812 else if (prop_name == "web-service-addresses")
814 foreach (var web_service in this.web_service_addresses.get_keys ())
816 var web_service_addresses =
817 this._web_service_addresses.get (web_service);
819 foreach (var ws_fd in web_service_addresses)
820 callback (web_service + ":" + ws_fd.value);
826 base.linkable_property_to_links (prop_name, callback);
832 debug ("Destroying Edsf.Persona '%s': %p", this.uid, this);
836 * Update attribs of the persona.
838 internal void _update (E.Contact updated_contact)
840 this.freeze_notify ();
842 /* We get a new E.Contact instance from EDS containing all the updates,
843 * so replace our existing contact with it. */
844 this._contact = updated_contact;
845 this.notify_property ("contact");
847 this._update_names ();
848 this._update_avatar ();
849 this._update_urls ();
850 this._update_phones ();
851 this._update_addresses ();
852 this._update_emails ();
854 /* Note: because we assume certain e-mail addresses
855 * (@gmail, @msn, etc) to also be IM IDs we /must/
856 * update the latter after we've taken care of the former.
858 this._update_im_addresses ();
860 this._update_groups ();
861 this._update_notes ();
862 this._update_local_ids ();
863 this._update_web_services_addresses ();
864 this._update_gender ();
865 this._update_birthday ();
866 this._update_roles ();
867 this._update_favourite ();
872 private void _update_params (AbstractFieldDetails details,
873 E.VCardAttribute attr)
875 foreach (unowned E.VCardAttributeParam param in attr.get_params ())
877 string param_name = param.get_name ().down ();
878 foreach (unowned string param_value in param.get_values ())
880 if (param_name == AbstractFieldDetails.PARAM_TYPE)
882 details.add_parameter (param_name, param_value.down ());
886 details.add_parameter (param_name, param_value);
892 private void _update_gender ()
894 var gender = Gender.UNSPECIFIED;
896 this.contact.get_attribute (Edsf.Persona.gender_attribute_name);
898 if (gender_attr != null)
900 var val = ((!) gender_attr).get_value ();
903 switch (((!) val).up ())
905 case Edsf.Persona.gender_male:
906 gender = Gender.MALE;
908 case Edsf.Persona.gender_female:
909 gender = Gender.FEMALE;
912 /* Unspecified, as above */
918 if (this._gender != gender)
920 this._gender = gender;
921 this.notify_property ("gender");
925 private void _update_birthday ()
927 var _bday = this._get_property<E.ContactDate> ("birth_date");
931 var bday = (!) _bday;
933 /* Since e-d-s stores birthdays as a plain date, we take the
934 * given date in local time and convert it to UTC as mandated
935 * by the BirthdayDetails interface.
936 * We cache the timezone since creating it requires mmapping
937 * /etc/localtime, which means lots of syscalls. */
938 var d = new DateTime (this._local_time_zone,
939 (int) bday.year, (int) bday.month, (int) bday.day, 0, 0, 0.0);
940 if (this._birthday == null ||
941 (this._birthday != null &&
942 !((!) this._birthday).equal (d.to_utc ())))
944 this._birthday = d.to_utc ();
945 this.notify_property ("birthday");
950 if (this._birthday != null)
952 this._birthday = null;
953 this.notify_property ("birthday");
958 private void _update_roles ()
960 var new_roles = new HashSet<RoleFieldDetails> (
961 (GLib.HashFunc) RoleFieldDetails.hash,
962 (GLib.EqualFunc) RoleFieldDetails.equal);
964 var default_role_fd = this._get_default_role ();
965 if (default_role_fd != null)
967 new_roles.add ((!) default_role_fd);
970 var vcard = (E.VCard) this.contact;
971 foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
973 if (attr.get_name () != "X-ROLES")
976 var val = attr.get_value ();
977 if (val == null || (!) val == "")
982 var role = new Role ("", "");
984 var role_fd = new RoleFieldDetails (role);
986 foreach (unowned E.VCardAttributeParam param in
989 string param_name = param.get_name ().down ();
991 if (param_name == "organisation_name")
993 foreach (unowned string param_value in
996 role.organisation_name = param_value;
1000 else if (param_name == "title")
1002 foreach (unowned string param_value in
1003 param.get_values ())
1005 role.title = param_value;
1011 foreach (unowned string param_value in
1012 param.get_values ())
1014 role_fd.add_parameter (param_name, param_value);
1019 new_roles.add (role_fd);
1022 if (!Folks.Internal.equal_sets<RoleFieldDetails> (new_roles, this._roles))
1024 this._roles = new_roles;
1025 this._roles_ro = new_roles.read_only_view;
1026 this.notify_property ("roles");
1030 private RoleFieldDetails? _get_default_role ()
1032 RoleFieldDetails? _default_role = null;
1034 var org = this._get_property<string> ("org");
1035 var org_unit = this._get_property<string> ("org_unit");
1036 var office = this._get_property<string> ("office");
1037 var title = this._get_property<string> ("title");
1038 var role = this._get_property<string> ("role");
1039 var manager = this._get_property<string> ("manager");
1040 var assistant = this._get_property<string> ("assistant");
1050 var new_role = new Role (title, org);
1051 if (role != null && (!) role != "")
1052 new_role.role = (!) role;
1054 /* Check if it's non-empty. */
1055 if (!new_role.is_empty ())
1057 var default_role = new RoleFieldDetails (new_role);
1059 if (org_unit != null && org_unit != "")
1060 default_role.set_parameter ("org_unit", (!) org_unit);
1062 if (office != null && office != "")
1063 default_role.set_parameter ("office", (!) office);
1065 if (manager != null && manager != "")
1066 default_role.set_parameter ("manager", (!) manager);
1068 if (assistant != null && manager != "")
1069 default_role.set_parameter ("assistant", (!) assistant);
1071 _default_role = default_role;
1075 return _default_role;
1078 private void _update_web_services_addresses ()
1080 var new_services = new HashMultiMap<string, WebServiceFieldDetails> (
1082 (GLib.HashFunc) WebServiceFieldDetails.hash,
1083 (GLib.EqualFunc) WebServiceFieldDetails.equal);
1085 var services = this.contact.get_attribute ("X-FOLKS-WEB-SERVICES-IDS");
1086 if (services != null)
1088 foreach (var service in ((!) services).get_params ())
1090 var service_name = service.get_name ().down ();
1091 foreach (var service_id in service.get_values ())
1093 if (service_id == "")
1098 new_services.set (service_name,
1099 new WebServiceFieldDetails (service_id));
1104 if (!Utils.multi_map_str_afd_equal (new_services,
1105 this._web_service_addresses))
1107 this._web_service_addresses = new_services;
1108 this.notify_property ("web-service-addresses");
1112 private void _update_emails ()
1114 var new_email_addresses = new HashSet<EmailFieldDetails> (
1115 (GLib.HashFunc) EmailFieldDetails.hash,
1116 (GLib.EqualFunc) EmailFieldDetails.equal);
1118 var attrs = this.contact.get_attributes (E.ContactField.EMAIL);
1119 foreach (var attr in attrs)
1121 var val = attr.get_value ();
1122 if (val == null || (!) val == "")
1127 var email_fd = new EmailFieldDetails ((!) val);
1128 this._update_params (email_fd, attr);
1129 new_email_addresses.add (email_fd);
1132 if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
1133 this._email_addresses))
1135 this._email_addresses = new_email_addresses;
1136 this._email_addresses_ro = new_email_addresses.read_only_view;
1137 this.notify_property ("email-addresses");
1141 private void _update_notes ()
1143 var new_notes = new HashSet<NoteFieldDetails> (
1144 (GLib.HashFunc) NoteFieldDetails.hash,
1145 (GLib.EqualFunc) NoteFieldDetails.equal);
1147 var n = this._get_property<string> ("note");
1148 if (n != null && n != "")
1150 var note = new NoteFieldDetails ((!) n);
1151 new_notes.add (note);
1154 if (!Folks.Internal.equal_sets<NoteFieldDetails> (new_notes, this._notes))
1156 this._notes = new_notes;
1157 this._notes_ro = this._notes.read_only_view;
1158 this.notify_property ("notes");
1162 private void _update_names ()
1164 var _full_name = this._get_property<string> ("full_name");
1166 if (_full_name == null)
1171 var full_name = (!) _full_name;
1173 if (this._full_name != full_name)
1175 this._full_name = full_name;
1176 this.notify_property ("full-name");
1179 var _nickname = this._get_property<string> ("nickname");
1181 if (_nickname == null)
1186 var nickname = (!) _nickname;
1188 if (this._nickname != nickname)
1190 this._nickname = nickname;
1191 this.notify_property ("nickname");
1194 StructuredName? structured_name = null;
1195 var _cn = this._get_property<E.ContactName> ("name");
1200 string family_name = cn.family;
1201 string given_name = cn.given;
1202 string additional_names = cn.additional;
1203 string prefixes = cn.prefixes;
1204 string suffixes = cn.suffixes;
1205 structured_name = new StructuredName (family_name, given_name,
1206 additional_names, prefixes,
1210 if (structured_name != null && !((!) structured_name).is_empty ())
1212 this._structured_name = (!) structured_name;
1213 this.notify_property ("structured-name");
1215 else if (this._structured_name != null)
1217 this._structured_name = null;
1218 this.notify_property ("structured-name");
1222 private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? _p)
1233 case ContactPhotoType.URI:
1234 var uri = p.get_uri ();
1240 return new FileIcon (File.new_for_uri ((!) uri));
1241 case ContactPhotoType.INLINED:
1242 var data = p.get_inlined ();
1243 var mime_type = p.get_mime_type ();
1244 if (data == null || mime_type == null)
1249 return new Edsf.MemoryIcon ((!) mime_type, (!) data);
1255 private void _update_avatar ()
1257 var p = this._get_property<E.ContactPhoto> ("photo");
1259 var cache = AvatarCache.dup ();
1261 // Convert the ContactPhoto to a LoadableIcon and store or update it.
1262 var new_avatar = this._contact_photo_to_loadable_icon (p);
1264 if (this._avatar != null && new_avatar == null)
1266 // Remove the old cached avatar, ignoring errors.
1267 cache.remove_avatar.begin (this.uid, (obj, res) =>
1271 cache.remove_avatar.end (res);
1273 catch (GLib.Error e1) {}
1275 this._avatar = null;
1276 this.notify_property ("avatar");
1279 else if ((this._avatar == null && new_avatar != null) ||
1280 (this._avatar != null && new_avatar != null &&
1281 ((!) this._avatar).equal (new_avatar) == false))
1283 /* Store the new avatar in the cache. new_avatar is guaranteed to be
1285 cache.store_avatar.begin (this.uid, (!) new_avatar, (obj, res) =>
1289 cache.store_avatar.end (res);
1290 this._avatar = new_avatar;
1291 this.notify_property ("avatar");
1293 catch (GLib.Error e2)
1295 warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
1296 this.uid, e2.message);
1297 new_avatar = null; /* failure */
1303 private void _update_urls ()
1305 var new_urls = new HashSet<UrlFieldDetails> ();
1307 /* First we get the standard Evo urls.. */
1308 foreach (var mapping in this._url_properties)
1310 var url_property = mapping.vcard_field_name;
1311 var folks_type = mapping.folks_type;
1313 var u = this._get_property<string> (url_property);
1314 if (u != null && u != "")
1316 var fd_u = new UrlFieldDetails ((!) u);
1317 fd_u.set_parameter (fd_u.PARAM_TYPE, folks_type);
1318 new_urls.add (fd_u);
1322 /* Now we go for extra URLs */
1323 var vcard = (E.VCard) this.contact;
1324 foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1326 if (attr.get_name () == "X-URIS")
1328 var val = attr.get_value ();
1329 if (val == null || (!) val == "")
1334 var url_fd = new UrlFieldDetails ((!) val);
1335 this._update_params (url_fd, attr);
1336 new_urls.add (url_fd);
1340 if (!Utils.set_afd_equal (new_urls, this._urls))
1342 this._urls = new_urls;
1343 this._urls_ro = new_urls.read_only_view;
1344 this.notify_property ("urls");
1348 private void _update_im_addresses ()
1350 var im_eds_map = this._get_im_eds_map ();
1351 var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (null,
1352 null, (GLib.HashFunc) ImFieldDetails.hash,
1353 (GLib.EqualFunc) ImFieldDetails.equal);
1355 foreach (var im_proto in im_eds_map.get_keys ())
1357 var addresses = this.contact.get_attributes (
1358 im_eds_map.lookup (im_proto));
1359 foreach (var attr in addresses)
1363 var addr = attr.get_value ();
1364 if (addr == null || (!) addr == "")
1369 string normalised_addr =
1370 (owned) ImDetails.normalise_im_address ((!) addr, im_proto);
1372 if (normalised_addr == "")
1377 var im_fd = new ImFieldDetails (normalised_addr);
1378 new_im_addresses.set (im_proto, im_fd);
1380 catch (Folks.ImDetailsError e)
1383 "Problem when trying to normalise address: %s\n",
1389 /* We consider some e-mail addresses to be IM IDs too. This
1390 * is pretty much a hack to make sure e-d-s contacts are
1391 * automatically linked with their corresponding Telepathy
1392 * Persona. As an undesired side effect we might end up having
1393 * IM addresses that aren't actually used as such (i.e.: people
1394 * who don't actually use GMail or MSN addresses for IM).
1398 foreach (var email in this.email_addresses)
1400 var _proto = this._im_proto_from_addr (email.value);
1403 var proto = (!) _proto;
1405 /* Has this already been added? */
1407 Collection<ImFieldDetails>? current_im_addrs =
1408 new_im_addresses.get (proto);
1409 if (current_im_addrs != null)
1411 foreach (var cur_im in (!) current_im_addrs)
1413 if (cur_im.value == email.value)
1426 string normalised_addr =
1427 (owned) ImDetails.normalise_im_address (email.value, proto);
1428 var im_fd = new ImFieldDetails (normalised_addr);
1429 new_im_addresses.set (proto, im_fd);
1431 catch (Folks.ImDetailsError e)
1434 "Problem when trying to normalise address: %s\n",
1440 if (!Utils.multi_map_str_afd_equal (new_im_addresses,
1441 this._im_addresses))
1443 this._im_addresses = new_im_addresses;
1444 this.notify_property ("im-addresses");
1448 private void _update_groups ()
1450 var category_names =
1451 this._contact.get<GLib.List<string>> (E.ContactField.CATEGORY_LIST);
1452 var new_categories = new HashSet<string> ();
1453 var added_categories = new LinkedList<string> ();
1455 foreach (var category_name in category_names)
1457 new_categories.add (category_name);
1459 /* Is this a new category? */
1460 if (!this._groups.contains (category_name))
1462 added_categories.add (category_name);
1466 /* Work out which categories have been removed. */
1467 var removed_categories = new LinkedList<string> ();
1469 foreach (var category_name in this._groups)
1471 if (!new_categories.contains (category_name))
1473 removed_categories.add (category_name);
1477 var old_is_favourite = this._is_favourite;
1478 var store = (Edsf.PersonaStore) this.store;
1480 /* Make the changes to this._groups and emit signals. */
1481 foreach (var category_name in removed_categories)
1483 /* We link the “Starred in Android” group to Google Contacts address
1484 * books. See: bgo#661490. */
1485 if (store._is_google_contacts_address_book () &&
1486 category_name == Edsf.PersonaStore.android_favourite_group_name)
1488 this._is_favourite = false;
1491 this.group_changed (category_name, false);
1492 this._groups.remove (category_name);
1495 foreach (var category_name in added_categories)
1497 if (store._is_google_contacts_address_book () &&
1498 category_name == Edsf.PersonaStore.android_favourite_group_name)
1500 this._is_favourite = true;
1503 this._groups.add (category_name);
1504 this.group_changed (category_name, true);
1507 /* Notify if anything's changed. */
1508 this.freeze_notify ();
1510 if (added_categories.size != 0 || removed_categories.size != 0)
1512 this.notify_property ("groups");
1514 if (this._is_favourite != old_is_favourite)
1516 this.notify_property ("is-favourite");
1519 this.thaw_notify ();
1523 * build a table of im protocols / im protocol aliases
1525 internal static HashTable<string, E.ContactField> _get_im_eds_map ()
1527 HashTable<string, E.ContactField> retval;
1529 lock (Edsf.Persona._im_eds_map)
1531 if (Edsf.Persona._im_eds_map == null)
1534 new HashTable<string, E.ContactField> (str_hash, str_equal);
1536 table.insert ("aim", ContactField.IM_AIM);
1537 table.insert ("yahoo", ContactField.IM_YAHOO);
1538 table.insert ("groupwise", ContactField.IM_GROUPWISE);
1539 table.insert ("jabber", ContactField.IM_JABBER);
1540 table.insert ("msn", ContactField.IM_MSN);
1541 table.insert ("icq", ContactField.IM_ICQ);
1542 table.insert ("gadugadu", ContactField.IM_GADUGADU);
1543 table.insert ("skype", ContactField.IM_SKYPE);
1545 Edsf.Persona._im_eds_map = table;
1548 retval = (!) Edsf.Persona._im_eds_map;
1554 private void _update_phones ()
1556 var new_phone_numbers = new HashSet<PhoneFieldDetails> (
1557 (GLib.HashFunc) PhoneFieldDetails.hash,
1558 (GLib.EqualFunc) PhoneFieldDetails.equal);
1560 var attrs = this.contact.get_attributes (E.ContactField.TEL);
1561 foreach (var attr in attrs)
1563 var val = attr.get_value ();
1564 if (val == null || (!) val == "")
1569 var phone_fd = new PhoneFieldDetails ((!) val);
1570 this._update_params (phone_fd, attr);
1571 new_phone_numbers.add (phone_fd);
1574 if (!Folks.Internal.equal_sets<PhoneFieldDetails> (new_phone_numbers,
1575 this._phone_numbers))
1577 this._phone_numbers = new_phone_numbers;
1578 this._phone_numbers_ro = new_phone_numbers.read_only_view;
1579 this.notify_property ("phone-numbers");
1583 private PostalAddress _postal_address_from_attribute (E.VCardAttribute attr)
1585 unowned GLib.List<string>? values = attr.get_values();
1586 unowned GLib.List<string>? l = values;
1588 var address_format = "";
1594 var postal_code = "";
1599 po_box = ((!) l).data;
1604 extension = ((!) l).data;
1609 street = ((!) l).data;
1614 locality = ((!) l).data;
1619 region = ((!) l).data;
1624 postal_code = ((!) l).data;
1629 country = ((!) l).data;
1633 return new PostalAddress (po_box, extension, street,
1634 locality, region, postal_code, country,
1635 address_format, null);
1639 * TODO: we should check if addresses corresponding to different types
1640 * are the same and if so instantiate only one PostalAddress
1641 * (with the given types).
1643 private void _update_addresses ()
1645 var new_postal_addresses = new HashSet<PostalAddressFieldDetails> (
1646 (GLib.HashFunc) PhoneFieldDetails.hash,
1647 (GLib.EqualFunc) PhoneFieldDetails.equal);
1649 var attrs = this.contact.get_attributes (E.ContactField.ADDRESS);
1650 foreach (unowned E.VCardAttribute attr in attrs)
1652 var address = this._postal_address_from_attribute (attr);
1653 if (address.is_empty ())
1658 var pa_fd = new PostalAddressFieldDetails (address);
1659 this._update_params (pa_fd, attr);
1660 new_postal_addresses.add (pa_fd);
1663 if (!Folks.Internal.equal_sets<PostalAddressFieldDetails> (
1664 new_postal_addresses,
1665 this._postal_addresses))
1667 this._postal_addresses = new_postal_addresses;
1668 this._postal_addresses_ro = new_postal_addresses.read_only_view;
1669 this.notify_property ("phone-numbers");
1672 this.notify_property ("postal-addresses");
1675 private void _update_local_ids ()
1677 var new_local_ids = new HashSet<string> ();
1679 var ids = this.contact.get_attribute ("X-FOLKS-CONTACTS-IDS");
1682 unowned GLib.List<string> ids_v = ((!) ids).get_values ();
1684 foreach (var local_id in ids_v)
1688 new_local_ids.add (local_id);
1693 /* Make sure it includes our local id */
1694 new_local_ids.add (this.iid);
1696 if (!Folks.Internal.equal_sets<string> (new_local_ids, this.local_ids))
1698 this._local_ids = new_local_ids;
1699 this._local_ids_ro = this._local_ids.read_only_view;
1700 this.notify_property ("local-ids");
1704 private void _update_favourite ()
1706 bool is_fav = false;
1708 var fav = this.contact.get_attribute ("X-FOLKS-FAVOURITE");
1711 var val = ((!) fav).get_value ();
1712 if (val != null && ((!) val).down () == "true")
1718 var store = (Edsf.PersonaStore) this.store;
1720 if (store._is_google_contacts_address_book ())
1723 (Edsf.PersonaStore.android_favourite_group_name in this._groups);
1726 if (is_fav != this._is_favourite)
1728 this._is_favourite = is_fav;
1730 var groups_changed = false;
1732 if (store._is_google_contacts_address_book () &&
1733 !(Edsf.PersonaStore.android_favourite_group_name in this._groups))
1735 this._groups.add (Edsf.PersonaStore.android_favourite_group_name);
1736 this.group_changed (
1737 Edsf.PersonaStore.android_favourite_group_name, true);
1738 groups_changed = true;
1741 this.freeze_notify ();
1743 if (groups_changed == true)
1745 this.notify_property ("groups");
1748 this.notify_property ("is-favourite");
1750 this.thaw_notify ();
1754 internal static T? _get_property_from_contact<T> (E.Contact contact,
1757 T? prop_value = null;
1758 prop_value = contact.get<T> (E.Contact.field_id (prop_name));
1762 private T? _get_property<T> (string prop_name)
1764 return Edsf.Persona._get_property_from_contact<T> (this.contact,
1768 private string? _im_proto_from_addr (string addr)
1770 if (addr.index_of ("@") == -1)
1773 var tokens = addr.split ("@", 2);
1775 if (tokens.length != 2)
1778 var domain = tokens[1];
1779 if (domain.index_of (".") == -1)
1782 tokens = domain.split (".", 2);
1784 if (tokens.length != 2)
1789 if (domain == "msn" ||
1790 domain == "hotmail" ||
1793 else if (domain == "gmail" ||
1794 domain == "googlemail")
1796 else if (domain == "yahoo")