/*
* 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 <http://www.gnu.org/licenses/>
*
*/
/**
* 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 <string.h>
#include <libebackend/libebackend.h>
#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;
/* 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, error);
if (client == NULL) {
success = FALSE;
break;
}
success = e_book_client_get_contacts_sync (
E_BOOK_CLIENT (client), book_query_string,
&contact_list, cancellable, error);
g_object_unref (client);
if (!success) {
g_warn_if_fail (contact_list == NULL);
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_photo 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);
}