[folks] bluez: Support updating contacts instead of replacing them when re-querying



commit ab765424681093e22b34ef44bda4b27b42177a01
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Mon Nov 11 08:19:29 2013 +0000

    bluez: Support updating contacts instead of replacing them when re-querying
    
    When downloading the contact list over PBAP for anything other than the
    first time, support updating the properties of existing personas, rather
    than replacing them all wholesale. This is a step on the way towards
    supporting periodic refreshes of the contact list.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=711827

 backends/bluez/bluez-persona-store.vala |   79 +++++++++++++++--
 backends/bluez/bluez-persona.vala       |  151 ++++++++++++++++++++++++-------
 2 files changed, 191 insertions(+), 39 deletions(-)
---
diff --git a/backends/bluez/bluez-persona-store.vala b/backends/bluez/bluez-persona-store.vala
index a95b875..b57cb6b 100644
--- a/backends/bluez/bluez-persona-store.vala
+++ b/backends/bluez/bluez-persona-store.vala
@@ -223,8 +223,13 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
    * the persona store accordingly. Contacts are stored in the file as a
    * sequence of vCards, separated by blank lines.
    *
+   * If a contact already exists in the store, its properties will be updated
+   * from the vCard; otherwise it will be added as a new contact to the store.
+   * Contacts which are in the store and not in the vCard will be removed from
+   * the store.
+   *
    * If this throws an error, it guarantees to leave the store’s internal state
-   * unchanged.
+   * unchanged, but may change the state of { link Persona}s in the store.
    *
    * @param file the file where the contacts are stored
    * @throws IOError if there was an error communicating with D-Bus
