From: Philip Withnall Date: Mon, 13 Jun 2011 09:54:23 +0000 (+0100) Subject: Bug 650414 — Need better APIs to handle image data X-Git-Tag: FOLKS_0_6_0~75 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=3862f876a9bef82f4fcf838a00c81ef5c57ae353;p=platform%2Fupstream%2Ffolks.git 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 --- diff --git a/NEWS b/NEWS index 3084178..07c095b 100644 --- a/NEWS +++ b/NEWS @@ -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 ========================================================= diff --git a/backends/eds/lib/Makefile.am b/backends/eds/lib/Makefile.am index 53905b3..4dc22bf 100644 --- a/backends/eds/lib/Makefile.am +++ b/backends/eds/lib/Makefile.am @@ -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 = \ diff --git a/backends/eds/lib/edsf-persona-store.vala b/backends/eds/lib/edsf-persona-store.vala index b3ba9ff..ae539fa 100644 --- a/backends/eds/lib/edsf-persona-store.vala +++ b/backends/eds/lib/edsf-persona-store.vala @@ -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) {} } } diff --git a/backends/eds/lib/edsf-persona.vala b/backends/eds/lib/edsf-persona.vala index d020811..291ab0d 100644 --- a/backends/eds/lib/edsf-persona.vala +++ b/backends/eds/lib/edsf-persona.vala @@ -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"); - } + }); } } @@ -765,60 +806,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 */ internal static HashTable _get_im_eds_map () diff --git a/backends/eds/lib/memory-icon.vala b/backends/eds/lib/memory-icon.vala new file mode 100644 index 0000000..16dab82 --- /dev/null +++ b/backends/eds/lib/memory-icon.vala @@ -0,0 +1,133 @@ +/* + * 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 . + * + * Authors: + * Philip Withnall + */ + +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: */ diff --git a/backends/libsocialweb/lib/swf-persona.vala b/backends/libsocialweb/lib/swf-persona.vala index 9dd8a76..f075cb9 100644 --- a/backends/libsocialweb/lib/swf-persona.vala +++ b/backends/libsocialweb/lib/swf-persona.vala @@ -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 ( diff --git a/backends/telepathy/lib/tpf-persona.vala b/backends/telepathy/lib/tpf-persona.vala index d31a4c3..18a06d9 100644 --- a/backends/telepathy/lib/tpf-persona.vala +++ b/backends/telepathy/lib/tpf-persona.vala @@ -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; } } diff --git a/backends/tracker/lib/trf-persona-store.vala b/backends/tracker/lib/trf-persona-store.vala index 47b4c29..2ac30cf 100644 --- a/backends/tracker/lib/trf-persona-store.vala +++ b/backends/tracker/lib/trf-persona-store.vala @@ -401,6 +401,10 @@ public class Trf.PersonaStore : Folks.PersonaStore public override async Folks.Persona? add_persona_from_details ( HashTable 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) { - query += query_i.printf (avatar.get_uri (), p_id); + try + { + // Cache the avatar so that it has a URI + var uri = yield cache.store_avatar (persona.uid, avatar); + + // Add the avatar to the query + query += query_i.printf (uri , p_id); + } + catch (GLib.Error e1) + { + warning ("Couldn't cache avatar for Trf.Persona '%s': %s", + persona.uid, e1.message); + } + } + else + { + // Delete any old avatar from the cache, ignoring errors + try + { + yield cache.remove_avatar (persona.uid); + } + catch (GLib.Error e2) {} } + yield this._tracker_update (query, "_set_avatar"); } diff --git a/backends/tracker/lib/trf-persona.vala b/backends/tracker/lib/trf-persona.vala index de06745..1e8777c 100644 --- a/backends/tracker/lib/trf-persona.vala +++ b/backends/tracker/lib/trf-persona.vala @@ -135,13 +135,15 @@ public class Trf.Persona : Folks.Persona, get { return this._writeable_properties; } } - private File _avatar; + private LoadableIcon? _avatar = null; /** * An avatar for the Persona. * * See {@link Folks.Avatar.avatar}. + * + * @since UNRELEASED */ - public File avatar + public LoadableIcon? avatar { get { return this._avatar; } public set @@ -817,17 +819,25 @@ public class Trf.Persona : Folks.Persona, { string avatar_url = this._cursor.get_string ( Trf.Fields.AVATAR_URL).dup (); - this._set_avatar (avatar_url); + this._set_avatar_from_uri (avatar_url); } - internal bool _set_avatar (string? avatar_url) + internal bool _set_avatar_from_uri (string? avatar_url) { - File _avatar = null; + LoadableIcon _avatar = null; if (avatar_url != null && avatar_url != "") { - _avatar = File.new_for_uri (avatar_url); + _avatar = new FileIcon (File.new_for_uri (avatar_url)); } - this._avatar = _avatar; + + this._set_avatar (_avatar); + + return true; + } + + internal bool _set_avatar (LoadableIcon? avatar) + { + this._avatar = avatar; this.notify_property ("avatar"); return true; } diff --git a/folks/Makefile.am b/folks/Makefile.am index 6c66c18..c145087 100644 --- a/folks/Makefile.am +++ b/folks/Makefile.am @@ -40,6 +40,7 @@ libfolks_la_SOURCES = \ debug.vala \ utils.vala \ potential-match.vala \ + avatar-cache.vala \ $(NULL) libfolks_la_VALAFLAGS = \ diff --git a/folks/avatar-cache.vala b/folks/avatar-cache.vala new file mode 100644 index 0000000..8c0f4de --- /dev/null +++ b/folks/avatar-cache.vala @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2011 Collabora Ltd. + * + * 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 . + * + * Authors: + * Philip Withnall + */ + +using GLib; + +/** + * A singleton persistent cache object for avatars used across backends in + * folks. Avatars may be added to the cache, and referred to by a persistent + * URI from that point onwards. + * + * @since UNRELEASED + */ +public class Folks.AvatarCache : Object +{ + private static weak AvatarCache _instance = null; /* needs to be locked */ + private File _cache_directory; + + /** + * Private constructor for an instance of the avatar cache. The singleton + * instance should be retrieved by calling {@link AvatarCache.dup()} instead. + * + * @since UNRELEASED + */ + private AvatarCache () + { + this._cache_directory = + File.new_for_path (Environment.get_user_cache_dir ()) + .get_child ("folks") + .get_child ("avatars"); + } + + /** + * Create or return the singleton {@link AvatarCache} class instance. + * If the instance doesn't exist already, it will be created. + * + * This function is thread-safe. + * + * @return Singleton {@link AvatarCache} instance + * @since UNRELEASED + */ + public static AvatarCache dup () + { + lock (AvatarCache._instance) + { + var retval = AvatarCache._instance; + + if (retval == null) + { + /* use an intermediate variable to force a strong reference */ + retval = new AvatarCache (); + AvatarCache._instance = retval; + } + + return retval; + } + } + + ~AvatarCache () + { + /* Manually clear the singleton _instance */ + lock (AvatarCache._instance) + { + AvatarCache._instance = null; + } + } + + /** + * Fetch an avatar from the cache by its globally unique ID. + * + * @param id the globally unique ID for the avatar + * @return Avatar from the cache, or `null` if it doesn't exist in the cache + * @throws GLib.Error if checking for existence of the cache file failed + * @since UNRELEASED + */ + public async LoadableIcon? load_avatar (string id) throws GLib.Error + { + var avatar_file = this._get_avatar_file (id); + + // Return null if the avatar doesn't exist + if (avatar_file.query_exists () == false) + { + return null; + } + + return new FileIcon (avatar_file); + } + + /** + * Store an avatar in the cache, assigning the given globally unique ID to it, + * which can later be used to load and remove the avatar from the cache. For + * example, this ID could be the UID of a persona. The URI of the cached + * avatar file will be returned. + * + * @param id the globally unique ID for the avatar + * @param avatar the avatar data to cache + * @return a URI for the file storing the cached avatar + * @throws GLib.Error if the avatar data couldn't be loaded, or if creating + * the avatar directory or cache file failed + * @since UNRELEASED + */ + public async string store_avatar (string id, LoadableIcon avatar) + throws GLib.Error + { + var dest_avatar_file = this._get_avatar_file (id); + + // Copy the icon data into a file + while (true) + { + InputStream src_avatar_stream = + yield avatar.load_async (-1, null, null); + + try + { + OutputStream dest_avatar_stream = + yield dest_avatar_file.replace_async (null, false, + FileCreateFlags.PRIVATE); + + yield dest_avatar_stream.splice_async (src_avatar_stream, + OutputStreamSpliceFlags.CLOSE_SOURCE | + OutputStreamSpliceFlags.CLOSE_TARGET); + + break; + } + catch (GLib.Error e) + { + /* If the parent directory wasn't found, create it and loop + * round to try again. */ + if (e is IOError.NOT_FOUND) + { + this._create_cache_directory (); + continue; + } + + throw e; + } + } + + return this.build_uri_for_avatar (id); + } + + /** + * Remove an avatar from the cache, if it exists in the cache. If the avatar + * exists in the cache but there is a problem in removing it, an + * {@link IOError} will be thrown. + * + * @param id the globally unique ID for the avatar + * @throws GLib.Error if deleting the cache file failed + * @since UNRELEASED + */ + public async void remove_avatar (string id) throws GLib.Error + { + var avatar_file = this._get_avatar_file (id); + try + { + avatar_file.delete (null); + } + catch (GLib.Error e) + { + // Ignore file not found errors + if (!(e is IOError.NOT_FOUND)) + { + throw e; + } + } + } + + /** + * Build the URI of an avatar file in the cache from a globally unique ID. + * This will always succeed, even if the avatar doesn't exist in the cache. + * + * @param id the globally unique ID for the avatar + * @return URI of the avatar file with the given globally unique ID + * @since UNRELEASED + */ + public string build_uri_for_avatar (string id) + { + return this._get_avatar_file (id).get_uri (); + } + + private File _get_avatar_file (string id) + { + var escaped_uri = Uri.escape_string (id, "", false); + var file = this._cache_directory.get_child (escaped_uri); + + assert (file.has_parent (this._cache_directory) == true); + + return file; + } + + private void _create_cache_directory () throws GLib.Error + { + try + { + this._cache_directory.make_directory_with_parents (); + } + catch (GLib.Error e) + { + // Ignore errors caused by the directory existing already + if (!(e is IOError.EXISTS)) + { + throw e; + } + } + } +} diff --git a/folks/avatar-details.vala b/folks/avatar-details.vala index f9732b9..135cfee 100644 --- a/folks/avatar-details.vala +++ b/folks/avatar-details.vala @@ -30,7 +30,10 @@ public interface Folks.AvatarDetails : Object * An avatar for the contact. * * An avatar is a small image file which represents the contact. It may be - * `null` if unset. + * `null` if unset. Otherwise, the image data may be asynchronously loaded + * using the methods of the {@link LoadableIcon} implementation. + * + * @since UNRELEASED */ - public abstract File avatar { get; set; } + public abstract LoadableIcon? avatar { get; set; } } diff --git a/folks/individual.vala b/folks/individual.vala index 435a299..1c964e5 100644 --- a/folks/individual.vala +++ b/folks/individual.vala @@ -115,8 +115,10 @@ public class Folks.Individual : Object, /** * {@inheritDoc} + * + * @since UNRELEASED */ - public File avatar { get; private set; } + public LoadableIcon? avatar { get; private set; } /** * {@inheritDoc} @@ -1013,7 +1015,7 @@ public class Folks.Individual : Object, private void _update_avatar () { - File avatar = null; + LoadableIcon? avatar = null; foreach (var p in this._persona_set) { @@ -1026,7 +1028,7 @@ public class Folks.Individual : Object, } /* only notify if the value has changed */ - if (this.avatar != avatar) + if (this.avatar == null || !this.avatar.equal (avatar)) this.avatar = avatar; } diff --git a/tests/eds/add-persona.vala b/tests/eds/add-persona.vala index acc0660..f4fc660 100644 --- a/tests/eds/add-persona.vala +++ b/tests/eds/add-persona.vala @@ -164,8 +164,8 @@ public class AddPersonaTests : Folks.TestCase Folks.PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES), (owned) v2); - Value? v3 = Value (typeof (File)); - File avatar = File.new_for_path (this._avatar_path); + Value? v3 = Value (typeof (LoadableIcon)); + var avatar = new FileIcon (File.new_for_path (this._avatar_path)); v3.set_object (avatar); details.insert (Folks.PersonaStore.detail_key (PersonaDetail.AVATAR), (owned) v3); @@ -300,24 +300,11 @@ public class AddPersonaTests : Folks.TestCase if (i.avatar != null) { - uint8[] content_a; - uint8[] content_b; - var b = File.new_for_path (this._avatar_path); + var b = new FileIcon (File.new_for_path (this._avatar_path)); - try + if (b.equal (i.avatar) == true) { - i.avatar.load_contents (null, out content_a); - b.load_contents (null, out content_b); - - if (((string) content_a) == ((string) content_b)) - { - this._properties_found.replace ("avatar", true); - } - } - catch (GLib.Error e) - { - GLib.warning ("Couldn't load avatars: %s", - e.message); + this._properties_found.replace ("avatar", true); } } diff --git a/tests/eds/avatar-details.vala b/tests/eds/avatar-details.vala index 63332ef..ae55c48 100644 --- a/tests/eds/avatar-details.vala +++ b/tests/eds/avatar-details.vala @@ -117,24 +117,12 @@ public class AvatarDetailsTests : Folks.TestCase if (i.full_name == "bernie h. innocenti") { - uint8[] content_a; - uint8[] content_b; - var b = File.new_for_path (this._avatar_path); + var b = new FileIcon (File.new_for_path (this._avatar_path)); - try + if (b.equal (i.avatar) == true) { - i.avatar.load_contents (null, out content_a); - b.load_contents (null, out content_b); - - if (((string) content_a) == ((string) content_b)) - { - this._avatars_are_equal = true; - this._main_loop.quit (); - } - } - catch (GLib.Error e) - { - GLib.warning ("couldn't load file a"); + this._avatars_are_equal = true; + this._main_loop.quit (); } } } diff --git a/tests/eds/set-avatar.vala b/tests/eds/set-avatar.vala index d12eaa7..5f17f0c 100644 --- a/tests/eds/set-avatar.vala +++ b/tests/eds/set-avatar.vala @@ -29,7 +29,7 @@ public class SetAvatarTests : Folks.TestCase private GLib.MainLoop _main_loop; private bool _found_before_update; private bool _found_after_update; - private File _avatar; + private LoadableIcon _avatar; public SetAvatarTests () { @@ -55,7 +55,7 @@ public class SetAvatarTests : Folks.TestCase Gee.HashMap c1 = new Gee.HashMap (); this._main_loop = new GLib.MainLoop (null, false); var avatar_path = Environment.get_variable ("AVATAR_FILE_PATH"); - this._avatar = File.new_for_path (avatar_path); + this._avatar = new FileIcon (File.new_for_path (avatar_path)); Value? v; this._found_before_update = false; @@ -132,23 +132,10 @@ public class SetAvatarTests : Folks.TestCase var name = (Folks.NameDetails) i; if (name.full_name == "bernie h. innocenti") { - uint8[] content_a; - uint8[] content_b; - - try - { - i.avatar.load_contents (null, out content_a); - this._avatar.load_contents (null, out content_b); - - if (((string) content_a) == ((string) content_b)) - { - this._found_after_update = true; - this._main_loop.quit (); - } - } - catch (GLib.Error e) + if (this._avatar.equal (i.avatar) == true) { - GLib.warning ("Couldn't compare avatars: %s\n", e.message); + this._found_after_update = true; + this._main_loop.quit (); } } } diff --git a/tests/folks/Makefile.am b/tests/folks/Makefile.am index 2bbaca8..8723a28 100644 --- a/tests/folks/Makefile.am +++ b/tests/folks/Makefile.am @@ -43,14 +43,17 @@ noinst_PROGRAMS = \ utils \ backend-loading \ aggregation \ + avatar-cache \ $(NULL) SESSION_CONF = $(top_builddir)/tests/lib/telepathy/contactlist/session.conf backend_store_key_file=$(top_srcdir)/tests/data/backend-store-all.ini +avatar_file=@abs_top_srcdir@/tests/data/avatar-01.jpg TESTS_ENVIRONMENT = \ GCONF_DEFAULT_SOURCE_PATH=@abs_top_srcdir@/tests/data/gconf.path \ FOLKS_BACKEND_PATH=$(BACKEND_UNINST_PATH) \ FOLKS_BACKEND_STORE_KEY_FILE_PATH=$(backend_store_key_file) \ + AVATAR_FILE_PATH=$(avatar_file) \ $(RUN_WITH_PRIVATE_BUS) \ --config-file=$(SESSION_CONF) \ -- @@ -73,6 +76,10 @@ utils_SOURCES = \ utils.vala \ $(NULL) +avatar_cache_SOURCES = \ + avatar-cache.vala \ + $(NULL) + CLEANFILES = \ *.pid \ *.address \ @@ -85,6 +92,7 @@ MAINTAINERCLEANFILES = \ aggregation_vala.stamp \ field_details_vala.stamp \ utils_vala.stamp \ + avatar_cache_vala.stamp \ $(NULL) EXTRA_DIST = \ diff --git a/tests/folks/avatar-cache.vala b/tests/folks/avatar-cache.vala new file mode 100644 index 0000000..f9d0062 --- /dev/null +++ b/tests/folks/avatar-cache.vala @@ -0,0 +1,301 @@ +/* + * 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 . + * + * Authors: + * Philip Withnall + */ + +using GLib; +using Gee; +using Folks; + +public class AvatarCacheTests : Folks.TestCase +{ + private AvatarCache _cache; + private File _cache_dir; + private LoadableIcon _avatar; + private MainLoop _main_loop; + + public AvatarCacheTests () + { + base ("AvatarCache"); + + /* Use a temporary cache directory */ + this._cache_dir = + File.new_for_path (Environment.get_tmp_dir ()). + get_child ("folks-avatar-cache-tests"); + + this.add_test ("store-and-load-avatar", this.test_store_and_load_avatar); + this.add_test ("store-avatar-overwrite", + this.test_store_avatar_overwrite); + this.add_test ("load-avatar-non-existent", + this.test_load_avatar_non_existent); + this.add_test ("remove-avatar", this.test_remove_avatar); + this.add_test ("remove-avatar-non-existent", + this.test_remove_avatar_non_existent); + this.add_test ("build-uri-for-avatar", this.test_build_uri_for_avatar); + } + + public override void set_up () + { + this._delete_cache_directory (); + Environment.set_variable ("XDG_CACHE_HOME", this._cache_dir.get_path (), + true); + + this._cache = AvatarCache.dup (); + this._avatar = + new FileIcon (File.new_for_path ( + Environment.get_variable ("AVATAR_FILE_PATH"))); + + this._main_loop = new GLib.MainLoop (null, false); + } + + public override void tear_down () + { + this._main_loop = null; + this._avatar = null; + this._cache = null; + this._delete_cache_directory (); + } + + protected void _delete_directory (File dir) throws GLib.Error + { + // Delete the files in the directory + var enumerator = + dir.enumerate_children (FILE_ATTRIBUTE_STANDARD_NAME, + FileQueryInfoFlags.NONE); + + FileInfo? file_info = enumerator.next_file (); + while (file_info != null) + { + var child_file = dir.get_child (file_info.get_name ()); + + if (child_file.query_file_type (FileQueryInfoFlags.NONE) == + FileType.DIRECTORY) + { + this._delete_directory (child_file); + } + else + { + child_file.delete (); + } + + file_info = enumerator.next_file (); + } + enumerator.close (); + + // Delete the directory itself + dir.delete (); + } + + protected void _delete_cache_directory () + { + try + { + this._delete_directory (this._cache_dir); + } + catch (Error e) + { + // Ignore it + } + } + + protected bool _avatars_equal (LoadableIcon avatar1, + LoadableIcon avatar2) + { + if (avatar1.equal (avatar2) == true) + { + return true; + } + + // Compare content instead. + try + { + var stream1 = avatar1.load (-1, null, null); + var stream2 = avatar2.load (-1, null, null); + + var content1 = new uint8[512]; + var content2 = new uint8[512]; + + ssize_t read1 = -1; + do + { + read1 = stream1.read (content1, null); + var read2 = stream2.read (content2, null); + + if (read1 != read2 || Memory.cmp (content1, content2, read1) != 0) + { + return false; + } + } + while (read1 > 0); + } + catch (GLib.Error e) + { + return false; + } + + return true; + } + + protected void _assert_store_avatar (string id, LoadableIcon avatar) + { + this._cache.store_avatar.begin (id, avatar, (obj, res) => + { + try + { + this._cache.store_avatar.end (res); + } + catch (GLib.Error e) + { + error ("Error storing avatar: %s", e.message); + } + + this._main_loop.quit (); + }); + + this._main_loop.run (); + } + + protected LoadableIcon? _assert_load_avatar (string id) + { + LoadableIcon? avatar = null; + + this._cache.load_avatar.begin (id, (obj, res) => + { + try + { + avatar = this._cache.load_avatar.end (res); + } + catch (GLib.Error e) + { + error ("Error loading avatar: %s", e.message); + } + + this._main_loop.quit (); + }); + + this._main_loop.run (); + + return avatar; + } + + protected void _assert_remove_avatar (string id) + { + this._cache.remove_avatar.begin (id, (obj, res) => + { + try + { + this._cache.remove_avatar.end (res); + } + catch (GLib.Error e) + { + error ("Error removing avatar: %s", e.message); + } + + this._main_loop.quit (); + }); + + this._main_loop.run (); + } + + public void test_store_and_load_avatar () + { + // Store the avatar. + this._assert_store_avatar ("test-store-avatar-id", this._avatar); + + // Load it again. + var avatar = this._assert_load_avatar ("test-store-avatar-id"); + + // Check the avatar's OK + assert (avatar != null); + assert (avatar is LoadableIcon); + assert (this._avatars_equal (this._avatar, avatar) == true); + } + + public void test_store_avatar_overwrite () + { + // Store the avatar twice. + this._assert_store_avatar ("test-store-avatar-ow-id", this._avatar); + this._assert_store_avatar ("test-store-avatar-ow-id", this._avatar); + + // Load it again. + var avatar = this._assert_load_avatar ("test-store-avatar-ow-id"); + + // Check the avatar's OK + assert (avatar != null); + assert (avatar is LoadableIcon); + assert (this._avatars_equal (this._avatar, avatar) == true); + } + + public void test_load_avatar_non_existent () + { + // Load a non-existent avatar. + var avatar = this._assert_load_avatar ("test-load-avatar-non-existent"); + assert (avatar == null); + } + + public void test_remove_avatar () + { + LoadableIcon? avatar = null; + + // Store the avatar. + this._assert_store_avatar ("test-remove-avatar", this._avatar); + + // Check it's been stored OK. + avatar = this._assert_load_avatar ("test-remove-avatar"); + assert (avatar != null); + + // Remove it. + this._assert_remove_avatar ("test-remove-avatar"); + + // Check it's been removed OK. + avatar = this._assert_load_avatar ("test-remove-avatar"); + assert (avatar == null); + } + + public void test_remove_avatar_non_existent () + { + // Check the avatar doesn't exist. + var avatar = this._assert_load_avatar ("test-remove-avatar-non-existent"); + assert (avatar == null); + + // Attempt to remove it. + this._assert_remove_avatar ("test-remove-avatar-non-existent"); + } + + public void test_build_uri_for_avatar () + { + // Basic checks on the constructed URI. + var uri = this._cache.build_uri_for_avatar ("test-id"); + assert (uri != null); + assert (Uri.parse_scheme (uri) != null); /* basic check for validity */ + } +} + +public int main (string[] args) +{ + Test.init (ref args); + + TestSuite root = TestSuite.get_root (); + root.add_suite (new AvatarCacheTests ().get_suite ()); + + Test.run (); + + return 0; +} + +/* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */ diff --git a/tests/tracker/add-persona.vala b/tests/tracker/add-persona.vala index 3c6fb36..328cf0e 100644 --- a/tests/tracker/add-persona.vala +++ b/tests/tracker/add-persona.vala @@ -34,6 +34,7 @@ public class AddPersonaTests : Folks.TestCase private string _given_name; private HashTable _properties_found; private string _persona_iid; + private LoadableIcon _avatar; private string _file_uri; private string _birthday; private DateTime _bday; @@ -85,6 +86,7 @@ public class AddPersonaTests : Folks.TestCase this._given_name = "given"; this._persona_iid = ""; this._file_uri = "file:///tmp/some-avatar.jpg"; + this._avatar = new FileIcon (File.new_for_uri (this._file_uri)); this._birthday = "2001-10-26T20:32:52Z"; this._email_1 = "someone-1@example.org"; this._email_2 = "someone-2@example.org"; @@ -202,9 +204,8 @@ public class AddPersonaTests : Folks.TestCase Folks.PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME), (owned) v4); - Value? v5 = Value (typeof (File)); - File avatar = File.new_for_uri (this._file_uri); - v5.set_object (avatar); + Value? v5 = Value (typeof (LoadableIcon)); + v5.set_object (this._avatar); details.insert (Folks.PersonaStore.detail_key (PersonaDetail.AVATAR), (owned) v5); @@ -403,7 +404,7 @@ public class AddPersonaTests : Folks.TestCase } if (i.avatar != null && - i.avatar.get_uri () == this._file_uri) + i.avatar.equal (this._avatar)) this._properties_found.replace ("avatar", true); if (i.birthday != null && diff --git a/tests/tracker/avatar-details-interface.vala b/tests/tracker/avatar-details-interface.vala index 77c92f5..fe6cba8 100644 --- a/tests/tracker/avatar-details-interface.vala +++ b/tests/tracker/avatar-details-interface.vala @@ -112,8 +112,8 @@ public class AvatarDetailsInterfaceTests : Folks.TestCase if (i.avatar != null) { var src_avatar = File.new_for_uri (this._avatar_uri); - this._avatars_are_equal = - this._compare_files (src_avatar, i.avatar); + var src_icon = new FileIcon (src_avatar); + this._avatars_are_equal = src_icon.equal (i.avatar); this._main_loop.quit (); } } @@ -124,36 +124,10 @@ public class AvatarDetailsInterfaceTests : Folks.TestCase { Folks.Individual individual = (Folks.Individual) individual_obj; var src_avatar = File.new_for_uri (this._avatar_uri); - this._avatars_are_equal = this._compare_files (src_avatar, - individual.avatar); + var src_icon = new FileIcon (src_avatar); + this._avatars_are_equal = src_icon.equal (individual.avatar); this._main_loop.quit (); } - - private bool _compare_files (File a, File b) - { - uint8 *content_a; - uint8 *content_b; - - try - { - a.load_contents (null, out content_a); - } - catch (GLib.Error e) - { - GLib.warning ("couldn't load file a"); - } - - try - { - b.load_contents (null, out content_b); - } - catch (GLib.Error e) - { - GLib.warning ("couldn't load file b"); - } - - return ((string) content_a) == ((string) content_b); - } } public int main (string[] args) diff --git a/tests/tracker/avatar-updates.vala b/tests/tracker/avatar-updates.vala index 998a5d7..9dfa881 100644 --- a/tests/tracker/avatar-updates.vala +++ b/tests/tracker/avatar-updates.vala @@ -28,12 +28,13 @@ public class AvatarUpdatesTests : Folks.TestCase private TrackerTest.Backend _tracker_backend; private IndividualAggregator _aggregator; private bool _updated_avatar_found; - private string _updated_avatar; + private string _updated_avatar_uri; + private LoadableIcon _updated_avatar; private string _individual_id; private GLib.MainLoop _main_loop; private bool _initial_avatar_found; private string _initial_fullname; - private string _initial_avatar; + private string _initial_avatar_uri; private string _contact_urn; private string _photo_urn; @@ -60,14 +61,16 @@ public class AvatarUpdatesTests : Folks.TestCase this._main_loop = new GLib.MainLoop (null, false); Gee.HashMap c1 = new Gee.HashMap (); this._initial_fullname = "persona #1"; - this._initial_avatar = "file:///tmp/avatar-01"; + this._initial_avatar_uri = "file:///tmp/avatar-01"; this._contact_urn = ""; - this._photo_urn = "<" + this._initial_avatar + ">"; - this._updated_avatar = "file:///tmp/avatar-02"; + this._photo_urn = "<" + this._initial_avatar_uri + ">"; + this._updated_avatar_uri = "file:///tmp/avatar-02"; + this._updated_avatar = + new FileIcon (File.new_for_uri (this._updated_avatar_uri)); c1.set (TrackerTest.Backend.URN, this._contact_urn); c1.set (Trf.OntologyDefs.NCO_FULLNAME, this._initial_fullname); - c1.set (Trf.OntologyDefs.NCO_PHOTO, this._initial_avatar); + c1.set (Trf.OntologyDefs.NCO_PHOTO, this._initial_avatar_uri); this._tracker_backend.add_contact (c1); this._tracker_backend.set_up (); @@ -123,20 +126,22 @@ public class AvatarUpdatesTests : Folks.TestCase i.notify["avatar"].connect (this._notify_avatar_cb); this._individual_id = i.id; - if (i.avatar != null && - i.avatar.get_uri () == this._initial_avatar) + var initial_avatar = + new FileIcon (File.new_for_uri (this._initial_avatar_uri)); + + if (i.avatar != null && i.avatar.equal (initial_avatar) == true) { this._initial_avatar_found = true; this._tracker_backend.remove_triplet (this._contact_urn, Trf.OntologyDefs.NCO_PHOTO, this._photo_urn); - string photo_urn_2 = "<" + this._updated_avatar; + string photo_urn_2 = "<" + this._updated_avatar_uri; photo_urn_2 += ">"; this._tracker_backend.insert_triplet (photo_urn_2, "a", "nfo:Image, nie:DataObject", Trf.OntologyDefs.NIE_URL, - this._updated_avatar); + this._updated_avatar_uri); this._tracker_backend.insert_triplet (this._contact_urn, @@ -156,7 +161,7 @@ public class AvatarUpdatesTests : Folks.TestCase return; if (i.avatar != null && - i.avatar.get_uri () == this._updated_avatar) + i.avatar.equal (this._updated_avatar)) { this._main_loop.quit (); this._updated_avatar_found = true; diff --git a/tests/tracker/set-avatar.vala b/tests/tracker/set-avatar.vala index 533c2ca..0bd9b4d 100644 --- a/tests/tracker/set-avatar.vala +++ b/tests/tracker/set-avatar.vala @@ -30,7 +30,7 @@ public class SetAvatarTests : Folks.TestCase private IndividualAggregator _aggregator; private string _persona_fullname; private string _avatar_uri; - private File _avatar; + private LoadableIcon _avatar; private bool _avatar_found; public SetAvatarTests () @@ -56,7 +56,7 @@ public class SetAvatarTests : Folks.TestCase Gee.HashMap c1 = new Gee.HashMap (); this._persona_fullname = "persona #1"; this._avatar_uri = "file:///tmp/some-avatar.jpg"; - this._avatar = File.new_for_uri (this._avatar_uri); + this._avatar = new FileIcon (File.new_for_uri (this._avatar_uri)); c1.set (Trf.OntologyDefs.NCO_FULLNAME, this._persona_fullname); this._tracker_backend.add_contact (c1); @@ -125,7 +125,7 @@ public class SetAvatarTests : Folks.TestCase Folks.Individual i = (Folks.Individual) individual_obj; if (i.full_name == this._persona_fullname) { - if (i.avatar.get_uri () == this._avatar_uri) + if (i.avatar.equal (this._avatar)) { this._avatar_found = true; this._main_loop.quit (); diff --git a/tools/inspect/utils.vala b/tools/inspect/utils.vala index ef1e5a5..a190fa9 100644 --- a/tools/inspect/utils.vala +++ b/tools/inspect/utils.vala @@ -264,9 +264,19 @@ private class Folks.Inspect.Utils else if (prop_name == "avatar") { string ret = null; - File avatar = (File) prop_value.get_object (); - if (avatar != null) - ret = avatar.get_uri (); + LoadableIcon? avatar = (LoadableIcon) prop_value.get_object (); + + if (avatar != null && + avatar is FileIcon && ((FileIcon) avatar).get_file () != null) + { + ret = "%p (file: %s)".printf (avatar, + ((FileIcon) avatar).get_file ().get_uri ()); + } + else if (avatar != null) + { + ret = "%p".printf (avatar); + } + return ret; } else if (prop_name == "im-addresses")