[folks] Support writing extended info for Telepathy user contacts



commit 0d22764d076f2eb0fd14629cf0191ed8d62c1b2f
Author: Travis Reitter <travis reitter collabora co uk>
Date:   Fri Sep 30 10:54:15 2011 -0700

    Support writing extended info for Telepathy user contacts
    
    Helps: bgo#657602 - Telepathy backend fails to set Personas' phone
    numbers from ContactInfo

 backends/telepathy/lib/tpf-persona-store.vala      |  104 +++++++++++++
 backends/telepathy/lib/tpf-persona.vala            |  158 ++++++++++++++++++--
 tests/lib/telepathy/contactlist/conn.c             |   30 ++++
 .../telepathy/contactlist/contact-list-manager.c   |   50 ++++++
 .../telepathy/contactlist/contact-list-manager.h   |    2 +
 tests/telepathy/individual-properties.vala         |  138 +++++++++++++++++
 6 files changed, 472 insertions(+), 10 deletions(-)
---
diff --git a/backends/telepathy/lib/tpf-persona-store.vala b/backends/telepathy/lib/tpf-persona-store.vala
index 14b07a4..3f8c6ce 100644
--- a/backends/telepathy/lib/tpf-persona-store.vala
+++ b/backends/telepathy/lib/tpf-persona-store.vala
@@ -2125,4 +2125,108 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       FolksTpLowlevel.connection_set_contact_alias (this._conn,
           (Handle) persona.contact.handle, alias);
     }
+
+  internal async void change_user_full_name (Tpf.Persona persona,
+      string full_name) throws PersonaStoreError
+    {
+      /* Deal with badly-behaved callers */
+      if (full_name == null)
+        {
+          full_name = "";
+        }
+
+      var info_set = new HashSet<ContactInfoField> ();
+      string[] values = { full_name };
+      string[] parameters = { null };
+
+      var field = new ContactInfoField ("fn", parameters, values);
+      info_set.add (field);
+
+      yield this._change_user_contact_info (persona, info_set);
+    }
+
+  internal async void _change_user_details (
+      Tpf.Persona persona, Set<AbstractFieldDetails<string>> details,
+      string field_name)
+        throws PersonaStoreError
+    {
+      var info_set = new HashSet<ContactInfoField> ();
+
+      foreach (var afd in details)
+        {
+          string[] values = { afd.value };
+          string[] parameters = {};
+
+          foreach (var param_name in afd.parameters.get_keys ())
+            {
+              var param_values = afd.parameters[param_name];
+              foreach (var param_value in param_values)
+                {
+                  parameters += @"$param_name=$param_value";
+                }
+            }
+
+          if (parameters.length == 0)
+            parameters = { null };
+
+          var field = new ContactInfoField (field_name, parameters, values);
+          info_set.add (field);
+        }
+
+      yield this._change_user_contact_info (persona, info_set);
+    }
+
+  private async void _change_user_contact_info (Tpf.Persona persona,
+      HashSet<ContactInfoField> info_set) throws PersonaStoreError
+    {
+      if (!persona.is_user)
+        {
+          throw new PersonaStoreError.INVALID_ARGUMENT (
+              _("Extended information may only be set on the user's Telepathy contact."));
+        }
+
+      var info_list = this._contact_info_set_to_list (info_set);
+      if (this.account.connection != null)
+        {
+          GLib.Error? error = null;
+          bool success = false;
+          try
+            {
+              success =
+                yield this.account.connection.set_contact_info_async (
+                  info_list);
+            }
+          catch (GLib.Error e)
+            {
+              error = e;
+            }
+
+          if (error != null || !success)
+            {
+              warning ("Failed to set extended information on user's " +
+                  "Telepathy contact: %s",
+                  error != null ? error.message : "(reason unknown)");
+            }
+        }
+      else
+        {
+          throw new PersonaStoreError.STORE_OFFLINE (
+              _("Extended information cannot be written because the store is disconnected."));
+        }
+    }
+
+  private static GLib.List<ContactInfoField> _contact_info_set_to_list (
+      HashSet<ContactInfoField> info_set)
+    {
+      var info_list = new GLib.List<ContactInfoField> ();
+      foreach (var info_field in info_set)
+        {
+          info_list.prepend (new ContactInfoField (
+                info_field.field_name, info_field.parameters,
+                info_field.field_value));
+        }
+      info_list.reverse ();
+
+      return info_list;
+    }
 }
