core: Add NameDetails.change_*() methods
[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     EmailDetails,
35     GenderDetails,
36     GroupDetails,
37     ImDetails,
38     LocalIdDetails,
39     NameDetails,
40     NoteDetails,
41     PhoneDetails,
42     UrlDetails,
43     PostalAddressDetails,
44     WebServiceDetails
45 {
46   /* The following 4 definitions are used by the tests */
47   public static const string[] phone_fields = {
48     "assistant_phone", "business_phone", "business_phone_2", "callback_phone",
49     "car_phone", "company_phone", "home_phone", "home_phone_2", "isdn_phone",
50     "mobile_phone", "other_phone", "primary_phone"
51   };
52   public static const string[] address_fields = {
53     "address_home", "address_other", "address_work"
54   };
55   public static const string[] email_fields = {
56     "email_1", "email_2", "email_3", "email_4"
57   };
58   public static const string[] url_properties = {
59     "blog_url", "fburl", "homepage_url", "video_url"
60   };
61
62   /**
63    * The vCard attribute used to specify a Contact's gender
64    *
65    * Based on:
66    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
67    *
68    * Note that the above document is a draft and the gender property
69    * is still considered experimental, hence the "X-" prefix in the
70    * attribute name. So this might change.
71    *
72    * @since 0.6.0
73    */
74   public static const string gender_attribute_name = "X-GENDER";
75
76   /**
77    * The value used to define the male gender for the
78    * X-GENDER vCard property.
79    *
80    * Based on:
81    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
82    *
83    * @since 0.6.0
84    */
85   public static const string gender_male = "M";
86
87   /**
88    * The value used to define the female gender for the
89    * X-GENDER vCard property.
90    *
91    * Based on:
92    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
93    *
94    * @since 0.6.0
95    */
96   public static const string gender_female = "F";
97
98   private const string[] _linkable_properties = { "im-addresses",
99                                                   "local-ids",
100                                                   "web-service-addresses" };
101   private const string[] _writeable_properties =
102     {
103       "web-service-addresses",
104       "local-ids",
105       "postal-addresses",
106       "phone-numbers",
107       "email-addresses",
108       "notes",
109       "avatar",
110       "structured-name",
111       "full-name",
112       "nickname",
113       "im-addresses",
114       "groups",
115       "urls"
116     };
117   private HashSet<PhoneFieldDetails> _phone_numbers;
118   private Set<PhoneFieldDetails> _phone_numbers_ro;
119   private HashSet<EmailFieldDetails> _email_addresses;
120   private Set<EmailFieldDetails> _email_addresses_ro;
121   private HashSet<NoteFieldDetails> _notes;
122   private Set<NoteFieldDetails> _notes_ro;
123   private static HashTable<string, E.ContactField> _im_eds_map = null;
124
125   private HashSet<PostalAddressFieldDetails> _postal_addresses;
126   private Set<PostalAddressFieldDetails> _postal_addresses_ro;
127
128   private HashSet<string> _local_ids;
129   private Set<string> _local_ids_ro;
130
131   private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
132
133   /**
134    * The e-d-s contact represented by this Persona
135    */
136   public E.Contact contact
137     {
138       get;
139       private set;
140     }
141
142   /**
143    * {@inheritDoc}
144    */
145   [CCode (notify = false)]
146   public MultiMap<string, WebServiceFieldDetails> web_service_addresses
147     {
148       get { return this._web_service_addresses; }
149       set
150         {
151           var store = (Edsf.PersonaStore) this.store;
152           store._set_web_service_addresses (this, value);
153         }
154     }
155
156   /**
157    * IDs used to link {@link Edsf.Persona}s.
158    */
159   [CCode (notify = false)]
160   public Set<string> local_ids
161     {
162       get
163         {
164           if (this._local_ids.contains (this.iid) == false)
165             {
166               this._local_ids.add (this.iid);
167             }
168           return this._local_ids_ro;
169         }
170       set { this.change_local_ids.begin (value); }
171     }
172
173   /**
174    * {@inheritDoc}
175    *
176    * @since UNRELEASED
177    */
178   public async void change_local_ids (Set<string> local_ids)
179       throws PropertyError
180     {
181       yield ((Edsf.PersonaStore) this.store)._set_local_ids (this, local_ids);
182     }
183
184   /**
185    * The postal addresses of the contact.
186    *
187    * A list of postal addresses associated to the contact.
188    *
189    * @since 0.6.0
190    */
191   [CCode (notify = false)]
192   public Set<PostalAddressFieldDetails> postal_addresses
193     {
194       get { return this._postal_addresses_ro; }
195       set
196         {
197           ((Edsf.PersonaStore) this.store)._set_postal_addresses (this, value);
198         }
199     }
200
201   /**
202    * {@inheritDoc}
203    *
204    * @since 0.6.0
205    */
206   [CCode (notify = false)]
207   public Set<PhoneFieldDetails> phone_numbers
208     {
209       get { return this._phone_numbers_ro; }
210       set
211         {
212           ((Edsf.PersonaStore) this.store)._set_phones (this, value);
213         }
214     }
215
216   /**
217    * {@inheritDoc}
218    *
219    * @since 0.6.0
220    */
221   [CCode (notify = false)]
222   public Set<EmailFieldDetails> email_addresses
223     {
224       get { return this._email_addresses_ro; }
225       set { this.change_email_addresses.begin (value); }
226     }
227
228   /**
229    * {@inheritDoc}
230    *
231    * @since UNRELEASED
232    */
233   public async void change_email_addresses (
234       Set<EmailFieldDetails> email_addresses) throws PropertyError
235     {
236       yield ((Edsf.PersonaStore) this.store)._set_emails (this,
237           email_addresses);
238     }
239
240   /**
241    * {@inheritDoc}
242    *
243    * @since 0.6.0
244    */
245   [CCode (notify = false)]
246   public Set<NoteFieldDetails> notes
247     {
248       get { return this._notes_ro; }
249       set
250         {
251           ((Edsf.PersonaStore) this.store)._set_notes (this, value);
252         }
253     }
254
255   /**
256    * {@inheritDoc}
257    *
258    * @since 0.6.0
259    */
260   public override string[] linkable_properties
261     {
262       get { return this._linkable_properties; }
263     }
264
265   /**
266    * {@inheritDoc}
267    *
268    * @since 0.6.0
269    */
270   public override string[] writeable_properties
271     {
272       get { return this._writeable_properties; }
273     }
274
275   private LoadableIcon? _avatar = null;
276   /**
277    * An avatar for the Persona.
278    *
279    * See {@link Folks.AvatarDetails.avatar}.
280    *
281    * @since 0.6.0
282    */
283   [CCode (notify = false)]
284   public LoadableIcon? avatar
285     {
286       get { return this._avatar; }
287       set { this.change_avatar.begin (value); }
288     }
289
290   /**
291    * {@inheritDoc}
292    *
293    * @since UNRELEASED
294    */
295   public async void change_avatar (LoadableIcon? avatar) throws PropertyError
296     {
297       if (this._avatar == null ||
298           !this._avatar.equal (avatar))
299         {
300           yield ((Edsf.PersonaStore) this.store)._set_avatar (this, avatar);
301         }
302     }
303
304   private StructuredName? _structured_name = null;
305   /**
306    * {@inheritDoc}
307    *
308    * @since 0.6.0
309    */
310   [CCode (notify = false)]
311   public StructuredName? structured_name
312     {
313       get { return this._structured_name; }
314       set { this.change_structured_name.begin (value); }
315     }
316
317   /**
318    * {@inheritDoc}
319    *
320    * @since UNRELEASED
321    */
322   public async void change_structured_name (StructuredName? structured_name)
323       throws PropertyError
324     {
325       yield ((Edsf.PersonaStore) this.store)._set_structured_name (this,
326           structured_name);
327     }
328
329   /**
330    * The e-d-s contact uid
331    *
332    * @since 0.6.0
333    */
334   public string contact_id { get; private set; }
335
336   private string _full_name = "";
337   /**
338    * {@inheritDoc}
339    *
340    * @since 0.6.0
341    */
342   [CCode (notify = false)]
343   public string full_name
344     {
345       get { return this._full_name; }
346       set { this.change_full_name.begin (value); }
347     }
348
349   /**
350    * {@inheritDoc}
351    *
352    * @since UNRELEASED
353    */
354   public async void change_full_name (string full_name) throws PropertyError
355     {
356       yield ((Edsf.PersonaStore) this.store)._set_full_name (this, full_name);
357     }
358
359   private string _nickname = "";
360   /**
361    * {@inheritDoc}
362    *
363    * @since 0.6.0
364    */
365   [CCode (notify = false)]
366   public string nickname
367     {
368       get { return this._nickname; }
369       set { this.change_nickname.begin (value); }
370     }
371
372   /**
373    * {@inheritDoc}
374    *
375    * @since UNRELEASED
376    */
377   public async void change_nickname (string nickname) throws PropertyError
378     {
379       yield ((Edsf.PersonaStore) this.store)._set_nickname (this, nickname);
380     }
381
382   private Gender _gender;
383   /**
384    * {@inheritDoc}
385    *
386    * @since 0.6.0
387    */
388   [CCode (notify = false)]
389   public Gender gender
390     {
391       get { return this._gender; }
392       set { this.change_gender.begin (value); }
393     }
394
395   /**
396    * {@inheritDoc}
397    *
398    * @since UNRELEASED
399    */
400   public async void change_gender (Gender gender) throws PropertyError
401     {
402       yield ((Edsf.PersonaStore) this.store)._set_gender (this, gender);
403     }
404
405   private HashSet<UrlFieldDetails> _urls;
406   private Set<UrlFieldDetails> _urls_ro;
407   /**
408    * {@inheritDoc}
409    *
410    * @since 0.6.0
411    */
412   [CCode (notify = false)]
413   public Set<UrlFieldDetails> urls
414     {
415       get { return this._urls_ro; }
416       set
417         {
418           ((Edsf.PersonaStore) this.store)._set_urls (this, value);
419         }
420     }
421
422   private HashMultiMap<string, ImFieldDetails> _im_addresses =
423       new HashMultiMap<string, ImFieldDetails> (null, null,
424           (GLib.HashFunc) ImFieldDetails.hash,
425           (GLib.EqualFunc) ImFieldDetails.equal);
426
427   /**
428    * {@inheritDoc}
429    *
430    * @since 0.6.0
431    */
432   [CCode (notify = false)]
433   public MultiMap<string, ImFieldDetails> im_addresses
434     {
435       get { return this._im_addresses; }
436       set { this.change_im_addresses.begin (value); }
437     }
438
439   /**
440    * {@inheritDoc}
441    *
442    * @since UNRELEASED
443    */
444   public async void change_im_addresses (
445       MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
446     {
447       yield ((Edsf.PersonaStore) this.store)._set_im_fds (this, im_addresses);
448     }
449
450   private HashSet<string> _groups;
451   private Set<string> _groups_ro;
452
453   /**
454    * {@inheritDoc}
455    *
456    * @since 0.6.0
457    */
458   [CCode (notify = false)]
459   public Set<string> groups
460     {
461       get { return this._groups_ro; }
462       set
463         {
464           ((Edsf.PersonaStore) this.store)._set_groups (this, value);
465         }
466     }
467
468   /**
469    * {@inheritDoc}
470    *
471    * @since 0.6.0
472    */
473   public async void change_group (string group, bool is_member)
474       throws GLib.Error
475     {
476       /* Nothing to do? */
477       if ((is_member == true && this._groups.contains (group) == true) ||
478           (is_member == false && this._groups.contains (group) == false))
479         {
480           return;
481         }
482
483       /* Replace the current set of groups with a modified one. */
484       var new_groups = new HashSet<string> ();
485       foreach (var category_name in this._groups)
486         {
487           new_groups.add (category_name);
488         }
489
490       if (is_member == false)
491         {
492           new_groups.remove (group);
493         }
494       else
495         {
496           new_groups.add (group);
497         }
498
499       this.groups = new_groups;
500     }
501
502   /**
503    * Build a IID.
504    *
505    * @param store_id the {@link PersonaStore.id}
506    * @param contact the Contact
507    * @return a valid IID
508    *
509    * @since 0.6.0
510    */
511   internal static string build_iid_from_contact (string store_id,
512       E.Contact contact)
513     {
514       var contact_id =
515           (string) Edsf.Persona._get_property_from_contact (contact, "id");
516       return Edsf.Persona.build_iid (store_id, contact_id);
517     }
518
519   /**
520    * Build a IID.
521    *
522    * @param store_id the {@link PersonaStore.id}
523    * @param contact_id the id belonging to the Contact
524    * @return a valid IID
525    *
526    * @since 0.6.0
527    */
528   internal static string build_iid (string store_id, string contact_id)
529     {
530       return "%s:%s".printf (store_id, contact_id);
531     }
532
533
534   /**
535    * Create a new persona.
536    *
537    * Create a new persona for the {@link PersonaStore} `store`, representing
538    * the EDS contact given by `contact`.
539    *
540    * @since 0.6.0
541    */
542   public Persona (PersonaStore store, E.Contact contact)
543     {
544       var contact_id =
545         (string) Edsf.Persona._get_property_from_contact (contact, "id");
546       var uid = this.build_uid (BACKEND_NAME, store.id, contact_id);
547       var iid = Edsf.Persona.build_iid (store.id, contact_id);
548       var is_user = BookClient.is_self (contact);
549       var full_name =
550           (string) Edsf.Persona._get_property_from_contact (contact,
551               "full_name");
552
553       debug ("Creating new Edsf.Persona with IID '%s'", iid);
554
555       Object (display_id: full_name,
556               uid: uid,
557               iid: iid,
558               store: store,
559               is_user: is_user);
560
561       this._gender = Gender.UNSPECIFIED;
562       this.contact_id = contact_id;
563       this._phone_numbers = new HashSet<PhoneFieldDetails> (
564           (GLib.HashFunc) PhoneFieldDetails.hash,
565           (GLib.EqualFunc) PhoneFieldDetails.equal);
566       this._phone_numbers_ro = this._phone_numbers.read_only_view;
567       this._email_addresses = new HashSet<EmailFieldDetails> (
568           (GLib.HashFunc) EmailFieldDetails.hash,
569           (GLib.EqualFunc) EmailFieldDetails.equal);
570       this._email_addresses_ro = this._email_addresses.read_only_view;
571       this._notes = new HashSet<NoteFieldDetails> (
572           (GLib.HashFunc) NoteFieldDetails.hash,
573           (GLib.EqualFunc) NoteFieldDetails.equal);
574       this._notes_ro = this._notes.read_only_view;
575       this._urls = new HashSet<UrlFieldDetails> (
576           (GLib.HashFunc) UrlFieldDetails.hash,
577           (GLib.EqualFunc) UrlFieldDetails.equal);
578       this._urls_ro = this._urls.read_only_view;
579       this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
580           (GLib.HashFunc) PostalAddressFieldDetails.hash,
581           (GLib.EqualFunc) PostalAddressFieldDetails.equal);
582       this._postal_addresses_ro = this._postal_addresses.read_only_view;
583       this._local_ids = new HashSet<string> ();
584       this._local_ids_ro = this._local_ids.read_only_view;
585       this._web_service_addresses =
586         new HashMultiMap<string, WebServiceFieldDetails> (
587             null, null,
588             (GLib.HashFunc) WebServiceFieldDetails.hash,
589             (GLib.EqualFunc) WebServiceFieldDetails.equal);
590       this._groups = new HashSet<string> ();
591       this._groups_ro = this._groups.read_only_view;
592
593       this._update (contact);
594     }
595
596   /**
597    * {@inheritDoc}
598    *
599    * @since 0.6.0
600    */
601   public override void linkable_property_to_links (string prop_name,
602       Folks.Persona.LinkablePropertyCallback callback)
603     {
604       if (prop_name == "im-addresses")
605         {
606           foreach (var protocol in this._im_addresses.get_keys ())
607             {
608               var im_fds = this._im_addresses.get (protocol);
609
610               foreach (var im_fd in im_fds)
611                   callback (protocol + ":" + im_fd.value);
612             }
613         }
614       else if (prop_name == "local-ids")
615         {
616           /* Note: we need to use this.local_ids and not this._local_ids,
617            * otherwise this can have a different  behaviour depending
618            * on the state of the current Persona depending on whether
619            * this.local_ids was called before or not. */
620           foreach (var id in this.local_ids)
621             {
622               callback (id);
623             }
624         }
625       else if (prop_name == "web-service-addresses")
626         {
627           foreach (var web_service in this.web_service_addresses.get_keys ())
628             {
629               var web_service_addresses =
630                   this._web_service_addresses.get (web_service);
631
632               foreach (var ws_fd in web_service_addresses)
633                   callback (web_service + ":" + ws_fd.value);
634             }
635         }
636       else
637         {
638           /* Chain up */
639           base.linkable_property_to_links (prop_name, callback);
640         }
641     }
642
643   ~Persona ()
644     {
645       debug ("Destroying Edsf.Persona '%s': %p", this.uid, this);
646     }
647
648   /**
649    * Update attribs of the persona.
650    */
651   internal void _update (E.Contact contact)
652     {
653       this.contact = contact;
654
655       this._update_names ();
656       this._update_avatar ();
657       this._update_urls ();
658       this._update_phones ();
659       this._update_addresses ();
660       this._update_emails ();
661       this._update_im_addresses ();
662       this._update_groups ();
663       this._update_notes ();
664       this._update_local_ids ();
665       this._update_web_services_addresses ();
666       this._update_gender ();
667     }
668
669   private void _update_params (AbstractFieldDetails details,
670       E.VCardAttribute attr)
671     {
672       foreach (unowned E.VCardAttributeParam param in attr.get_params ())
673         {
674           string param_name = param.get_name ().down ();
675           foreach (unowned string param_value in param.get_values ())
676             {
677               details.add_parameter (param_name, param_value);
678             }
679         }
680     }
681
682   private void _update_gender ()
683     {
684       var gender = Gender.UNSPECIFIED;
685       var gender_attr =
686           this.contact.get_attribute (Edsf.Persona.gender_attribute_name);
687
688       if (gender_attr != null)
689         {
690           var gender_str = gender_attr.get_value ().up ();
691
692           if (gender_str == Edsf.Persona.gender_male)
693             {
694               gender = Gender.MALE;
695             }
696           else if (gender_str == Edsf.Persona.gender_female)
697             {
698               gender = Gender.FEMALE;
699             }
700         }
701
702       if (this._gender != gender)
703         {
704           this._gender = gender;
705           this.notify_property ("gender");
706         }
707     }
708
709   private void _update_web_services_addresses ()
710     {
711       this._web_service_addresses.clear ();
712
713       var services = this.contact.get_attribute ("X-FOLKS-WEB-SERVICES-IDS");
714       if (services != null)
715         {
716           foreach (var service in services.get_params ())
717             {
718               var service_name = service.get_name ().down ();
719               foreach (var service_id in service.get_values ())
720                 {
721                   this._web_service_addresses.set (service_name,
722                       new WebServiceFieldDetails (service_id));
723                 }
724             }
725         }
726
727       this.notify_property ("web-service-addresses");
728     }
729
730   private void _update_emails ()
731     {
732       this._email_addresses.clear ();
733
734       var attrs = this.contact.get_attributes (E.ContactField.EMAIL);
735       foreach (var attr in attrs)
736         {
737           var email_fd = new EmailFieldDetails (attr.get_value ());
738           this._update_params (email_fd, attr);
739           this._email_addresses.add (email_fd);
740         }
741
742       this.notify_property ("email-addresses");
743     }
744
745   private void _update_notes ()
746     {
747       this._notes.clear ();
748
749       string n = (string) this._get_property ("note");
750       if (n != null && n != "")
751         {
752           var note = new NoteFieldDetails (n);
753           this._notes.add (note);
754         }
755
756       this.notify_property ("notes");
757     }
758
759   private void _update_names ()
760     {
761       string full_name = (string) this._get_property ("full_name");
762       if (this._full_name != full_name)
763         {
764           this._full_name = full_name;
765           this.notify_property ("full-name");
766         }
767
768       string nickname = (string) this._get_property ("nickname");
769       if (this.nickname != nickname)
770         {
771           this._nickname = nickname;
772           this.notify_property ("nickname");
773         }
774
775       StructuredName? structured_name = null;
776       E.ContactName? cn = (E.ContactName) this._get_property ("name");
777       if (cn != null)
778         {
779           string family_name = cn.family;
780           string given_name  = cn.given;
781           string additional_names = cn.additional;
782           string prefixes = cn.prefixes;
783           string suffixes = cn.suffixes;
784           structured_name = new StructuredName (family_name, given_name,
785                                                 additional_names, prefixes,
786                                                 suffixes);
787         }
788
789       if (structured_name != null && !structured_name.is_empty ())
790         {
791           this._structured_name = structured_name;
792           this.notify_property ("structured-name");
793         }
794       else if (this._structured_name != null)
795         {
796           this._structured_name = null;
797           this.notify_property ("structured-name");
798         }
799     }
800
801   private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? p)
802     {
803       if (p == null)
804         {
805           return null;
806         }
807
808       switch (p.type)
809         {
810           case ContactPhotoType.URI:
811             if (p.get_uri () == null)
812               {
813                 return null;
814               }
815
816             return new FileIcon (File.new_for_uri (p.get_uri ()));
817           case ContactPhotoType.INLINED:
818             if (p.get_mime_type () == null || p.get_inlined () == null)
819               {
820                 return null;
821               }
822
823             return new Edsf.MemoryIcon (p.get_mime_type (), p.get_inlined ());
824           default:
825             return null;
826         }
827     }
828
829   private void _update_avatar ()
830     {
831       E.ContactPhoto? p = (E.ContactPhoto) this._get_property ("photo");
832
833       var cache = AvatarCache.dup ();
834
835       // Convert the ContactPhoto to a LoadableIcon and store or update it.
836       var new_avatar = this._contact_photo_to_loadable_icon (p);
837
838       if (this._avatar != null && new_avatar == null)
839         {
840           // Remove the old cached avatar, ignoring errors.
841           cache.remove_avatar.begin (this.uid, (obj, res) =>
842             {
843               try
844                 {
845                   cache.remove_avatar.end (res);
846                 }
847               catch (GLib.Error e1) {}
848
849               this._avatar = null;
850               this.notify_property ("avatar");
851             });
852         }
853       else if ((this.avatar == null && new_avatar != null) ||
854           (this.avatar != null && new_avatar != null &&
855            this._avatar.equal (new_avatar) == false))
856         {
857           // Store the new avatar in the cache.
858           cache.store_avatar.begin (this.uid, new_avatar, (obj, res) =>
859             {
860               try
861                 {
862                   cache.store_avatar.end (res);
863                   this._avatar = new_avatar;
864                   this.notify_property ("avatar");
865                 }
866               catch (GLib.Error e2)
867                 {
868                   warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
869                       this.uid, e2.message);
870                   new_avatar = null; /* failure */
871                 }
872             });
873         }
874     }
875
876   private void _update_urls ()
877     {
878       this._urls.clear ();
879       var urls_temp = new HashSet<UrlFieldDetails> ();
880
881       /* First we get the standard Evo urls.. */
882       foreach (string url_property in this.url_properties)
883         {
884           string u = (string) this._get_property (url_property);
885           if (u != null && u != "")
886             {
887               var fd_u = new UrlFieldDetails (u);
888               fd_u.set_parameter("type", url_property);
889               urls_temp.add (fd_u);
890             }
891         }
892
893       /* Now we go for extra URLs */
894       var vcard = (E.VCard) this.contact;
895       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
896         {
897           if (attr.get_name () == "X-URIS")
898             {
899               var url_fd = new UrlFieldDetails (attr.get_value ());
900               this._update_params (url_fd, attr);
901               urls_temp.add (url_fd);
902             }
903         }
904
905       if (!Utils.set_afd_equal (urls_temp, this._urls))
906         {
907           this._urls.clear ();
908
909           foreach (var url_fd in urls_temp)
910             {
911               this._urls.add (url_fd);
912             }
913
914          this.notify_property ("urls");
915         }
916     }
917
918   private void _update_im_addresses ()
919     {
920       var im_eds_map = this._get_im_eds_map ();
921       this._im_addresses.clear ();
922
923       foreach (var im_proto in im_eds_map.get_keys ())
924         {
925           var addresses = this.contact.get_attributes (
926               im_eds_map.lookup (im_proto));
927           foreach (var attr in addresses)
928             {
929               try
930                 {
931                   var addr = attr.get_value ();
932                   string normalised_addr =
933                     (owned) ImDetails.normalise_im_address (addr, im_proto);
934                   var im_fd = new ImFieldDetails (normalised_addr);
935                   this._im_addresses.set (im_proto, im_fd);
936                 }
937               catch (Folks.ImDetailsError e)
938                 {
939                   GLib.warning (
940                       "Problem when trying to normalise address: %s\n",
941                       e.message);
942                 }
943             }
944         }
945
946       this.notify_property ("im-addresses");
947     }
948
949   private void _update_groups ()
950     {
951       unowned GLib.List<string> category_names =
952           (GLib.List<string>) this._contact.get (E.ContactField.CATEGORY_LIST);
953       var new_categories = new HashSet<string> ();
954       var added_categories = new LinkedList<string> ();
955
956       foreach (var category_name in category_names)
957         {
958           new_categories.add (category_name);
959
960           /* Is this a new category? */
961           if (!this._groups.contains (category_name))
962             {
963               added_categories.add (category_name);
964             }
965         }
966
967       /* Work out which categories have been removed. */
968       var removed_categories = new LinkedList<string> ();
969
970       foreach (var category_name in this._groups)
971         {
972           if (!new_categories.contains (category_name))
973             {
974               removed_categories.add (category_name);
975             }
976         }
977
978       /* Make the changes to this._groups and emit signals. */
979       foreach (var category_name in removed_categories)
980         {
981           this.group_changed (category_name, false);
982           this._groups.remove (category_name);
983         }
984
985       foreach (var category_name in added_categories)
986         {
987           this._groups.add (category_name);
988           this.group_changed (category_name, true);
989         }
990
991       /* Notify if anything's changed. */
992       if (added_categories.size != 0 || removed_categories.size != 0)
993         {
994           this.notify_property ("groups");
995         }
996    }
997
998   /**
999    * build a table of im protocols / im protocol aliases
1000    */
1001   internal static HashTable<string, E.ContactField> _get_im_eds_map ()
1002     {
1003       lock (Edsf.Persona._im_eds_map)
1004         {
1005           if (Edsf.Persona._im_eds_map == null)
1006             {
1007               Edsf.Persona._im_eds_map =
1008                 new HashTable<string, E.ContactField> (str_hash, str_equal);
1009               Edsf.Persona._im_eds_map.insert ("aim", ContactField.IM_AIM);
1010               Edsf.Persona._im_eds_map.insert ("yahoo", ContactField.IM_YAHOO);
1011               Edsf.Persona._im_eds_map.insert ("groupwise",
1012                   ContactField.IM_GROUPWISE);
1013               Edsf.Persona._im_eds_map.insert ("jabber",
1014                   ContactField.IM_JABBER);
1015               Edsf.Persona._im_eds_map.insert ("msn",
1016                   ContactField.IM_MSN);
1017               Edsf.Persona._im_eds_map.insert ("icq",
1018                   ContactField.IM_ICQ);
1019               Edsf.Persona._im_eds_map.insert ("gadugadu",
1020                   ContactField.IM_GADUGADU);
1021               Edsf.Persona._im_eds_map.insert ("skype",
1022                   ContactField.IM_SKYPE);
1023             }
1024         }
1025
1026       return Edsf.Persona._im_eds_map;
1027     }
1028
1029   private void _update_phones ()
1030     {
1031       this._phone_numbers.clear ();
1032
1033       var attrs = this.contact.get_attributes (E.ContactField.TEL);
1034       foreach (var attr in attrs)
1035         {
1036           var phone_fd = new PhoneFieldDetails (attr.get_value ());
1037           this._update_params (phone_fd, attr);
1038           this._phone_numbers.add (phone_fd);
1039         }
1040
1041      this.notify_property ("phone-numbers");
1042    }
1043
1044   private PostalAddress _postal_address_from_attribute (E.VCardAttribute attr)
1045     {
1046       unowned GLib.List<string?> values = attr.get_values();
1047       unowned GLib.List<string?> l = values;
1048
1049       var address_format = "";
1050       var po_box = "";
1051       var extension = "";
1052       var street = "";
1053       var locality = "";
1054       var region = "";
1055       var postal_code = "";
1056       var country = "";
1057
1058       if (l != null)
1059         {
1060           po_box = l.data;
1061           l = l.next;
1062         }
1063       if (l != null)
1064         {
1065           extension = l.data;
1066           l = l.next;
1067         }
1068       if (l != null)
1069         {
1070           street = l.data;
1071           l = l.next;
1072         }
1073       if (l != null)
1074         {
1075           locality = l.data;
1076           l = l.next;
1077         }
1078       if (l != null)
1079         {
1080           region = l.data;
1081           l = l.next;
1082         }
1083       if (l != null)
1084         {
1085           postal_code = l.data;
1086           l = l.next;
1087         }
1088       if (l != null)
1089         {
1090           country = l.data;
1091           l = l.next;
1092         }
1093
1094       return new PostalAddress (po_box, extension, street,
1095                                 locality, region, postal_code, country,
1096                                 address_format, null);
1097     }
1098
1099   /*
1100    * TODO: we should check if addresses corresponding to different types
1101    *       are the same and if so instantiate only one PostalAddress
1102    *       (with the given types).
1103    */
1104   private void _update_addresses ()
1105     {
1106       this._postal_addresses.clear ();
1107
1108       var attrs = this.contact.get_attributes (E.ContactField.ADDRESS);
1109       foreach (unowned E.VCardAttribute attr in attrs)
1110         {
1111           var pa_fd = new PostalAddressFieldDetails (
1112               this._postal_address_from_attribute (attr));
1113           this._update_params (pa_fd, attr);
1114           this._postal_addresses.add (pa_fd);
1115         }
1116
1117       this.notify_property ("postal-addresses");
1118     }
1119
1120   private void _update_local_ids ()
1121     {
1122       this._local_ids.clear ();
1123
1124       var ids = this.contact.get_attribute ("X-FOLKS-CONTACTS-IDS");
1125       if (ids != null)
1126         {
1127           unowned GLib.List<string> ids_v = ids.get_values ();
1128
1129           foreach (var local_id in ids_v)
1130             {
1131               this._local_ids.add (local_id);
1132             }
1133         }
1134
1135       this.notify_property ("local-ids");
1136     }
1137
1138   internal static void * _get_property_from_contact (E.Contact contact,
1139       string prop_name)
1140     {
1141       void *prop_value = null;
1142       prop_value = contact.get (E.Contact.field_id (prop_name));
1143       return prop_value;
1144     }
1145
1146   private void * _get_property (string prop_name)
1147     {
1148       return Edsf.Persona._get_property_from_contact (this.contact,
1149           prop_name);
1150     }
1151 }