@@ -235,6 +240,11 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
   private async void _update_contacts_from_file (File file) throws IOError
     {
       var added_personas = new HashSet<Persona> ();
+      var removed_personas = new HashSet<Persona> ();
+
+      /* Start with all personas being marked as removed, and then eliminate the
+       * ones which are found in the vCard. */
+      removed_personas.add_all (this._personas.values);
 
       try
         {
@@ -243,7 +253,7 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
           string? line = null;
           StringBuilder vcard = new StringBuilder ();
 
-          /* For each vCard in the file create a new Persona */
+          /* For each vCard in the file create or update a Persona. */
           while ((line = yield dis.read_line_async ()) != null)
             {
               /* Ignore blank lines between vCards. */
@@ -254,11 +264,60 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
               vcard.append_c ('\n');
               if (line.strip () == "END:VCARD")
                 {
+                  var card = new E.VCard.from_string (vcard.str);
+
                   /* The first vCard is always the user themselves. */
                   var is_user = (i == 0);
 
-                  var persona = new Persona (vcard.str, this, is_user);
-                  added_personas.add (persona);
+                  /* Construct the card’s IID. */
+                  var iid_is_checksum = false;
+                  string iid;
+
+                  /* This prefers the ‘UID’ attribute from the vCard, if it’s
+                   * available. However, it is not a required attribute, so many
+                   * phones do not implement it; in those cases, fall back to a
+                   * checksum of the vCard data itself. This means that whenever
+                   * a contact’s properties change in the vCard its IID will
+                   * change and hence the persona will be removed and re-added,
+                   * but without stable UIDs this is unavoidable. */
+                  var attribute = card.get_attribute ("UID");
+                  if (attribute != null)
+                    {
+                      /* Try the UID attribute. */
+                      iid = attribute.get_value_decoded ().str;
+                    }
+                  else
+                    {
+                      /* Fallback. */
+                      iid =
+                           Checksum.compute_for_string (ChecksumType.SHA1,
+                               vcard.str);
+                      iid_is_checksum = true;
+                    }
+
+                  /* Create or update the persona. */
+                  var persona = this._personas.get (iid);
+                  if (persona == null)
+                    {
+                      persona =
+                          new Persona (vcard.str, card, this, is_user, iid);
+                    }
+                  else
+                    {
+                      /* If the IID is a checksum and we found the persona in
+                       * the store, that means their properties havent’t
+                       * changed, so as an optimisation, don’t bother updating
+                       * the Persona from the vCard in that case. */
+                      if (iid_is_checksum == false)
+                        {
+                          /* Note: This updates persona’s state, which could be
+                           * left updated if we later throw an error. */
+                          persona.update_from_vcard (card);
+                        }
+                    }
+
+                  if (removed_personas.remove (persona) == false)
+                      added_personas.add (persona);
 
                   i++;
                   vcard.erase ();
@@ -278,9 +337,14 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
        * the store’s internal state. */
       foreach (var p in added_personas)
           this._personas.set (p.iid, p);
+      foreach (var p in removed_personas)
+          this._personas.unset (p.iid);
 
-      if (added_personas.is_empty == false)
-          this._emit_personas_changed (added_personas, null);
+      if (added_personas.is_empty == false ||
+          removed_personas.is_empty == false)
+        {
+          this._emit_personas_changed (added_personas, removed_personas);
+        }
     }
 
   /**
@@ -655,7 +719,8 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
               phonebook_filter.insert ("Format", "Vcard30");
               phonebook_filter.insert ("Fields",
                   new Variant.strv ({
-                      "N", "FN", "NICKNAME", "TEL", "URL", "EMAIL", "PHOTO"
+                      "UID", "N", "FN", "NICKNAME", "TEL", "URL", "EMAIL",
+                      "PHOTO"
                   }));
 
               obex_pbap.pull_all ("", phonebook_filter, out path, out props);
diff --git a/backends/bluez/bluez-persona.vala b/backends/bluez/bluez-persona.vala
index 9dca903..43e0590 100644
--- a/backends/bluez/bluez-persona.vala
+++ b/backends/bluez/bluez-persona.vala
@@ -169,15 +169,17 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
    * Create a new persona for the { link PersonaStore} ``store``, representing
    * the Persona in the given ``vcard``.
    *
-   * @param vcard the Vcard stored as a string.
+   * @param vcard the vCard stored as a string
+   * @param card a parsed version of the vCard
    * @param store the store to which the Persona belongs.
    * @param is_user whether the Persona is the user itself or not.
+   * @param iid pre-calculated IID for the persona
    *
    * @since 0.9.6
    */
-  public Persona (string vcard, Folks.PersonaStore store, bool is_user)
+  public Persona (string vcard, E.VCard card, Folks.PersonaStore store,
+      bool is_user, string iid)
     {
-      var iid = Checksum.compute_for_string (ChecksumType.SHA1, vcard);
       var uid = Folks.Persona.build_uid ("bluez", store.id, iid);
 
       /* Have to use the IID as the display ID, since PBAP vCards provide no
@@ -188,7 +190,7 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
               store: store,
               is_user: is_user);
 
-      this._set_vcard (vcard);
+      this.update_from_vcard (card);
     }
 
   construct
@@ -205,77 +207,162 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
       this._urls_ro = this._urls.read_only_view;
     }
 
-  private void _set_vcard (string vcard)
+  /**
+   * Update the Persona’s properties from a vCard.
+   *
+   * Parse the given ``vcard`` and set the persona’s properties from it. This
+   * emits property change notifications as appropriate.
+   *
+   * @param vcard pre-parsed vCard
+   *
+   * @since UNRELEASED
+   */
+  internal void update_from_vcard (E.VCard card)
     {
-      E.VCard card = new E.VCard.from_string (vcard);
+      this.freeze_notify ();
+
+      /* Phone numbers. */
+      var attribute = card.get_attribute ("TEL");
+      var new_phone_numbers = new HashSet<PhoneFieldDetails> ();
 
-      E.VCardAttribute? attribute = card.get_attribute ("TEL");
       if (attribute != null)
         {
-          this._phone_numbers.add (
-              new PhoneFieldDetails (attribute.get_value_decoded ().str));
+          unowned GLib.List<unowned StringBuilder> vals =
+              attribute.get_values_decoded ();
+          foreach (unowned StringBuilder v in vals)
+              new_phone_numbers.add (new PhoneFieldDetails (v.str));
         }
 
+      if (!Folks.Internal.equal_sets<PhoneFieldDetails> (this._phone_numbers,
+              new_phone_numbers))
+        {
+          this._phone_numbers = new_phone_numbers;
+          this._phone_numbers_ro = new_phone_numbers.read_only_view;
+          this.notify_property ("phone-numbers");
+        }
+
+      /* Full name. */
       attribute = card.get_attribute ("FN");
+      var new_full_name = "";
+
       if (attribute != null)
+          new_full_name = attribute.get_value_decoded ().str;
+
+      if (this._full_name != new_full_name)
         {
-          /* Also the display-id. */
-          this._full_name = attribute.get_value_decoded ().str;
+          this._full_name = new_full_name;
+          this.notify_property ("full-name");
         }
 
+      /* Nickname. */
       attribute = card.get_attribute ("NICKNAME");
+      var new_nickname = "";
+
       if (attribute != null)
+          new_nickname = attribute.get_value_decoded ().str;
+
+      if (this._nickname != new_nickname)
         {
-          this._nickname = attribute.get_value_decoded ().str;
+          this._nickname = new_nickname;
+          this.notify_property ("nickname");
         }
 
+      /* URIs. */
       attribute = card.get_attribute ("URL");
+      var new_uris = new HashSet<UrlFieldDetails> ();
+
       if (attribute != null)
         {
-          var url = attribute.get_value_decoded ().str;
-          this._urls.add (new UrlFieldDetails (url));
+          unowned GLib.List<unowned StringBuilder> vals =
+              attribute.get_values_decoded ();
+          foreach (unowned StringBuilder v in vals)
+              new_uris.add (new UrlFieldDetails (v.str));
         }
 
-      attribute = card.get_attribute ("PHOTO");
-      if (attribute != null)
+      if (!Folks.Internal.equal_sets<UrlFieldDetails> (this._urls, new_uris))
         {
-          var encoded_data = (string) attribute.get_value ().data;
-          var bytes = new Bytes (Base64.decode (encoded_data));
-          this._avatar = new BytesIcon (bytes);
+          this._urls = new_uris;
+          this._urls_ro = new_uris.read_only_view;
+          this.notify_property ("urls");
         }
 
+      /* Structured name. */
       attribute = card.get_attribute ("N");
+      StructuredName? new_structured_name = null;
+
       if (attribute != null)
         {
-          string[] components = {"", "", "", "", ""};
-          uint components_size = 5;
-          unowned GLib.List<StringBuilder> values =
+          string[] components = { "", "", "", "", "" };
+          unowned GLib.List<unowned StringBuilder> values =
               attribute.get_values_decoded ();
 
-          if (values.length () < components_size)
-            components_size = values.length ();
-
-          for (int i = 0; i < components_size; i++)
+          uint i = 0;
+          foreach (unowned StringBuilder b in values)
             {
-              components[i] = values.nth_data (i).str;
+              if (i >= components.length)
+                  break;
+
+              components[i++] = b.str;
             }
 
           this._structured_name = new StructuredName (components[0],
               components[1], components[2], components[3], components[4]);
 
-          if (values.length () != 5)
+          if (i != 5)
             {
-              debug ("Expected 5 components to N value of vcard, got %u",
-                  values.length ());
+              debug ("Expected 5 components in N value of vCard, but got %u.",
+                  i);
             }
         }
 
+      if ((new_structured_name == null) != (this._structured_name == null) ||
+          (new_structured_name != null && this._structured_name != null &&
+           !new_structured_name.equal (this._structured_name)))
+        {
+          this._structured_name = new_structured_name;
+          this.notify_property ("structured-name");
+        }
+
+      /* E-mail addresses. */
       attribute = card.get_attribute ("EMAIL");
+      var new_email_addresses = new HashSet<EmailFieldDetails> ();
+
       if (attribute != null)
         {
-          this._email_addresses.add (
-              new EmailFieldDetails (attribute.get_value_decoded ().str));
+          unowned GLib.List<unowned StringBuilder> vals =
+              attribute.get_values_decoded ();
+          foreach (unowned StringBuilder v in vals)
+              new_email_addresses.add (new EmailFieldDetails (v.str));
         }
+
+      if (!Folks.Internal.equal_sets<EmailFieldDetails> (this._email_addresses,
+              new_email_addresses))
+        {
+          this._email_addresses = new_email_addresses;
+          this._email_addresses_ro = new_email_addresses.read_only_view;
+          this.notify_property ("email-addresses");
+        }
+
+      /* Photo. */
+      attribute = card.get_attribute ("PHOTO");
+      BytesIcon? new_avatar = null;
+
+      if (attribute != null)
+        {
+          var encoded_data = (string) attribute.get_value ().data;
+          var bytes = new Bytes (Base64.decode (encoded_data));
+          new_avatar = new BytesIcon (bytes);
+        }
+
+      if ((new_avatar == null) != (this._avatar == null) ||
+          (new_avatar != null && this._avatar != null &&
+           !new_avatar.equal (this._avatar)))
+        {
+          this._avatar = new_avatar;
+          this.notify_property ("avatar");
+        }
+
+      this.thaw_notify ();
     }
 
   /**


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]