diff --git a/backends/telepathy/lib/tpf-persona.vala b/backends/telepathy/lib/tpf-persona.vala
index 8e05a34..1eb299c 100644
--- a/backends/telepathy/lib/tpf-persona.vala
+++ b/backends/telepathy/lib/tpf-persona.vala
@@ -33,6 +33,7 @@ public class Tpf.Persona : Folks.Persona,
     FavouriteDetails,
     GroupDetails,
     ImDetails,
+    NameDetails,
     PhoneDetails,
     PresenceDetails
 {
@@ -40,6 +41,7 @@ public class Tpf.Persona : Folks.Persona,
   private Set<string> _groups_ro;
   private bool _is_favourite;
   private string _alias; /* must never be null */
+  private string _full_name; /* must never be null */
   private HashMultiMap<string, ImFieldDetails> _im_addresses;
   private const string[] _linkable_properties = { "im-addresses" };
   private const string[] _writeable_properties =
@@ -84,6 +86,78 @@ public class Tpf.Persona : Folks.Persona,
     }
 
   /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public StructuredName? structured_name
+    {
+      get { return null; }
+      set { this.change_structured_name.begin (value); } /* not writeable */
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public string full_name
+    {
+      get { return this._full_name; }
+      set { this.change_full_name.begin (value); }
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_full_name (string full_name) throws PropertyError
+    {
+      var tpf_store = this.store as Tpf.PersonaStore;
+
+      if (full_name == this._full_name)
+        return;
+
+      if (this._is_constructed)
+        {
+          try
+            {
+              yield tpf_store.change_user_full_name (this, full_name);
+            }
+          catch (PersonaStoreError.INVALID_ARGUMENT e1)
+            {
+              throw new PropertyError.NOT_WRITEABLE (e1.message);
+            }
+          catch (PersonaStoreError.STORE_OFFLINE e2)
+            {
+              throw new PropertyError.UNKNOWN_ERROR (e2.message);
+            }
+          catch (PersonaStoreError e3)
+            {
+              throw new PropertyError.UNKNOWN_ERROR (e3.message);
+            }
+        }
+
+      /* the change will be notified when we receive changes to
+       * contact.contact_info */
+    }
+
+  /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public string nickname
+    {
+      get { return ""; }
+      set { this.change_nickname.begin (value); } /* not writeable */
+    }
+
+  /**
    * The Persona's presence type.
    *
    * See { link Folks.PresenceDetails.presence_type}.
@@ -303,6 +377,56 @@ public class Tpf.Persona : Folks.Persona,
     }
 
   /**
+   * { inheritDoc}
+   *
+   * @since UNRELEASED
+   */
+  public async void change_phone_numbers (
+      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
+    {
+      yield this._change_details<PhoneFieldDetails> (phone_numbers,
+          this._phone_numbers, "tel");
+    }
+
+  private async void _change_details<T> (
+      Set<AbstractFieldDetails<string>> details,
+      Set<AbstractFieldDetails<string>> member_set,
+      string field_name)
+        throws PropertyError
+    {
+      var tpf_store = this.store as Tpf.PersonaStore;
+
+      if (Folks.PersonaStore.equal_sets<PhoneFieldDetails> (phone_numbers,
+              this._phone_numbers))
+        {
+          return;
+        }
+
+      if (this._is_constructed)
+        {
+          try
+            {
+              yield tpf_store._change_user_details (this, details, field_name);
+            }
+          catch (PersonaStoreError.INVALID_ARGUMENT e1)
+            {
+              throw new PropertyError.NOT_WRITEABLE (e1.message);
+            }
+          catch (PersonaStoreError.STORE_OFFLINE e2)
+            {
+              throw new PropertyError.UNKNOWN_ERROR (e2.message);
+            }
+          catch (PersonaStoreError e3)
+            {
+              throw new PropertyError.UNKNOWN_ERROR (e3.message);
+            }
+        }
+
+      /* the change will be notified when we receive changes to
+       * contact.contact_info */
+    }
+
+  /**
    * Create a new persona.
    *
    * Create a new persona for the { link PersonaStore} `store`, representing
@@ -330,6 +454,8 @@ public class Tpf.Persona : Folks.Persona,
               store: store,
               is_user: contact.handle == connection.self_handle);
 
+      this._full_name = "";
+
       contact.notify["alias"].connect ((s, p) =>
           {
             /* Tp guarantees that aliases are always non-null. */
@@ -398,9 +524,9 @@ public class Tpf.Persona : Folks.Persona,
 
       contact.notify["contact-info"].connect ((s, p) =>
         {
-          this._contact_notify_phones ();
+          this._contact_notify_contact_info ();
         });
-      this._contact_notify_phones ();
+      this._contact_notify_contact_info ();
 
       ((Tpf.PersonaStore) this.store).group_members_changed.connect (
           (s, group, added, removed) =>
@@ -428,8 +554,9 @@ public class Tpf.Persona : Folks.Persona,
             });
     }
 
-  private void _contact_notify_phones ()
+  private void _contact_notify_contact_info ()
     {
+      var new_full_name = "";
       var new_phone_numbers = new HashSet<PhoneFieldDetails> (
           (GLib.HashFunc) PhoneFieldDetails.hash,
           (GLib.EqualFunc) PhoneFieldDetails.equal);
@@ -437,17 +564,28 @@ public class Tpf.Persona : Folks.Persona,
       var contact_info = this.contact.get_contact_info ();
       foreach (var info in contact_info)
         {
-          if (info.field_name != "tel")
-            continue;
-
-          foreach (var phone_num in info.field_value)
+          if (info.field_name == "") {}
+          else if (info.field_name == "fn")
+            {
+              new_full_name = info.field_value[0];
+            }
+          else if (info.field_name == "tel")
             {
-              var parameters = this._afd_params_from_strv (info.parameters);
-              var phone_fd = new PhoneFieldDetails (phone_num, parameters);
-              new_phone_numbers.add (phone_fd);
+              foreach (var phone_num in info.field_value)
+                {
+                  var parameters = this._afd_params_from_strv (info.parameters);
+                  var phone_fd = new PhoneFieldDetails (phone_num, parameters);
+                  new_phone_numbers.add (phone_fd);
+                }
             }
         }
 
+      if (new_full_name != this._full_name)
+        {
+          this._full_name = new_full_name;
+          this.notify_property ("full-name");
+        }
+
       if (!Folks.PersonaStore.equal_sets<PhoneFieldDetails> (new_phone_numbers,
               this._phone_numbers))
         {
diff --git a/tests/lib/telepathy/contactlist/conn.c b/tests/lib/telepathy/contactlist/conn.c
index cc35c4b..dbd403e 100644
--- a/tests/lib/telepathy/contactlist/conn.c
+++ b/tests/lib/telepathy/contactlist/conn.c
@@ -411,6 +411,13 @@ conn_contact_info_properties_getter (GObject *object,
           supported_fields = g_ptr_array_new ();
 
           g_ptr_array_add (supported_fields, tp_value_array_build (4,
+              G_TYPE_STRING, "fn",
+              G_TYPE_STRV, NULL,
+              G_TYPE_UINT, 0,
+              G_TYPE_UINT, 1,
+              G_TYPE_INVALID));
+
+          g_ptr_array_add (supported_fields, tp_value_array_build (4,
               G_TYPE_STRING, "tel",
               G_TYPE_STRV, NULL,
               G_TYPE_UINT, 0,
@@ -884,6 +891,28 @@ request_contact_info (TpSvcConnectionInterfaceContactInfo *iface,
 }
 
 static void
+set_contact_info (TpSvcConnectionInterfaceContactInfo *iface,
+                  const GPtrArray *contact_info,
+                  DBusGMethodInvocation *context)
+{
+  TpTestContactListConnection *self = TP_TEST_CONTACT_LIST_CONNECTION (iface);
+  GError *error = NULL;
+
+  if (contact_info == NULL)
+    {
+      dbus_g_method_return_error (context, error);
+      g_error_free (error);
+      return;
+    }
+
+  tp_test_contact_list_manager_set_contact_info (self->priv->list_manager,
+      contact_info);
+
+  tp_svc_connection_interface_contact_info_return_from_set_contact_info (
+      context);
+}
+
+static void
 init_contact_info (gpointer iface,
                    gpointer iface_data G_GNUC_UNUSED)
 {
@@ -894,6 +923,7 @@ init_contact_info (gpointer iface,
   IMPLEMENT(get_contact_info);
   IMPLEMENT(refresh_contact_info);
   IMPLEMENT(request_contact_info);
+  IMPLEMENT(set_contact_info);
 #undef IMPLEMENT
 }
 
diff --git a/tests/lib/telepathy/contactlist/contact-list-manager.c b/tests/lib/telepathy/contactlist/contact-list-manager.c
index e979f8e..1e047b1 100644
--- a/tests/lib/telepathy/contactlist/contact-list-manager.c
+++ b/tests/lib/telepathy/contactlist/contact-list-manager.c
@@ -522,6 +522,11 @@ receive_contact_lists (gpointer p)
     _insert_contact_field (d->contact_info, "fn", NULL,
         (const gchar * const *) values);
   }
+  {
+    const gchar * values[] = { id, NULL };
+    _insert_contact_field (d->contact_info, "email", NULL,
+        (const gchar * const *) values);
+  }
   tp_handle_unref (self->priv->contact_repo, handle);
 
   id = "travis example com";
@@ -1763,3 +1768,48 @@ tp_test_contact_list_manager_get_contact_info (TpTestContactListManager *self,
 
   return NULL;
 }
+
+void
+tp_test_contact_list_manager_set_contact_info (TpTestContactListManager *self,
+                                               const GPtrArray *contact_info)
+{
+  TpTestContactList *stored = self->priv->lists[
+    TP_TEST_CONTACT_LIST_STORED];
+  TpTestContactDetails *d = ensure_contact (self, self->priv->conn->self_handle,
+      NULL);
+  GPtrArray *old = d->contact_info;
+
+  /* FIXME: if stored list hasn't been retrieved yet, queue the change for
+   * later */
+
+  /* if shutting down, do nothing */
+  if (stored == NULL)
+    return;
+
+  d->contact_info = dbus_g_type_specialized_construct (
+      TP_ARRAY_TYPE_CONTACT_INFO_FIELD_LIST);
+  {
+    guint i;
+    for (i = 0; i < contact_info->len; i++)
+      {
+        const gchar *name;
+        const gchar * const * params;
+        const gchar * const * values;
+        GValueArray *va = g_ptr_array_index (contact_info, i);
+
+        tp_value_array_unpack (va, 3,
+          &name,
+          &params,
+          &values);
+
+        _insert_contact_field (d->contact_info, name, params, values);
+      }
+  }
+
+  /* always send the updated roster, since it's not worth checking the
+   * contact_info for changes */
+  send_updated_roster (self, self->priv->conn->self_handle);
+
+  if (old != NULL)
+    g_ptr_array_unref (old);
+}
diff --git a/tests/lib/telepathy/contactlist/contact-list-manager.h b/tests/lib/telepathy/contactlist/contact-list-manager.h
index 797f3fb..4d9f050 100644
--- a/tests/lib/telepathy/contactlist/contact-list-manager.h
+++ b/tests/lib/telepathy/contactlist/contact-list-manager.h
@@ -104,6 +104,8 @@ void tp_test_contact_list_manager_set_alias (
     TpTestContactListManager *self, TpHandle contact, const gchar *alias);
 GPtrArray * tp_test_contact_list_manager_get_contact_info (
     TpTestContactListManager *self, TpHandle contact);
+void tp_test_contact_list_manager_set_contact_info (
+    TpTestContactListManager *self, const GPtrArray *contact_info);
 
 G_END_DECLS
 
diff --git a/tests/telepathy/individual-properties.vala b/tests/telepathy/individual-properties.vala
index ae8e84c..b56245b 100644
--- a/tests/telepathy/individual-properties.vala
+++ b/tests/telepathy/individual-properties.vala
@@ -29,6 +29,7 @@ public class IndividualPropertiesTests : Folks.TestCase
   private TpTest.Backend tp_backend;
   private void* _account_handle;
   private int _test_timeout = 3;
+  private HashSet<string> _changes_pending;
 
   public IndividualPropertiesTests ()
     {
@@ -42,6 +43,8 @@ public class IndividualPropertiesTests : Folks.TestCase
           this.test_individual_properties_change_alias_through_tp_backend);
       this.add_test ("individual properties:change alias through test cm",
           this.test_individual_properties_change_alias_through_test_cm);
+      this.add_test ("individual properties:change contact info",
+          this.test_individual_properties_change_contact_info);
 
       if (Environment.get_variable ("FOLKS_TEST_VALGRIND") != null)
           this._test_timeout = 10;
@@ -52,6 +55,7 @@ public class IndividualPropertiesTests : Folks.TestCase
       this.tp_backend.set_up ();
       this._account_handle = this.tp_backend.add_account ("protocol",
           "me example com", "cm", "account");
+      this._changes_pending = new HashSet<string> ();
     }
 
   public override void tear_down ()
@@ -281,6 +285,140 @@ public class IndividualPropertiesTests : Folks.TestCase
       /* necessary to reset the aggregator for the next test */
       aggregator = null;
     }
+
+  public void test_individual_properties_change_contact_info ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      this._changes_pending.add ("phone-numbers");
+      this._changes_pending.add ("full-name");
+
+      /* Set up the aggregator */
+      var aggregator = new IndividualAggregator ();
+      aggregator.individuals_changed_detailed.connect ((changes) =>
+        {
+          this._change_contact_info_aggregator_individuals_added (changes);
+        });
+
+      aggregator.prepare ();
+
+      /* Kill the main loop after a few seconds. If the alias hasn't been
+       * notified, something along the way failed or been too slow (which we can
+       * consider to be failure). */
+      Timeout.add_seconds (this._test_timeout, () =>
+        {
+          main_loop.quit ();
+          return false;
+        });
+
+      main_loop.run ();
+
+      assert (this._changes_pending.size == 0);
+
+      /* necessary to reset the aggregator for the next test */
+      aggregator = null;
+    }
+
+  private async void _change_contact_info_aggregator_individuals_added (
+      MultiMap<Individual?, Individual?> changes)
+    {
+      var added = changes.get_values ();
+      var removed = changes.get_keys ();
+
+      var new_phone_fd = new PhoneFieldDetails ("+112233445566");
+      new_phone_fd.set_parameter (AbstractFieldDetails.PARAM_TYPE,
+          AbstractFieldDetails.PARAM_TYPE_HOME);
+      var new_full_name = "Cave Johnson";
+
+      foreach (Individual i in added)
+        {
+          assert (i != null);
+
+          /* Check properties */
+          assert (new_full_name != i.full_name);
+          assert (!(new_phone_fd in i.phone_numbers));
+
+          i.notify["full-name"].connect ((s, p) =>
+              {
+                /* we can't re-use i here due to Vala's implementation */
+                var ind = (Individual) s;
+
+                if (ind.full_name == new_full_name)
+                  this._changes_pending.remove ("full-name");
+              });
+
+          i.notify["phone-numbers"].connect ((s, p) =>
+              {
+                /* we can't re-use i here due to Vala's implementation */
+                var ind = (Individual) s;
+
+                if (new_phone_fd in ind.phone_numbers)
+                  {
+                    this._changes_pending.remove ("phone-numbers");
+                  }
+              });
+
+          /* the contact list this aggregator is based upon has exactly 1
+           * Tpf.Persona per Individual */
+          Folks.Persona persona = null;
+          foreach (var p in i.personas)
+            {
+              persona = p;
+              break;
+            }
+          assert (persona is Tpf.Persona);
+
+          var phones = new HashSet<PhoneFieldDetails> (
+              (GLib.HashFunc) PhoneFieldDetails.hash,
+              (GLib.EqualFunc) PhoneFieldDetails.equal);
+          phones.add (new_phone_fd);
+
+          /* set the extended info through Telepathy's ContactInfo interface and
+           * wait for it to hit our notification callback above */
+
+          /* setting the extended info on a non-user is invalid for the
+           * Telepathy backend, so this tracks the number of expected errors for
+           * intentionally-invalid property changes */
+          int uncaught_errors = 0;
+
+          if (!i.is_user)
+            uncaught_errors++;
+          try
+            {
+              yield ((Tpf.Persona) persona).change_full_name (new_full_name);
+            }
+          catch (PropertyError e1)
+            {
+              if (!i.is_user)
+                uncaught_errors--;
+            }
+
+          if (!i.is_user)
+            uncaught_errors++;
+          try
+            {
+              yield ((Tpf.Persona) persona).change_phone_numbers (phones);
+            }
+          catch (PropertyError e2)
+            {
+              /* setting the extended info on a non-user is invalid for the
+               * Telepathy backend */
+              if (!i.is_user)
+                uncaught_errors--;
+            }
+
+          if (!i.is_user)
+            {
+              assert (uncaught_errors == 0);
+            }
+        }
+
+      assert (removed.size == 1);
+
+      foreach (var r in removed)
+        {
+          assert (r == null);
+        }
+    }
 }
 
 public int main (string[] args)



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