[folks] Bug 650414 — Need better APIs to handle image data



commit 3862f876a9bef82f4fcf838a00c81ef5c57ae353
Author: Philip Withnall <philip tecnocode co uk>
Date:   Mon Jun 13 10:54:23 2011 +0100

    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

 NEWS                                        |    3 +
 backends/eds/lib/Makefile.am                |    1 +
 backends/eds/lib/edsf-persona-store.vala    |   89 +++------
 backends/eds/lib/edsf-persona.vala          |  155 +++++++--------
 backends/eds/lib/memory-icon.vala           |  133 ++++++++++++
 backends/libsocialweb/lib/swf-persona.vala  |   14 +-
 backends/telepathy/lib/tpf-persona.vala     |   13 +-
 backends/tracker/lib/trf-persona-store.vala |   56 ++++-
 backends/tracker/lib/trf-persona.vala       |   24 ++-
 folks/Makefile.am                           |    1 +
 folks/avatar-cache.vala                     |  222 ++++++++++++++++++++
 folks/avatar-details.vala                   |    7 +-
 folks/individual.vala                       |    8 +-
 tests/eds/add-persona.vala                  |   23 +--
 tests/eds/avatar-details.vala               |   20 +--
 tests/eds/set-avatar.vala                   |   23 +--
 tests/folks/Makefile.am                     |    8 +
 tests/folks/avatar-cache.vala               |  301 +++++++++++++++++++++++++++
 tests/tracker/add-persona.vala              |    9 +-
 tests/tracker/avatar-details-interface.vala |   34 +---
 tests/tracker/avatar-updates.vala           |   27 ++-
 tests/tracker/set-avatar.vala               |    6 +-
 tools/inspect/utils.vala                    |   16 ++-
 23 files changed, 915 insertions(+), 278 deletions(-)
---
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<string, E.ContactField> _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 <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: */
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<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)
         {
-          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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+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<string, Value?> c1 = new Gee.HashMap<string, Value?> ();
       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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip tecnocode co uk>
+ */
+
+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<string, bool> _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<string, string> c1 = new Gee.HashMap<string, string> ();
       this._initial_fullname = "persona #1";
-      this._initial_avatar = "file:///tmp/avatar-01";
+      this._initial_avatar_uri = "file:///tmp/avatar-01";
       this._contact_urn = "<urn:contact001>";
-      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<string, string> c1 = new Gee.HashMap<string, string> ();
       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")



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]