eds: Fix memory leaks in EDS backend
[platform/upstream/folks.git] / backends / eds / lib / edsf-persona.vala
1 /*
2  * Copyright (C) 2011 Collabora Ltd.
3  *
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.
8  *
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.
13  *
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/>.
16  *
17  * Authors:
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>
21  */
22
23 using E;
24 using Folks;
25 using Gee;
26 using GLib;
27 using Xml;
28
29 /**
30  * A persona subclass which represents a single EDS contact.
31  */
32 public class Edsf.Persona : Folks.Persona,
33     AvatarDetails,
34     BirthdayDetails,
35     EmailDetails,
36     FavouriteDetails,
37     GenderDetails,
38     GroupDetails,
39     ImDetails,
40     LocalIdDetails,
41     NameDetails,
42     NoteDetails,
43     PhoneDetails,
44     RoleDetails,
45     UrlDetails,
46     PostalAddressDetails,
47     WebServiceDetails
48 {
49   /* The following 4 definitions are used by the tests */
50   /**
51    * vCard field names for telephone numbers.
52    *
53    * @since 0.6.0
54    */
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"
59   };
60   /**
61    * vCard field names for postal addresses.
62    *
63    * @since 0.6.0
64    */
65   public static const string[] address_fields = {
66     "address_home", "address_other", "address_work"
67   };
68   /**
69    * vCard field names for e-mail addresses.
70    *
71    * @since 0.6.0
72    */
73   public static const string[] email_fields = {
74     "email_1", "email_2", "email_3", "email_4"
75   };
76
77   [Deprecated]
78   /**
79    * vCard field names for miscellaneous URIs.
80    *
81    * @since 0.6.0
82    */
83   public static const string[] url_properties = {
84     "blog_url", "fburl", "homepage_url", "video_url"
85   };
86
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
90    * them. */
91   private struct UrlTypeMapping
92     {
93       string vcard_field_name;
94       string folks_type;
95     }
96
97   internal static const UrlTypeMapping[] _url_properties =
98     {
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" }
103     };
104
105   /**
106    * The vCard attribute used to specify a Contact's gender
107    *
108    * Based on:
109    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
110    *
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.
114    *
115    * @since 0.6.0
116    */
117   public static const string gender_attribute_name = "X-GENDER";
118
119   /**
120    * The value used to define the male gender for the
121    * X-GENDER vCard property.
122    *
123    * Based on:
124    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
125    *
126    * @since 0.6.0
127    */
128   public static const string gender_male = "M";
129
130   /**
131    * The value used to define the female gender for the
132    * X-GENDER vCard property.
133    *
134    * Based on:
135    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
136    *
137    * @since 0.6.0
138    */
139   public static const string gender_female = "F";
140
141   private const string[] _linkable_properties = { "im-addresses",
142                                                   "local-ids",
143                                                   "web-service-addresses" };
144
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;
152
153   private HashSet<PostalAddressFieldDetails> _postal_addresses;
154   private Set<PostalAddressFieldDetails> _postal_addresses_ro;
155
156   private HashSet<string> _local_ids;
157   private Set<string> _local_ids_ro;
158
159   private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
160
161   private bool _is_favourite;
162
163   private E.Contact _contact; /* should be set on construct */
164
165   /**
166    * The e-d-s contact represented by this Persona
167    */
168   public E.Contact contact
169     {
170       get { return this._contact; }
171       construct { this._contact = value; }
172     }
173
174   /**
175    * {@inheritDoc}
176    */
177   [CCode (notify = false)]
178   public MultiMap<string, WebServiceFieldDetails> web_service_addresses
179     {
180       get { return this._web_service_addresses; }
181       set { this.change_web_service_addresses.begin (value); }
182     }
183
184   /**
185    * {@inheritDoc}
186    *
187    * @since 0.6.2
188    */
189   public async void change_web_service_addresses (
190       MultiMap<string, WebServiceFieldDetails> web_service_addresses)
191           throws PropertyError
192     {
193       yield ((Edsf.PersonaStore) this.store)._set_web_service_addresses (this,
194           web_service_addresses);
195     }
196
197   /**
198    * IDs used to link {@link Edsf.Persona}s.
199    */
200   [CCode (notify = false)]
201   public Set<string> local_ids
202     {
203       get
204         {
205           if (this._local_ids.contains (this.iid) == false)
206             {
207               this._local_ids.add (this.iid);
208             }
209           return this._local_ids_ro;
210         }
211       set { this.change_local_ids.begin (value); }
212     }
213
214   /**
215    * {@inheritDoc}
216    *
217    * @since 0.6.2
218    */
219   public async void change_local_ids (Set<string> local_ids)
220       throws PropertyError
221     {
222       yield ((Edsf.PersonaStore) this.store)._set_local_ids (this, local_ids);
223     }
224
225   /**
226    * The postal addresses of the contact.
227    *
228    * A list of postal addresses associated to the contact.
229    *
230    * @since 0.6.0
231    */
232   [CCode (notify = false)]
233   public Set<PostalAddressFieldDetails> postal_addresses
234     {
235       get { return this._postal_addresses_ro; }
236       set { this.change_postal_addresses.begin (value); }
237     }
238
239   /**
240    * {@inheritDoc}
241    *
242    * @since 0.6.2
243    */
244   public async void change_postal_addresses (
245       Set<PostalAddressFieldDetails> postal_addresses) throws PropertyError
246     {
247       yield ((Edsf.PersonaStore) this.store)._set_postal_addresses (this,
248           postal_addresses);
249     }
250
251   /**
252    * {@inheritDoc}
253    *
254    * @since 0.6.0
255    */
256   [CCode (notify = false)]
257   public Set<PhoneFieldDetails> phone_numbers
258     {
259       get { return this._phone_numbers_ro; }
260       set { this.change_phone_numbers.begin (value); }
261     }
262
263   /**
264    * {@inheritDoc}
265    *
266    * @since 0.6.2
267    */
268   public async void change_phone_numbers (
269       Set<PhoneFieldDetails> phone_numbers) throws PropertyError
270     {
271       yield ((Edsf.PersonaStore) this.store)._set_phones (this, phone_numbers);
272     }
273
274   /**
275    * {@inheritDoc}
276    *
277    * @since 0.6.0
278    */
279   [CCode (notify = false)]
280   public Set<EmailFieldDetails> email_addresses
281     {
282       get { return this._email_addresses_ro; }
283       set { this.change_email_addresses.begin (value); }
284     }
285
286   /**
287    * {@inheritDoc}
288    *
289    * @since 0.6.2
290    */
291   public async void change_email_addresses (
292       Set<EmailFieldDetails> email_addresses) throws PropertyError
293     {
294       yield ((Edsf.PersonaStore) this.store)._set_emails (this,
295           email_addresses);
296     }
297
298   /**
299    * {@inheritDoc}
300    *
301    * @since 0.6.0
302    */
303   [CCode (notify = false)]
304   public Set<NoteFieldDetails> notes
305     {
306       get { return this._notes_ro; }
307       set { this.change_notes.begin (value); }
308     }
309
310   /**
311    * {@inheritDoc}
312    *
313    * @since 0.6.2
314    */
315   public async void change_notes (Set<NoteFieldDetails> notes)
316       throws PropertyError
317     {
318       yield ((Edsf.PersonaStore) this.store)._set_notes (this, notes);
319     }
320
321   /**
322    * {@inheritDoc}
323    *
324    * @since 0.6.0
325    */
326   public override string[] linkable_properties
327     {
328       get { return this._linkable_properties; }
329     }
330
331   /**
332    * {@inheritDoc}
333    *
334    * @since 0.6.0
335    */
336   public override string[] writeable_properties
337     {
338       get { return this.store.always_writeable_properties; }
339     }
340
341   private LoadableIcon? _avatar = null;
342   /**
343    * An avatar for the Persona.
344    *
345    * See {@link Folks.AvatarDetails.avatar}.
346    *
347    * @since 0.6.0
348    */
349   [CCode (notify = false)]
350   public LoadableIcon? avatar
351     {
352       get { return this._avatar; }
353       set { this.change_avatar.begin (value); }
354     }
355
356   /**
357    * {@inheritDoc}
358    *
359    * @since 0.6.2
360    */
361   public async void change_avatar (LoadableIcon? avatar) throws PropertyError
362     {
363       yield ((Edsf.PersonaStore) this.store)._set_avatar (this, avatar);
364     }
365
366   private StructuredName? _structured_name = null;
367   /**
368    * {@inheritDoc}
369    *
370    * @since 0.6.0
371    */
372   [CCode (notify = false)]
373   public StructuredName? structured_name
374     {
375       get { return this._structured_name; }
376       set { this.change_structured_name.begin (value); }
377     }
378
379   /**
380    * {@inheritDoc}
381    *
382    * @since 0.6.2
383    */
384   public async void change_structured_name (StructuredName? structured_name)
385       throws PropertyError
386     {
387       yield ((Edsf.PersonaStore) this.store)._set_structured_name (this,
388           structured_name);
389     }
390
391   /**
392    * The e-d-s contact uid
393    *
394    * @since 0.6.0
395    */
396   public string contact_id { get; construct; }
397
398   private string _full_name = "";
399   /**
400    * {@inheritDoc}
401    *
402    * @since 0.6.0
403    */
404   [CCode (notify = false)]
405   public string full_name
406     {
407       get { return this._full_name; }
408       set { this.change_full_name.begin (value); }
409     }
410
411   /**
412    * {@inheritDoc}
413    *
414    * @since 0.6.2
415    */
416   public async void change_full_name (string full_name) throws PropertyError
417     {
418       yield ((Edsf.PersonaStore) this.store)._set_full_name (this, full_name);
419     }
420
421   private string _nickname = "";
422   /**
423    * {@inheritDoc}
424    *
425    * @since 0.6.0
426    */
427   [CCode (notify = false)]
428   public string nickname
429     {
430       get { return this._nickname; }
431       set { this.change_nickname.begin (value); }
432     }
433
434   /**
435    * {@inheritDoc}
436    *
437    * @since 0.6.2
438    */
439   public async void change_nickname (string nickname) throws PropertyError
440     {
441       yield ((Edsf.PersonaStore) this.store)._set_nickname (this, nickname);
442     }
443
444   private Gender _gender;
445   /**
446    * {@inheritDoc}
447    *
448    * @since 0.6.0
449    */
450   [CCode (notify = false)]
451   public Gender gender
452     {
453       get { return this._gender; }
454       set { this.change_gender.begin (value); }
455     }
456
457   /**
458    * {@inheritDoc}
459    *
460    * @since 0.6.2
461    */
462   public async void change_gender (Gender gender) throws PropertyError
463     {
464       yield ((Edsf.PersonaStore) this.store)._set_gender (this, gender);
465     }
466
467   private HashSet<UrlFieldDetails> _urls;
468   private Set<UrlFieldDetails> _urls_ro;
469   /**
470    * {@inheritDoc}
471    *
472    * @since 0.6.0
473    */
474   [CCode (notify = false)]
475   public Set<UrlFieldDetails> urls
476     {
477       get { return this._urls_ro; }
478       set { this.change_urls.begin (value); }
479     }
480
481   /**
482    * {@inheritDoc}
483    *
484    * @since 0.6.2
485    */
486   public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
487     {
488       yield ((Edsf.PersonaStore) this.store)._set_urls (this, urls);
489     }
490
491   private HashMultiMap<string, ImFieldDetails> _im_addresses =
492       new HashMultiMap<string, ImFieldDetails> (null, null,
493           (GLib.HashFunc) ImFieldDetails.hash,
494           (GLib.EqualFunc) ImFieldDetails.equal);
495
496   /**
497    * {@inheritDoc}
498    *
499    * @since 0.6.0
500    */
501   [CCode (notify = false)]
502   public MultiMap<string, ImFieldDetails> im_addresses
503     {
504       get { return this._im_addresses; }
505       set { this.change_im_addresses.begin (value); }
506     }
507
508   /**
509    * {@inheritDoc}
510    *
511    * @since 0.6.2
512    */
513   public async void change_im_addresses (
514       MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
515     {
516       yield ((Edsf.PersonaStore) this.store)._set_im_fds (this, im_addresses);
517     }
518
519   private HashSet<string> _groups;
520   private Set<string> _groups_ro;
521
522   /**
523    * {@inheritDoc}
524    *
525    * @since 0.6.0
526    */
527   [CCode (notify = false)]
528   public Set<string> groups
529     {
530       get { return this._groups_ro; }
531       set { this.change_groups.begin (value); }
532     }
533
534   /**
535    * {@inheritDoc}
536    *
537    * @since 0.6.0
538    */
539   public async void change_group (string group, bool is_member)
540       throws GLib.Error
541     {
542       /* Nothing to do? */
543       if ((is_member == true && this._groups.contains (group) == true) ||
544           (is_member == false && this._groups.contains (group) == false))
545         {
546           return;
547         }
548
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)
552         {
553           new_groups.add (category_name);
554         }
555
556       if (is_member == false)
557         {
558           new_groups.remove (group);
559         }
560       else
561         {
562           new_groups.add (group);
563         }
564
565       yield this.change_groups (new_groups);
566     }
567
568   /**
569    * {@inheritDoc}
570    *
571    * @since 0.6.2
572    */
573   public async void change_groups (Set<string> groups) throws PropertyError
574     {
575       yield ((Edsf.PersonaStore) this.store)._set_groups (this, groups);
576     }
577
578   /**
579    * {@inheritDoc}
580    *
581    * e-d-s has no equivalent field, so this is unsupported.
582    *
583    * @since 0.6.2
584    */
585   [CCode (notify = false)]
586   public string? calendar_event_id
587     {
588       get { return null; } /* unsupported */
589       set { this.change_calendar_event_id.begin (value); } /* not writeable */
590     }
591
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 ();
595
596   private DateTime? _birthday = null;
597   /**
598    * {@inheritDoc}
599    *
600    * @since 0.6.2
601    */
602   [CCode (notify = false)]
603   public DateTime? birthday
604     {
605       get { return this._birthday; }
606       set { this.change_birthday.begin (value); }
607     }
608
609   /**
610    * {@inheritDoc}
611    *
612    * @since 0.6.2
613    */
614   public async void change_birthday (DateTime? bday)
615       throws PropertyError
616     {
617       yield ((Edsf.PersonaStore) this.store)._set_birthday (this,
618           bday);
619     }
620
621   private HashSet<RoleFieldDetails> _roles;
622   private Set<RoleFieldDetails> _roles_ro;
623
624   /**
625    * {@inheritDoc}
626    *
627    * @since 0.6.2
628    */
629   [CCode (notify = false)]
630   public Set<RoleFieldDetails> roles
631     {
632       get { return this._roles_ro; }
633       set { this.change_roles.begin (value); }
634     }
635
636   /**
637    * {@inheritDoc}
638    *
639    * @since 0.6.2
640    */
641   public async void change_roles (Set<RoleFieldDetails> roles)
642       throws PropertyError
643     {
644       yield ((Edsf.PersonaStore) this.store)._set_roles (this, roles);
645     }
646
647   /**
648    * Whether this contact is a user-defined favourite.
649    *
650    * @since 0.6.5
651    */
652   [CCode (notify = false)]
653   public bool is_favourite
654       {
655         get { return this._is_favourite; }
656         set { this.change_is_favourite.begin (value); }
657       }
658
659   /**
660    * {@inheritDoc}
661    *
662    * @since 0.6.5
663    */
664   public async void change_is_favourite (bool is_favourite) throws PropertyError
665     {
666       if (this._is_favourite == is_favourite)
667         {
668           return;
669         }
670
671       yield ((Edsf.PersonaStore) this.store)._set_is_favourite (this,
672           is_favourite);
673     }
674
675   /**
676    * Build a IID.
677    *
678    * @param store_id the {@link PersonaStore.id}
679    * @param contact the Contact
680    * @return a valid IID
681    *
682    * @since 0.6.0
683    */
684   internal static string build_iid_from_contact (string store_id,
685       E.Contact contact)
686     {
687       var contact_id =
688           Edsf.Persona._get_property_from_contact<string> (contact, "id");
689       return Edsf.Persona.build_iid (store_id, (!) (contact_id ?? ""));
690     }
691
692   /**
693    * Build a IID.
694    *
695    * @param store_id the {@link PersonaStore.id}
696    * @param contact_id the id belonging to the Contact
697    * @return a valid IID
698    *
699    * @since 0.6.0
700    */
701   internal static string build_iid (string store_id, string contact_id)
702     {
703       return "%s:%s".printf (store_id, contact_id);
704     }
705
706
707   /**
708    * Create a new persona.
709    *
710    * Create a new persona for the {@link PersonaStore} `store`, representing
711    * the EDS contact given by `contact`.
712    *
713    * @param store the store which will contain the persona
714    * @param contact the EDS contact being represented by the persona
715    *
716    * @since 0.6.0
717    */
718   public Persona (PersonaStore store, E.Contact contact)
719     {
720       var _contact_id =
721           Edsf.Persona._get_property_from_contact<string> (contact, "id");
722       var contact_id = (!) (_contact_id ?? "");
723
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);
727       var _full_name =
728           Edsf.Persona._get_property_from_contact<string> (contact,
729               "full_name");
730       var full_name = (!) (_full_name ?? "");
731
732       Object (display_id: full_name,
733               uid: uid,
734               iid: iid,
735               store: store,
736               is_user: is_user,
737               contact_id: contact_id,
738               contact: contact);
739     }
740
741   construct
742     {
743       debug ("Creating new Edsf.Persona with IID '%s'", this.iid);
744
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> (
770             null, null,
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;
779
780       this._update (this._contact);
781     }
782
783   /**
784    * {@inheritDoc}
785    *
786    * @since 0.6.0
787    */
788   public override void linkable_property_to_links (string prop_name,
789       Folks.Persona.LinkablePropertyCallback callback)
790     {
791       if (prop_name == "im-addresses")
792         {
793           foreach (var protocol in this._im_addresses.get_keys ())
794             {
795               var im_fds = this._im_addresses.get (protocol);
796
797               foreach (var im_fd in im_fds)
798                   callback (protocol + ":" + im_fd.value);
799             }
800         }
801       else if (prop_name == "local-ids")
802         {
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)
808             {
809               callback (id);
810             }
811         }
812       else if (prop_name == "web-service-addresses")
813         {
814           foreach (var web_service in this.web_service_addresses.get_keys ())
815             {
816               var web_service_addresses =
817                   this._web_service_addresses.get (web_service);
818
819               foreach (var ws_fd in web_service_addresses)
820                   callback (web_service + ":" + ws_fd.value);
821             }
822         }
823       else
824         {
825           /* Chain up */
826           base.linkable_property_to_links (prop_name, callback);
827         }
828     }
829
830   ~Persona ()
831     {
832       debug ("Destroying Edsf.Persona '%s': %p", this.uid, this);
833     }
834
835   /**
836    * Update attribs of the persona.
837    */
838   internal void _update (E.Contact updated_contact)
839     {
840       this.freeze_notify ();
841
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");
846
847       this._update_names ();
848       this._update_avatar ();
849       this._update_urls ();
850       this._update_phones ();
851       this._update_addresses ();
852       this._update_emails ();
853
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.
857        */
858       this._update_im_addresses ();
859
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 ();
868
869       this.thaw_notify ();
870     }
871
872   private void _update_params (AbstractFieldDetails details,
873       E.VCardAttribute attr)
874     {
875       foreach (unowned E.VCardAttributeParam param in attr.get_params ())
876         {
877           string param_name = param.get_name ().down ();
878           foreach (unowned string param_value in param.get_values ())
879             {
880               if (param_name == AbstractFieldDetails.PARAM_TYPE)
881                 {
882                   details.add_parameter (param_name, param_value.down ());
883                 }
884               else
885                 {
886                   details.add_parameter (param_name, param_value);
887                 }
888             }
889         }
890     }
891
892   private void _update_gender ()
893     {
894       var gender = Gender.UNSPECIFIED;
895       var gender_attr =
896           this.contact.get_attribute (Edsf.Persona.gender_attribute_name);
897
898       if (gender_attr != null)
899         {
900           var val = ((!) gender_attr).get_value ();
901           if (val != null)
902             {
903               switch (((!) val).up ())
904                 {
905                   case Edsf.Persona.gender_male:
906                     gender = Gender.MALE;
907                     break;
908                   case Edsf.Persona.gender_female:
909                     gender = Gender.FEMALE;
910                     break;
911                   default:
912                     /* Unspecified, as above */
913                     break;
914                 }
915             }
916         }
917
918       if (this._gender != gender)
919         {
920           this._gender = gender;
921           this.notify_property ("gender");
922         }
923     }
924
925   private void _update_birthday ()
926     {
927       var _bday = this._get_property<E.ContactDate> ("birth_date");
928
929       if (_bday != null)
930         {
931           var bday = (!) _bday;
932
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 ())))
943             {
944               this._birthday = d.to_utc ();
945               this.notify_property ("birthday");
946             }
947         }
948       else
949         {
950           if (this._birthday != null)
951             {
952               this._birthday = null;
953               this.notify_property ("birthday");
954             }
955         }
956     }
957
958   private void _update_roles ()
959     {
960       var new_roles = new HashSet<RoleFieldDetails> (
961           (GLib.HashFunc) RoleFieldDetails.hash,
962           (GLib.EqualFunc) RoleFieldDetails.equal);
963
964       var default_role_fd = this._get_default_role ();
965       if (default_role_fd != null)
966         {
967           new_roles.add ((!) default_role_fd);
968         }
969
970       var vcard = (E.VCard) this.contact;
971       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
972         {
973           if (attr.get_name () != "X-ROLES")
974             continue;
975
976           var val = attr.get_value ();
977           if (val == null || (!) val == "")
978              {
979               continue;
980             }
981
982           var role = new Role ("", "");
983           role.role = (!) val;
984           var role_fd = new RoleFieldDetails (role);
985
986           foreach (unowned E.VCardAttributeParam param in
987               attr.get_params ())
988             {
989               string param_name = param.get_name ().down ();
990
991               if (param_name == "organisation_name")
992                 {
993                   foreach (unowned string param_value in
994                       param.get_values ())
995                     {
996                       role.organisation_name = param_value;
997                       break;
998                     }
999                 }
1000               else if (param_name == "title")
1001                 {
1002                   foreach (unowned string param_value in
1003                       param.get_values ())
1004                     {
1005                       role.title = param_value;
1006                       break;
1007                     }
1008                 }
1009               else
1010                 {
1011                   foreach (unowned string param_value in
1012                       param.get_values ())
1013                     {
1014                       role_fd.add_parameter (param_name, param_value);
1015                     }
1016                 }
1017             }
1018
1019             new_roles.add (role_fd);
1020         }
1021
1022       if (!Folks.Internal.equal_sets<RoleFieldDetails> (new_roles, this._roles))
1023         {
1024           this._roles = new_roles;
1025           this._roles_ro = new_roles.read_only_view;
1026           this.notify_property ("roles");
1027         }
1028     }
1029
1030   private RoleFieldDetails? _get_default_role ()
1031     {
1032       RoleFieldDetails? _default_role = null;
1033
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");
1041
1042       if (org != null ||
1043           org_unit != null ||
1044           office != null ||
1045           title != null ||
1046           role != null ||
1047           manager != null ||
1048           assistant != null)
1049         {
1050           var new_role = new Role (title, org);
1051           if (role != null && (!) role != "")
1052             new_role.role = (!) role;
1053
1054           /* Check if it's non-empty. */
1055           if (!new_role.is_empty ())
1056             {
1057               var default_role = new RoleFieldDetails (new_role);
1058
1059               if (org_unit != null && org_unit != "")
1060                 default_role.set_parameter ("org_unit", (!) org_unit);
1061
1062               if (office != null && office != "")
1063                 default_role.set_parameter ("office", (!) office);
1064
1065               if (manager != null && manager != "")
1066                 default_role.set_parameter ("manager", (!) manager);
1067
1068               if (assistant != null && manager != "")
1069                 default_role.set_parameter ("assistant", (!) assistant);
1070
1071               _default_role = default_role;
1072             }
1073         }
1074
1075       return _default_role;
1076     }
1077
1078   private void _update_web_services_addresses ()
1079     {
1080       var new_services = new HashMultiMap<string, WebServiceFieldDetails> (
1081           null, null,
1082           (GLib.HashFunc) WebServiceFieldDetails.hash,
1083           (GLib.EqualFunc) WebServiceFieldDetails.equal);
1084
1085       var services = this.contact.get_attribute ("X-FOLKS-WEB-SERVICES-IDS");
1086       if (services != null)
1087         {
1088           foreach (var service in ((!) services).get_params ())
1089             {
1090               var service_name = service.get_name ().down ();
1091               foreach (var service_id in service.get_values ())
1092                 {
1093                   if (service_id == "")
1094                     {
1095                       continue;
1096                     }
1097
1098                   new_services.set (service_name,
1099                       new WebServiceFieldDetails (service_id));
1100                 }
1101             }
1102         }
1103
1104       if (!Utils.multi_map_str_afd_equal (new_services,
1105               this._web_service_addresses))
1106         {
1107           this._web_service_addresses = new_services;
1108           this.notify_property ("web-service-addresses");
1109         }
1110     }
1111
1112   private void _update_emails ()
1113     {
1114       var new_email_addresses = new HashSet<EmailFieldDetails> (
1115           (GLib.HashFunc) EmailFieldDetails.hash,
1116           (GLib.EqualFunc) EmailFieldDetails.equal);
1117
1118       var attrs = this.contact.get_attributes (E.ContactField.EMAIL);
1119       foreach (var attr in attrs)
1120         {
1121           var val = attr.get_value ();
1122           if (val == null || (!) val == "")
1123             {
1124               continue;
1125             }
1126
1127           var email_fd = new EmailFieldDetails ((!) val);
1128           this._update_params (email_fd, attr);
1129           new_email_addresses.add (email_fd);
1130         }
1131
1132       if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
1133               this._email_addresses))
1134         {
1135          this._email_addresses = new_email_addresses;
1136          this._email_addresses_ro = new_email_addresses.read_only_view;
1137          this.notify_property ("email-addresses");
1138        }
1139     }
1140
1141   private void _update_notes ()
1142     {
1143       var new_notes = new HashSet<NoteFieldDetails> (
1144           (GLib.HashFunc) NoteFieldDetails.hash,
1145           (GLib.EqualFunc) NoteFieldDetails.equal);
1146
1147       var n = this._get_property<string> ("note");
1148       if (n != null && n != "")
1149         {
1150           var note = new NoteFieldDetails ((!) n);
1151           new_notes.add (note);
1152         }
1153
1154       if (!Folks.Internal.equal_sets<NoteFieldDetails> (new_notes, this._notes))
1155         {
1156           this._notes = new_notes;
1157           this._notes_ro = this._notes.read_only_view;
1158           this.notify_property ("notes");
1159         }
1160     }
1161
1162   private void _update_names ()
1163     {
1164       var _full_name = this._get_property<string> ("full_name");
1165
1166       if (_full_name == null)
1167         {
1168           _full_name = "";
1169         }
1170
1171       var full_name = (!) _full_name;
1172
1173       if (this._full_name != full_name)
1174         {
1175           this._full_name = full_name;
1176           this.notify_property ("full-name");
1177         }
1178
1179       var _nickname = this._get_property<string> ("nickname");
1180
1181       if (_nickname == null)
1182         {
1183           _nickname = "";
1184         }
1185
1186       var nickname = (!) _nickname;
1187
1188       if (this._nickname != nickname)
1189         {
1190           this._nickname = nickname;
1191           this.notify_property ("nickname");
1192         }
1193
1194       StructuredName? structured_name = null;
1195       var _cn = this._get_property<E.ContactName> ("name");
1196       if (_cn != null)
1197         {
1198           var cn = (!) _cn;
1199
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,
1207                                                 suffixes);
1208         }
1209
1210       if (structured_name != null && !((!) structured_name).is_empty ())
1211         {
1212           this._structured_name = (!) structured_name;
1213           this.notify_property ("structured-name");
1214         }
1215       else if (this._structured_name != null)
1216         {
1217           this._structured_name = null;
1218           this.notify_property ("structured-name");
1219         }
1220     }
1221
1222   private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? _p)
1223     {
1224       if (_p == null)
1225         {
1226           return null;
1227         }
1228
1229       var p = (!) _p;
1230
1231       switch (p.type)
1232         {
1233           case ContactPhotoType.URI:
1234             var uri = p.get_uri ();
1235             if (uri == null)
1236               {
1237                 return null;
1238               }
1239
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)
1245               {
1246                 return null;
1247               }
1248
1249             return new Edsf.MemoryIcon ((!) mime_type, (!) data);
1250           default:
1251             return null;
1252         }
1253     }
1254
1255   private void _update_avatar ()
1256     {
1257       var p = this._get_property<E.ContactPhoto> ("photo");
1258
1259       var cache = AvatarCache.dup ();
1260
1261       // Convert the ContactPhoto to a LoadableIcon and store or update it.
1262       var new_avatar = this._contact_photo_to_loadable_icon (p);
1263
1264       if (this._avatar != null && new_avatar == null)
1265         {
1266           // Remove the old cached avatar, ignoring errors.
1267           cache.remove_avatar.begin (this.uid, (obj, res) =>
1268             {
1269               try
1270                 {
1271                   cache.remove_avatar.end (res);
1272                 }
1273               catch (GLib.Error e1) {}
1274
1275               this._avatar = null;
1276               this.notify_property ("avatar");
1277             });
1278         }
1279       else if ((this._avatar == null && new_avatar != null) ||
1280           (this._avatar != null && new_avatar != null &&
1281            ((!) this._avatar).equal (new_avatar) == false))
1282         {
1283           /* Store the new avatar in the cache. new_avatar is guaranteed to be
1284            * non-null. */
1285           cache.store_avatar.begin (this.uid, (!) new_avatar, (obj, res) =>
1286             {
1287               try
1288                 {
1289                   cache.store_avatar.end (res);
1290                   this._avatar = new_avatar;
1291                   this.notify_property ("avatar");
1292                 }
1293               catch (GLib.Error e2)
1294                 {
1295                   warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
1296                       this.uid, e2.message);
1297                   new_avatar = null; /* failure */
1298                 }
1299             });
1300         }
1301     }
1302
1303   private void _update_urls ()
1304     {
1305       var new_urls = new HashSet<UrlFieldDetails> ();
1306
1307       /* First we get the standard Evo urls.. */
1308       foreach (var mapping in this._url_properties)
1309         {
1310           var url_property = mapping.vcard_field_name;
1311           var folks_type = mapping.folks_type;
1312
1313           var u = this._get_property<string> (url_property);
1314           if (u != null && u != "")
1315             {
1316               var fd_u = new UrlFieldDetails ((!) u);
1317               fd_u.set_parameter (fd_u.PARAM_TYPE, folks_type);
1318               new_urls.add (fd_u);
1319             }
1320         }
1321
1322       /* Now we go for extra URLs */
1323       var vcard = (E.VCard) this.contact;
1324       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1325         {
1326           if (attr.get_name () == "X-URIS")
1327             {
1328               var val = attr.get_value ();
1329               if (val == null || (!) val == "")
1330                 {
1331                   continue;
1332                 }
1333
1334               var url_fd = new UrlFieldDetails ((!) val);
1335               this._update_params (url_fd, attr);
1336               new_urls.add (url_fd);
1337             }
1338         }
1339
1340       if (!Utils.set_afd_equal (new_urls, this._urls))
1341         {
1342           this._urls = new_urls;
1343           this._urls_ro = new_urls.read_only_view;
1344           this.notify_property ("urls");
1345         }
1346     }
1347
1348   private void _update_im_addresses ()
1349     {
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);
1354
1355       foreach (var im_proto in im_eds_map.get_keys ())
1356         {
1357           var addresses = this.contact.get_attributes (
1358               im_eds_map.lookup (im_proto));
1359           foreach (var attr in addresses)
1360             {
1361               try
1362                 {
1363                   var addr = attr.get_value ();
1364                   if (addr == null || (!) addr == "")
1365                     {
1366                       continue;
1367                     }
1368
1369                   string normalised_addr =
1370                     (owned) ImDetails.normalise_im_address ((!) addr, im_proto);
1371
1372                   if (normalised_addr == "")
1373                     {
1374                       continue;
1375                     }
1376
1377                   var im_fd = new ImFieldDetails (normalised_addr);
1378                   new_im_addresses.set (im_proto, im_fd);
1379                 }
1380               catch (Folks.ImDetailsError e)
1381                 {
1382                   GLib.warning (
1383                       "Problem when trying to normalise address: %s\n",
1384                       e.message);
1385                 }
1386             }
1387         }
1388
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).
1395        *
1396        * See bgo#657142
1397        */
1398       foreach (var email in this.email_addresses)
1399         {
1400           var _proto = this._im_proto_from_addr (email.value);
1401           if (_proto != null)
1402             {
1403               var proto = (!) _proto;
1404
1405               /* Has this already been added? */
1406               var exists = false;
1407               Collection<ImFieldDetails>? current_im_addrs =
1408                   new_im_addresses.get (proto);
1409               if (current_im_addrs != null)
1410                 {
1411                   foreach (var cur_im in (!) current_im_addrs)
1412                     {
1413                       if (cur_im.value == email.value)
1414                         {
1415                           exists = true;
1416                           break;
1417                         }
1418                     }
1419                 }
1420
1421               if (exists)
1422                 continue;
1423
1424               try
1425                 {
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);
1430                 }
1431               catch (Folks.ImDetailsError e)
1432                 {
1433                   GLib.warning (
1434                       "Problem when trying to normalise address: %s\n",
1435                       e.message);
1436                 }
1437             }
1438         }
1439
1440       if (!Utils.multi_map_str_afd_equal (new_im_addresses,
1441               this._im_addresses))
1442         {
1443           this._im_addresses = new_im_addresses;
1444           this.notify_property ("im-addresses");
1445         }
1446     }
1447
1448   private void _update_groups ()
1449     {
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> ();
1454
1455       foreach (var category_name in category_names)
1456         {
1457           new_categories.add (category_name);
1458
1459           /* Is this a new category? */
1460           if (!this._groups.contains (category_name))
1461             {
1462               added_categories.add (category_name);
1463             }
1464         }
1465
1466       /* Work out which categories have been removed. */
1467       var removed_categories = new LinkedList<string> ();
1468
1469       foreach (var category_name in this._groups)
1470         {
1471           if (!new_categories.contains (category_name))
1472             {
1473               removed_categories.add (category_name);
1474             }
1475         }
1476
1477       var old_is_favourite = this._is_favourite;
1478       var store = (Edsf.PersonaStore) this.store;
1479
1480       /* Make the changes to this._groups and emit signals. */
1481       foreach (var category_name in removed_categories)
1482         {
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)
1487             {
1488               this._is_favourite = false;
1489             }
1490
1491           this.group_changed (category_name, false);
1492           this._groups.remove (category_name);
1493         }
1494
1495       foreach (var category_name in added_categories)
1496         {
1497           if (store._is_google_contacts_address_book () &&
1498               category_name == Edsf.PersonaStore.android_favourite_group_name)
1499             {
1500               this._is_favourite = true;
1501             }
1502
1503           this._groups.add (category_name);
1504           this.group_changed (category_name, true);
1505         }
1506
1507       /* Notify if anything's changed. */
1508       this.freeze_notify ();
1509
1510       if (added_categories.size != 0 || removed_categories.size != 0)
1511         {
1512           this.notify_property ("groups");
1513         }
1514       if (this._is_favourite != old_is_favourite)
1515         {
1516           this.notify_property ("is-favourite");
1517         }
1518
1519       this.thaw_notify ();
1520    }
1521
1522   /**
1523    * build a table of im protocols / im protocol aliases
1524    */
1525   internal static HashTable<string, E.ContactField> _get_im_eds_map ()
1526     {
1527       HashTable<string, E.ContactField> retval;
1528
1529       lock (Edsf.Persona._im_eds_map)
1530         {
1531           if (Edsf.Persona._im_eds_map == null)
1532             {
1533               var table =
1534                   new HashTable<string, E.ContactField> (str_hash, str_equal);
1535
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);
1544
1545               Edsf.Persona._im_eds_map = table;
1546             }
1547
1548           retval = (!) Edsf.Persona._im_eds_map;
1549         }
1550
1551       return retval;
1552     }
1553
1554   private void _update_phones ()
1555     {
1556       var new_phone_numbers = new HashSet<PhoneFieldDetails> (
1557           (GLib.HashFunc) PhoneFieldDetails.hash,
1558           (GLib.EqualFunc) PhoneFieldDetails.equal);
1559
1560       var attrs = this.contact.get_attributes (E.ContactField.TEL);
1561       foreach (var attr in attrs)
1562         {
1563           var val = attr.get_value ();
1564           if (val == null || (!) val == "")
1565             {
1566               continue;
1567             }
1568
1569           var phone_fd = new PhoneFieldDetails ((!) val);
1570           this._update_params (phone_fd, attr);
1571           new_phone_numbers.add (phone_fd);
1572         }
1573
1574       if (!Folks.Internal.equal_sets<PhoneFieldDetails>  (new_phone_numbers,
1575               this._phone_numbers))
1576         {
1577           this._phone_numbers = new_phone_numbers;
1578           this._phone_numbers_ro = new_phone_numbers.read_only_view;
1579           this.notify_property ("phone-numbers");
1580         }
1581    }
1582
1583   private PostalAddress _postal_address_from_attribute (E.VCardAttribute attr)
1584     {
1585       unowned GLib.List<string>? values = attr.get_values();
1586       unowned GLib.List<string>? l = values;
1587
1588       var address_format = "";
1589       var po_box = "";
1590       var extension = "";
1591       var street = "";
1592       var locality = "";
1593       var region = "";
1594       var postal_code = "";
1595       var country = "";
1596
1597       if (l != null)
1598         {
1599           po_box = ((!) l).data;
1600           l = ((!) l).next;
1601         }
1602       if (l != null)
1603         {
1604           extension = ((!) l).data;
1605           l = ((!) l).next;
1606         }
1607       if (l != null)
1608         {
1609           street = ((!) l).data;
1610           l = ((!) l).next;
1611         }
1612       if (l != null)
1613         {
1614           locality = ((!) l).data;
1615           l = ((!) l).next;
1616         }
1617       if (l != null)
1618         {
1619           region = ((!) l).data;
1620           l = ((!) l).next;
1621         }
1622       if (l != null)
1623         {
1624           postal_code = ((!) l).data;
1625           l = ((!) l).next;
1626         }
1627       if (l != null)
1628         {
1629           country = ((!) l).data;
1630           l = ((!) l).next;
1631         }
1632
1633       return new PostalAddress (po_box, extension, street,
1634                                 locality, region, postal_code, country,
1635                                 address_format, null);
1636     }
1637
1638   /*
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).
1642    */
1643   private void _update_addresses ()
1644     {
1645       var new_postal_addresses = new HashSet<PostalAddressFieldDetails> (
1646           (GLib.HashFunc) PhoneFieldDetails.hash,
1647           (GLib.EqualFunc) PhoneFieldDetails.equal);
1648
1649       var attrs = this.contact.get_attributes (E.ContactField.ADDRESS);
1650       foreach (unowned E.VCardAttribute attr in attrs)
1651         {
1652           var address = this._postal_address_from_attribute (attr);
1653           if (address.is_empty ())
1654             {
1655               continue;
1656             }
1657
1658           var pa_fd = new PostalAddressFieldDetails (address);
1659           this._update_params (pa_fd, attr);
1660           new_postal_addresses.add (pa_fd);
1661         }
1662
1663       if (!Folks.Internal.equal_sets<PostalAddressFieldDetails> (
1664               new_postal_addresses,
1665               this._postal_addresses))
1666         {
1667           this._postal_addresses = new_postal_addresses;
1668           this._postal_addresses_ro = new_postal_addresses.read_only_view;
1669           this.notify_property ("phone-numbers");
1670         }
1671
1672       this.notify_property ("postal-addresses");
1673     }
1674
1675   private void _update_local_ids ()
1676     {
1677       var new_local_ids = new HashSet<string> ();
1678
1679       var ids = this.contact.get_attribute ("X-FOLKS-CONTACTS-IDS");
1680       if (ids != null)
1681         {
1682           unowned GLib.List<string> ids_v = ((!) ids).get_values ();
1683
1684           foreach (var local_id in ids_v)
1685             {
1686               if (local_id != "")
1687                 {
1688                   new_local_ids.add (local_id);
1689                 }
1690             }
1691         }
1692
1693       /* Make sure it includes our local id */
1694       new_local_ids.add (this.iid);
1695
1696       if (!Folks.Internal.equal_sets<string> (new_local_ids, this.local_ids))
1697         {
1698           this._local_ids = new_local_ids;
1699           this._local_ids_ro = this._local_ids.read_only_view;
1700           this.notify_property ("local-ids");
1701         }
1702     }
1703
1704   private void _update_favourite ()
1705     {
1706       bool is_fav = false;
1707
1708       var fav = this.contact.get_attribute ("X-FOLKS-FAVOURITE");
1709       if (fav != null)
1710         {
1711           var val = ((!) fav).get_value ();
1712           if (val != null && ((!) val).down () == "true")
1713             {
1714               is_fav = true;
1715             }
1716         }
1717
1718       var store = (Edsf.PersonaStore) this.store;
1719
1720       if (store._is_google_contacts_address_book ())
1721         {
1722           is_fav = is_fav ||
1723               (Edsf.PersonaStore.android_favourite_group_name in this._groups);
1724         }
1725
1726       if (is_fav != this._is_favourite)
1727         {
1728           this._is_favourite = is_fav;
1729
1730           var groups_changed = false;
1731
1732           if (store._is_google_contacts_address_book () &&
1733               !(Edsf.PersonaStore.android_favourite_group_name in this._groups))
1734             {
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;
1739             }
1740
1741           this.freeze_notify ();
1742
1743           if (groups_changed == true)
1744             {
1745               this.notify_property ("groups");
1746             }
1747
1748           this.notify_property ("is-favourite");
1749
1750           this.thaw_notify ();
1751         }
1752     }
1753
1754   internal static T? _get_property_from_contact<T> (E.Contact contact,
1755       string prop_name)
1756     {
1757       T? prop_value = null;
1758       prop_value = contact.get<T> (E.Contact.field_id (prop_name));
1759       return prop_value;
1760     }
1761
1762   private T? _get_property<T> (string prop_name)
1763     {
1764       return Edsf.Persona._get_property_from_contact<T> (this.contact,
1765           prop_name);
1766     }
1767
1768   private string? _im_proto_from_addr (string addr)
1769     {
1770       if (addr.index_of ("@") == -1)
1771         return null;
1772
1773       var tokens = addr.split ("@", 2);
1774
1775       if (tokens.length != 2)
1776         return null;
1777
1778       var domain = tokens[1];
1779       if (domain.index_of (".") == -1)
1780         return null;
1781
1782       tokens = domain.split (".", 2);
1783
1784       if (tokens.length != 2)
1785         return null;
1786
1787       domain = tokens[0];
1788
1789       if (domain == "msn" ||
1790           domain == "hotmail" ||
1791           domain == "live")
1792         return "msn";
1793       else if (domain == "gmail" ||
1794           domain == "googlemail")
1795         return "jabber";
1796       else if (domain == "yahoo")
1797         return "yahoo";
1798
1799       return null;
1800     }
1801 }