diff options
author | Xavier Claessens <xavier.claessens@collabora.co.uk> | 2012-06-09 07:49:36 +0200 |
---|---|---|
committer | Xavier Claessens <xavier.claessens@collabora.co.uk> | 2012-09-13 10:54:40 +0200 |
commit | 37c2ff7347415907ef4025643ba9e7b51aac240c (patch) | |
tree | 7785ec7e34aa5c4acca3a3554974ba7336a88b3c | |
parent | 0acfcbd3b79a15a6f3004ce3790f372246353f84 (diff) |
Add TpAvatarsMixin
This is a helper to implement the new Avatars iface with CM-side caching
-rw-r--r-- | docs/reference/telepathy-glib-docs.sgml | 1 | ||||
-rw-r--r-- | docs/reference/telepathy-glib-sections.txt | 21 | ||||
-rw-r--r-- | telepathy-glib/Makefile.am | 2 | ||||
-rw-r--r-- | telepathy-glib/avatars-mixin.c | 878 | ||||
-rw-r--r-- | telepathy-glib/avatars-mixin.h | 81 | ||||
-rw-r--r-- | telepathy-glib/base-connection-internal.h | 6 | ||||
-rw-r--r-- | telepathy-glib/base-connection.c | 24 | ||||
-rw-r--r-- | telepathy-glib/connection-internal.h | 2 | ||||
-rw-r--r-- | telepathy-glib/connection.c | 12 | ||||
-rw-r--r-- | telepathy-glib/telepathy-glib.h | 1 |
10 files changed, 1014 insertions, 14 deletions
diff --git a/docs/reference/telepathy-glib-docs.sgml b/docs/reference/telepathy-glib-docs.sgml index 5e75db4e9..0dbed5b6c 100644 --- a/docs/reference/telepathy-glib-docs.sgml +++ b/docs/reference/telepathy-glib-docs.sgml @@ -75,6 +75,7 @@ <xi:include href="xml/channel-manager.xml"/> <xi:include href="xml/base-contact-list.xml"/> <xi:include href="xml/contacts-mixin.xml"/> + <xi:include href="xml/avatars-mixin.xml"/> <xi:include href="xml/dbus-properties-mixin.xml"/> <xi:include href="xml/exportable-channel.xml"/> <xi:include href="xml/base-channel.xml"/> diff --git a/docs/reference/telepathy-glib-sections.txt b/docs/reference/telepathy-glib-sections.txt index 1a549229e..4267e5373 100644 --- a/docs/reference/telepathy-glib-sections.txt +++ b/docs/reference/telepathy-glib-sections.txt @@ -1701,6 +1701,27 @@ TP_CONTACTS_MIXIN <SECTION> <INCLUDE>telepathy-glib/telepathy-glib.h</INCLUDE> +<FILE>avatars-mixin</FILE> +TpAvatarsMixin +TpAvatarsMixinClearAvatarFunc +TpAvatarsMixinRequestAvatarsFunc +TpAvatarsMixinSetAvatarFunc +tp_avatars_mixin_avatar_changed +tp_avatars_mixin_avatar_retrieved +tp_avatars_mixin_drop_avatar +tp_avatars_mixin_finalize +tp_avatars_mixin_iface_init +tp_avatars_mixin_init +tp_avatars_mixin_init_dbus_properties +tp_avatars_mixin_register_with_contacts_mixin +<SUBSECTION Private> +TpAvatarsMixinPrivate +<SUBSECTION Standard> +TP_AVATARS_MIXIN +</SECTION> + +<SECTION> +<INCLUDE>telepathy-glib/telepathy-glib.h</INCLUDE> <FILE>presence-mixin</FILE> TpPresenceStatusOptionalArgumentSpec TpPresenceStatusSpec diff --git a/telepathy-glib/Makefile.am b/telepathy-glib/Makefile.am index d43ab4c36..00e7bebc5 100644 --- a/telepathy-glib/Makefile.am +++ b/telepathy-glib/Makefile.am @@ -46,6 +46,7 @@ tpginclude_HEADERS = \ account-request.h \ automatic-client-factory.h \ add-dispatch-operation-context.h \ + avatars-mixin.h \ base-call-channel.h \ base-call-content.h \ base-call-stream.h \ @@ -219,6 +220,7 @@ libtelepathy_glib_main_internal_la_SOURCES = \ automatic-client-factory.c \ add-dispatch-operation-context-internal.h \ add-dispatch-operation-context.c \ + avatars-mixin.c \ base-call-channel.c \ base-call-content.c \ base-call-stream.c \ diff --git a/telepathy-glib/avatars-mixin.c b/telepathy-glib/avatars-mixin.c new file mode 100644 index 000000000..bcc4bf65f --- /dev/null +++ b/telepathy-glib/avatars-mixin.c @@ -0,0 +1,878 @@ +/* + * avatars-mixin.c - Source for TpAvatarsMixin + * Copyright (C) 2012 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * SECTION:avatars-mixin + * @title: TpAvatarsMixin + * @short_description: a mixin implementation of the Avatars connection + * interface + * + * This mixin can be added to a #TpBaseConnection subclass to implement the + * Avatars interface. + * + * To use the avatars mixin, include a #TpAvatarsMixin somewhere in your + * instance structure, and call tp_avatars_mixin_init() from your init function + * or constructor, and tp_avatars_mixin_finalize() from your finalize function. + * + * <section> + * <title>Implementing Avatars</title> + * <para> + * You can implement #TpSvcConnectionInterfaceAvatars as follows: + * <itemizedlist> + * <listitem> + * <para>use the #TpContactsMixin and + * <link linkend="telepathy-glib-dbus-properties-mixin">TpDBusPropertiesMixin</link>;</para> + * </listitem> + * <listitem> + * <para>pass tp_avatars_mixin_iface_init() as an + * argument to G_IMPLEMENT_INTERFACE(), like so: + * </para> + * |[ + * G_DEFINE_TYPE_WITH_CODE (MyConnection, my_connection, + * TP_TYPE_BASE_CONNECTION, + * // ... + * G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_AVATARS, + * tp_avatars_mixin_iface_init); + * // ... + * ) + * ]| + * </listitem> + * <listitem> + * <para> + * call tp_avatars_mixin_init_dbus_properties() in the + * #GTypeInfo class_init function; + * </para> + * </listitem> + * <listitem> + * <para> + * call tp_avatars_mixin_register_with_contacts_mixin() + * in the #GObjectClass constructed function. + * </para> + * </listitem> + * </itemizedlist> + * </para> + * </section> + * + * Since: 0.UNRELEASED + */ + +/** + * TpAvatarsMixinSetAvatarFunc: + * @object: An instance of a #TpBaseConnection subclass implementing the avatars + * interface with this mixin + * @avatar: An array containing the avatar data to set + * @mime_type: The MIME Type of @avatar is known + * @error: Used to return a Telepathy D-Bus error if %FALSE is returned + * + * Signature of a callback to be used to set the user's avatar. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 0.UNRELEASED + */ + +/** + * TpAvatarsMixinClearAvatarFunc: + * @object: An instance of a #TpBaseConnection subclass implementing the avatars + * interface with this mixin + * @error: Used to return a Telepathy D-Bus error if %FALSE is returned + * + * Signature of a callback to be used to clear the user's avatar. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 0.UNRELEASED + */ + +/** + * TpAvatarsMixinRequestAvatarsFunc: + * @object: An instance of a #TpBaseConnection subclass implementing the avatars + * interface with this mixin + * @contacts: An array of #TpHandle for the contacts to request avatars for + * @error: Used to return a Telepathy D-Bus error if %FALSE is returned + * + * Signature of a callback to be used to start avatar request for the given + * contacts. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 0.UNRELEASED + */ + +/** + * TpAvatarsMixin: + * + * Structure to be included in the instance structure of objects that + * use this mixin. Initialize it with tp_avatars_mixin_init(). + * + * There are no public fields. + * Since: 0.UNRELEASED + */ + +#include "config.h" + +#include <telepathy-glib/avatars-mixin.h> + +#include <dbus/dbus-glib.h> +#include <string.h> +#include <errno.h> + +#include <telepathy-glib/base-connection.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/dbus-properties-mixin.h> +#include <telepathy-glib/enums.h> +#include <telepathy-glib/errors.h> +#include <telepathy-glib/gtypes.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/connection-internal.h> +#include <telepathy-glib/contacts-mixin.h> +#include <telepathy-glib/svc-connection.h> + +#define DEBUG_FLAG TP_DEBUG_CONTACTS +#include "debug-internal.h" +#include "base-connection-internal.h" + +/* TYPE MACROS */ +#define TP_AVATARS_MIXIN_OFFSET_QUARK (tp_avatars_mixin_get_offset_quark ()) +#define TP_AVATARS_MIXIN_OFFSET(o) \ + tp_mixin_instance_get_offset (o, TP_AVATARS_MIXIN_OFFSET_QUARK) +#define TP_AVATARS_MIXIN(o) \ + ((TpAvatarsMixin *) tp_mixin_offset_cast (o, TP_AVATARS_MIXIN_OFFSET (o))) + +struct _TpAvatarsMixinPrivate +{ + /* Virtual functions */ + TpAvatarsMixinSetAvatarFunc set_avatar; + TpAvatarsMixinClearAvatarFunc clear_avatar; + TpAvatarsMixinRequestAvatarsFunc request_avatars; + + /* Immutable properties */ + gboolean avatar_persists; + TpAvatarRequirements *requirements; + + /* TpHandle -> owned AvatarData + * Contacts whose avatar is known and cached. If the contact is known to have + * no avatar, value is NULL. */ + GHashTable *avatars; + + /* This is the set of contacts whose avatar needs to be requested but no + * client is currently interested. This is used to request them all once a + * client claims interest. */ + TpIntset *needs_request; +}; + +typedef struct +{ + gchar *token; + gchar *uri; +} AvatarData; + +static AvatarData * +avatar_data_new (const gchar *token, + GFile *file) +{ + AvatarData *a; + + a = g_slice_new0 (AvatarData); + + a->token = g_strdup (token); + a->uri = g_file_get_uri (file); + + return a; +} + +static void +avatar_data_free (AvatarData *a) +{ + if (a != NULL) + { + g_free (a->token); + g_free (a->uri); + g_slice_free (AvatarData, a); + } +} + +static GQuark +tp_avatars_mixin_get_offset_quark (void) +{ + static GQuark offset_quark = 0; + if (!offset_quark) + offset_quark = g_quark_from_static_string ("TpAvatarsMixinOffsetQuark"); + return offset_quark; +} + +static void +clients_interested_cb (TpBaseConnection *connection, + gchar *token, + gpointer user_data) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (connection); + GArray *handles; + + DEBUG ("A client is now interested in avatars"); + + if (tp_intset_is_empty (self->priv->needs_request)) + return; + + handles = tp_intset_to_array (self->priv->needs_request); + self->priv->request_avatars ((GObject *) connection, handles, NULL); + + g_array_unref (handles); + tp_intset_clear (self->priv->needs_request); +} + +/** + * tp_avatars_mixin_init: (skip) + * @object: An instance of the implementation that uses this mixin + * @offset: The byte offset of the TpAvatarsMixin within the object structure + * @set_avatar: a #TpAvatarsMixinSetAvatarFunc + * @clear_avatar: a #TpAvatarsMixinClearAvatarFunc + * @request_avatars: a #TpAvatarsMixinRequestAvatarsFunc + * @avatar_persists: whether or not user's avatar is stored on server + * @requirements: a #TpAvatarRequirements + * + * Initialize the avatars mixin. Should be called from the implementation's + * instance init function like so: + * + * <informalexample><programlisting> + * tp_avatars_mixin_init ((GObject *) self, + * G_STRUCT_OFFSET (SomeObject, avatars_mixin), + * _set_avatar, _clear_avatar, _request_avatars, + * TRUE, requirements); + * </programlisting></informalexample> + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_init (GObject *object, + glong offset, + TpAvatarsMixinSetAvatarFunc set_avatar, + TpAvatarsMixinClearAvatarFunc clear_avatar, + TpAvatarsMixinRequestAvatarsFunc request_avatars, + gboolean avatar_persists, + TpAvatarRequirements *requirements) +{ + TpBaseConnection *base = (TpBaseConnection *) object; + TpAvatarsMixin *self; + + g_return_if_fail (G_IS_OBJECT (object)); + g_return_if_fail (set_avatar != NULL); + g_return_if_fail (clear_avatar != NULL); + g_return_if_fail (request_avatars != NULL); + g_return_if_fail (requirements != NULL); + + g_type_set_qdata (G_OBJECT_TYPE (object), + TP_AVATARS_MIXIN_OFFSET_QUARK, + GINT_TO_POINTER (offset)); + + self = TP_AVATARS_MIXIN (object); + self->priv = g_slice_new0 (TpAvatarsMixinPrivate); + + self->priv->set_avatar = set_avatar; + self->priv->clear_avatar = clear_avatar; + self->priv->request_avatars = request_avatars; + + self->priv->avatar_persists = avatar_persists; + self->priv->requirements = tp_avatar_requirements_copy (requirements); + + self->priv->avatars = g_hash_table_new_full (NULL, NULL, + NULL, (GDestroyNotify) avatar_data_free); + self->priv->needs_request = tp_intset_new (); + + tp_base_connection_add_possible_client_interest (base, + TP_IFACE_QUARK_CONNECTION_INTERFACE_AVATARS); + g_signal_connect (object, + "clients-interested::" TP_IFACE_CONNECTION_INTERFACE_AVATARS, + G_CALLBACK (clients_interested_cb), NULL); +} + +/** + * tp_avatars_mixin_finalize: (skip) + * @object: An object with this mixin. + * + * Free resources held by the avatars mixin. + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_finalize (GObject *object) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + + tp_avatar_requirements_destroy (self->priv->requirements); + g_hash_table_unref (self->priv->avatars); + tp_intset_destroy (self->priv->needs_request); + + g_slice_free (TpAvatarsMixinPrivate, self->priv); +} + +enum { + MIXIN_DP_AVATAR_PERSISTS, + MIXIN_DP_SUPPORTED_AVATAR_MIME_TYPES, + MIXIN_DP_MINIMUM_AVATAR_HEIGHT, + MIXIN_DP_MINIMUM_AVATAR_WIDTH, + MIXIN_DP_RECOMMENDED_AVATAR_HEIGHT, + MIXIN_DP_RECOMMENDED_AVATAR_WIDTH, + MIXIN_DP_MAXIMUM_AVATAR_HEIGHT, + MIXIN_DP_MAXIMUM_AVATAR_WIDTH, + MIXIN_DP_MAXIMUM_AVATAR_BYTES, + NUM_MIXIN_DBUS_PROPERTIES +}; + +static TpDBusPropertiesMixinPropImpl known_avatars_props[] = { + { "AvatarPersists", + GUINT_TO_POINTER (MIXIN_DP_AVATAR_PERSISTS), NULL }, + { "SupportedAvatarMIMETypes", + GUINT_TO_POINTER (MIXIN_DP_SUPPORTED_AVATAR_MIME_TYPES), NULL }, + { "MinimumAvatarHeight", + GUINT_TO_POINTER (MIXIN_DP_MINIMUM_AVATAR_HEIGHT), NULL }, + { "MinimumAvatarWidth", + GUINT_TO_POINTER (MIXIN_DP_MINIMUM_AVATAR_WIDTH), NULL }, + { "RecommendedAvatarHeight", + GUINT_TO_POINTER (MIXIN_DP_RECOMMENDED_AVATAR_HEIGHT), NULL }, + { "RecommendedAvatarWidth", + GUINT_TO_POINTER (MIXIN_DP_RECOMMENDED_AVATAR_WIDTH), NULL }, + { "MaximumAvatarHeight", + GUINT_TO_POINTER (MIXIN_DP_MAXIMUM_AVATAR_HEIGHT), NULL }, + { "MaximumAvatarWidth", + GUINT_TO_POINTER (MIXIN_DP_MAXIMUM_AVATAR_WIDTH), NULL }, + { "MaximumAvatarBytes", + GUINT_TO_POINTER (MIXIN_DP_MAXIMUM_AVATAR_BYTES), NULL }, + { NULL } +}; + +static void +tp_avatars_mixin_get_dbus_property (GObject *object, + GQuark interface, + GQuark name, + GValue *value, + gpointer user_data) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + + switch (GPOINTER_TO_UINT (user_data)) + { + case MIXIN_DP_AVATAR_PERSISTS: + g_value_set_boolean (value, self->priv->avatar_persists); + break; + + case MIXIN_DP_SUPPORTED_AVATAR_MIME_TYPES: + g_value_set_boxed (value, self->priv->requirements->supported_mime_types); + break; + + case MIXIN_DP_MINIMUM_AVATAR_HEIGHT: + g_value_set_uint (value, self->priv->requirements->minimum_height); + break; + + case MIXIN_DP_MINIMUM_AVATAR_WIDTH: + g_value_set_uint (value, self->priv->requirements->minimum_width); + break; + + case MIXIN_DP_RECOMMENDED_AVATAR_HEIGHT: + g_value_set_uint (value, self->priv->requirements->recommended_height); + break; + + case MIXIN_DP_RECOMMENDED_AVATAR_WIDTH: + g_value_set_uint (value, self->priv->requirements->recommended_width); + break; + + case MIXIN_DP_MAXIMUM_AVATAR_HEIGHT: + g_value_set_uint (value, self->priv->requirements->maximum_height); + break; + + case MIXIN_DP_MAXIMUM_AVATAR_WIDTH: + g_value_set_uint (value, self->priv->requirements->maximum_width); + break; + + case MIXIN_DP_MAXIMUM_AVATAR_BYTES: + g_value_set_uint (value, self->priv->requirements->maximum_bytes); + break; + + default: + g_assert_not_reached (); + } +} + +/** + * tp_avatars_mixin_init_dbus_properties: (skip) + * @klass: The class of an object with this mixin + * + * Set up #TpDBusPropertiesMixinClass to use this mixin's implementation of + * the Avatars interface's properties. + * + * This automatically sets up a list of the supported properties for the + * Avatars interface. + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_init_dbus_properties (GObjectClass *klass) +{ + tp_dbus_properties_mixin_implement_interface (klass, + TP_IFACE_QUARK_CONNECTION_INTERFACE_AVATARS, + tp_avatars_mixin_get_dbus_property, + NULL, known_avatars_props); +} + +static void +tp_avatars_mixin_refresh_avatars (TpSvcConnectionInterfaceAvatars *iface, + const GArray *contacts, + DBusGMethodInvocation *context) +{ + GObject *object = (GObject *) iface; + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + TpBaseConnection *conn = TP_BASE_CONNECTION (object); + TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + GArray *real_contacts; + guint i; + GError *error = NULL; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (conn, context); + + if (!tp_handles_are_valid (contact_repo, contacts, FALSE, &error)) + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + return; + } + + real_contacts = g_array_sized_new (FALSE, FALSE, sizeof (TpHandle), + contacts->len); + + /* Keep only contacts for which we don't already have the avatar image */ + for (i = 0; i < contacts->len; i++) + { + TpHandle contact = g_array_index (contacts, TpHandle, i); + + if (g_hash_table_contains (self->priv->avatars, + GUINT_TO_POINTER (contact))) + continue; + + g_array_append_val (real_contacts, contact); + } + + if (real_contacts->len > 0 && + !self->priv->request_avatars (object, real_contacts, &error)) + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + } + else + { + tp_svc_connection_interface_avatars_return_from_refresh_avatars (context); + } + + g_array_unref (real_contacts); +} + +static void +tp_avatars_mixin_set_avatar (TpSvcConnectionInterfaceAvatars *iface, + const GArray *avatar, + const gchar *mime_type, + DBusGMethodInvocation *context) +{ + GObject *object = (GObject *) iface; + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + TpBaseConnection *conn = TP_BASE_CONNECTION (object); + GError *error = NULL; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (conn, context); + + if (!self->priv->set_avatar (object, avatar, mime_type, &error)) + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + return; + } + + tp_svc_connection_interface_avatars_return_from_set_avatar (context); +} + +static void +tp_avatars_mixin_clear_avatar (TpSvcConnectionInterfaceAvatars *iface, + DBusGMethodInvocation *context) +{ + GObject *object = (GObject *) iface; + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + TpBaseConnection *conn = TP_BASE_CONNECTION (object); + GError *error = NULL; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (conn, context); + + if (!self->priv->clear_avatar (object, &error)) + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + return; + } + + tp_svc_connection_interface_avatars_return_from_clear_avatar (context); +} + +/** + * tp_avatars_mixin_iface_init: (skip) + * @g_iface: A pointer to the #TpSvcConnectionInterfaceAvatarsClass in + * an object class + * @iface_data: Ignored + * + * Fill in the vtable entries needed to implement the avatars interface + * using this mixin. This function should usually be called via + * G_IMPLEMENT_INTERFACE. + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpSvcConnectionInterfaceAvatarsClass *klass = g_iface; + +#define IMPLEMENT(x) tp_svc_connection_interface_avatars_implement_##x\ + (klass, tp_avatars_mixin_##x) + IMPLEMENT(refresh_avatars); + IMPLEMENT(set_avatar); + IMPLEMENT(clear_avatar); +#undef IMPLEMENT +} + +static void +tp_avatars_mixin_fill_contact_attributes (GObject *object, + const GArray *contacts, + GHashTable *attributes) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + guint i; + + for (i = 0; i < contacts->len; i++) + { + TpHandle contact = g_array_index (contacts, guint, i); + AvatarData *a; + + /* If we don't know the avatar, omit it from reply */ + if (!g_hash_table_lookup_extended (self->priv->avatars, + GUINT_TO_POINTER (contact), NULL, (gpointer *) &a)) + continue; + + tp_contacts_mixin_set_contact_attribute (attributes, contact, + TP_TOKEN_CONNECTION_INTERFACE_AVATARS_AVATAR, + tp_g_value_slice_new_string ((a != NULL) ? a->uri : "")); + } +} + +/** + * tp_avatars_mixin_register_with_contacts_mixin: (skip) + * @object: An instance of the implementation that uses both the Contacts + * mixin and this mixin + * + * Register the Avatars interface with the Contacts interface to make it + * inspectable. The Contacts mixin should be initialized before this function + * is called + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_register_with_contacts_mixin (GObject *object) +{ + tp_contacts_mixin_add_contact_attributes_iface (object, + TP_IFACE_CONNECTION_INTERFACE_AVATARS, + tp_avatars_mixin_fill_contact_attributes); +} + +static gboolean +build_avatar_filename (GObject *object, + const gchar *avatar_token, + gboolean create_dir, + gchar **ret_filename, + gchar **ret_mime_filename) +{ + TpBaseConnection *base = (TpBaseConnection *) object; + gchar *dir; + gchar *token_escaped; + gboolean success = TRUE; + + token_escaped = tp_escape_as_identifier (avatar_token); + dir = g_build_filename (g_get_user_cache_dir (), "telepathy", "avatars", + _tp_base_connection_get_cm_name (base), + _tp_base_connection_get_protocol_name (base), NULL); + + if (create_dir) + { + if (g_mkdir_with_parents (dir, 0700) == -1) + { + DEBUG ("Error creating avatar cache dir: %s", g_strerror (errno)); + success = FALSE; + goto out; + } + } + + if (ret_filename != NULL) + *ret_filename = g_strconcat (dir, G_DIR_SEPARATOR_S, token_escaped, NULL); + + if (ret_mime_filename != NULL) + *ret_mime_filename = g_strconcat (dir, G_DIR_SEPARATOR_S, token_escaped, + ".mime", NULL); + +out: + g_free (dir); + g_free (token_escaped); + + return success; +} + +static GFile * +avatar_cache_save (GObject *object, + const gchar *avatar_token, + const GArray *avatar, + const gchar *mime_type) +{ + gchar *filename; + gchar *mime_filename; + GFile *file = NULL; + GError *error = NULL; + + if (!build_avatar_filename (object, avatar_token, TRUE, &filename, + &mime_filename)) + return NULL; + + if (!g_file_set_contents (filename, avatar->data, avatar->len, &error)) + { + DEBUG ("Failed to store avatar in cache (%s): %s", filename, + error ? error->message : "No error message"); + g_clear_error (&error); + goto OUT; + } + + if (!g_file_set_contents (mime_filename, mime_type, -1, &error)) + { + DEBUG ("Failed to store MIME type in cache (%s): %s", mime_filename, + error ? error->message : "No error message"); + g_clear_error (&error); + goto OUT; + } + + DEBUG ("Avatar stored in cache: %s, %s", filename, mime_type); + + file = g_file_new_for_path (filename); + +OUT: + g_free (filename); + g_free (mime_filename); + + return file; +} + +static GFile * +avatar_cache_lookup (GObject *object, + const gchar *avatar_token) +{ + gchar *filename; + GFile *file = NULL; + + if (!build_avatar_filename (object, avatar_token, + FALSE, &filename, NULL)) + return NULL; + + if (g_file_test (filename, G_FILE_TEST_EXISTS)) + { + DEBUG ("Avatar found in cache: %s", filename); + file = g_file_new_for_path (filename); + } + + g_free (filename); + + return file; +} + +static void +update_avatar_take_data (GObject *object, + TpHandle contact, + AvatarData *a) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + GHashTable *table; + + DEBUG ("Update avatar for handle %u: %s", + contact, (a != NULL) ? a->uri : "no avatar"); + + g_hash_table_insert (self->priv->avatars, GUINT_TO_POINTER (contact), a); + tp_intset_remove (self->priv->needs_request, contact); + + /* FIXME: Could queue to aggregate signals */ + table = g_hash_table_new (NULL, NULL); + g_hash_table_insert (table, GUINT_TO_POINTER (contact), + (a != NULL) ? a->uri : ""); + + tp_svc_connection_interface_avatars_emit_avatars_updated (object, table); + + g_hash_table_unref (table); +} + +/** + * tp_avatars_mixin_avatar_retrieved: (skip) + * @object: An instance of the implementation that uses this mixin + * @contact: A contact #TpHandle + * @token: The new token for @contact's avatar + * @data: The new image data for @contact's avatar + * @mime_type: The new image MIME type for @contact's avatar, or %NULL if unknown + * + * Update @contact's avatar. This should be called by the Connection Manager + * when avatar data is received from the server for any contact. + * + * The image is stored on a disk cache, to avoid unnecessary future refetch of + * the data from the server. + * + * Use tp_avatars_mixin_avatar_changed() in the case the avatar data is unknown. + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_avatar_retrieved (GObject *object, + TpHandle contact, + const gchar *token, + GArray *data, + const gchar *mime_type) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + AvatarData *a; + GFile *file; + + g_return_if_fail (contact != 0); + g_return_if_fail (!tp_str_empty (token)); + g_return_if_fail (data != NULL); + + /* Check if we already have the same in memory */ + a = g_hash_table_lookup (self->priv->avatars, GUINT_TO_POINTER (contact)); + if (a != NULL && !tp_strdiff (a->token, token)) + return; + + /* Store on disk cache */ + file = avatar_cache_save (object, token, data, mime_type); + + /* Update */ + update_avatar_take_data (object, contact, + avatar_data_new (token, file)); + + g_object_unref (file); +} + +/** + * tp_avatars_mixin_avatar_changed: (skip) + * @object: An instance of the implementation that uses this mixin + * @contact: A contact #TpHandle + * @token: The new token for @contact's avatar + * + * Update @contact's avatar. This should be called by the Connection Manager + * when it knows that the avatar image changed, but did not receive the image + * data. If the avatar got removed, then this should be called with %NULL + * @token. + * + * If @token is not empty and the image data is found on disk cache, it will be + * used. Otherwise request_avatars virtual method will be called to fetch the + * avatar from server. + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_avatar_changed (GObject *object, + TpHandle contact, + const gchar *token) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + TpBaseConnection *base = (TpBaseConnection *) object; + AvatarData *a; + GFile *file; + + g_return_if_fail (contact != 0); + + /* Avoid confusion between NULL and "" */ + if (tp_str_empty (token)) + token = NULL; + + /* Check if we already have the same in memory */ + if (g_hash_table_lookup_extended (self->priv->avatars, + GUINT_TO_POINTER (contact), NULL, (gpointer *) &a) && + ((a == NULL && token == NULL) || + (a != NULL && !tp_strdiff (a->token, token)))) + return; + + /* Avatar has been removed? */ + if (token == NULL) + { + update_avatar_take_data (object, contact, NULL); + return; + } + + /* There is an avatar set, search it in the cache */ + file = avatar_cache_lookup (object, token); + if (file == NULL) + { + GArray *handles; + + /* Avatar not found in cache. Request the avatar if it's for self contact + * or if a client claims interest in avatars. Keep the last known avatar + * in the meantime. */ + if (contact != tp_base_connection_get_self_handle (base) && + !_tp_base_connection_has_client_interest (base, + TP_IFACE_QUARK_CONNECTION_INTERFACE_AVATARS)) + { + tp_intset_add (self->priv->needs_request, contact); + return; + } + + /* FIXME: Could queue to aggregate calls */ + handles = g_array_new (FALSE, FALSE, sizeof (TpHandle)); + g_array_append_val (handles, contact); + + self->priv->request_avatars (object, handles, NULL); + + g_array_unref (handles); + return; + } + + update_avatar_take_data (object, contact, + avatar_data_new (token, file)); + + g_object_unref (file); +} + +/** + * tp_avatars_mixin_drop_avatar: (skip) + * @object: An instance of the implementation that uses this mixin + * @contact: A contact #TpHandle + * + * To be called to free allocated memory when the contact's avatar is not + * relevant anymore. For example when the contact is removed from roster, or + * when a channel with channel-specific contacts is left. + * + * With XMPP, this could also be called when a contact goes offline because its + * avatar is not known anymore. + * + * Note that this won't tell the client about the change, so last known avatar + * will still be displayed. If it is known that the contact has no avatar, + * tp_avatars_mixin_avatar_changed() with %NULL token should be used instead. + * + * Since: 0.UNRELEASED + */ +void +tp_avatars_mixin_drop_avatar (GObject *object, + TpHandle contact) +{ + TpAvatarsMixin *self = TP_AVATARS_MIXIN (object); + + g_hash_table_remove (self->priv->avatars, GUINT_TO_POINTER (contact)); + tp_intset_remove (self->priv->needs_request, contact); +} diff --git a/telepathy-glib/avatars-mixin.h b/telepathy-glib/avatars-mixin.h new file mode 100644 index 000000000..84c44a64d --- /dev/null +++ b/telepathy-glib/avatars-mixin.h @@ -0,0 +1,81 @@ +/* + * avatars-mixin.h - Header for TpAvatarsMixin + * Copyright (C) 2012 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TP_AVATARS_MIXIN_H__ +#define __TP_AVATARS_MIXIN_H__ + +#include <telepathy-glib/enums.h> +#include <telepathy-glib/handle.h> +#include <telepathy-glib/connection.h> + +G_BEGIN_DECLS + +typedef struct _TpAvatarsMixin TpAvatarsMixin; +typedef struct _TpAvatarsMixinPrivate TpAvatarsMixinPrivate; + +typedef gboolean (*TpAvatarsMixinSetAvatarFunc) (GObject *object, + const GArray *avatar, + const gchar *mime_type, + GError **error); +typedef gboolean (*TpAvatarsMixinClearAvatarFunc) (GObject *object, + GError **error); +typedef gboolean (*TpAvatarsMixinRequestAvatarsFunc) (GObject *object, + const GArray *contacts, + GError **error); + +struct _TpAvatarsMixin { + /*<private>*/ + TpAvatarsMixinPrivate *priv; +}; + +/* Update avatar */ +void tp_avatars_mixin_avatar_retrieved (GObject *object, + TpHandle contact, + const gchar *token, + GArray *data, + const gchar *mime_type); + +void tp_avatars_mixin_avatar_changed (GObject *object, + TpHandle contact, + const gchar *token); + +void tp_avatars_mixin_drop_avatar (GObject *object, + TpHandle contact); + +/* Initialisation */ +void tp_avatars_mixin_init (GObject *object, + glong offset, + TpAvatarsMixinSetAvatarFunc set_avatar, + TpAvatarsMixinClearAvatarFunc clear_avatar, + TpAvatarsMixinRequestAvatarsFunc request_avatars, + gboolean avatar_persists, + TpAvatarRequirements *requirements); + +void tp_avatars_mixin_finalize (GObject *object); + +void tp_avatars_mixin_iface_init (gpointer g_iface, + gpointer iface_data); + +void tp_avatars_mixin_init_dbus_properties (GObjectClass *klass); + +void tp_avatars_mixin_register_with_contacts_mixin (GObject *object); + +G_END_DECLS + +#endif /* #ifndef __TP_AVATARS_MIXIN_H__ */ diff --git a/telepathy-glib/base-connection-internal.h b/telepathy-glib/base-connection-internal.h index cd731d4cf..fec06a67d 100644 --- a/telepathy-glib/base-connection-internal.h +++ b/telepathy-glib/base-connection-internal.h @@ -33,6 +33,12 @@ void _tp_base_connection_set_handle_repo (TpBaseConnection *self, gpointer _tp_base_connection_find_channel_manager (TpBaseConnection *self, GType type); +gboolean _tp_base_connection_has_client_interest (TpBaseConnection *self, + GQuark token); + +const gchar *_tp_base_connection_get_cm_name (TpBaseConnection *self); +const gchar *_tp_base_connection_get_protocol_name (TpBaseConnection *self); + G_END_DECLS #endif diff --git a/telepathy-glib/base-connection.c b/telepathy-glib/base-connection.c index 52e2c3726..27a33e450 100644 --- a/telepathy-glib/base-connection.c +++ b/telepathy-glib/base-connection.c @@ -418,6 +418,8 @@ struct _TpBaseConnectionPrivate /* GQuark iface => GHashTable { * unique name borrowed from interested_clients => gsize count } */ GHashTable *client_interests; + + gchar *cm_name; }; static const gchar * const *tp_base_connection_get_interfaces ( @@ -601,6 +603,7 @@ tp_base_connection_finalize (GObject *object) TpBaseConnection *self = TP_BASE_CONNECTION (object); TpBaseConnectionPrivate *priv = self->priv; + g_free (priv->cm_name); g_free (priv->protocol); g_free (priv->bus_name); g_free (priv->object_path); @@ -1014,6 +1017,14 @@ tp_base_connection_add_possible_client_interest (TpBaseConnection *self, g_hash_table_new (g_str_hash, g_str_equal)); } +gboolean +_tp_base_connection_has_client_interest (TpBaseConnection *self, + GQuark token) +{ + return g_hash_table_contains (self->priv->client_interests, + GUINT_TO_POINTER (token)); +} + /* D-Bus properties for the Requests interface */ static void @@ -1468,6 +1479,7 @@ tp_base_connection_register (TpBaseConnection *self, return FALSE; } + priv->cm_name = g_strdup (cm_name); priv->bus_name = g_strdup_printf (TP_CONN_BUS_NAME_BASE "%s.%s.%s", cm_name, safe_proto, unique_name); g_assert (strlen (priv->bus_name) <= 255); @@ -3005,3 +3017,15 @@ tp_base_connection_get_object_path (TpBaseConnection *self) return self->priv->object_path; } + +const gchar * +_tp_base_connection_get_cm_name (TpBaseConnection *self) +{ + return self->priv->cm_name; +} + +const gchar * +_tp_base_connection_get_protocol_name (TpBaseConnection *self) +{ + return self->priv->protocol; +} diff --git a/telepathy-glib/connection-internal.h b/telepathy-glib/connection-internal.h index 03a1d9b63..757c3fb5c 100644 --- a/telepathy-glib/connection-internal.h +++ b/telepathy-glib/connection-internal.h @@ -63,8 +63,6 @@ struct _TpConnectionPrivate { GQueue capabilities_queue; TpAvatarRequirements *avatar_requirements; - GArray *avatar_request_queue; - guint avatar_request_idle_id; TpContactInfoFlags contact_info_flags; GList *contact_info_supported_fields; diff --git a/telepathy-glib/connection.c b/telepathy-glib/connection.c index 704e0d322..f41587085 100644 --- a/telepathy-glib/connection.c +++ b/telepathy-glib/connection.c @@ -1205,18 +1205,6 @@ tp_connection_finalize (GObject *object) self->priv->connection_error_details = NULL; } - if (self->priv->avatar_request_queue != NULL) - { - g_array_unref (self->priv->avatar_request_queue); - self->priv->avatar_request_queue = NULL; - } - - if (self->priv->avatar_request_idle_id != 0) - { - g_source_remove (self->priv->avatar_request_idle_id); - self->priv->avatar_request_idle_id = 0; - } - tp_contact_info_spec_list_free (self->priv->contact_info_supported_fields); self->priv->contact_info_supported_fields = NULL; diff --git a/telepathy-glib/telepathy-glib.h b/telepathy-glib/telepathy-glib.h index a638fc7fa..e35d94c4f 100644 --- a/telepathy-glib/telepathy-glib.h +++ b/telepathy-glib/telepathy-glib.h @@ -37,6 +37,7 @@ #include <telepathy-glib/account.h> #include <telepathy-glib/add-dispatch-operation-context.h> #include <telepathy-glib/automatic-client-factory.h> +#include <telepathy-glib/avatars-mixin.h> #include <telepathy-glib/base-call-channel.h> #include <telepathy-glib/base-call-content.h> #include <telepathy-glib/base-call-stream.h> |