Commit 0d22764d authored by Travis Reitter's avatar Travis Reitter
Browse files

Support writing extended info for Telepathy user contacts

Helps: bgo#657602 - Telepathy backend fails to set Personas' phone
numbers from ContactInfo
parent 9000d07c
......@@ -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;
}
}
......@@ -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 =
......@@ -83,6 +85,78 @@ public class Tpf.Persona : Folks.Persona,
set { this.change_avatar.begin (value); } /* not writeable */
}
/**
* {@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.
*
......@@ -302,6 +376,56 @@ public class Tpf.Persona : Folks.Persona,
set { this.change_phone_numbers.begin (value); }
}
/**
* {@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.
*
......@@ -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))
{
......
......@@ -410,6 +410,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,
......@@ -883,6 +890,28 @@ request_contact_info (TpSvcConnectionInterfaceContactInfo *iface,
_return_from_request_contact_info (self, contact, context);
}
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
}
......
......@@ -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);
}
......@@ -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
......
......@@ -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)