/*
* 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.
*
* 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 General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* 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 <string.h>
#include <libebackend/libebackend.h>
#include <e-util/e-data-capture.h>
#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_hash_table_destroy (priv->sources_ht);
g_mutex_clear (&priv->photo_ht_lock);
g_mutex_clear (&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;
}