From caa357ac95e371785da53e95a698566483014410 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Mon, 13 Jun 2011 20:14:14 +0200 Subject: [PATCH] Prepare TpContact objects for the ContactList If TpConnection has TP_CONNECTION_FEATURE_CONTACT_LIST, also create TpContact objects for the roster. Add tp_connection_dup_contact_list() to get them. --- docs/reference/telepathy-glib-sections.txt | 1 + telepathy-glib/connection-contact-list.c | 330 +++++++++++++++++++++++++++- telepathy-glib/connection-contact-list.h | 1 + telepathy-glib/connection-internal.h | 7 + telepathy-glib/connection.c | 49 ++++ telepathy-glib/signals-marshal.list | 1 + 6 files changed, 386 insertions(+), 3 deletions(-) diff --git a/docs/reference/telepathy-glib-sections.txt b/docs/reference/telepathy-glib-sections.txt index f369dcf..2a7f9a4 100644 --- a/docs/reference/telepathy-glib-sections.txt +++ b/docs/reference/telepathy-glib-sections.txt @@ -3554,6 +3554,7 @@ tp_connection_get_contact_list_state tp_connection_get_contact_list_persists tp_connection_get_can_change_contact_list tp_connection_get_request_uses_message +tp_connection_dup_contact_list tp_connection_request_subscription_async tp_connection_request_subscription_finish tp_connection_authorize_publication_async diff --git a/telepathy-glib/connection-contact-list.c b/telepathy-glib/connection-contact-list.c index bca5554..cf1f498 100644 --- a/telepathy-glib/connection-contact-list.c +++ b/telepathy-glib/connection-contact-list.c @@ -22,21 +22,302 @@ #include #include +#include #include #define DEBUG_FLAG TP_DEBUG_CONNECTION #include "telepathy-glib/debug-internal.h" #include "telepathy-glib/connection-internal.h" +#include "telepathy-glib/contact-internal.h" #include "telepathy-glib/util-internal.h" +typedef struct +{ + GHashTable *changes; + GHashTable *identifiers; + GHashTable *removals; + GPtrArray *new_contacts; +} ContactsChangedItem; + +static ContactsChangedItem * +contacts_changed_item_new (GHashTable *changes, + GHashTable *identifiers, + GHashTable *removals) +{ + ContactsChangedItem *item; + + item = g_slice_new0 (ContactsChangedItem); + item->changes = g_hash_table_ref (changes); + item->identifiers = g_hash_table_ref (identifiers); + item->removals = g_hash_table_ref (removals); + item->new_contacts = g_ptr_array_new_with_free_func (g_object_unref); + + return item; +} + +void +_tp_connection_contacts_changed_item_free (gpointer data) +{ + ContactsChangedItem *item = data; + + tp_clear_pointer (&item->changes, g_hash_table_unref); + tp_clear_pointer (&item->identifiers, g_hash_table_unref); + tp_clear_pointer (&item->removals, g_hash_table_unref); + tp_clear_pointer (&item->new_contacts, g_ptr_array_unref); + g_slice_free (ContactsChangedItem, item); +} + +static void process_queued_contacts_changed (TpConnection *self); + +static void +contacts_changed_head_ready (TpConnection *self) +{ + ContactsChangedItem *item; + GHashTableIter iter; + gpointer key; + GPtrArray *added; + GPtrArray *removed; + guint i; + + item = g_queue_pop_head (self->priv->contacts_changed_queue); + + added = _tp_g_ptr_array_new_full (g_hash_table_size (item->removals), + g_object_unref); + removed = _tp_g_ptr_array_new_full (item->new_contacts->len, + g_object_unref); + + /* Remove contacts from roster, and build a list of contacts really removed */ + g_hash_table_iter_init (&iter, item->removals); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + TpContact *contact; + + contact = g_hash_table_lookup (self->priv->roster, key); + if (contact != NULL) + { + g_ptr_array_add (removed, g_object_ref (contact)); + g_hash_table_remove (self->priv->roster, key); + } + } + + /* Add contacts to roster and build a list of contacts really added */ + for (i = 0; i < item->new_contacts->len; i++) + { + TpContact *contact = g_ptr_array_index (item->new_contacts, i); + + g_ptr_array_add (added, g_object_ref (contact)); + g_hash_table_insert (self->priv->roster, + GUINT_TO_POINTER (tp_contact_get_handle (contact)), + g_object_ref (contact)); + } + + DEBUG ("roster changed: %d added, %d removed", added->len, removed->len); + if (added->len > 0 || removed->len > 0) + g_signal_emit_by_name (self, "contact-list-changed", added, removed); + + g_ptr_array_unref (added); + g_ptr_array_unref (removed); + _tp_connection_contacts_changed_item_free (item); + + process_queued_contacts_changed (self); +} + +static void +new_contacts_upgraded_cb (TpConnection *self, + guint n_contacts, + TpContact * const *contacts, + const GError *error, + gpointer user_data, + GObject *weak_object) +{ + if (error != NULL) + { + DEBUG ("Error upgrading new roster contacts: %s", error->message); + } + + contacts_changed_head_ready (self); +} + +static void +process_queued_contacts_changed (TpConnection *self) +{ + ContactsChangedItem *item; + GHashTableIter iter; + gpointer key, value; + GArray *features; + + item = g_queue_peek_head (self->priv->contacts_changed_queue); + if (item == NULL) + return; + + g_hash_table_iter_init (&iter, item->changes); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + TpContact *contact = g_hash_table_lookup (self->priv->roster, key); + const gchar *identifier = g_hash_table_lookup (item->identifiers, key); + TpHandle handle = GPOINTER_TO_UINT (key); + + /* If the contact is already in the roster, it is only a change of + * subscription states. That's already handled by the TpContact itself so + * we have nothing more to do for it here. */ + if (contact != NULL) + continue; + + contact = tp_simple_client_factory_ensure_contact ( + tp_proxy_get_factory (self), self, handle, identifier); + _tp_contact_set_subscription_states (contact, value); + g_ptr_array_add (item->new_contacts, contact); + } + + if (item->new_contacts->len == 0) + { + contacts_changed_head_ready (self); + return; + } + + features = tp_simple_client_factory_dup_contact_features ( + tp_proxy_get_factory (self), self); + + tp_connection_upgrade_contacts (self, + item->new_contacts->len, (TpContact **) item->new_contacts->pdata, + features->len, (TpContactFeature *) features->data, + new_contacts_upgraded_cb, NULL, NULL, NULL); + + g_array_unref (features); +} + +static void +contacts_changed_cb (TpConnection *self, + GHashTable *changes, + GHashTable *identifiers, + GHashTable *removals, + gpointer user_data, + GObject *weak_object) +{ + ContactsChangedItem *item; + + /* Ignore ContactsChanged signal if we didn't receive initial roster yet */ + if (!self->priv->roster_fetched) + return; + + /* We need a queue to make sure we don't reorder signals if we get a 2nd + * ContactsChanged signal before the previous one finished preparing TpContact + * objects. */ + item = contacts_changed_item_new (changes, identifiers, removals); + g_queue_push_tail (self->priv->contacts_changed_queue, item); + + /* If this is the only item in the queue, we can process it right away */ + if (self->priv->contacts_changed_queue->length == 1) + process_queued_contacts_changed (self); +} + +static void +got_contact_list_attributes_cb (TpConnection *self, + GHashTable *attributes, + const GError *error, + gpointer user_data, + GObject *weak_object) +{ + GSimpleAsyncResult *result = (GSimpleAsyncResult *) weak_object; + GArray *features = user_data; + GHashTableIter iter; + gpointer key, value; + + if (error != NULL) + { + self->priv->contact_list_state = TP_CONTACT_LIST_STATE_FAILURE; + g_object_notify ((GObject *) self, "contact-list-state"); + + if (result != NULL) + g_simple_async_result_set_from_error (result, error); + + goto OUT; + } + + DEBUG ("roster fetched with %d contacts", g_hash_table_size (attributes)); + self->priv->roster_fetched = TRUE; + + g_hash_table_iter_init (&iter, attributes); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + TpHandle handle = GPOINTER_TO_UINT (key); + const gchar *id = tp_asv_get_string (value, + TP_TOKEN_CONNECTION_CONTACT_ID); + TpContact *contact; + GError *e = NULL; + + contact = tp_simple_client_factory_ensure_contact ( + tp_proxy_get_factory (self), self, handle, id); + if (!_tp_contact_set_attributes (contact, value, + features->len, (TpContactFeature *) features->data, &e)) + { + DEBUG ("Error setting contact attributes: %s", e->message); + g_clear_error (&e); + } + + /* Give the contact ref to the table */ + g_hash_table_insert (self->priv->roster, key, contact); + } + + self->priv->contact_list_state = TP_CONTACT_LIST_STATE_SUCCESS; + g_object_notify ((GObject *) self, "contact-list-state"); + +OUT: + if (result != NULL) + { + g_simple_async_result_complete (result); + g_object_unref (result); + } +} + +static void +prepare_roster (TpConnection *self, + GSimpleAsyncResult *result) +{ + GArray *features; + const gchar **supported_interfaces; + + DEBUG ("CM has the roster, fetch it now"); + + tp_cli_connection_interface_contact_list_connect_to_contacts_changed_with_id ( + self, contacts_changed_cb, NULL, NULL, NULL, NULL); + + features = tp_simple_client_factory_dup_contact_features ( + tp_proxy_get_factory (self), self); + + supported_interfaces = _tp_contacts_bind_to_signals (self, features->len, + (TpContactFeature *) features->data); + + tp_connection_get_contact_list_attributes (self, -1, + supported_interfaces, TRUE, + got_contact_list_attributes_cb, + features, (GDestroyNotify) g_array_unref, + result ? g_object_ref (result) : NULL); + + g_free (supported_interfaces); +} + static void contact_list_state_changed_cb (TpConnection *self, guint state, gpointer user_data, GObject *weak_object) { + /* Ignore StateChanged if we didn't had the initial state or if + * duplicate signal */ + if (!self->priv->contact_list_properties_fetched || + state == self->priv->contact_list_state) + return; + DEBUG ("contact list state changed: %d", state); + /* If state goes to success, delay notification until roster is ready */ + if (state == TP_CONTACT_LIST_STATE_SUCCESS) + { + prepare_roster (self, NULL); + return; + } + self->priv->contact_list_state = state; g_object_notify ((GObject *) self, "contact-list-state"); } @@ -52,6 +333,8 @@ prepare_contact_list_cb (TpProxy *proxy, GSimpleAsyncResult *result = user_data; gboolean valid; + self->priv->contact_list_properties_fetched = TRUE; + if (error != NULL) { DEBUG ("Error preparing ContactList properties: %s", error->message); @@ -94,6 +377,13 @@ prepare_contact_list_cb (TpProxy *proxy, DEBUG ("Got contact list properties; state=%d", self->priv->contact_list_state); + /* If the CM has the contact list, prepare it right away */ + if (self->priv->contact_list_state == TP_CONTACT_LIST_STATE_SUCCESS) + { + prepare_roster (self, result); + return; + } + OUT: g_simple_async_result_complete (result); } @@ -106,8 +396,8 @@ void _tp_connection_prepare_contact_list_async (TpProxy *proxy, TpConnection *self = (TpConnection *) proxy; GSimpleAsyncResult *result; - tp_cli_connection_interface_contact_list_connect_to_contact_list_state_changed ( - self, contact_list_state_changed_cb, NULL, NULL, NULL, NULL); + tp_cli_connection_interface_contact_list_connect_to_contact_list_state_changed + (self, contact_list_state_changed_cb, NULL, NULL, NULL, NULL); result = g_simple_async_result_new ((GObject *) self, callback, user_data, _tp_connection_prepare_contact_list_async); @@ -313,7 +603,10 @@ _tp_connection_prepare_contact_groups_async (TpProxy *proxy, * "contact-list" feature. * * When this feature is prepared, the contact list properties of the Connection - * has been retrieved. + * has been retrieved, and all #TpContact objects has been prepared with the + * desired features. See tp_connection_dup_contact_list() to get the list of + * contacts, and tp_simple_client_factory_add_contact_features() to define which + * features needs to be prepared on them. * * One can ask for a feature to be prepared using the * tp_proxy_prepare_async() function, and waiting for it to callback. @@ -399,6 +692,37 @@ tp_connection_get_request_uses_message (TpConnection *self) return self->priv->request_uses_message; } +/** + * tp_connection_dup_contact_list: + * @self: a #TpConnection + * + * The list of contacts associated with the user, but MAY contain other + * contacts. This list does not contain every visible contact: for instance, + * contacts seen in XMPP or IRC chatrooms does not appear here. Blocked contacts + * does not appear here, unless they still have a non-No subscribe or publish + * attribute for some reason. + * + * It is guaranteed that all those contacts have the desired features prepared. + * See tp_simple_client_factory_add_contact_features() to define which features + * needs to be prepared. + * + * For this method to be valid, you must first call tp_proxy_prepare_async() + * with the feature %TP_CONNECTION_FEATURE_CONTACT_LIST and verify the + * #TpConnection:contact-list-state is set to %TP_CONTACT_LIST_STATE_SUCCESS. + * + * Returns: (transfer container) (type GLib.PtrArray) (element-type TelepathyGLib.Contact): + * a new #GPtrArray of #TpContact. Use g_ptr_array_unref() when done. + * + * Since: 0.UNRELEASED + */ +GPtrArray * +tp_connection_dup_contact_list (TpConnection *self) +{ + g_return_val_if_fail (TP_IS_CONNECTION (self), NULL); + + return _tp_contacts_from_values (self->priv->roster); +} + static void generic_callback (TpConnection *self, const GError *error, diff --git a/telepathy-glib/connection-contact-list.h b/telepathy-glib/connection-contact-list.h index 4908ee8..8f74cbd 100644 --- a/telepathy-glib/connection-contact-list.h +++ b/telepathy-glib/connection-contact-list.h @@ -35,6 +35,7 @@ TpContactListState tp_connection_get_contact_list_state (TpConnection *self); gboolean tp_connection_get_contact_list_persists (TpConnection *self); gboolean tp_connection_get_can_change_contact_list (TpConnection *self); gboolean tp_connection_get_request_uses_message (TpConnection *self); +GPtrArray *tp_connection_dup_contact_list (TpConnection *self); void tp_connection_request_subscription_async (TpConnection *self, guint n_contacts, diff --git a/telepathy-glib/connection-internal.h b/telepathy-glib/connection-internal.h index c4d9ed2..fe064bc 100644 --- a/telepathy-glib/connection-internal.h +++ b/telepathy-glib/connection-internal.h @@ -77,6 +77,12 @@ struct _TpConnectionPrivate { gboolean contact_list_persists; gboolean can_change_contact_list; gboolean request_uses_message; + /* TpHandle => ref to TpContact */ + GHashTable *roster; + /* Queue of owned ContactsChangedItem */ + GQueue *contacts_changed_queue; + gboolean roster_fetched; + gboolean contact_list_properties_fetched; /* ContactGroups properties */ gboolean disjoint_groups; @@ -144,6 +150,7 @@ void _tp_connection_prepare_contact_groups_async (TpProxy *proxy, const TpProxyFeature *feature, GAsyncReadyCallback callback, gpointer user_data); +void _tp_connection_contacts_changed_item_free (gpointer data); G_END_DECLS diff --git a/telepathy-glib/connection.c b/telepathy-glib/connection.c index f8e00fd..bf47ca9 100644 --- a/telepathy-glib/connection.c +++ b/telepathy-glib/connection.c @@ -289,6 +289,7 @@ enum { SIGNAL_GROUPS_CREATED, SIGNAL_GROUPS_REMOVED, SIGNAL_GROUP_RENAMED, + SIGNAL_CONTACT_LIST_CHANGED, N_SIGNALS }; @@ -1187,6 +1188,19 @@ tp_connection_invalidated (TpConnection *self) tp_proxy_pending_call_cancel (self->priv->introspection_call); self->priv->introspection_call = NULL; } + + /* Drop the ref we have on all roster contacts, this is to break the refcycle + * we have between TpConnection and TpContact, otherwise self would never + * run dispose. + * Note that invalidated is also called from dispose, so self->priv->roster + * could already be NULL. + * + * FIXME: When we decide to break tp-glib API/guarantees, we should stop + * TpContact taking a strong ref on its TpConnection and force user to keep + * a ref on the TpConnection to use its TpContact, this would avoid the + * refcycle completely. */ + if (self->priv->roster != NULL) + g_hash_table_remove_all (self->priv->roster); } static gboolean @@ -1357,6 +1371,9 @@ tp_connection_init (TpConnection *self) self->priv->interests = tp_intset_new (); self->priv->contact_groups = g_ptr_array_new_with_free_func (g_free); g_ptr_array_add (self->priv->contact_groups, NULL); + self->priv->roster = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, g_object_unref); + self->priv->contacts_changed_queue = g_queue_new (); } static void @@ -1436,6 +1453,10 @@ tp_connection_dispose (GObject *object) } tp_clear_pointer (&self->priv->contact_groups, g_ptr_array_unref); + tp_clear_pointer (&self->priv->roster, g_hash_table_unref); + g_queue_foreach (self->priv->contacts_changed_queue, + (GFunc) _tp_connection_contacts_changed_item_free, NULL); + tp_clear_pointer (&self->priv->contacts_changed_queue, g_queue_free); if (self->priv->contacts != NULL) { @@ -2049,6 +2070,34 @@ tp_connection_class_init (TpConnectionClass *klass) NULL, NULL, _tp_marshal_VOID__STRING_STRING, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); + /** + * TpConnection::contact-list-changed: + * @self: a #TpConnection + * @added: (type GLib.PtrArray) (element-type TelepathyGLib.Contact): + * a #GPtrArray of #TpContact added to contacts list + * @removed: (type GLib.PtrArray) (element-type TelepathyGLib.Contact): + * a #GPtrArray of #TpContact removed from contacts list + * + * Notify of changes in the list of contacts as returned by + * tp_connection_dup_contact_list(). It is guaranteed that all contacts have + * desired features prepared. See + * tp_simple_client_factory_add_contact_features() to define which features + * needs to be prepared. + * + * For this signal to be emitted, you must first call + * tp_proxy_prepare_async() with the feature + * %TP_CONNECTION_FEATURE_CONTACT_LIST. + * + * Since: 0.UNRELEASED + */ + signals[SIGNAL_CONTACT_LIST_CHANGED] = g_signal_new ( + "contact-list-changed", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + _tp_marshal_VOID__BOXED_BOXED, + G_TYPE_NONE, 2, G_TYPE_PTR_ARRAY, G_TYPE_PTR_ARRAY); } /** diff --git a/telepathy-glib/signals-marshal.list b/telepathy-glib/signals-marshal.list index 7686599..1e1ad0e 100644 --- a/telepathy-glib/signals-marshal.list +++ b/telepathy-glib/signals-marshal.list @@ -1,4 +1,5 @@ VOID:BOXED +VOID:BOXED,BOXED VOID:BOXED,UINT,INT,STRING VOID:INT,UINT,STRING VOID:OBJECT,BOOLEAN -- 1.7.4.1