From 0d22764d076f2eb0fd14629cf0191ed8d62c1b2f Mon Sep 17 00:00:00 2001 From: Travis Reitter Date: Fri, 30 Sep 2011 10:54:15 -0700 Subject: [PATCH] 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 (); + 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> details, + string field_name) + throws PersonaStoreError + { + var info_set = new HashSet (); + + 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 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 _contact_info_set_to_list ( + HashSet info_set) + { + var info_list = new GLib.List (); + 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 _groups_ro; private bool _is_favourite; private string _alias; /* must never be null */ + private string _full_name; /* must never be null */ private HashMultiMap _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 phone_numbers) throws PropertyError + { + yield this._change_details (phone_numbers, + this._phone_numbers, "tel"); + } + + private async void _change_details ( + Set> details, + Set> member_set, + string field_name) + throws PropertyError + { + var tpf_store = this.store as Tpf.PersonaStore; + + if (Folks.PersonaStore.equal_sets (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 ( (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 (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, + ¶ms, + &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 _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 (); } 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 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 ( + (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) -- 2.7.4