/* * 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 finds photos associated with an email address. * * A limited internal cache is employed to speed up frequently searched * email addresses. The exact caching semantics are private and subject * to change. **/ #include "e-photo-cache.h" #include #include #include #define E_PHOTO_CACHE_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), E_TYPE_PHOTO_CACHE, EPhotoCachePrivate)) /* How long (in seconds) to hold out for a hit from the highest * priority photo source, after which we settle for what we have. */ #define ASYNC_TIMEOUT_SECONDS 3.0 /* 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 #define ERROR_IS_CANCELLED(error) \ (g_error_matches ((error), G_IO_ERROR, G_IO_ERROR_CANCELLED)) typedef struct _AsyncContext AsyncContext; typedef struct _AsyncSubtask AsyncSubtask; typedef struct _DataCaptureClosure DataCaptureClosure; typedef struct _PhotoData PhotoData; struct _EPhotoCachePrivate { EClientCache *client_cache; GMainContext *main_context; GHashTable *photo_ht; GQueue photo_ht_keys; GMutex photo_ht_lock; GHashTable *sources_ht; GMutex sources_ht_lock; }; struct _AsyncContext { GMutex lock; GTimer *timer; GHashTable *subtasks; GQueue results; GInputStream *stream; GConverter *data_capture; GCancellable *cancellable; gulong cancelled_handler_id; }; struct _AsyncSubtask { volatile gint ref_count; EPhotoSource *photo_source; GSimpleAsyncResult *simple; GCancellable *cancellable; GInputStream *stream; gint priority; GError *error; }; struct _DataCaptureClosure { GWeakRef photo_cache; gchar *email_address; }; struct _PhotoData { volatile gint ref_count; GMutex lock; GBytes *bytes; }; enum { PROP_0, PROP_CLIENT_CACHE }; /* Forward Declarations */ static void async_context_cancel_subtasks (AsyncContext *async_context); G_DEFINE_TYPE_WITH_CODE ( EPhotoCache, e_photo_cache, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE ( E_TYPE_EXTENSIBLE, NULL)) static AsyncSubtask * async_subtask_new (EPhotoSource *photo_source, GSimpleAsyncResult *simple) { AsyncSubtask *async_subtask; async_subtask = g_slice_new0 (AsyncSubtask); async_subtask->ref_count = 1; async_subtask->photo_source = g_object_ref (photo_source); async_subtask->simple = g_object_ref (simple); async_subtask->cancellable = g_cancellable_new (); async_subtask->priority = G_PRIORITY_DEFAULT; return async_subtask; } static AsyncSubtask * async_subtask_ref (AsyncSubtask *async_subtask) { g_return_val_if_fail (async_subtask != NULL, NULL); g_return_val_if_fail (async_subtask->ref_count > 0, NULL); g_atomic_int_inc (&async_subtask->ref_count); return async_subtask; } static void async_subtask_unref (AsyncSubtask *async_subtask) { g_return_if_fail (async_subtask != NULL); g_return_if_fail (async_subtask->ref_count > 0); if (g_atomic_int_dec_and_test (&async_subtask->ref_count)) { /* Ignore cancellations. */ if (ERROR_IS_CANCELLED (async_subtask->error)) g_clear_error (&async_subtask->error); /* Leave a breadcrumb on the console * about unpropagated subtask errors. */ if (async_subtask->error != NULL) { g_warning ( "%s: Unpropagated error in %s subtask: %s", __FILE__, G_OBJECT_TYPE_NAME ( async_subtask->photo_source), async_subtask->error->message); g_error_free (async_subtask->error); } g_clear_object (&async_subtask->photo_source); g_clear_object (&async_subtask->simple); g_clear_object (&async_subtask->cancellable); g_clear_object (&async_subtask->stream); g_slice_free (AsyncSubtask, async_subtask); } } static gboolean async_subtask_cancel_idle_cb (gpointer user_data) { AsyncSubtask *async_subtask = user_data; g_cancellable_cancel (async_subtask->cancellable); return FALSE; } static gint async_subtask_compare (gconstpointer a, gconstpointer b) { const AsyncSubtask *subtask_a = a; const AsyncSubtask *subtask_b = b; /* Without error is always less than with error. */ if (subtask_a->error != NULL && subtask_b->error != NULL) return 0; if (subtask_a->error == NULL && subtask_b->error != NULL) return -1; if (subtask_a->error != NULL && subtask_b->error == NULL) return 1; if (subtask_a->priority == subtask_b->priority) return 0; return (subtask_a->priority < subtask_b->priority) ? -1 : 1; } static void async_subtask_complete (AsyncSubtask *async_subtask) { GSimpleAsyncResult *simple; AsyncContext *async_context; gboolean cancel_subtasks = FALSE; gdouble seconds_elapsed; simple = async_subtask->simple; async_context = g_simple_async_result_get_op_res_gpointer (simple); g_mutex_lock (&async_context->lock); seconds_elapsed = g_timer_elapsed (async_context->timer, NULL); /* Discard successfully completed subtasks with no match found. * Keep failed subtasks around so we have a GError to propagate * if we need one, but those go on the end of the queue. */ if (async_subtask->stream != NULL) { g_queue_insert_sorted ( &async_context->results, async_subtask_ref (async_subtask), (GCompareDataFunc) async_subtask_compare, NULL); /* If enough seconds have elapsed, just take the highest * priority input stream we have. Cancel the unfinished * subtasks and let them complete with an error. */ if (seconds_elapsed > ASYNC_TIMEOUT_SECONDS) cancel_subtasks = TRUE; } else if (async_subtask->error != NULL) { g_queue_push_tail ( &async_context->results, async_subtask_ref (async_subtask)); } g_hash_table_remove (async_context->subtasks, async_subtask); if (g_hash_table_size (async_context->subtasks) > 0) { /* Let the remaining subtasks finish. */ goto exit; } /* The queue should be ordered now such that subtasks * with input streams are before subtasks with errors. * So just evaluate the first subtask on the queue. */ async_subtask = g_queue_pop_head (&async_context->results); if (async_subtask != NULL) { if (async_subtask->stream != NULL) { async_context->stream = g_converter_input_stream_new ( async_subtask->stream, async_context->data_capture); } if (async_subtask->error != NULL) { g_simple_async_result_take_error ( simple, async_subtask->error); async_subtask->error = NULL; } async_subtask_unref (async_subtask); } g_simple_async_result_complete_in_idle (simple); exit: g_mutex_unlock (&async_context->lock); if (cancel_subtasks) { /* Call this after the mutex is unlocked. */ async_context_cancel_subtasks (async_context); } } static void async_context_cancelled_cb (GCancellable *cancellable, AsyncContext *async_context) { async_context_cancel_subtasks (async_context); } static AsyncContext * async_context_new (EDataCapture *data_capture, GCancellable *cancellable) { AsyncContext *async_context; async_context = g_slice_new0 (AsyncContext); g_mutex_init (&async_context->lock); async_context->timer = g_timer_new (); async_context->subtasks = g_hash_table_new_full ( (GHashFunc) g_direct_hash, (GEqualFunc) g_direct_equal, (GDestroyNotify) async_subtask_unref, (GDestroyNotify) NULL); async_context->data_capture = g_object_ref (data_capture); if (G_IS_CANCELLABLE (cancellable)) { gulong handler_id; async_context->cancellable = g_object_ref (cancellable); handler_id = g_cancellable_connect ( async_context->cancellable, G_CALLBACK (async_context_cancelled_cb), async_context, (GDestroyNotify) NULL); async_context->cancelled_handler_id = handler_id; } return async_context; } static void async_context_free (AsyncContext *async_context) { /* Do this first so the callback won't fire * while we're dismantling the AsyncContext. */ if (async_context->cancelled_handler_id > 0) g_cancellable_disconnect ( async_context->cancellable, async_context->cancelled_handler_id); g_mutex_clear (&async_context->lock); g_timer_destroy (async_context->timer); g_hash_table_destroy (async_context->subtasks); g_clear_object (&async_context->stream); g_clear_object (&async_context->data_capture); g_clear_object (&async_context->cancellable); g_slice_free (AsyncContext, async_context); } static void async_context_cancel_subtasks (AsyncContext *async_context) { GMainContext *main_context; GList *list, *link; main_context = g_main_context_ref_thread_default (); g_mutex_lock (&async_context->lock); list = g_hash_table_get_keys (async_context->subtasks); /* XXX Cancel subtasks from idle callbacks to make sure we don't * finalize the GSimpleAsyncResult during a "cancelled" signal * emission from the main task's GCancellable. That will make * g_cancellable_disconnect() in async_context_free() deadlock. */ for (link = list; link != NULL; link = g_list_next (link)) { AsyncSubtask *async_subtask = link->data; GSource *idle_source; idle_source = g_idle_source_new (); g_source_set_priority (idle_source, G_PRIORITY_HIGH_IDLE); g_source_set_callback ( idle_source, async_subtask_cancel_idle_cb, async_subtask_ref (async_subtask), (GDestroyNotify) async_subtask_unref); g_source_attach (idle_source, main_context); g_source_unref (idle_source); } g_list_free (list); g_mutex_unlock (&async_context->lock); g_main_context_unref (main_context); } static DataCaptureClosure * data_capture_closure_new (EPhotoCache *photo_cache, const gchar *email_address) { DataCaptureClosure *closure; closure = g_slice_new0 (DataCaptureClosure); g_weak_ref_set (&closure->photo_cache, photo_cache); closure->email_address = g_strdup (email_address); return closure; } static void data_capture_closure_free (DataCaptureClosure *closure) { g_weak_ref_set (&closure->photo_cache, NULL); g_free (closure->email_address); g_slice_free (DataCaptureClosure, closure); } static PhotoData * photo_data_new (GBytes *bytes) { PhotoData *photo_data; photo_data = g_slice_new0 (PhotoData); photo_data->ref_count = 1; g_mutex_init (&photo_data->lock); if (bytes != NULL) photo_data->bytes = g_bytes_ref (bytes); 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)) { g_mutex_clear (&photo_data->lock); if (photo_data->bytes != NULL) g_bytes_unref (photo_data->bytes); g_slice_free (PhotoData, photo_data); } } static GBytes * photo_data_ref_bytes (PhotoData *photo_data) { GBytes *bytes = NULL; g_mutex_lock (&photo_data->lock); if (photo_data->bytes != NULL) bytes = g_bytes_ref (photo_data->bytes); g_mutex_unlock (&photo_data->lock); return bytes; } static void photo_data_set_bytes (PhotoData *photo_data, GBytes *bytes) { g_mutex_lock (&photo_data->lock); if (photo_data->bytes != NULL) { g_bytes_unref (photo_data->bytes); photo_data->bytes = NULL; } if (bytes != NULL) photo_data->bytes = g_bytes_ref (bytes); 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 void photo_ht_insert (EPhotoCache *photo_cache, const gchar *email_address, GBytes *bytes) { GHashTable *photo_ht; GQueue *photo_ht_keys; PhotoData *photo_data; gchar *key; g_return_if_fail (email_address != 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; /* Replace the old photo data if we have new photo * data, otherwise leave the old photo data alone. */ if (bytes != NULL) photo_data_set_bytes (photo_data, bytes); /* 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 (bytes); 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); } photo_data_unref (photo_data); } /* 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); } static gboolean photo_ht_lookup (EPhotoCache *photo_cache, const gchar *email_address, GInputStream **out_stream) { GHashTable *photo_ht; PhotoData *photo_data; gboolean found = FALSE; gchar *key; g_return_val_if_fail (email_address != NULL, FALSE); g_return_val_if_fail (out_stream != NULL, FALSE); photo_ht = photo_cache->priv->photo_ht; 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) { GBytes *bytes; bytes = photo_data_ref_bytes (photo_data); if (bytes != NULL) { *out_stream = g_memory_input_stream_new_from_bytes (bytes); g_bytes_unref (bytes); } else { *out_stream = NULL; } found = TRUE; } g_mutex_unlock (&photo_cache->priv->photo_ht_lock); g_free (key); return found; } 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 void photo_cache_data_captured_cb (EDataCapture *data_capture, GBytes *bytes, DataCaptureClosure *closure) { EPhotoCache *photo_cache; photo_cache = g_weak_ref_get (&closure->photo_cache); if (photo_cache != NULL) { e_photo_cache_add_photo ( photo_cache, closure->email_address, bytes); g_object_unref (photo_cache); } } static void photo_cache_async_subtask_done_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { AsyncSubtask *async_subtask = user_data; e_photo_source_get_photo_finish ( E_PHOTO_SOURCE (source_object), result, &async_subtask->stream, &async_subtask->priority, &async_subtask->error); async_subtask_complete (async_subtask); async_subtask_unref (async_subtask); } 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; } 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; } 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_main_context_unref (priv->main_context); g_hash_table_destroy (priv->photo_ht); g_mutex_lock (&priv->photo_ht_lock); g_mutex_lock (&priv->sources_ht_lock); /* 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)); } static void e_photo_cache_init (EPhotoCache *photo_cache) { GHashTable *photo_ht; GHashTable *sources_ht; photo_ht = g_hash_table_new_full ( (GHashFunc) g_str_hash, (GEqualFunc) g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) photo_data_unref); sources_ht = g_hash_table_new_full ( (GHashFunc) g_direct_hash, (GEqualFunc) g_direct_equal, (GDestroyNotify) g_object_unref, (GDestroyNotify) NULL); photo_cache->priv = E_PHOTO_CACHE_GET_PRIVATE (photo_cache); photo_cache->priv->main_context = g_main_context_ref_thread_default (); photo_cache->priv->photo_ht = photo_ht; photo_cache->priv->sources_ht = sources_ht; g_mutex_init (&photo_cache->priv->photo_ht_lock); g_mutex_init (&photo_cache->priv->sources_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_add_photo_source: * @photo_cache: an #EPhotoCache * @photo_source: an #EPhotoSource * * Adds @photo_source as a potential source of photos. **/ void e_photo_cache_add_photo_source (EPhotoCache *photo_cache, EPhotoSource *photo_source) { GHashTable *sources_ht; g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache)); g_return_if_fail (E_IS_PHOTO_SOURCE (photo_source)); sources_ht = photo_cache->priv->sources_ht; g_mutex_lock (&photo_cache->priv->sources_ht_lock); g_hash_table_add (sources_ht, g_object_ref (photo_source)); g_mutex_unlock (&photo_cache->priv->sources_ht_lock); } /** * e_photo_cache_list_photo_sources: * @photo_cache: an #EPhotoCache * * Returns a list of photo sources for @photo_cache. * * The sources returned in the list are referenced for thread-safety. * They must each be unreferenced with g_object_unref() when finished * with them. Free the returned list itself with g_list_free(). * * An easy way to free the list property in one step is as follows: * * |[ * g_list_free_full (list, g_object_unref); * ]| * * Returns: a sorted list of photo sources **/ GList * e_photo_cache_list_photo_sources (EPhotoCache *photo_cache) { GHashTable *sources_ht; GList *list; g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), NULL); sources_ht = photo_cache->priv->sources_ht; g_mutex_lock (&photo_cache->priv->sources_ht_lock); list = g_hash_table_get_keys (sources_ht); g_list_foreach (list, (GFunc) g_object_ref, NULL); g_mutex_unlock (&photo_cache->priv->sources_ht_lock); return list; } /** * e_photo_cache_remove_photo_source: * @photo_cache: an #EPhotoCache * @photo_source: an #EPhotoSource * * Removes @photo_source as a potential source of photos. * * Returns: %TRUE if @photo_source was found and removed, %FALSE if not **/ gboolean e_photo_cache_remove_photo_source (EPhotoCache *photo_cache, EPhotoSource *photo_source) { GHashTable *sources_ht; gboolean removed; g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE); g_return_val_if_fail (E_IS_PHOTO_SOURCE (photo_source), FALSE); sources_ht = photo_cache->priv->sources_ht; g_mutex_lock (&photo_cache->priv->sources_ht_lock); removed = g_hash_table_remove (sources_ht, photo_source); g_mutex_unlock (&photo_cache->priv->sources_ht_lock); return removed; } /** * e_photo_cache_add_photo: * @photo_cache: an #EPhotoCache * @email_address: an email address * @bytes: a #GBytes containing photo data, or %NULL * * Adds a cache entry for @email_address, such that subsequent photo requests * for @email_address will yield a #GMemoryInputStream loaded with @bytes * without consulting available photo sources. * * The @bytes argument can also be %NULL to indicate no photo is available for * @email_address. Subsequent photo requests for @email_address will yield no * input stream. * * The entry may be removed without notice however, subject to @photo_cache's * internal caching policy. **/ void e_photo_cache_add_photo (EPhotoCache *photo_cache, const gchar *email_address, GBytes *bytes) { g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache)); g_return_if_fail (email_address != NULL); photo_ht_insert (photo_cache, email_address, bytes); } /** * e_photo_cache_remove_photo: * @photo_cache: an #EPhotoCache * @email_address: an email address * * Removes the cache entry for @email_address, if such an entry exists. * * Returns: %TRUE if a cache entry was found and removed **/ gboolean e_photo_cache_remove_photo (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); } /** * 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 available photo sources for a photo 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) { EAsyncClosure *closure; GAsyncResult *result; gboolean success; closure = e_async_closure_new (); e_photo_cache_get_photo ( photo_cache, email_address, cancellable, e_async_closure_callback, closure); result = e_async_closure_wait (closure); success = e_photo_cache_get_photo_finish ( photo_cache, result, out_stream, error); e_async_closure_free (closure); return success; } /** * 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 available photo sources for a photo 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; EDataCapture *data_capture; GInputStream *stream = NULL; GList *list, *link; g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache)); g_return_if_fail (email_address != NULL); /* This will be used to eavesdrop on the resulting input stream * for the purpose of adding the photo data to the photo cache. */ data_capture = e_data_capture_new (photo_cache->priv->main_context); g_signal_connect_data ( data_capture, "finished", G_CALLBACK (photo_cache_data_captured_cb), data_capture_closure_new (photo_cache, email_address), (GClosureNotify) data_capture_closure_free, 0); async_context = async_context_new (data_capture, cancellable); 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); /* Check if we have this email address already cached. */ if (photo_ht_lookup (photo_cache, email_address, &stream)) { async_context->stream = stream; /* takes ownership */ g_simple_async_result_complete_in_idle (simple); goto exit; } list = e_photo_cache_list_photo_sources (photo_cache); if (list == NULL) { g_simple_async_result_complete_in_idle (simple); goto exit; } g_mutex_lock (&async_context->lock); /* Dispatch a subtask for each photo source. */ for (link = list; link != NULL; link = g_list_next (link)) { EPhotoSource *photo_source; AsyncSubtask *async_subtask; photo_source = E_PHOTO_SOURCE (link->data); async_subtask = async_subtask_new (photo_source, simple); g_hash_table_add ( async_context->subtasks, async_subtask_ref (async_subtask)); e_photo_source_get_photo ( photo_source, email_address, async_subtask->cancellable, photo_cache_async_subtask_done_cb, async_subtask_ref (async_subtask)); async_subtask_unref (async_subtask); } g_mutex_unlock (&async_context->lock); g_list_free_full (list, (GDestroyNotify) g_object_unref); /* Check if we were cancelled while dispatching subtasks. */ if (g_cancellable_is_cancelled (cancellable)) async_context_cancel_subtasks (async_context); exit: g_object_unref (simple); g_object_unref (data_capture); } /** * 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) { if (async_context->stream != NULL) *out_stream = g_object_ref (async_context->stream); else *out_stream = NULL; } return TRUE; }