[gnome-shell] telepathyClient: cache avatar images



commit e83656969e6431b1bd6fb1c57742d6623da9505f
Author: Dan Winship <danw gnome org>
Date:   Wed Apr 14 10:24:14 2010 -0400

    telepathyClient: cache avatar images
    
    Cache avatars to avoid having to re-download them every session.
    
    We use a cache format that is compatible with empathy's, but we don't
    actually use empathy's. This could be changed if we wanted.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=614974

 js/misc/telepathy.js     |   19 ++++++
 js/ui/telepathyClient.js |  140 ++++++++++++++++++++++++++++------------------
 2 files changed, 104 insertions(+), 55 deletions(-)
---
diff --git a/js/misc/telepathy.js b/js/misc/telepathy.js
index fb809d7..9802c69 100644
--- a/js/misc/telepathy.js
+++ b/js/misc/telepathy.js
@@ -26,6 +26,25 @@ function pathToName(path) {
     return path.substr(1).replace('/', '.', 'g');
 };
 
+// This is tp_escape_as_identifier() from telepathy-glib
+function escapeAsIdentifier(name) {
+    if (!name)
+        return '_';
+
+    // first char is replaced with _XX if it's non-alpha,
+    // later chars are replaced with _XX if they're non-alphanumeric
+    if (name.length == 1) {
+        return name.replace(/[^a-zA-Z]/, _hexEscape);
+    } else {
+        return (name[0].replace(/[^a-zA-Z]/, _hexEscape) +
+                name.substring(1).replace(/[^a-zA-Z0-9]/g, _hexEscape));
+    }
+}
+
+function _hexEscape(ch) {
+    return '_' + ch.charCodeAt(0).toString(16);
+}
+
 // Telepathy D-Bus interface definitions. Note that most of these are
 // incomplete, and only cover the methods/properties/signals that
 // we're currently using.
diff --git a/js/ui/telepathyClient.js b/js/ui/telepathyClient.js
index 1c66ff1..1bb6123 100644
--- a/js/ui/telepathyClient.js
+++ b/js/ui/telepathyClient.js
@@ -2,6 +2,7 @@
 
 const Clutter = imports.gi.Clutter;
 const DBus = imports.dbus;
+const GLib = imports.gi.GLib;
 const Lang = imports.lang;
 const Shell = imports.gi.Shell;
 const St = imports.gi.St;
