/* * e-photo-cache.c * * This program 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 of the License, or (at your option) version 3. * * This program 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 the program; if not, see * */ /** * SECTION: e-photo-cache * @include: e-util/e-util.h * @short_description: Search for photos by email address * * #EPhotoCache helps search for contact photo or logo images associated * with an email address. * * A limited internal cache is employed to speed up searches for recently * searched email addresses. The exact caching semantics are private and * subject to change. **/ #include "e-photo-cache.h" #include #include #define E_PHOTO_CACHE_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), E_TYPE_PHOTO_CACHE, EPhotoCachePrivate)) /* How many email addresses we track at once, regardless of whether * the email address has a photo. As new cache entries are added, we * discard the least recently accessed entries to keep the cache size * within the limit. */ #define MAX_CACHE_SIZE 20 typedef struct _AsyncContext AsyncContext; typedef struct _PhotoData PhotoData; struct _EPhotoCachePrivate { EClientCache *client_cache; gboolean local_only; GHashTable *photo_ht; GQueue photo_ht_keys; GMutex photo_ht_lock; }; struct _AsyncContext { gchar *email_address; GInputStream *input_stream; }; struct _PhotoData { volatile gint ref_count; GMutex lock; EContactPhoto *photo; gboolean photo_is_set; }; enum { PROP_0, PROP_CLIENT_CACHE, PROP_LOCAL_ONLY }; G_DEFINE_TYPE_WITH_CODE ( EPhotoCache, e_photo_cache, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE ( E_TYPE_EXTENSIBLE, NULL)) static void async_context_free (AsyncContext *async_context) { g_free (async_context->email_address); if (async_context->input_stream != NULL) g_object_unref (async_context->input_stream); g_slice_free (AsyncContext, async_context); } static PhotoData * photo_data_new (void) { PhotoData *photo_data; photo_data = g_slice_new0 (PhotoData); photo_data->ref_count = 1; g_mutex_init (&photo_data->lock); return photo_data; } static PhotoData * photo_data_ref (PhotoData *photo_data) { g_return_val_if_fail (photo_data != NULL, NULL); g_return_val_if_fail (photo_data->ref_count > 0, NULL); g_atomic_int_inc (&photo_data->ref_count); return photo_data; } static void photo_data_unref (PhotoData *photo_data) { g_return_if_fail (photo_data != NULL); g_return_if_fail (photo_data->ref_count > 0); if (g_atomic_int_dec_and_test (&photo_data->ref_count)) { if (photo_data->photo != NULL) e_contact_photo_free (photo_data->photo); g_mutex_clear (&photo_data->lock); g_slice_free (PhotoData, photo_data); } } static gboolean photo_data_dup_photo (PhotoData *photo_data, EContactPhoto **out_photo) { gboolean photo_is_set; g_return_val_if_fail (out_photo != NULL, FALSE); g_mutex_lock (&photo_data->lock); if (photo_data->photo != NULL) *out_photo = e_contact_photo_copy (photo_data->photo); else *out_photo = NULL; photo_is_set = photo_data->photo_is_set; g_mutex_unlock (&photo_data->lock); return photo_is_set; } static void photo_data_set_photo (PhotoData *photo_data, EContactPhoto *photo) { g_mutex_lock (&photo_data->lock); if (photo_data->photo != NULL) { e_contact_photo_free (photo_data->photo); photo_data->photo = NULL; } if (photo != NULL) photo_data->photo = e_contact_photo_copy (photo); photo_data->photo_is_set = TRUE; g_mutex_unlock (&photo_data->lock); } static gchar * photo_ht_normalize_key (const gchar *email_address) { gchar *lowercase_email_address; gchar *collation_key; lowercase_email_address = g_utf8_strdown (email_address, -1); collation_key = g_utf8_collate_key (lowercase_email_address, -1); g_free (lowercase_email_address); return collation_key; } static PhotoData * photo_ht_lookup (EPhotoCache *photo_cache, const gchar *email_address) { GHashTable *photo_ht; GQueue *photo_ht_keys; PhotoData *photo_data; gchar *key; g_return_val_if_fail (email_address != NULL, NULL); photo_ht = photo_cache->priv->photo_ht; photo_ht_keys = &photo_cache->priv->photo_ht_keys; key = photo_ht_normalize_key (email_address); g_mutex_lock (&photo_cache->priv->photo_ht_lock); photo_data = g_hash_table_lookup (photo_ht, key); if (photo_data != NULL) { GList *link; photo_data_ref (photo_data); /* Move the key to the head of the MRU queue. */ link = g_queue_find_custom ( photo_ht_keys, key, (GCompareFunc) strcmp); if (link != NULL) { g_queue_unlink (photo_ht_keys, link); g_queue_push_head_link (photo_ht_keys, link); } } else { photo_data = photo_data_new (); g_hash_table_insert ( photo_ht, g_strdup (key), photo_data_ref (photo_data)); /* Push the key to the head of the MRU queue. */ g_queue_push_head (photo_ht_keys, g_strdup (key)); /* Trim the cache if necessary. */ while (g_queue_get_length (photo_ht_keys) > MAX_CACHE_SIZE) { gchar *oldest_key; oldest_key = g_queue_pop_tail (photo_ht_keys); g_hash_table_remove (photo_ht, oldest_key); g_free (oldest_key); } } /* Hash table and queue sizes should be equal at all times. */ g_warn_if_fail ( g_hash_table_size (photo_ht) == g_queue_get_length (photo_ht_keys)); g_mutex_unlock (&photo_cache->priv->photo_ht_lock); g_free (key); return photo_data; } static gboolean photo_ht_remove (EPhotoCache *photo_cache, const gchar *email_address) { GHashTable *photo_ht; GQueue *photo_ht_keys; gchar *key; gboolean removed = FALSE; g_return_val_if_fail (email_address != NULL, FALSE); photo_ht = photo_cache->priv->photo_ht; photo_ht_keys = &photo_cache->priv->photo_ht_keys; key = photo_ht_normalize_key (email_address); g_mutex_lock (&photo_cache->priv->photo_ht_lock); if (g_hash_table_remove (photo_ht, key)) { GList *link; link = g_queue_find_custom ( photo_ht_keys, key, (GCompareFunc) strcmp); if (link != NULL) { g_free (link->data); g_queue_delete_link (photo_ht_keys, link); removed = TRUE; } } /* Hash table and queue sizes should be equal at all times. */ g_warn_if_fail ( g_hash_table_size (photo_ht) == g_queue_get_length (photo_ht_keys)); g_mutex_unlock (&photo_cache->priv->photo_ht_lock); g_free (key); return removed; } static void photo_ht_remove_all (EPhotoCache *photo_cache) { GHashTable *photo_ht; GQueue *photo_ht_keys; photo_ht = photo_cache->priv->photo_ht; photo_ht_keys = &photo_cache->priv->photo_ht_keys; g_mutex_lock (&photo_cache->priv->photo_ht_lock); g_hash_table_remove_all (photo_ht); while (!g_queue_is_empty (photo_ht_keys)) g_free (g_queue_pop_head (photo_ht_keys)); g_mutex_unlock (&photo_cache->priv->photo_ht_lock); } static EContactPhoto * photo_cache_extract_photo (EContact *contact) { EContactPhoto *photo; photo = e_contact_get (contact, E_CONTACT_PHOTO); if (photo == NULL) photo = e_contact_get (contact, E_CONTACT_LOGO); return photo; } static GList * photo_cache_list_searchable_sources (EPhotoCache *photo_cache) { EClientCache *client_cache; ESourceRegistry *registry; GList *list; client_cache = e_photo_cache_ref_client_cache (photo_cache); registry = e_client_cache_ref_registry (client_cache); if (e_photo_cache_get_local_only (photo_cache)) { ESource *source; source = e_source_registry_ref_builtin_address_book (registry); list = g_list_prepend (NULL, g_object_ref (source)); g_object_unref (source); } else { list = e_source_registry_list_sources ( registry, E_SOURCE_EXTENSION_ADDRESS_BOOK); } g_object_unref (client_cache); g_object_unref (registry); return list; } static gboolean photo_cache_find_contacts (EPhotoCache *photo_cache, const gchar *email_address, GCancellable *cancellable, GQueue *out_contacts, GError **error) { EClientCache *client_cache; EBookQuery *book_query; GList *list, *link; gchar *book_query_string; gboolean success = TRUE; book_query = e_book_query_field_test ( E_CONTACT_EMAIL, E_BOOK_QUERY_IS, email_address); book_query_string = e_book_query_to_string (book_query); e_book_query_unref (book_query); client_cache = e_photo_cache_ref_client_cache (photo_cache); list = photo_cache_list_searchable_sources (photo_cache); for (link = list; link != NULL; link = g_list_next (link)) { ESource *source = E_SOURCE (link->data); EClient *client; GSList *contact_list = NULL; GError *local_error = NULL; /* Skip disabled sources. */ if (!e_source_get_enabled (source)) continue; client = e_client_cache_get_client_sync ( client_cache, source, E_SOURCE_EXTENSION_ADDRESS_BOOK, cancellable, &local_error); if (local_error != NULL) { g_warn_if_fail (client == NULL); if (g_queue_is_empty (out_contacts)) { g_propagate_error (error, local_error); success = FALSE; } else { /* Clear the error if we already * have matching contacts queued. */ g_clear_error (&local_error); } break; } e_book_client_get_contacts_sync ( E_BOOK_CLIENT (client), book_query_string, &contact_list, cancellable, &local_error); g_object_unref (client); if (local_error != NULL) { g_warn_if_fail (contact_list == NULL); if (g_queue_is_empty (out_contacts)) { g_propagate_error (error, local_error); success = FALSE; } else { /* Clear the error if we already * have matching contacts queued. */ g_clear_error (&local_error); } break; } while (contact_list != NULL) { EContact *contact; /* Transfer ownership to queue. */ contact = E_CONTACT (contact_list->data); g_queue_push_tail (out_contacts, contact); contact_list = g_slist_delete_link ( contact_list, contact_list); } } g_list_free_full (list, (GDestroyNotify) g_object_unref); g_object_unref (client_cache); g_free (book_query_string); return success; } static GInputStream * photo_cache_new_stream_from_photo (EContactPhoto *photo, GCancellable *cancellable, GError **error) { GInputStream *stream = NULL; /* Stream takes ownership of the inlined data. */ if (photo->type == E_CONTACT_PHOTO_TYPE_INLINED) { stream = g_memory_input_stream_new_from_data ( photo->data.inlined.data, photo->data.inlined.length, (GDestroyNotify) g_free); photo->data.inlined.data = NULL; photo->data.inlined.length = 0; } else { GFileInputStream *file_stream; GFile *file; file = g_file_new_for_uri (photo->data.uri); /* XXX Return type should have been GInputStream. */ file_stream = g_file_read (file, cancellable, error); if (file_stream != NULL) stream = G_INPUT_STREAM (file_stream); g_object_unref (file); } return stream; } static void photo_cache_set_client_cache (EPhotoCache *photo_cache, EClientCache *client_cache) { g_return_if_fail (E_IS_CLIENT_CACHE (client_cache)); g_return_if_fail (photo_cache->priv->client_cache == NULL); photo_cache->priv->client_cache = g_object_ref (client_cache); } static void photo_cache_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_CLIENT_CACHE: photo_cache_set_client_cache ( E_PHOTO_CACHE (object), g_value_get_object (value)); return; case PROP_LOCAL_ONLY: e_photo_cache_set_local_only ( E_PHOTO_CACHE (object), g_value_get_boolean (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void photo_cache_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_CLIENT_CACHE: g_value_take_object ( value, e_photo_cache_ref_client_cache ( E_PHOTO_CACHE (object))); return; case PROP_LOCAL_ONLY: g_value_set_boolean ( value, e_photo_cache_get_local_only ( E_PHOTO_CACHE (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void photo_cache_dispose (GObject *object) { EPhotoCachePrivate *priv; priv = E_PHOTO_CACHE_GET_PRIVATE (object); g_clear_object (&priv->client_cache); photo_ht_remove_all (E_PHOTO_CACHE (object)); /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (e_photo_cache_parent_class)->dispose (object); } static void photo_cache_finalize (GObject *object) { EPhotoCachePrivate *priv; priv = E_PHOTO_CACHE_GET_PRIVATE (object); g_hash_table_destroy (priv->photo_ht); /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (e_photo_cache_parent_class)->finalize (object); } static void photo_cache_constructed (GObject *object) { /* Chain up to parent's constructed() method. */ G_OBJECT_CLASS (e_photo_cache_parent_class)->constructed (object); e_extensible_load_extensions (E_EXTENSIBLE (object)); } static void e_photo_cache_class_init (EPhotoCacheClass *class) { GObjectClass *object_class; g_type_class_add_private (class, sizeof (EPhotoCachePrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = photo_cache_set_property; object_class->get_property = photo_cache_get_property; object_class->dispose = photo_cache_dispose; object_class->finalize = photo_cache_finalize; object_class->constructed = photo_cache_constructed; /** * EPhotoCache:client-cache: * * Cache of shared #EClient instances. **/ g_object_class_install_property ( object_class, PROP_CLIENT_CACHE, g_param_spec_object ( "client-cache", "Client Cache", "Cache of shared EClient instances", E_TYPE_CLIENT_CACHE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * EPhotoCache:local-only: * * Whether to restrict searches to the built-in address book. **/ g_object_class_install_property ( object_class, PROP_LOCAL_ONLY, g_param_spec_boolean ( "local-only", "Local Only", "Whether to restruct searches " "to the built-in address book", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); } static void e_photo_cache_init (EPhotoCache *photo_cache) { GHashTable *photo_ht; photo_ht = g_hash_table_new_full ( (GHashFunc) g_str_hash, (GEqualFunc) g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) photo_data_unref); photo_cache->priv = E_PHOTO_CACHE_GET_PRIVATE (photo_cache); photo_cache->priv->photo_ht = photo_ht; g_mutex_init (&photo_cache->priv->photo_ht_lock); } /** * e_photo_cache_new: * @client_cache: an #EClientCache * * Creates a new #EPhotoCache instance. * * Returns: an #EPhotoCache **/ EPhotoCache * e_photo_cache_new (EClientCache *client_cache) { g_return_val_if_fail (E_IS_CLIENT_CACHE (client_cache), NULL); return g_object_new ( E_TYPE_PHOTO_CACHE, "client-cache", client_cache, NULL); } /** * e_photo_cache_ref_client_cache: * @photo_cache: an #EPhotoCache * * Returns the #EClientCache passed to e_photo_cache_new(). * * The returned #EClientCache is referenced for thread-safety and must be * unreferenced with g_object_unref() when finished with it. * * Returns: an #EClientCache **/ EClientCache * e_photo_cache_ref_client_cache (EPhotoCache *photo_cache) { g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), NULL); return g_object_ref (photo_cache->priv->client_cache); } /** * e_photo_cache_get_local_only: * @photo_cache: an #EPhotoCache * * Returns whether to limit photo searches to the built-in ("local") * address book returned by e_source_registry_ref_builtin_address_book(). * * If this property is %FALSE then all enabled address books are searched. * * Returns: whether to search only the built-in address book **/ gboolean e_photo_cache_get_local_only (EPhotoCache *photo_cache) { g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE); return photo_cache->priv->local_only; } /** * e_photo_cache_set_local_only: * @photo_cache: an #EPhotoCache * @local_only: whether to search only the built-in address book * * Sets whether to limit photo searches to the built-in ("local") * address book returned by e_source_registry_ref_builtin_address_book(). * * If this property is %FALSE then all enabled address books are searched. **/ void e_photo_cache_set_local_only (EPhotoCache *photo_cache, gboolean local_only) { g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache)); photo_cache->priv->local_only = local_only; /* Reset the cache. */ photo_ht_remove_all (photo_cache); g_object_notify (G_OBJECT (photo_cache), "local-only"); } /** * e_photo_cache_get_photo_sync: * @photo_cache: an #EPhotoCache * @email_address: an email address * @cancellable: optional #GCancellable object, or %NULL * @out_stream: return location for a #GInputStream, or %NULL * @error: return location for a #GError, or %NULL * * Searches enabled address books (subject to the #EPhotoCache:local-only * preference) for a contact photo or logo associated with @email_address. * * If a match is found, a #GInputStream from which to read image data is * returned through the @out_stream return location. If no match is found, * the @out_stream return location is set to %NULL. * * The return value indicates whether the search completed successfully, * not whether a match was found. If an error occurs, the function will * set @error and return %FALSE. * * Returns: whether the search completed successfully **/ gboolean e_photo_cache_get_photo_sync (EPhotoCache *photo_cache, const gchar *email_address, GCancellable *cancellable, GInputStream **out_stream, GError **error) { EContactPhoto *photo = NULL; EClientCache *client_cache; GQueue queue = G_QUEUE_INIT; PhotoData *photo_data; gboolean success = TRUE; g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE); g_return_val_if_fail (email_address != NULL, FALSE); client_cache = e_photo_cache_ref_client_cache (photo_cache); /* Try the cache first. */ photo_data = photo_ht_lookup (photo_cache, email_address); if (photo_data_dup_photo (photo_data, &photo)) goto exit; /* Find contacts with a matching email address. */ success = photo_cache_find_contacts ( photo_cache, email_address, cancellable, &queue, error); if (!success) { g_warn_if_fail (g_queue_is_empty (&queue)); goto exit; } /* Extract the first available photo from contacts. */ while (!g_queue_is_empty (&queue)) { EContact *contact; contact = g_queue_pop_head (&queue); if (photo == NULL) photo = photo_cache_extract_photo (contact); g_object_unref (contact); } /* Passing a NULL photo here is fine. We want to cache not * only the photo itself, but whether a photo was found for * this email address. */ photo_data_set_photo (photo_data, photo); exit: photo_data_unref (photo_data); g_object_unref (client_cache); /* Try opening an input stream to the photo data. */ if (photo != NULL) { GInputStream *stream; stream = photo_cache_new_stream_from_photo ( photo, cancellable, error); success = (stream != NULL); if (stream != NULL) { if (out_stream != NULL) *out_stream = g_object_ref (stream); g_object_unref (stream); } e_contact_photo_free (photo); } return success; } /* Helper for e_photo_cache_get_photo() */ static void photo_cache_get_photo_thread (GSimpleAsyncResult *simple, GObject *source_object, GCancellable *cancellable) { AsyncContext *async_context; GError *error = NULL; async_context = g_simple_async_result_get_op_res_gpointer (simple); e_photo_cache_get_photo_sync ( E_PHOTO_CACHE (source_object), async_context->email_address, cancellable, &async_context->input_stream, &error); if (error != NULL) g_simple_async_result_take_error (simple, error); } /** * e_photo_cache_get_photo: * @photo_cache: an #EPhotoCache * @email_address: an email address * @cancellable: optional #GCancellable object, or %NULL * @callback: a #GAsyncReadyCallback to call when the request is satisfied * @user_data: data to pass to the callback function * * Asynchronously searches enabled address books (subject to the * #EPhotoCache:local-only preference) for a contact photo or logo * associated with @email_address. * * When the operation is finished, @callback will be called. You can then * call e_photo_cache_get_photo_finish() to get the result of the operation. **/ void e_photo_cache_get_photo (EPhotoCache *photo_cache, const gchar *email_address, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *simple; AsyncContext *async_context; g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache)); g_return_if_fail (email_address != NULL); async_context = g_slice_new0 (AsyncContext); async_context->email_address = g_strdup (email_address); simple = g_simple_async_result_new ( G_OBJECT (photo_cache), callback, user_data, e_photo_cache_get_photo); g_simple_async_result_set_check_cancellable (simple, cancellable); g_simple_async_result_set_op_res_gpointer ( simple, async_context, (GDestroyNotify) async_context_free); g_simple_async_result_run_in_thread ( simple, photo_cache_get_photo_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (simple); } /** * e_photo_cache_get_photo_finish: * @photo_cache: an #EPhotoCache * @result: a #GAsyncResult * @out_stream: return location for a #GInputStream, or %NULL * @error: return location for a #GError, or %NULL * * Finishes the operation started with e_photo_cache_get_photo(). * * If a match was found, a #GInputStream from which to read image data is * returned through the @out_stream return location. If no match was found, * the @out_stream return location is set to %NULL. * * The return value indicates whether the search completed successfully, * not whether a match was found. If an error occurred, the function will * set @error and return %FALSE. * * Returns: whether the search completed successfully **/ gboolean e_photo_cache_get_photo_finish (EPhotoCache *photo_cache, GAsyncResult *result, GInputStream **out_stream, GError **error) { GSimpleAsyncResult *simple; AsyncContext *async_context; g_return_val_if_fail ( g_simple_async_result_is_valid ( result, G_OBJECT (photo_cache), e_photo_cache_get_photo), FALSE); simple = G_SIMPLE_ASYNC_RESULT (result); async_context = g_simple_async_result_get_op_res_gpointer (simple); if (g_simple_async_result_propagate_error (simple, error)) return FALSE; if (out_stream != NULL) { *out_stream = async_context->input_stream; async_context->input_stream = NULL; } return TRUE; } /** * e_photo_cache_remove: * @photo_cache: an #EPhotoCache * @email_address: an email address * * Removes the cache entry for @email_address, if such an entry exists. **/ gboolean e_photo_cache_remove (EPhotoCache *photo_cache, const gchar *email_address) { g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE); g_return_val_if_fail (email_address != NULL, FALSE); return photo_ht_remove (photo_cache, email_address); }