build fix: Only depend on Gee 0.8.3, since 0.8.4 isn't released yet
[platform/upstream/folks.git] / backends / eds / lib / edsf-persona.vala
1 /*
2  * Copyright (C) 2011 Collabora Ltd.
3  * Copyright (C) 2013 Philip Withnall
4  *
5  * This library is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation, either version 2.1 of the License, or
8  * (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * Authors:
19  *       Travis Reitter <travis.reitter@collabora.co.uk>
20  *       Marco Barisione <marco.barisione@collabora.co.uk>
21  *       Raul Gutierrez Segales <raul.gutierrez.segales@collabora.co.uk>
22  */
23
24 using E;
25 using Folks;
26 using Gee;
27 using GLib;
28 using Xml;
29
30 /**
31  * A persona subclass which represents a single EDS contact.
32  *
33  * Each {@link Edsf.Persona} instance represents a single EDS {@link E.Contact}.
34  * When the contact is modified (either by this folks client, or a different
35  * client), the {@link Edsf.Persona} remains the same, but is assigned a new
36  * {@link E.Contact}. It then updates its properties from this new contact.
37  */
38 public class Edsf.Persona : Folks.Persona,
39     AntiLinkable,
40     AvatarDetails,
41     BirthdayDetails,
42     EmailDetails,
43     FavouriteDetails,
44     GenderDetails,
45     GroupDetails,
46     ImDetails,
47     LocalIdDetails,
48     NameDetails,
49     NoteDetails,
50     PhoneDetails,
51     RoleDetails,
52     UrlDetails,
53     PostalAddressDetails,
54     WebServiceDetails
55 {
56   /* The following 4 definitions are used by the tests */
57   /**
58    * vCard field names for telephone numbers.
59    *
60    * @since 0.6.0
61    */
62   public static const string[] phone_fields = {
63     "assistant_phone", "business_phone", "business_phone_2", "callback_phone",
64     "car_phone", "company_phone", "home_phone", "home_phone_2", "isdn_phone",
65     "mobile_phone", "other_phone", "primary_phone"
66   };
67   /**
68    * vCard field names for postal addresses.
69    *
70    * @since 0.6.0
71    */
72   public static const string[] address_fields = {
73     "address_home", "address_other", "address_work"
74   };
75   /**
76    * vCard field names for e-mail addresses.
77    *
78    * @since 0.6.0
79    */
80   public static const string[] email_fields = {
81     "email_1", "email_2", "email_3", "email_4"
82   };
83
84   /**
85    * vCard field names for miscellaneous URIs.
86    *
87    * @since 0.6.0
88    */
89   [Deprecated (since = "0.6.3",
90       replacement = "Folks.UrlFieldDetails.PARAM_TYPE_BLOG")]
91   public static const string[] url_properties = {
92     "blog_url", "fburl", "homepage_url", "video_url"
93   };
94
95   /* Some types of URLs are represented in EDS using custom vCard fields rather
96    * than the X-URIS field. Here are mappings between the custom vCard field
97    * names which EDS uses, and the TYPE values which folks uses which map to
98    * them. */
99   private struct UrlTypeMapping
100     {
101       string vcard_field_name;
102       string folks_type;
103     }
104
105   internal static const UrlTypeMapping[] _url_properties =
106     {
107       { "homepage_url", UrlFieldDetails.PARAM_TYPE_HOME_PAGE },
108       { "blog_url", UrlFieldDetails.PARAM_TYPE_BLOG },
109       { "fburl", "x-free-busy" },
110       { "video_url", "x-video" }
111     };
112
113   /**
114    * The vCard attribute used to specify a Contact's gender
115    *
116    * Based on:
117    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
118    *
119    * Note that the above document is a draft and the gender property
120    * is still considered experimental, hence the "X-" prefix in the
121    * attribute name. So this might change.
122    *
123    * @since 0.6.0
124    */
125   public static const string gender_attribute_name = "X-GENDER";
126
127   /**
128    * The value used to define the male gender for the
129    * X-GENDER vCard property.
130    *
131    * Based on:
132    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
133    *
134    * @since 0.6.0
135    */
136   public static const string gender_male = "M";
137
138   /**
139    * The value used to define the female gender for the
140    * X-GENDER vCard property.
141    *
142    * Based on:
143    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
144    *
145    * @since 0.6.0
146    */
147   public static const string gender_female = "F";
148
149   private const string[] _linkable_properties = { "im-addresses",
150                                                   "email-addresses",
151                                                   "local-ids",
152                                                   "web-service-addresses" };
153
154   private static GLib.HashTable<string, E.ContactField>? _im_eds_map = null;
155
156   private E.Contact _contact; /* should be set on construct */
157
158   /**
159    * The e-d-s contact represented by this Persona
160    */
161   public E.Contact contact
162     {
163       get { return this._contact; }
164       construct { this._contact = value; }
165     }
166
167   /* NOTE: Other properties support lazy initialisation, but
168    * web-service-addresses doesn't as it's a linkable property, so always has to
169    * be loaded anyway. */
170   private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
171
172   /**
173    * {@inheritDoc}
174    */
175   [CCode (notify = false)]
176   public MultiMap<string, WebServiceFieldDetails> web_service_addresses
177     {
178       get { return this._web_service_addresses; }
179       set { this.change_web_service_addresses.begin (value); }
180     }
181
182   /**
183    * {@inheritDoc}
184    *
185    * @since 0.6.2
186    */
187   public async void change_web_service_addresses (
188       MultiMap<string, WebServiceFieldDetails> web_service_addresses)
189           throws PropertyError
190     {
191       yield ((Edsf.PersonaStore) this.store)._set_web_service_addresses (this,
192           web_service_addresses);
193     }
194
195   /* NOTE: Other properties support lazy initialisation, but local-ids doesn't
196    * as it's a linkable property, so always has to be loaded anyway. */
197   private HashSet<string> _local_ids = new HashSet<string> ();
198   private Set<string> _local_ids_ro;
199
200   /**
201    * IDs used to link {@link Edsf.Persona}s.
202    */
203   [CCode (notify = false)]
204   public Set<string> local_ids
205     {
206       get
207         {
208           if (this._local_ids.contains (this.iid) == false)
209             {
210               this._local_ids.add (this.iid);
211             }
212           return this._local_ids_ro;
213         }
214       set { this.change_local_ids.begin (value); }
215     }
216
217   /**
218    * {@inheritDoc}
219    *
220    * @since 0.6.2
221    */
222   public async void change_local_ids (Set<string> local_ids)
223       throws PropertyError
224     {
225       yield ((Edsf.PersonaStore) this.store)._set_local_ids (this, local_ids);
226     }
227
228   private HashSet<PostalAddressFieldDetails>? _postal_addresses = null;
229   private Set<PostalAddressFieldDetails>? _postal_addresses_ro = null;
230
231   /**
232    * The postal addresses of the contact.
233    *
234    * A list of postal addresses associated to the contact.
235    *
236    * @since 0.6.0
237    */
238   [CCode (notify = false)]
239   public Set<PostalAddressFieldDetails> postal_addresses
240     {
241       get
242         {
243           this._update_addresses (true, false);
244           return this._postal_addresses_ro;
245         }
246       set { this.change_postal_addresses.begin (value); }
247     }
248
249   /**
250    * {@inheritDoc}
251    *
252    * @since 0.6.2
253    */
254   public async void change_postal_addresses (
255       Set<PostalAddressFieldDetails> postal_addresses) throws PropertyError
256     {
257       yield ((Edsf.PersonaStore) this.store)._set_postal_addresses (this,
258           postal_addresses);
259     }
260
261   private HashSet<PhoneFieldDetails>? _phone_numbers = null;
262   private Set<PhoneFieldDetails>? _phone_numbers_ro = null;
263
264   /**
265    * {@inheritDoc}
266    *
267    * @since 0.6.0
268    */
269   [CCode (notify = false)]
270   public Set<PhoneFieldDetails> phone_numbers
271     {
272       get
273         {
274           this._update_phones (true, false);
275           return this._phone_numbers_ro;
276         }
277       set { this.change_phone_numbers.begin (value); }
278     }
279
280   /**
281    * {@inheritDoc}
282    *
283    * @since 0.6.2
284    */
285   public async void change_phone_numbers (
286       Set<PhoneFieldDetails> phone_numbers) throws PropertyError
287     {
288       yield ((Edsf.PersonaStore) this.store)._set_phones (this, phone_numbers);
289     }
290
291   private HashSet<EmailFieldDetails>? _email_addresses =
292           new HashSet<EmailFieldDetails> (
293                   AbstractFieldDetails<string>.hash_static,
294                   AbstractFieldDetails<string>.equal_static);
295   private Set<EmailFieldDetails> _email_addresses_ro;
296
297   /**
298    * {@inheritDoc}
299    *
300    * @since 0.6.0
301    */
302   [CCode (notify = false)]
303   public Set<EmailFieldDetails> email_addresses
304     {
305       get { return this._email_addresses_ro; }
306       set { this.change_email_addresses.begin (value); }
307     }
308
309   /**
310    * {@inheritDoc}
311    *
312    * @since 0.6.2
313    */
314   public async void change_email_addresses (
315       Set<EmailFieldDetails> email_addresses) throws PropertyError
316     {
317       yield ((Edsf.PersonaStore) this.store)._set_emails (this,
318           email_addresses);
319     }
320
321   private HashSet<NoteFieldDetails>? _notes = null;
322   private Set<NoteFieldDetails>? _notes_ro = null;
323
324   /**
325    * {@inheritDoc}
326    *
327    * @since 0.6.0
328    */
329   [CCode (notify = false)]
330   public Set<NoteFieldDetails> notes
331     {
332       get
333         {
334           this._update_notes (true, false);
335           return this._notes_ro;
336         }
337       set { this.change_notes.begin (value); }
338     }
339
340   /**
341    * {@inheritDoc}
342    *
343    * @since 0.6.2
344    */
345   public async void change_notes (Set<NoteFieldDetails> notes)
346       throws PropertyError
347     {
348       yield ((Edsf.PersonaStore) this.store)._set_notes (this, notes);
349     }
350
351   /**
352    * {@inheritDoc}
353    *
354    * @since 0.6.0
355    */
356   public override string[] linkable_properties
357     {
358       get { return Persona._linkable_properties; }
359     }
360
361   /**
362    * {@inheritDoc}
363    *
364    * @since 0.6.0
365    */
366   public override string[] writeable_properties
367     {
368       get { return this.store.always_writeable_properties; }
369     }
370
371   private LoadableIcon? _avatar = null;
372   /**
373    * An avatar for the Persona.
374    *
375    * See {@link Folks.AvatarDetails.avatar}.
376    *
377    * @since 0.6.0
378    */
379   [CCode (notify = false)]
380   public LoadableIcon? avatar
381     {
382       get { return this._avatar; }
383       set { this.change_avatar.begin (value); }
384     }
385
386   /**
387    * {@inheritDoc}
388    *
389    * @since 0.6.2
390    */
391   public async void change_avatar (LoadableIcon? avatar) throws PropertyError
392     {
393       yield ((Edsf.PersonaStore) this.store)._set_avatar (this, avatar);
394     }
395
396   private StructuredName? _structured_name = null;
397   /**
398    * {@inheritDoc}
399    *
400    * @since 0.6.0
401    */
402   [CCode (notify = false)]
403   public StructuredName? structured_name
404     {
405       get { return this._structured_name; }
406       set { this.change_structured_name.begin (value); }
407     }
408
409   /**
410    * {@inheritDoc}
411    *
412    * @since 0.6.2
413    */
414   public async void change_structured_name (StructuredName? structured_name)
415       throws PropertyError
416     {
417       yield ((Edsf.PersonaStore) this.store)._set_structured_name (this,
418           structured_name);
419     }
420
421   /**
422    * The e-d-s contact uid
423    *
424    * @since 0.6.0
425    */
426   public string contact_id { get; construct; }
427
428   private string _full_name = "";
429   /**
430    * {@inheritDoc}
431    *
432    * @since 0.6.0
433    */
434   [CCode (notify = false)]
435   public string full_name
436     {
437       get { return this._full_name; }
438       set { this.change_full_name.begin (value); }
439     }
440
441   /**
442    * {@inheritDoc}
443    *
444    * @since 0.6.2
445    */
446   public async void change_full_name (string full_name) throws PropertyError
447     {
448       yield ((Edsf.PersonaStore) this.store)._set_full_name (this, full_name);
449     }
450
451   private string _nickname = "";
452   /**
453    * {@inheritDoc}
454    *
455    * @since 0.6.0
456    */
457   [CCode (notify = false)]
458   public string nickname
459     {
460       get { return this._nickname; }
461       set { this.change_nickname.begin (value); }
462     }
463
464   /**
465    * {@inheritDoc}
466    *
467    * @since 0.6.2
468    */
469   public async void change_nickname (string nickname) throws PropertyError
470     {
471       yield ((Edsf.PersonaStore) this.store)._set_nickname (this, nickname);
472     }
473
474   private Gender _gender;
475   /**
476    * {@inheritDoc}
477    *
478    * @since 0.6.0
479    */
480   [CCode (notify = false)]
481   public Gender gender
482     {
483       get { return this._gender; }
484       set { this.change_gender.begin (value); }
485     }
486
487   /**
488    * {@inheritDoc}
489    *
490    * @since 0.6.2
491    */
492   public async void change_gender (Gender gender) throws PropertyError
493     {
494       yield ((Edsf.PersonaStore) this.store)._set_gender (this, gender);
495     }
496
497   private HashSet<UrlFieldDetails>? _urls = null;
498   private Set<UrlFieldDetails>? _urls_ro = null;
499   /**
500    * {@inheritDoc}
501    *
502    * @since 0.6.0
503    */
504   [CCode (notify = false)]
505   public Set<UrlFieldDetails> urls
506     {
507       get
508         {
509           this._update_urls (true, false);
510           return this._urls_ro;
511         }
512       set { this.change_urls.begin (value); }
513     }
514
515   /**
516    * {@inheritDoc}
517    *
518    * @since 0.6.2
519    */
520   public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
521     {
522       yield ((Edsf.PersonaStore) this.store)._set_urls (this, urls);
523     }
524
525   /* NOTE: Other properties support lazy initialisation, but im-addresses
526    * doesn't as it's a linkable property, so always has to be loaded anyway. */
527   private HashMultiMap<string, ImFieldDetails> _im_addresses =
528       new HashMultiMap<string, ImFieldDetails> (null, null,
529           (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
530           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
531
532   /**
533    * {@inheritDoc}
534    *
535    * @since 0.6.0
536    */
537   [CCode (notify = false)]
538   public MultiMap<string, ImFieldDetails> im_addresses
539     {
540       get { return this._im_addresses; }
541       set { this.change_im_addresses.begin (value); }
542     }
543
544   /**
545    * {@inheritDoc}
546    *
547    * @since 0.6.2
548    */
549   public async void change_im_addresses (
550       MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
551     {
552       yield ((Edsf.PersonaStore) this.store)._set_im_fds (this, im_addresses);
553     }
554
555   private HashSet<string>? _groups = null;
556   private Set<string>? _groups_ro = null;
557
558   /**
559    * {@inheritDoc}
560    *
561    * @since 0.6.0
562    */
563   [CCode (notify = false)]
564   public Set<string> groups
565     {
566       get
567         {
568           this._update_groups (true, false);
569           return this._groups_ro;
570         }
571       set { this.change_groups.begin (value); }
572     }
573
574   /**
575    * {@inheritDoc}
576    *
577    * @since 0.6.0
578    */
579   public async void change_group (string group, bool is_member)
580       throws GLib.Error
581     {
582       /* NOTE: This method specifically accesses this.groups rather than
583        * this._groups, so that lazy loading is guaranteed to happen if
584        * necessary. */
585       /* Nothing to do? */
586       if ((is_member == true && this.groups.contains (group) == true) ||
587           (is_member == false && this.groups.contains (group) == false))
588         {
589           return;
590         }
591
592       /* Replace the current set of groups with a modified one. */
593       var new_groups = new HashSet<string> ();
594       foreach (var category_name in this.groups)
595         {
596           new_groups.add (category_name);
597         }
598
599       if (is_member == false)
600         {
601           new_groups.remove (group);
602         }
603       else
604         {
605           new_groups.add (group);
606         }
607
608       yield this.change_groups (new_groups);
609     }
610
611   /**
612    * {@inheritDoc}
613    *
614    * @since 0.6.2
615    */
616   public async void change_groups (Set<string> groups) throws PropertyError
617     {
618       yield ((Edsf.PersonaStore) this.store)._set_groups (this, groups);
619     }
620
621   /**
622    * Change the contact's system groups.
623    *
624    * The system groups are a property exposed by Google Contacts address books,
625    * and can include any combination of the following identifier:
626    * - "Contacts"
627    * - "Family"
628    * - "Friends"
629    * - "Coworkers"
630    *
631    * Setting the system groups will also change the group membership to include
632    * the localized version of those groups, and may change the value of
633    * {@link Edsf.Persona.in_google_personal_group}.
634    *
635    * Attempting to call this method on a persona beloging to a PersonaStore which
636    * is not Google will throw a PropertyError.
637    *
638    * It's preferred to call this rather than setting {@link Persona.system_groups}
639    * directly, as this method gives error notification and will only return once
640    * the groups have been written to the relevant backing store (or the
641    * operation's failed).
642    *
643    * @param system_groups the complete set of system group ids the contact should be a member of
644    * @throws PropertyError if setting the groups failed
645    * @since UNRELEASED
646    */
647   public async void change_system_groups (Set<string> system_groups) throws PropertyError
648     {
649       yield ((Edsf.PersonaStore) this.store)._set_system_groups (this, system_groups);
650     }
651
652   private static const string GOOGLE_PERSONAL_GROUP_NAME = "Contacts";
653
654   /**
655    * Change whether this contact belongs to the personal group or not.
656    *
657    * The personal contact group is a concept that exists only in Google
658    * address books. Other backends will throw a PropertyError.
659    *
660    * It's preferred to call this rather than setting {@link Persona.in_google_personal_group}
661    * directly, as this method gives error notification and will only return once
662    * the membership has been written to the relevant backing store (or the
663    * operation's failed).
664    *
665    * @param in_personal Whether to add or remove the personal group membership
666    * @throws PropertyError if the address book is not Google, or if setting the property failed
667    * @since UNRELEASED
668    */
669   public async void change_in_google_personal_group (bool in_personal) throws PropertyError
670     {
671       if (in_personal == this._in_google_personal_group)
672         {
673           return;
674         }
675
676       HashSet<string> new_system_groups = new HashSet<string> ();
677       foreach (var sg in this._system_groups)
678         {
679           if (sg == GOOGLE_PERSONAL_GROUP_NAME && !in_personal)
680             {
681               continue;
682             }
683
684           new_system_groups.add (sg);
685         }
686
687       if (in_personal)
688         {
689           new_system_groups.add (GOOGLE_PERSONAL_GROUP_NAME);
690         }
691
692       yield ((Edsf.PersonaStore) this.store)._set_system_groups (this, new_system_groups);
693     }
694
695   /**
696    * {@inheritDoc}
697    *
698    * e-d-s has no equivalent field, so this is unsupported.
699    *
700    * @since 0.6.2
701    */
702   [CCode (notify = false)]
703   public string? calendar_event_id
704     {
705       get { return null; } /* unsupported */
706       set { this.change_calendar_event_id.begin (value); } /* not writeable */
707     }
708
709   /* We cache the timezone we use for converting birthdays to UTC since creating
710    * it requires mmapping /etc/localtime, which means lots of syscalls. */
711   private static TimeZone _local_time_zone = new TimeZone.local ();
712
713   private DateTime? _birthday = null;
714   /**
715    * {@inheritDoc}
716    *
717    * @since 0.6.2
718    */
719   [CCode (notify = false)]
720   public DateTime? birthday
721     {
722       get { return this._birthday; }
723       set { this.change_birthday.begin (value); }
724     }
725
726   /**
727    * {@inheritDoc}
728    *
729    * @since 0.6.2
730    */
731   public async void change_birthday (DateTime? bday)
732       throws PropertyError
733     {
734       yield ((Edsf.PersonaStore) this.store)._set_birthday (this,
735           bday);
736     }
737
738   private HashSet<RoleFieldDetails>? _roles = null;
739   private Set<RoleFieldDetails>? _roles_ro = null;
740
741   /**
742    * {@inheritDoc}
743    *
744    * @since 0.6.2
745    */
746   [CCode (notify = false)]
747   public Set<RoleFieldDetails> roles
748     {
749       get
750         {
751           this._update_roles (true, false);
752           return this._roles_ro;
753         }
754       set { this.change_roles.begin (value); }
755     }
756
757   /**
758    * {@inheritDoc}
759    *
760    * @since 0.6.2
761    */
762   public async void change_roles (Set<RoleFieldDetails> roles)
763       throws PropertyError
764     {
765       yield ((Edsf.PersonaStore) this.store)._set_roles (this, roles);
766     }
767
768   private bool _is_favourite = false;
769
770   /**
771    * Whether this contact is a user-defined favourite.
772    *
773    * @since 0.6.5
774    */
775   [CCode (notify = false)]
776   public bool is_favourite
777       {
778         get
779           {
780             this._update_groups (true, false); /* also checks for favourites */
781             return this._is_favourite;
782           }
783         set { this.change_is_favourite.begin (value); }
784       }
785
786   /**
787    * {@inheritDoc}
788    *
789    * @since 0.6.5
790    */
791   public async void change_is_favourite (bool is_favourite) throws PropertyError
792     {
793       if (this._is_favourite == is_favourite)
794         {
795           return;
796         }
797
798       yield ((Edsf.PersonaStore) this.store)._set_is_favourite (this,
799           is_favourite);
800     }
801
802   private HashSet<string> _anti_links;
803   private Set<string> _anti_links_ro;
804
805   /**
806    * {@inheritDoc}
807    *
808    * @since 0.7.3
809    */
810   [CCode (notify = false)]
811   public Set<string> anti_links
812     {
813       get { return this._anti_links_ro; }
814       set { this.change_anti_links.begin (value); }
815     }
816
817   /**
818    * {@inheritDoc}
819    *
820    * @since 0.7.3
821    */
822   public async void change_anti_links (Set<string> anti_links)
823       throws PropertyError
824     {
825       yield ((Edsf.PersonaStore) this.store)._set_anti_links (this, anti_links);
826     }
827
828   private HashSet<string>? _system_groups = null;
829   private Set<string>? _system_groups_ro = null;
830   private bool _in_google_personal_group;
831
832   /**
833    * The complete set of system group identifiers the contact belongs to.
834    * See {@link Persona.change_system_groups} for details.
835    *
836    * @since UNRELEASED
837    */
838   [CCode (notify = false)]
839   public Set<string>? system_groups
840     {
841       get
842         {
843           this._update_groups (true);
844           return this._system_groups_ro;
845         }
846
847       set { this.change_system_groups.begin (value); }
848     }
849
850   /**
851    * Whether this contact is in the “My Contacts” section of the user’s address
852    * book, rather than the “Other” section.
853    *
854    * @since 0.7.3
855    */
856   [CCode (notify = false)]
857   public bool in_google_personal_group
858     {
859       get
860         {
861           this._update_groups (true); /* also checks for the personal group */
862           return this._in_google_personal_group;
863         }
864
865       set { this.change_in_google_personal_group.begin (value); }
866     }
867
868   /**
869    * Build a IID.
870    *
871    * @param store_id the {@link PersonaStore.id}
872    * @param contact the Contact
873    * @return a valid IID
874    *
875    * @since 0.6.0
876    */
877   internal static string build_iid_from_contact (string store_id,
878       E.Contact contact)
879     {
880       var contact_id =
881           Edsf.Persona._get_property_from_contact<string> (contact, "id");
882       return Edsf.Persona.build_iid (store_id, (!) (contact_id ?? ""));
883     }
884
885   /**
886    * Build a IID.
887    *
888    * @param store_id the {@link PersonaStore.id}
889    * @param contact_id the id belonging to the Contact
890    * @return a valid IID
891    *
892    * @since 0.6.0
893    */
894   internal static string build_iid (string store_id, string contact_id)
895     {
896       return "%s:%s".printf (store_id, contact_id);
897     }
898
899
900   /**
901    * Create a new persona.
902    *
903    * Create a new persona for the {@link PersonaStore} ``store``, representing
904    * the EDS contact given by ``contact``.
905    *
906    * @param store the store which will contain the persona
907    * @param contact the EDS contact being represented by the persona
908    *
909    * @since 0.6.0
910    */
911   public Persona (PersonaStore store, E.Contact contact)
912     {
913       var _contact_id =
914           Edsf.Persona._get_property_from_contact<string> (contact, "id");
915       var contact_id = (!) (_contact_id ?? "");
916
917       var uid = Folks.Persona.build_uid (BACKEND_NAME, store.id, contact_id);
918       var iid = Edsf.Persona.build_iid (store.id, contact_id);
919       var is_user = BookClient.is_self (contact);
920       var _full_name =
921           Edsf.Persona._get_property_from_contact<string> (contact,
922               "full_name");
923       var full_name = (!) (_full_name ?? "");
924
925       Object (display_id: full_name,
926               uid: uid,
927               iid: iid,
928               store: store,
929               is_user: is_user,
930               contact_id: contact_id,
931               contact: contact);
932     }
933
934   construct
935     {
936       debug ("Creating new Edsf.Persona with IID '%s'", this.iid);
937
938       this._gender = Gender.UNSPECIFIED;
939       this._phone_numbers = new HashSet<PhoneFieldDetails> (
940            AbstractFieldDetails<string>.hash_static,
941            AbstractFieldDetails<string>.equal_static);
942       this._phone_numbers_ro = this._phone_numbers.read_only_view;
943       this._email_addresses = new HashSet<EmailFieldDetails> (
944            AbstractFieldDetails<string>.hash_static,
945            AbstractFieldDetails<string>.equal_static);
946       this._email_addresses_ro = this._email_addresses.read_only_view;
947       this._notes = new HashSet<NoteFieldDetails> (
948            AbstractFieldDetails<string>.hash_static,
949            AbstractFieldDetails<string>.equal_static);
950       this._notes_ro = this._notes.read_only_view;
951       this._urls = new HashSet<UrlFieldDetails> (
952            AbstractFieldDetails<string>.hash_static,
953            AbstractFieldDetails<string>.equal_static);
954       this._urls_ro = this._urls.read_only_view;
955       this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
956            AbstractFieldDetails<PostalAddress>.hash_static,
957            AbstractFieldDetails<PostalAddress>.equal_static);
958       this._postal_addresses_ro = this._postal_addresses.read_only_view;
959       this._local_ids = new HashSet<string> ();
960       this._local_ids_ro = this._local_ids.read_only_view;
961       this._web_service_addresses =
962         new HashMultiMap<string, WebServiceFieldDetails> (
963           null, null,
964           (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
965           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
966       this._email_addresses_ro = this._email_addresses.read_only_view;
967       this._groups = new HashSet<string> ();
968       this._groups_ro = this._groups.read_only_view;
969       this._roles = new HashSet<RoleFieldDetails> (
970            AbstractFieldDetails<Role>.hash_static,
971            AbstractFieldDetails<Role>.equal_static);
972       this._roles_ro = this._roles.read_only_view;
973       this._anti_links = new HashSet<string> ();
974       this._anti_links_ro = this._anti_links.read_only_view;
975
976       this._update (this._contact);
977     }
978
979   /**
980    * {@inheritDoc}
981    *
982    * @since 0.6.0
983    */
984   public override void linkable_property_to_links (string prop_name,
985       Folks.Persona.LinkablePropertyCallback callback)
986     {
987       if (prop_name == "im-addresses")
988         {
989           foreach (var protocol in this._im_addresses.get_keys ())
990             {
991               var im_fds = this._im_addresses.get (protocol);
992
993               foreach (var im_fd in im_fds)
994                   callback (protocol + ":" + im_fd.value);
995             }
996         }
997       else if (prop_name == "local-ids")
998         {
999           /* Note: we need to use this.local_ids and not this._local_ids,
1000            * otherwise this can have a different  behaviour depending
1001            * on the state of the current Persona depending on whether
1002            * this.local_ids was called before or not. */
1003           foreach (var id in this.local_ids)
1004             {
1005               callback (id);
1006             }
1007         }
1008       else if (prop_name == "web-service-addresses")
1009         {
1010           foreach (var web_service in this.web_service_addresses.get_keys ())
1011             {
1012               var web_service_addresses =
1013                   this._web_service_addresses.get (web_service);
1014
1015               foreach (var ws_fd in web_service_addresses)
1016                   callback (web_service + ":" + ws_fd.value);
1017             }
1018         }
1019       else if (prop_name == "email-addresses")
1020         {
1021           foreach (var email in this._email_addresses)
1022               callback (email.value);
1023         }
1024       else
1025         {
1026           /* Chain up */
1027           base.linkable_property_to_links (prop_name, callback);
1028         }
1029     }
1030
1031   ~Persona ()
1032     {
1033       debug ("Destroying Edsf.Persona '%s': %p", this.uid, this);
1034     }
1035
1036   /**
1037    * Update attribs of the persona.
1038    */
1039   internal void _update (E.Contact updated_contact)
1040     {
1041       this.freeze_notify ();
1042
1043       /* We get a new E.Contact instance from EDS containing all the updates,
1044        * so replace our existing contact with it. */
1045       this._contact = updated_contact;
1046       this.notify_property ("contact");
1047
1048       this._update_names ();
1049       this._update_avatar ();
1050       this._update_urls (false);
1051       this._update_phones (false);
1052       this._update_addresses (false);
1053       this._update_emails ();
1054
1055       /* Note: because we assume certain e-mail addresses
1056        * (@gmail, @msn, etc) to also be IM IDs we /must/
1057        * update the latter after we've taken care of the former.
1058        */
1059       this._update_im_addresses ();
1060
1061       this._update_groups (false);
1062       this._update_notes (false);
1063       this._update_local_ids ();
1064       this._update_web_services_addresses ();
1065       this._update_gender ();
1066       this._update_birthday ();
1067       this._update_roles (false);
1068       this._update_favourite ();
1069       this._update_anti_links ();
1070
1071       this.thaw_notify ();
1072     }
1073
1074   private void _update_params (AbstractFieldDetails details,
1075       E.VCardAttribute attr)
1076     {
1077       foreach (unowned E.VCardAttributeParam param in attr.get_params ())
1078         {
1079           string param_name = param.get_name ().down ();
1080           foreach (unowned string param_value in param.get_values ())
1081             {
1082               if (param_name == AbstractFieldDetails.PARAM_TYPE)
1083                 {
1084                   details.add_parameter (param_name, param_value.down ());
1085                 }
1086               else
1087                 {
1088                   details.add_parameter (param_name, param_value);
1089                 }
1090             }
1091         }
1092     }
1093
1094   private void _update_gender ()
1095     {
1096       var gender = Gender.UNSPECIFIED;
1097       var gender_attr =
1098           this.contact.get_attribute (Edsf.Persona.gender_attribute_name);
1099
1100       if (gender_attr != null)
1101         {
1102           var val = ((!) gender_attr).get_value ();
1103           if (val != null)
1104             {
1105               switch (((!) val).up ())
1106                 {
1107                   case Edsf.Persona.gender_male:
1108                     gender = Gender.MALE;
1109                     break;
1110                   case Edsf.Persona.gender_female:
1111                     gender = Gender.FEMALE;
1112                     break;
1113                   default:
1114                     /* Unspecified, as above */
1115                     break;
1116                 }
1117             }
1118         }
1119
1120       if (this._gender != gender)
1121         {
1122           this._gender = gender;
1123           this.notify_property ("gender");
1124         }
1125     }
1126
1127   private void _update_birthday ()
1128     {
1129       var _bday = this._get_property<E.ContactDate> ("birth_date");
1130
1131       if (_bday != null)
1132         {
1133           var bday = (!) _bday;
1134
1135           /* Since e-d-s stores birthdays as a plain date, we take the
1136            * given date in local time and convert it to UTC as mandated
1137            * by the BirthdayDetails interface.
1138            * We cache the timezone since creating it requires mmapping
1139            * /etc/localtime, which means lots of syscalls. */
1140           var d = new DateTime (Persona._local_time_zone,
1141               (int) bday.year, (int) bday.month, (int) bday.day, 0, 0, 0.0);
1142           if (this._birthday == null ||
1143               (this._birthday != null &&
1144                   !((!) this._birthday).equal (d.to_utc ())))
1145             {
1146               this._birthday = d.to_utc ();
1147               this.notify_property ("birthday");
1148             }
1149         }
1150       else
1151         {
1152           if (this._birthday != null)
1153             {
1154               this._birthday = null;
1155               this.notify_property ("birthday");
1156             }
1157         }
1158     }
1159
1160   private void _update_roles (bool create_if_not_exist, bool emit_notification = true)
1161     {
1162       /* See the comments in Folks.Individual about the lazy instantiation
1163        * strategy for roles. */
1164       if (this._roles == null && create_if_not_exist == false)
1165         {
1166           if (emit_notification)
1167             {
1168               this.notify_property ("roles");
1169             }
1170           return;
1171         }
1172       else if (this._roles == null)
1173         {
1174           this._roles = new HashSet<RoleFieldDetails> (
1175               AbstractFieldDetails<Role>.hash_static,
1176               AbstractFieldDetails<Role>.equal_static);
1177           this._roles_ro = this._roles.read_only_view;
1178         }
1179
1180       var new_roles = new HashSet<RoleFieldDetails> (
1181           AbstractFieldDetails<Role>.hash_static,
1182           AbstractFieldDetails<Role>.equal_static);
1183
1184       var default_role_fd = this._get_default_role ();
1185       if (default_role_fd != null)
1186         {
1187           new_roles.add ((!) default_role_fd);
1188         }
1189
1190       var vcard = (E.VCard) this.contact;
1191       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1192         {
1193           if (attr.get_name () != "X-ROLES")
1194             continue;
1195
1196           var val = attr.get_value ();
1197           if (val == null || (!) val == "")
1198              {
1199               continue;
1200             }
1201
1202           var role = new Role ("", "");
1203           role.role = (!) val;
1204           var role_fd = new RoleFieldDetails (role);
1205
1206           foreach (unowned E.VCardAttributeParam param in
1207               attr.get_params ())
1208             {
1209               string param_name = param.get_name ().down ();
1210
1211               if (param_name == "organisation_name")
1212                 {
1213                   foreach (unowned string param_value in
1214                       param.get_values ())
1215                     {
1216                       role.organisation_name = param_value;
1217                       break;
1218                     }
1219                 }
1220               else if (param_name == "title")
1221                 {
1222                   foreach (unowned string param_value in
1223                       param.get_values ())
1224                     {
1225                       role.title = param_value;
1226                       break;
1227                     }
1228                 }
1229               else
1230                 {
1231                   foreach (unowned string param_value in
1232                       param.get_values ())
1233                     {
1234                       role_fd.add_parameter (param_name, param_value);
1235                     }
1236                 }
1237             }
1238
1239             new_roles.add (role_fd);
1240         }
1241
1242       if (!Folks.Internal.equal_sets<RoleFieldDetails> (new_roles, this._roles))
1243         {
1244           this._roles = new_roles;
1245           this._roles_ro = new_roles.read_only_view;
1246           if (emit_notification)
1247             {
1248               this.notify_property ("roles");
1249             }
1250         }
1251     }
1252
1253   private RoleFieldDetails? _get_default_role ()
1254     {
1255       RoleFieldDetails? _default_role = null;
1256
1257       var org = this._get_property<string> ("org");
1258       var org_unit = this._get_property<string> ("org_unit");
1259       var office = this._get_property<string> ("office");
1260       var title = this._get_property<string> ("title");
1261       var role = this._get_property<string> ("role");
1262       var manager = this._get_property<string> ("manager");
1263       var assistant = this._get_property<string> ("assistant");
1264
1265       if (org != null ||
1266           org_unit != null ||
1267           office != null ||
1268           title != null ||
1269           role != null ||
1270           manager != null ||
1271           assistant != null)
1272         {
1273           var new_role = new Role (title, org);
1274           if (role != null && (!) role != "")
1275             new_role.role = (!) role;
1276
1277           /* Check if it's non-empty. */
1278           if (!new_role.is_empty ())
1279             {
1280               var default_role = new RoleFieldDetails (new_role);
1281
1282               if (org_unit != null && org_unit != "")
1283                 default_role.set_parameter ("org_unit", (!) org_unit);
1284
1285               if (office != null && office != "")
1286                 default_role.set_parameter ("office", (!) office);
1287
1288               if (manager != null && manager != "")
1289                 default_role.set_parameter ("manager", (!) manager);
1290
1291               if (assistant != null && manager != "")
1292                 default_role.set_parameter ("assistant", (!) assistant);
1293
1294               _default_role = default_role;
1295             }
1296         }
1297
1298       return _default_role;
1299     }
1300
1301   private void _update_web_services_addresses ()
1302     {
1303       var new_services = new HashMultiMap<string, WebServiceFieldDetails> (
1304           null, null,
1305           (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
1306           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
1307
1308       var services = this.contact.get_attribute ("X-FOLKS-WEB-SERVICES-IDS");
1309       if (services != null)
1310         {
1311           foreach (var service in ((!) services).get_params ())
1312             {
1313               var service_name = service.get_name ().down ();
1314               foreach (var service_id in service.get_values ())
1315                 {
1316                   if (service_id == "")
1317                     {
1318                       continue;
1319                     }
1320
1321                   new_services.set (service_name,
1322                       new WebServiceFieldDetails (service_id));
1323                 }
1324             }
1325         }
1326
1327       if (!Utils.multi_map_str_afd_equal (new_services,
1328               this._web_service_addresses))
1329         {
1330           this._web_service_addresses = new_services;
1331           this.notify_property ("web-service-addresses");
1332         }
1333     }
1334
1335   private void _update_emails (bool emit_notification = true)
1336     {
1337       var new_email_addresses = new HashSet<EmailFieldDetails> (
1338           AbstractFieldDetails<string>.hash_static,
1339           AbstractFieldDetails<string>.equal_static);
1340
1341       var attrs = this.contact.get_attributes (E.ContactField.EMAIL);
1342       foreach (var attr in attrs)
1343         {
1344           var val = attr.get_value ();
1345           if (val == null || (!) val == "")
1346             {
1347               continue;
1348             }
1349
1350           var email_fd = new EmailFieldDetails ((!) val);
1351           this._update_params (email_fd, attr);
1352           new_email_addresses.add (email_fd);
1353         }
1354
1355       if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
1356               this._email_addresses))
1357         {
1358           this._email_addresses = new_email_addresses;
1359           this._email_addresses_ro = new_email_addresses.read_only_view;
1360           if (emit_notification)
1361             {
1362               this.notify_property ("email-addresses");
1363             }
1364        }
1365     }
1366
1367   private void _update_notes (bool create_if_not_exist, bool emit_notification = true)
1368     {
1369       /* See the comments in Folks.Individual about the lazy instantiation
1370        * strategy for notes. */
1371       if (this._notes == null && create_if_not_exist == false)
1372         {
1373           if (emit_notification)
1374             {
1375               this.notify_property ("notes");
1376             }
1377           return;
1378         }
1379       else if (this._notes == null)
1380         {
1381           this._notes = new HashSet<NoteFieldDetails> (
1382               AbstractFieldDetails<string>.hash_static,
1383               AbstractFieldDetails<string>.equal_static);
1384           this._notes_ro = this._notes.read_only_view;
1385         }
1386
1387       var new_notes = new HashSet<NoteFieldDetails> (
1388           AbstractFieldDetails<string>.hash_static,
1389           AbstractFieldDetails<string>.equal_static);
1390
1391       var n = this._get_property<string> ("note");
1392       if (n != null && n != "")
1393         {
1394           var note = new NoteFieldDetails ((!) n);
1395           new_notes.add (note);
1396         }
1397
1398       if (!Folks.Internal.equal_sets<NoteFieldDetails> (new_notes, this._notes))
1399         {
1400           this._notes = new_notes;
1401           this._notes_ro = this._notes.read_only_view;
1402           if (emit_notification)
1403             {
1404               this.notify_property ("notes");
1405             }
1406         }
1407     }
1408
1409   private void _update_names ()
1410     {
1411       var _full_name = this._get_property<string> ("full_name");
1412
1413       if (_full_name == null)
1414         {
1415           _full_name = "";
1416         }
1417
1418       var full_name = (!) _full_name;
1419
1420       if (this._full_name != full_name)
1421         {
1422           this._full_name = full_name;
1423           this.notify_property ("full-name");
1424         }
1425
1426       var _nickname = this._get_property<string> ("nickname");
1427
1428       if (_nickname == null)
1429         {
1430           _nickname = "";
1431         }
1432
1433       var nickname = (!) _nickname;
1434
1435       if (this._nickname != nickname)
1436         {
1437           this._nickname = nickname;
1438           this.notify_property ("nickname");
1439         }
1440
1441       StructuredName? structured_name = null;
1442       var _cn = this._get_property<E.ContactName> ("name");
1443       if (_cn != null)
1444         {
1445           var cn = (!) _cn;
1446
1447           string family_name = cn.family;
1448           string given_name  = cn.given;
1449           string additional_names = cn.additional;
1450           string prefixes = cn.prefixes;
1451           string suffixes = cn.suffixes;
1452           structured_name = new StructuredName (family_name, given_name,
1453                                                 additional_names, prefixes,
1454                                                 suffixes);
1455         }
1456
1457       if (structured_name != null && !((!) structured_name).is_empty ())
1458         {
1459           this._structured_name = (!) structured_name;
1460           this.notify_property ("structured-name");
1461         }
1462       else if (this._structured_name != null)
1463         {
1464           this._structured_name = null;
1465           this.notify_property ("structured-name");
1466         }
1467     }
1468
1469   private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? _p)
1470     {
1471       if (_p == null)
1472         {
1473           return null;
1474         }
1475
1476       var p = (!) _p;
1477
1478       switch (p.type)
1479         {
1480           case ContactPhotoType.URI:
1481             var uri = p.get_uri ();
1482             if (uri == null)
1483               {
1484                 return null;
1485               }
1486
1487             return new FileIcon (File.new_for_uri ((!) uri));
1488           case ContactPhotoType.INLINED:
1489             var data = p.get_inlined ();
1490             var mime_type = p.get_mime_type ();
1491             if (data == null || mime_type == null)
1492               {
1493                 return null;
1494               }
1495
1496             return new Edsf.MemoryIcon ((!) mime_type, (!) data);
1497           default:
1498             return null;
1499         }
1500     }
1501
1502   private void _update_avatar ()
1503     {
1504       var p = this._get_property<E.ContactPhoto> ("photo");
1505
1506       var cache = AvatarCache.dup ();
1507
1508       // Convert the ContactPhoto to a LoadableIcon and store or update it.
1509       var new_avatar = this._contact_photo_to_loadable_icon (p);
1510
1511       if (this._avatar != null && new_avatar == null)
1512         {
1513           // Remove the old cached avatar, ignoring errors.
1514           cache.remove_avatar.begin (this.uid, (obj, res) =>
1515             {
1516               try
1517                 {
1518                   cache.remove_avatar.end (res);
1519                 }
1520               catch (GLib.Error e1) {}
1521
1522               this._avatar = null;
1523               this.notify_property ("avatar");
1524             });
1525         }
1526       else if ((this._avatar == null && new_avatar != null) ||
1527           (this._avatar != null && new_avatar != null &&
1528            ((!) this._avatar).equal (new_avatar) == false))
1529         {
1530           /* Store the new avatar in the cache. new_avatar is guaranteed to be
1531            * non-null. */
1532           cache.store_avatar.begin (this.uid, (!) new_avatar, (obj, res) =>
1533             {
1534               try
1535                 {
1536                   cache.store_avatar.end (res);
1537                   this._avatar = new_avatar;
1538                   this.notify_property ("avatar");
1539                 }
1540               catch (GLib.Error e2)
1541                 {
1542                   warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
1543                       this.uid, e2.message);
1544                   new_avatar = null; /* failure */
1545                 }
1546             });
1547         }
1548     }
1549
1550   private void _update_urls (bool create_if_not_exist, bool emit_notification = true)
1551     {
1552       /* See the comments in Folks.Individual about the lazy instantiation
1553        * strategy for URIs. */
1554       if (this._urls == null && create_if_not_exist == false)
1555         {
1556           if (emit_notification)
1557             {
1558               this.notify_property ("urls");
1559             }
1560           return;
1561         }
1562       else if (this._urls == null)
1563         {
1564           this._urls = new HashSet<UrlFieldDetails> (
1565               AbstractFieldDetails<string>.hash_static,
1566               AbstractFieldDetails<string>.equal_static);
1567           this._urls_ro = this._urls.read_only_view;
1568         }
1569
1570       var new_urls = new HashSet<UrlFieldDetails> (
1571           AbstractFieldDetails<string>.hash_static,
1572           AbstractFieldDetails<string>.equal_static);
1573
1574       /* First we get the standard Evo urls.. */
1575       foreach (var mapping in Persona._url_properties)
1576         {
1577           var url_property = mapping.vcard_field_name;
1578           var folks_type = mapping.folks_type;
1579
1580           var u = this._get_property<string> (url_property);
1581           if (u != null && u != "")
1582             {
1583               var fd_u = new UrlFieldDetails ((!) u);
1584               fd_u.set_parameter (AbstractFieldDetails.PARAM_TYPE, folks_type);
1585               new_urls.add (fd_u);
1586             }
1587         }
1588
1589       /* Now we go for extra URLs */
1590       var vcard = (E.VCard) this.contact;
1591       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1592         {
1593           if (attr.get_name () == "X-URIS")
1594             {
1595               var val = attr.get_value ();
1596               if (val == null || (!) val == "")
1597                 {
1598                   continue;
1599                 }
1600
1601               var url_fd = new UrlFieldDetails ((!) val);
1602               this._update_params (url_fd, attr);
1603               new_urls.add (url_fd);
1604             }
1605         }
1606
1607       if (!Utils.set_afd_equal (new_urls, this._urls))
1608         {
1609           this._urls = new_urls;
1610           this._urls_ro = new_urls.read_only_view;
1611           if (emit_notification)
1612             {
1613               this.notify_property ("urls");
1614             }
1615         }
1616     }
1617
1618   private void _update_im_addresses ()
1619     {
1620       var im_eds_map = Persona._get_im_eds_map ();
1621       var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (null,
1622           null, (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
1623           (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
1624
1625       foreach (var im_proto in im_eds_map.get_keys ())
1626         {
1627           var addresses = this.contact.get_attributes (
1628               im_eds_map.lookup (im_proto));
1629           foreach (var attr in addresses)
1630             {
1631               try
1632                 {
1633                   var addr = attr.get_value ();
1634                   if (addr == null || (!) addr == "")
1635                     {
1636                       continue;
1637                     }
1638
1639                   string normalised_addr =
1640                     (owned) ImDetails.normalise_im_address ((!) addr, im_proto);
1641
1642                   if (normalised_addr == "")
1643                     {
1644                       continue;
1645                     }
1646
1647                   var im_fd = new ImFieldDetails (normalised_addr);
1648                   new_im_addresses.set (im_proto, im_fd);
1649                 }
1650               catch (Folks.ImDetailsError e)
1651                 {
1652                   GLib.warning (
1653                       "Problem when trying to normalise address: %s\n",
1654                       e.message);
1655                 }
1656             }
1657         }
1658
1659       /* We consider some e-mail addresses to be IM IDs too. This
1660        * is pretty much a hack to make sure e-d-s contacts are
1661        * automatically linked with their corresponding Telepathy
1662        * Persona. As an undesired side effect we might end up having
1663        * IM addresses that aren't actually used as such (i.e.: people
1664        * who don't actually use GMail or MSN addresses for IM).
1665        *
1666        * See bgo#657142
1667        */
1668       foreach (var email in this._email_addresses)
1669         {
1670           var _proto = this._im_proto_from_addr (email.value);
1671           if (_proto != null)
1672             {
1673               var proto = (!) _proto;
1674
1675               /* Has this already been added? */
1676               var exists = false;
1677               Collection<ImFieldDetails>? current_im_addrs =
1678                   new_im_addresses.get (proto);
1679               if (current_im_addrs != null)
1680                 {
1681                   foreach (var cur_im in (!) current_im_addrs)
1682                     {
1683                       if (cur_im.value == email.value)
1684                         {
1685                           exists = true;
1686                           break;
1687                         }
1688                     }
1689                 }
1690
1691               if (exists)
1692                 continue;
1693
1694               try
1695                 {
1696                   string normalised_addr =
1697                     (owned) ImDetails.normalise_im_address (email.value, proto);
1698                   var im_fd = new ImFieldDetails (normalised_addr);
1699                   new_im_addresses.set (proto, im_fd);
1700                 }
1701               catch (Folks.ImDetailsError e)
1702                 {
1703                   GLib.warning (
1704                       "Problem when trying to normalise address: %s\n",
1705                       e.message);
1706                 }
1707             }
1708         }
1709
1710       if (!Utils.multi_map_str_afd_equal (new_im_addresses,
1711               this._im_addresses))
1712         {
1713           this._im_addresses = new_im_addresses;
1714           this.notify_property ("im-addresses");
1715         }
1716     }
1717
1718   private void _update_groups (bool create_if_not_exist, bool emit_notification = true)
1719     {
1720       /* See the comments in Folks.Individual about the lazy instantiation
1721        * strategy for groups. */
1722       if (this._groups == null && create_if_not_exist == false)
1723         {
1724           if (emit_notification)
1725             {
1726               this.notify_property ("groups");
1727             }
1728           return;
1729         }
1730
1731       var category_names =
1732           this._contact.get<GLib.List<string>> (E.ContactField.CATEGORY_LIST);
1733       var new_categories = new HashSet<string> ();
1734       bool any_added_categories = false;
1735
1736       foreach (var category_name in category_names)
1737         {
1738           /* Skip the “Starred in Android” group for Google personas; we handle
1739            * it later. */
1740           if (((Edsf.PersonaStore) store)._is_google_contacts_address_book () &&
1741               category_name == Edsf.PersonaStore.android_favourite_group_name)
1742             {
1743               continue;
1744             }
1745
1746           new_categories.add (category_name);
1747
1748           /* Is this a new category? */
1749           if (this._groups == null || !this._groups.contains (category_name))
1750             {
1751               any_added_categories = true;
1752             }
1753         }
1754
1755       /* Work out if categories have been removed. */
1756       bool any_removed_categories = false;
1757
1758       if (this._groups != null)
1759         {
1760           foreach (var category_name in this._groups)
1761             {
1762               /* Skip the “Starred in Android” group for Google personas; we handle
1763                * it later. */
1764               if (((Edsf.PersonaStore) store)._is_google_contacts_address_book () &&
1765                   category_name == Edsf.PersonaStore.android_favourite_group_name)
1766                 {
1767                   continue;
1768                 }
1769
1770               if (!new_categories.contains (category_name))
1771                 {
1772                   any_removed_categories = true;
1773                 }
1774             }
1775         }
1776
1777       this._groups = new_categories;
1778       this._groups_ro = this._groups.read_only_view;
1779
1780       /* Check our new set of system groups if this is a Google address book. */
1781       var store = (Edsf.PersonaStore) this.store;
1782       var in_google_personal_group = false;
1783       var should_notify_sysgroups = false;
1784
1785       if (store._is_google_contacts_address_book ())
1786         {
1787           var vcard = (E.VCard) this.contact;
1788           unowned E.VCardAttribute? attr =
1789              vcard.get_attribute ("X-GOOGLE-SYSTEM-GROUP-IDS");
1790           if (attr != null)
1791             {
1792               unowned GLib.List<string> system_group_ids = attr.get_values();
1793               var new_sysgroups = new HashSet<string> ();
1794               bool any_added_sysgroups = false;
1795
1796               foreach (var system_group_id in system_group_ids)
1797                 {
1798                   new_sysgroups.add (system_group_id);
1799
1800                   if (this._system_groups == null ||
1801                       !this._system_groups.contains (system_group_id))
1802                     {
1803                       any_added_sysgroups = true;
1804                     }
1805                 }
1806
1807               /* Work out if categories have been removed. */
1808               bool any_removed_sysgroups = false;
1809
1810               if (this._system_groups != null)
1811                 {
1812                   foreach (var system_group_id in this._system_groups)
1813                     {
1814                       if (!new_sysgroups.contains (system_group_id))
1815                         {
1816                           any_removed_sysgroups = true;
1817                         }
1818                     }
1819                 }
1820
1821               /* If we're in the GDATA_CONTACTS_GROUP_CONTACTS group, then
1822                * we're in the user's "My Contacts" address book, as opposed
1823                * to their "Other" address book. */
1824               if (new_sysgroups.contains (GOOGLE_PERSONAL_GROUP_NAME))
1825                 {
1826                   in_google_personal_group = true;
1827                 }
1828
1829               this._system_groups = new_sysgroups;
1830               this._system_groups_ro = new_sysgroups.read_only_view;
1831
1832               if (any_added_sysgroups || any_removed_sysgroups)
1833                 should_notify_sysgroups = true;
1834             }
1835           else
1836             {
1837               this._system_groups = new HashSet<string>();
1838               this._system_groups_ro = this._system_groups.read_only_view;
1839             }
1840         }
1841
1842       /* Check whether our favourite status needs updating. */
1843       var old_is_favourite = this._is_favourite;
1844
1845       if (store._is_google_contacts_address_book ())
1846         {
1847           this._is_favourite = false;
1848
1849           foreach (var category_name in category_names)
1850             {
1851               /* We link the “Starred in Android” group to Google Contacts
1852                * address books. See: bgo#661490. */
1853               if (category_name ==
1854                   Edsf.PersonaStore.android_favourite_group_name)
1855                 {
1856                   this._is_favourite = true;
1857                 }
1858             }
1859         }
1860
1861       /* Notify if anything's changed. */
1862       this.freeze_notify ();
1863
1864       if ((any_added_categories || any_removed_categories) &&
1865           emit_notification)
1866         {
1867           this.notify_property ("groups");
1868         }
1869       if (should_notify_sysgroups && emit_notification)
1870         {
1871           this.notify_property ("system-groups");
1872         }
1873       if (this._is_favourite != old_is_favourite && emit_notification)
1874         {
1875           this.notify_property ("is-favourite");
1876         }
1877       if (in_google_personal_group != this._in_google_personal_group)
1878         {
1879           this._in_google_personal_group = in_google_personal_group;
1880           if (emit_notification)
1881             {
1882               this.notify_property ("in-google-personal-group");
1883             }
1884         }
1885
1886       this.thaw_notify ();
1887    }
1888
1889   /**
1890    * build a table of im protocols / im protocol aliases
1891    */
1892   internal static GLib.HashTable<string, E.ContactField> _get_im_eds_map ()
1893     {
1894       GLib.HashTable<string, E.ContactField> retval;
1895
1896       lock (Edsf.Persona._im_eds_map)
1897         {
1898           if (Edsf.Persona._im_eds_map == null)
1899             {
1900               var table =
1901                   new GLib.HashTable<string, E.ContactField> (str_hash,
1902                       str_equal);
1903
1904               table.insert ("aim", ContactField.IM_AIM);
1905               table.insert ("yahoo", ContactField.IM_YAHOO);
1906               table.insert ("groupwise", ContactField.IM_GROUPWISE);
1907               table.insert ("jabber", ContactField.IM_JABBER);
1908               table.insert ("msn", ContactField.IM_MSN);
1909               table.insert ("icq", ContactField.IM_ICQ);
1910               table.insert ("gadugadu", ContactField.IM_GADUGADU);
1911               table.insert ("skype", ContactField.IM_SKYPE);
1912
1913               Edsf.Persona._im_eds_map = table;
1914             }
1915
1916           retval = (!) Edsf.Persona._im_eds_map;
1917         }
1918
1919       return retval;
1920     }
1921
1922   private void _update_phones (bool create_if_not_exist, bool emit_notification = true)
1923     {
1924       /* See the comments in Folks.Individual about the lazy instantiation
1925        * strategy for phone numbers. */
1926       if (this._phone_numbers == null && create_if_not_exist == false)
1927         {
1928           this.notify_property ("phone-numbers");
1929           return;
1930         }
1931       else if (this._phone_numbers == null)
1932         {
1933           this._phone_numbers = new HashSet<PhoneFieldDetails> (
1934               AbstractFieldDetails<string>.hash_static,
1935               AbstractFieldDetails<string>.equal_static);
1936           this._phone_numbers_ro = this._phone_numbers.read_only_view;
1937         }
1938
1939       var new_phone_numbers = new HashSet<PhoneFieldDetails> (
1940           AbstractFieldDetails<string>.hash_static,
1941           AbstractFieldDetails<string>.equal_static);
1942
1943       var attrs = this.contact.get_attributes (E.ContactField.TEL);
1944       foreach (var attr in attrs)
1945         {
1946           var val = attr.get_value ();
1947           if (val == null || (!) val == "")
1948             {
1949               continue;
1950             }
1951
1952           var phone_fd = new PhoneFieldDetails ((!) val);
1953           this._update_params (phone_fd, attr);
1954           new_phone_numbers.add (phone_fd);
1955         }
1956
1957       if (!Folks.Internal.equal_sets<PhoneFieldDetails>  (new_phone_numbers,
1958               this._phone_numbers))
1959         {
1960           this._phone_numbers = new_phone_numbers;
1961           this._phone_numbers_ro = new_phone_numbers.read_only_view;
1962           if (emit_notification)
1963             {
1964               this.notify_property ("phone-numbers");
1965             }
1966         }
1967    }
1968
1969   private PostalAddress _postal_address_from_attribute (E.VCardAttribute attr)
1970     {
1971       unowned GLib.List<string>? values = attr.get_values();
1972       unowned GLib.List<string>? l = values;
1973
1974       var address_format = "";
1975       var po_box = "";
1976       var extension = "";
1977       var street = "";
1978       var locality = "";
1979       var region = "";
1980       var postal_code = "";
1981       var country = "";
1982
1983       if (l != null)
1984         {
1985           po_box = ((!) l).data;
1986           l = ((!) l).next;
1987         }
1988       if (l != null)
1989         {
1990           extension = ((!) l).data;
1991           l = ((!) l).next;
1992         }
1993       if (l != null)
1994         {
1995           street = ((!) l).data;
1996           l = ((!) l).next;
1997         }
1998       if (l != null)
1999         {
2000           locality = ((!) l).data;
2001           l = ((!) l).next;
2002         }
2003       if (l != null)
2004         {
2005           region = ((!) l).data;
2006           l = ((!) l).next;
2007         }
2008       if (l != null)
2009         {
2010           postal_code = ((!) l).data;
2011           l = ((!) l).next;
2012         }
2013       if (l != null)
2014         {
2015           country = ((!) l).data;
2016           l = ((!) l).next;
2017         }
2018
2019       return new PostalAddress (po_box, extension, street,
2020                                 locality, region, postal_code, country,
2021                                 address_format, null);
2022     }
2023
2024   /*
2025    * TODO: we should check if addresses corresponding to different types
2026    *       are the same and if so instantiate only one PostalAddress
2027    *       (with the given types).
2028    */
2029   private void _update_addresses (bool create_if_not_exist, bool emit_notification = true)
2030     {
2031       /* See the comments in Folks.Individual about the lazy instantiation
2032        * strategy for addresses. */
2033       if (this._postal_addresses == null && create_if_not_exist == false)
2034         {
2035           if (emit_notification)
2036             {
2037               this.notify_property ("postal-addresses");
2038             }
2039           return;
2040         }
2041       else if (this._postal_addresses == null)
2042         {
2043           this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
2044               AbstractFieldDetails<PostalAddress>.hash_static,
2045               AbstractFieldDetails<PostalAddress>.equal_static);
2046           this._postal_addresses_ro = this._postal_addresses.read_only_view;
2047         }
2048
2049       var new_postal_addresses = new HashSet<PostalAddressFieldDetails> (
2050           AbstractFieldDetails<PostalAddress>.hash_static,
2051           AbstractFieldDetails<PostalAddress>.equal_static);
2052
2053       var attrs = this.contact.get_attributes (E.ContactField.ADDRESS);
2054       foreach (unowned E.VCardAttribute attr in attrs)
2055         {
2056           var address = this._postal_address_from_attribute (attr);
2057           if (address.is_empty ())
2058             {
2059               continue;
2060             }
2061
2062           var pa_fd = new PostalAddressFieldDetails (address);
2063           this._update_params (pa_fd, attr);
2064           new_postal_addresses.add (pa_fd);
2065         }
2066
2067       if (!Folks.Internal.equal_sets<PostalAddressFieldDetails> (
2068               new_postal_addresses,
2069               this._postal_addresses))
2070         {
2071           this._postal_addresses = new_postal_addresses;
2072           this._postal_addresses_ro = new_postal_addresses.read_only_view;
2073           if (emit_notification)
2074             {
2075               this.notify_property ("postal-addresses");
2076             }
2077         }
2078     }
2079
2080   private void _update_local_ids ()
2081     {
2082       var new_local_ids = new HashSet<string> ();
2083
2084       var ids = this.contact.get_attribute ("X-FOLKS-CONTACTS-IDS");
2085       if (ids != null)
2086         {
2087           unowned GLib.List<string> ids_v = ((!) ids).get_values ();
2088
2089           foreach (var local_id in ids_v)
2090             {
2091               if (local_id != "")
2092                 {
2093                   new_local_ids.add (local_id);
2094                 }
2095             }
2096         }
2097
2098       /* Make sure it includes our local id */
2099       new_local_ids.add (this.iid);
2100
2101       if (!Folks.Internal.equal_sets<string> (new_local_ids, this.local_ids))
2102         {
2103           this._local_ids = new_local_ids;
2104           this._local_ids_ro = this._local_ids.read_only_view;
2105           this.notify_property ("local-ids");
2106         }
2107     }
2108
2109   private void _update_favourite ()
2110     {
2111       bool is_fav = false;
2112
2113       var fav = this.contact.get_attribute ("X-FOLKS-FAVOURITE");
2114       if (fav != null)
2115         {
2116           var val = ((!) fav).get_value ();
2117           if (val != null && ((!) val).down () == "true")
2118             {
2119               is_fav = true;
2120             }
2121         }
2122
2123       if (is_fav != this._is_favourite)
2124         {
2125           this._is_favourite = is_fav;
2126           this.notify_property ("is-favourite");
2127         }
2128     }
2129
2130   private void _update_anti_links ()
2131     {
2132       var new_anti_links = new HashSet<string> ();
2133
2134       var vcard = (E.VCard) this.contact;
2135       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
2136         {
2137           if (attr.get_name () != Edsf.PersonaStore.anti_links_attribute_name)
2138             {
2139               continue;
2140             }
2141
2142           var val = attr.get_value ();
2143           if (val == null || (!) val == "")
2144              {
2145               continue;
2146             }
2147
2148           new_anti_links.add ((!) val);
2149         }
2150
2151       if (!Folks.Internal.equal_sets<string> (new_anti_links, this._anti_links))
2152         {
2153           this._anti_links = new_anti_links;
2154           this._anti_links_ro = new_anti_links.read_only_view;
2155           this.notify_property ("anti-links");
2156         }
2157     }
2158
2159   internal static T? _get_property_from_contact<T> (E.Contact contact,
2160       string prop_name)
2161     {
2162       T? prop_value = null;
2163       prop_value = contact.get<T> (E.Contact.field_id (prop_name));
2164       return prop_value;
2165     }
2166
2167   private T? _get_property<T> (string prop_name)
2168     {
2169       return Edsf.Persona._get_property_from_contact<T> (this.contact,
2170           prop_name);
2171     }
2172
2173   private string? _im_proto_from_addr (string addr)
2174     {
2175       if (addr.index_of ("@") == -1)
2176         return null;
2177
2178       var tokens = addr.split ("@", 2);
2179
2180       if (tokens.length != 2)
2181         return null;
2182
2183       var domain = tokens[1];
2184       if (domain.index_of (".") == -1)
2185         return null;
2186
2187       tokens = domain.split (".", 2);
2188
2189       if (tokens.length != 2)
2190         return null;
2191
2192       domain = tokens[0];
2193
2194       if (domain == "msn" ||
2195           domain == "hotmail" ||
2196           domain == "live")
2197         return "msn";
2198       else if (domain == "gmail" ||
2199           domain == "googlemail")
2200         return "jabber";
2201       else if (domain == "yahoo")
2202         return "yahoo";
2203
2204       return null;
2205     }
2206 }