Commit 3862f876 authored by Philip Withnall's avatar Philip Withnall
Browse files

Bug 650414 — Need better APIs to handle image data

Change AvatarDetails.avatar to have type LoadableIcon. By introducing
a libfolks-wide avatar cache, propagate this change to all the backends.

This breaks the API of AvatarDetails.

Closes: bgo#650414
parent 7fcf7cdb
......@@ -21,6 +21,7 @@ Bugs fixed:
* Bug 645549 — Add a way to get the individual from a persona
* Bug 650422 — Add API for easily checking whether details are writeable
* Bug 655019 — Don't notify twice for nickname changes
* Bug 650414 — Need better APIs to handle image data
API changes:
* Swf.Persona retains and exposes its libsocialweb Contact
......@@ -35,6 +36,8 @@ API changes:
Persona subclasses
* Make BirthdayDetails.calendar_event_id nullable
* Make Folks.Utils public and add Gee structure equality functions
* AvatarDetails.avatar is now of type LoadableIcon?
* Add AvatarCache class
Overview of changes from libfolks 0.5.1 to libfolks 0.5.2
=========================================================
......
......@@ -29,6 +29,7 @@ libfolks_eds_la_vala.stamp:
folks_eds_valasources = \
edsf-persona.vala \
edsf-persona-store.vala \
memory-icon.vala \
$(NULL)
libfolks_eds_la_SOURCES = \
......
......@@ -242,7 +242,7 @@ public class Edsf.PersonaStore : Folks.PersonaStore
}
else if (k == Folks.PersonaStore.detail_key (PersonaDetail.AVATAR))
{
var avatar = (File) v.get_object ();
var avatar = (LoadableIcon?) v.get_object ();
yield this._set_contact_avatar (contact, avatar);
}
else if (k == Folks.PersonaStore.detail_key (
......@@ -471,7 +471,7 @@ public class Edsf.PersonaStore : Folks.PersonaStore
}
}
internal async void _set_avatar (Edsf.Persona persona, File? avatar)
internal async void _set_avatar (Edsf.Persona persona, LoadableIcon? avatar)
{
/* Return early if there will be no change */
if ((persona.avatar == null && avatar == null) ||
......@@ -479,50 +479,6 @@ public class Edsf.PersonaStore : Folks.PersonaStore
{
return;
}
else
{
if (persona.avatar != null && avatar != null)
{
try
{
var persona_avatar_input = yield persona.avatar.read_async ();
var persona_avatar_info =
yield persona_avatar_input.query_info_async (
FILE_ATTRIBUTE_STANDARD_SIZE, FileQueryInfoFlags.NONE);
var persona_avatar_size =
persona_avatar_info.get_attribute_uint32 (
FILE_ATTRIBUTE_STANDARD_SIZE);
var avatar_input = yield avatar.read_async ();
var avatar_info = yield avatar_input.query_info_async (
FILE_ATTRIBUTE_STANDARD_SIZE, FileQueryInfoFlags.NONE);
var avatar_size = avatar_info.get_attribute_uint32 (
FILE_ATTRIBUTE_STANDARD_SIZE);
if (persona_avatar_size == avatar_size)
{
var persona_avatar_data = new uint8[persona_avatar_size];
var avatar_data = new uint8[avatar_size];
yield persona_avatar_input.read_async (
persona_avatar_data);
yield avatar_input.read_async (avatar_data);
var persona_avatar_sum = Checksum.compute_for_data (
ChecksumType.MD5, persona_avatar_data);
var avatar_sum = Checksum.compute_for_data (
ChecksumType.MD5, avatar_data);
if (persona_avatar_sum == avatar_sum)
return;
}
}
catch (GLib.Error e1)
{
warning ("Failed to read an avatar file for comparison: %s",
e1.message);
}
}
}
try
{
......@@ -612,27 +568,40 @@ public class Edsf.PersonaStore : Folks.PersonaStore
}
private async void _set_contact_avatar (E.Contact contact,
File? avatar)
LoadableIcon? avatar)
{
try
{
uint8[] photo_content;
var cp = new ContactPhoto ();
var uid = Folks.Persona.build_uid (BACKEND_NAME, this.id,
(string) Edsf.Persona._get_property_from_contact (contact, "id"));
if (avatar != null)
var cache = AvatarCache.dup ();
if (avatar != null)
{
try
{
yield avatar.load_contents_async (null, out photo_content);
// Cache the avatar so that it has a URI
var uri = yield cache.store_avatar (uid, avatar);
cp.type = ContactPhotoType.INLINED;
cp.set_inlined (photo_content);
}
// Set the avatar on the contact
var cp = new ContactPhoto ();
cp.type = ContactPhotoType.URI;
cp.set_uri (uri);
contact.set (E.Contact.field_id ("photo"), cp);
contact.set (ContactField.PHOTO, cp);
}
catch (GLib.Error e1)
{
warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
uid, e1.message);
}
}
catch (GLib.Error e_avatar)
else
{
GLib.warning ("Can't load avatar %s: %s\n\n", avatar.get_path (),
e_avatar.message);
// Delete any old avatar from the cache, ignoring errors
try
{
yield cache.remove_avatar (uid);
}
catch (GLib.Error e2) {}
}
}
......
......@@ -211,7 +211,7 @@ public class Edsf.Persona : Folks.Persona,
get { return this._writeable_properties; }
}
private File _avatar;
private LoadableIcon? _avatar = null;
/**
* An avatar for the Persona.
*
......@@ -219,7 +219,7 @@ public class Edsf.Persona : Folks.Persona,
*
* @since 0.5.UNRELEASED
*/
public File avatar
public LoadableIcon? avatar
{
get { return this._avatar; }
set
......@@ -623,49 +623,90 @@ public class Edsf.Persona : Folks.Persona,
}
}
private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? p)
{
if (p == null)
{
return null;
}
switch (p.type)
{
case ContactPhotoType.URI:
if (p.get_uri () == null)
{
return null;
}
return new FileIcon (File.new_for_uri (p.get_uri ()));
case ContactPhotoType.INLINED:
if (p.get_mime_type () == null || p.get_inlined () == null)
{
return null;
}
return new Edsf.MemoryIcon (p.get_mime_type (), p.get_inlined ());
default:
return null;
}
}
private void _update_avatar ()
{
string filename = this.uid.delimit (Path.DIR_SEPARATOR.to_string (), '-');
string cached_avatar_path = GLib.Path.build_filename (
GLib.Environment.get_user_cache_dir (), "folks",
"avatars", filename);
E.ContactPhoto? p = (E.ContactPhoto) this._get_property ("photo");
this._avatar = File.new_for_path (cached_avatar_path);
var cache = AvatarCache.dup ();
var cache_uri = cache.build_uri_for_avatar (this.uid);
if (p != null)
/* Check the avatar isn't being set by our PersonaStore; if it is, just
* notify the property and bail. This avoids circular updates to the
* cache. */
if (p != null &&
p.type == ContactPhotoType.URI && p.get_uri () == cache_uri)
{
var content_old = this.get_avatar_content ();
var content_new = this._get_avatar_content_from_contact (p);
this.notify_property ("avatar");
return;
}
if (content_old != content_new)
// Convert the ContactPhoto to a LoadableIcon and store or update it.
var new_avatar = this._contact_photo_to_loadable_icon (p);
if (this._avatar != null && new_avatar == null)
{
// Remove the old cached avatar, ignoring errors.
cache.remove_avatar.begin (this.uid, (obj, res) =>
{
try
{
this._avatar.replace_contents (content_new,
content_new.length,
null, false, FileCreateFlags.REPLACE_DESTINATION,
null);
this.notify_property ("avatar");
cache.remove_avatar.end (res);
}
catch (GLib.Error e)
{
GLib.warning ("Can't write avatar: %s\n", e.message);
}
}
catch (GLib.Error e1) {}
this._avatar = new_avatar;
this.notify_property ("avatar");
});
}
else
else if ((this.avatar == null && new_avatar != null) ||
(this.avatar != null && new_avatar != null &&
this._avatar.equal (new_avatar) == false))
{
try
{
this._avatar.delete ();
}
catch (GLib.Error e) {}
finally
// Store the new avatar in the cache.
cache.store_avatar.begin (this.uid, new_avatar, (obj, res) =>
{
this._avatar = null;
try
{
cache.store_avatar.end (res);
}
catch (GLib.Error e2)
{
warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
this.uid, e2.message);
new_avatar = null; /* failure */
}
this._avatar = new_avatar;
this.notify_property ("avatar");
}
});
}
}
......@@ -764,60 +805,6 @@ public class Edsf.Persona : Folks.Persona,
}
}
/**
* Get the avatars content
*
* @since 0.5.UNRELEASED
*/
public string get_avatar_content ()
{
string content = "";
if (this._avatar != null &&
this._avatar.query_exists ())
{
try
{
uint8[] content_temp;
this._avatar.load_contents (null, out content_temp);
content = (string) content_temp;
}
catch (GLib.Error e)
{
GLib.warning ("Can't compare avatars: %s\n", e.message);
}
}
return content;
}
private string _get_avatar_content_from_contact (E.ContactPhoto p)
{
string content = "";
if (p.type == ContactPhotoType.INLINED)
{
content = (string) p.get_inlined ();
}
else if (p.type == ContactPhotoType.URI)
{
try
{
uint8[] temp_content;
var file = File.new_for_uri (p.get_uri ());
file.load_contents (null, out temp_content);
content = (string) temp_content;
}
catch (GLib.Error e)
{
GLib.warning ("Couldn't load content for avatar: %s\n",
p.get_uri ());
}
}
return content;
}
/**
* build a table of im protocols / im protocol aliases
*/
......
/*
* Copyright (C) 2011 Philip Withnall
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Philip Withnall <philip@tecnocode.co.uk>
*/
using GLib;
/**
* A wrapper around a blob of image data (with an associated content type) which
* presents it as a {@link LoadableIcon}. This allows inlined avatars to be
* returned as {@link LoadableIcon}s.
*
* @since UNRELEASED
*/
internal class Edsf.MemoryIcon : Object, Icon, LoadableIcon
{
private uint8[] _image_data;
private string _image_type;
/**
* Construct a new in-memory icon.
*
* @param image_type the content type of the image
* @param image_data the binary data of the image
* @since UNRELEASED
*/
public MemoryIcon (string image_type, uint8[] image_data)
{
this._image_data = image_data;
this._image_type = image_type;
}
/**
* Decide whether two {@link MemoryIcon} instances are equal. This compares
* their image types and image data, and only returns `true` if both are
* identical.
*
* @param icon2 the {@link MemoryIcon} instance to compare against
* @return `true` if the instances are equal, `false` otherwise
* @since UNRELEASED
*/
public bool equal (Icon icon2)
{
// This type check be taken care of by the interface wrapper.
var icon = icon2 as MemoryIcon;
assert (icon != null);
return (this._image_data.length == icon._image_data.length &&
this._image_type == icon._image_type &&
Memory.cmp (this._image_data, icon._image_data,
this._image_data.length) == 0);
}
/**
* Calculate a hash value of the image type and data, suitable for use as a
* hash table key. This is not a cryptographic hash.
*
* @return hash value over the image type and data
* @since UNRELEASED
*/
public uint hash ()
{
/* Implementation based on g_str_hash() from GLib. We initialise the hash
* with the g_str_hash() hash of the image type (which itself is
* initialised with the magic number in GLib thought up by cleverer people
* than myself), then add each byte in the image data to the hash value
* by multiplying the hash value by 33 and adding the image data, as is
* done on all bytes in g_str_hash(). I leave the rationale for this
* calculation to the author of g_str_hash().
*
* Basically, this is just a nul-safe version of g_str_hash(). Which is
* calculated over both the image type and image data. */
uint hash = this._image_type.hash ();
for (uint i = 0; i < this._image_data.length; i++)
{
hash = (hash << 5) + hash + this._image_data[i];
}
return hash;
}
/**
* Build an input stream for loading the image data. This will return
* without blocking on I/O.
*
* @param size the square dimensions to output the image at (unused), or -1
* @param type return location for the content type of the image, or `null`
* @param cancellable optional {@link GLib.Cancellable}, or `null`
* @return an input stream providing access to the image data
* @since UNRELEASED
*/
public InputStream load (int size, out string? type,
Cancellable? cancellable = null)
{
type = this._image_type;
return new MemoryInputStream.from_data (this._image_data, free);
}
/**
* Asynchronously build an input stream for loading the image data. This
* will complete without blocking on I/O.
*
* @param size the square dimensions to output the image at (unused), or -1
* @param cancellable optional {@link GLib.Cancellable}, or `null`
* @param type return location for the content type of the image, or `null`
* @return an input stream providing access to the image data
* @since UNRELEASED
*/
public async InputStream load_async (int size,
GLib.Cancellable? cancellable, out string? type)
{
type = this._image_type;
return new MemoryInputStream.from_data (this._image_data, free);
}
}
/* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */
......@@ -66,8 +66,10 @@ public class Swf.Persona : Folks.Persona,
* An avatar for the Persona.
*
* See {@link Folks.AvatarOwner.avatar}.
*
* @since UNRELEASED
*/
public File avatar { get; private set; }
public LoadableIcon? avatar { get; private set; }
/**
* {@inheritDoc}
......@@ -273,9 +275,13 @@ public class Swf.Persona : Folks.Persona,
var avatar_path = contact.get_value ("icon");
if (avatar_path != null)
{
var avatar_file = File.new_for_path (avatar_path);
if (this.avatar != avatar_file)
this.avatar = avatar_file;
var icon = new FileIcon (File.new_for_path (avatar_path));
if (this.avatar == null || !this.avatar.equal (icon))
this.avatar = icon;
}
else
{
this.avatar = null;
}
var structured_name = new StructuredName.simple (
......
......@@ -70,8 +70,10 @@ public class Tpf.Persona : Folks.Persona,
* An avatar for the Persona.
*
* See {@link Folks.AvatarDetails.avatar}.
*
* @since UNRELEASED
*/
public File avatar { get; private set; }
public LoadableIcon? avatar { get; private set; }
/**
* The Persona's presence type.
......@@ -416,7 +418,12 @@ public class Tpf.Persona : Folks.Persona,
private void _contact_notify_avatar ()
{
var file = this.contact.avatar_file;
if (this.avatar != file)
this.avatar = file;
Icon? icon = null;
if (file != null)
icon = new FileIcon (file);
if (this.avatar == null || icon == null || !this.avatar.equal (icon))
this.avatar = (LoadableIcon) icon;
}
}
......@@ -401,6 +401,10 @@ public class Trf.PersonaStore : Folks.PersonaStore
public override async Folks.Persona? add_persona_from_details (
HashTable<string, Value?> details) throws Folks.PersonaStoreError
{
/* We have to set the avatar after pushing the new persona to Tracker,
* as we need a UID so that we can cache the avatar. */
LoadableIcon? avatar = null;
var builder = new Tracker.Sparql.Builder.update ();
builder.insert_open (null);
builder.subject ("_:p");
......@@ -451,15 +455,13 @@ public class Trf.PersonaStore : Folks.PersonaStore
}
else if (k == Folks.PersonaStore.detail_key (PersonaDetail.AVATAR))
{
var avatar = (File) v.get_object ();
builder.subject ("_:photo");
builder.predicate ("a");
builder.object ("nfo:Image, nie:DataObject");
builder.predicate (Trf.OntologyDefs.NIE_URL);
builder.object_string (avatar.get_uri ());
builder.subject ("_:p");
builder.predicate (Trf.OntologyDefs.NCO_PHOTO);
builder.object ("_:photo");
/* Update the avatar which we'll set later (once we have the
* persona's UID) */
var new_avatar = (LoadableIcon) v.get_object ();
if (new_avatar != null)
{
avatar = new_avatar;
}
}
else if (k == Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY))
{
......@@ -708,6 +710,12 @@ public class Trf.PersonaStore : Folks.PersonaStore
}
}
// Set the avatar on the persona now that we know the persona's UID
if (ret != null && avatar != null)
{
yield this._set_avatar (ret, avatar);
}
return ret;
}
......@@ -1532,7 +1540,7 @@ public class Trf.PersonaStore : Folks.PersonaStore
avatar_url = yield this._get_property (e.object_id,
Trf.OntologyDefs.NIE_URL, Trf.OntologyDefs.NFO_IMAGE);
}
p._set_avatar (avatar_url);
p._set_avatar_from_uri (avatar_url);
}
else if (e.pred_id == this._prefix_tracker_id.get
(Trf.OntologyDefs.NAO_PROPERTY))
......@@ -2166,7 +2174,7 @@ public class Trf.PersonaStore : Folks.PersonaStore
}
internal async void _set_avatar (Folks.Persona persona,
File? avatar)
LoadableIcon? avatar)
{
const string query_d = "DELETE {" +
" ?c " + Trf.OntologyDefs.NCO_PHOTO + " ?p " +
......@@ -2196,10 +2204,34 @@ public class Trf.PersonaStore : Folks.PersonaStore
this._delete_resource ("<%s>".printf (image_urn));
string query = query_d.printf (p_id);
var cache = AvatarCache.dup ();
if (avatar != null)