@@ -181,20 +182,32 @@ function AvatarManager() {
 AvatarManager.prototype = {
     _init: function() {
         this._connections = {};
+        // Note that if we changed this to '/telepathy/avatars' then
+        // we would share cache files with empathy. But since this is
+        // not documented/guaranteed, it seems a little sketchy
+        this._cacheDir = GLib.get_user_cache_dir() + '/gnome-shell/avatars';
     },
 
     _addConnection: function(conn) {
-        if (this._connections[conn.getPath()])
-            return this._connections[conn.getPath()];
-
         let info = {};
 
-        // avatarData[handle] describes the icon for @handle:
-        // either the string 'default', meaning to use the default
-        // avatar, or an array of bytes containing, eg, PNG data.
-        info.avatarData = {};
+        // Figure out the cache subdirectory for this connection by
+        // parsing the connection manager name (eg, 'gabble') and
+        // protocol name (eg, 'jabber') from the Connection's path.
+        // Telepathy requires the D-Bus path for a connection to have
+        // a specific form, and explicitly says that clients are
+        // allowed to parse it.
+        let match = conn.getPath().match(/\/org\/freedesktop\/Telepathy\/Connection\/([^\/]*\/[^\/]*)\/.*/);
+        if (!match)
+            throw new Error('Could not parse connection path ' + conn.getPath());
+
+        info.cacheDir = this._cacheDir + '/' + match[1];
+        GLib.mkdir_with_parents(info.cacheDir, 0700);
+
+        // info.token[handle] is the token for @handle's avatar
+        info.token = {};
 
-        // icons[handle] is an array of the icon actors currently
+        // info.icons[handle] is an array of the icon actors currently
         // being displayed for @handle. These will be updated
         // automatically if @handle's avatar changes.
         info.icons = {};
@@ -229,39 +242,57 @@ AvatarManager.prototype = {
         delete this._connections[conn.getPath()];
     },
 
+    _getFileForToken: function(info, token) {
+        return info.cacheDir + '/' + Telepathy.escapeAsIdentifier(token);
+    },
+
+    _setIcon: function(iconBox, info, handle) {
+        let textureCache = St.TextureCache.get_default();
+        let token = info.token[handle];
+        let file;
+
+        if (token) {
+            file = this._getFileForToken(info, token);
+            if (!GLib.file_test(file, GLib.FileTest.EXISTS))
+                file = null;
+        }
+
+        if (file) {
+            let uri = GLib.filename_to_uri(file, null);
+            iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size);
+        } else {
+            iconBox.child = textureCache.load_icon_name('stock_person', iconBox._size);
+        }
+    },
+
+    _updateIcons: function(info, handle) {
+        if (!info.icons[handle])
+            return;
+
+        for (let i = 0; i < info.icons[handle].length; i++) {
+            let iconBox = info.icons[handle][i];
+            this._setIcon(iconBox, info, handle);
+        }
+    },
+
     _avatarUpdated: function(conn, handle, token) {
         let info = this._connections[conn.getPath()];
         if (!info)
             return;
 
-        if (!info.avatarData[handle]) {
-            // This would only happen if either (a) the initial
-            // RequestAvatars() call hasn't returned yet, or (b)
-            // Telepathy is informing us about avatars we didn't ask
-            // about. Either way, we don't have to do anything here.
+        if (info.token[handle] == token)
             return;
-        }
 
-        if (token == '') {
-            // Invoke the next async callback in the chain, telling
-            // it to use the default image.
-            this._avatarRetrieved(conn, handle, token, 'default', null);
-        } else {
-            // In this case, @token is some sort of UUID. Telepathy
-            // expects us to cache avatar images to disk and use the
-            // tokens to figure out when we already have the right
-            // images cached. But we don't do that, we just
-            // ignore @token and request the image unconditionally.
-            info.connectionAvatars.RequestAvatarsRemote([handle]);
+        info.token[handle] = token;
+        if (token != '') {
+            let file = this._getFileForToken(info, token);
+            if (!GLib.file_test(file, GLib.FileTest.EXISTS)) {
+                info.connectionAvatars.RequestAvatarsRemote([handle]);
+                return;
+            }
         }
-    },
 
-    _createIcon: function(iconData, size) {
-        let textureCache = St.TextureCache.get_default();
-        if (iconData == 'default')
-            return textureCache.load_icon_name('stock_person', size);
-        else
-            return textureCache.load_from_data(iconData, iconData.length, size);
+        this._updateIcons(info, handle);
     },
 
     _avatarRetrieved: function(conn, handle, token, avatarData, mimeType) {
@@ -269,19 +300,21 @@ AvatarManager.prototype = {
         if (!info)
             return;
 
-        info.avatarData[handle] = avatarData;
-        if (!info.icons[handle])
-            return;
-
-        for (let i = 0; i < info.icons[handle].length; i++) {
-            let iconBox = info.icons[handle][i];
-            let size = iconBox.child.height;
-            iconBox.child = this._createIcon(avatarData, size);
+        let file = this._getFileForToken(info, token);
+        let success = false;
+        try {
+            success = GLib.file_set_contents(file, avatarData, avatarData.length);
+        } catch (e) {
+            logError(e, 'Error caching avatar data');
         }
+
+        if (success)
+            this._updateIcons(info, handle);
     },
 
     createAvatar: function(conn, handle, size) {
         let iconBox = new St.Bin({ style_class: 'avatar-box' });
+        iconBox._size = size;
 
         let info = this._connections[conn.getPath()];
         if (!info)
@@ -298,23 +331,20 @@ AvatarManager.prototype = {
                     info.icons[handle].splice(i, 1);
             }));
 
-        let avatarData = info.avatarData[handle];
-        if (avatarData) {
-            iconBox.child = this._createIcon(avatarData, size);
-            return iconBox;
+        // If we already have the icon cached and know its token, this
+        // will fill it in. Otherwise it will fill in the default
+        // icon.
+        this._setIcon(iconBox, info, handle);
+
+        // Asynchronously load the real avatar if we don't have it yet.
+        if (info.token[handle] == null) {
+            info.connectionAvatars.GetKnownAvatarTokensRemote([handle], Lang.bind(this,
+                function (tokens, err) {
+                    let token = tokens && tokens[handle] ? tokens[handle] : '';
+                    this._avatarUpdated(conn, handle, token);
+                }));
         }
 
-        // Fill in the default icon and then asynchronously load
-        // the real avatar.
-        iconBox.child = this._createIcon('default', size);
-        info.connectionAvatars.GetKnownAvatarTokensRemote([handle], Lang.bind(this,
-            function (tokens, err) {
-                if (tokens && tokens[handle])
-                    info.connectionAvatars.RequestAvatarsRemote([handle]);
-                else
-                    info.avatarData[handle] = 'default';
-            }));
-
         return iconBox;
     }
 };



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