[folks] Bug 652643 — Add PersonaStore cache



commit 5b90ebe8fde8229f30f56ea78feff17cea70ed34
Author: Philip Withnall <philip tecnocode co uk>
Date:   Thu Jun 16 18:35:52 2011 +0100

    Bug 652643 â Add PersonaStore cache
    
    Use ObjectCache in Tpf.PersonaStore.
    
    Closes: bgo#652643

 NEWS                                               |    1 +
 backends/telepathy/lib/Makefile.am                 |    1 +
 .../telepathy/lib/tpf-persona-store-cache.vala     |  154 ++++++++++++++++++++
 backends/telepathy/lib/tpf-persona-store.vala      |  121 +++++++++++++++-
 backends/telepathy/lib/tpf-persona.vala            |   66 ++++++++-
 5 files changed, 335 insertions(+), 8 deletions(-)
---
diff --git a/NEWS b/NEWS
index 9c05d18..1050242 100644
--- a/NEWS
+++ b/NEWS
@@ -22,6 +22,7 @@ Bugs fixed:
 * 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
+* Bug 652643 â Add PersonaStore cache
 
 API changes:
 * Swf.Persona retains and exposes its libsocialweb Contact
diff --git a/backends/telepathy/lib/Makefile.am b/backends/telepathy/lib/Makefile.am
index 167fba3..7d3cd01 100644
--- a/backends/telepathy/lib/Makefile.am
+++ b/backends/telepathy/lib/Makefile.am
@@ -96,6 +96,7 @@ folks_telepathy_valasources = \
 	tpf-persona.vala \
 	tpf-persona-store.vala \
 	tpf-logger.vala \
+	tpf-persona-store-cache.vala \
 	$(NULL)
 
 libfolks_telepathy_la_SOURCES = \
diff --git a/backends/telepathy/lib/tpf-persona-store-cache.vala b/backends/telepathy/lib/tpf-persona-store-cache.vala
new file mode 100644
index 0000000..d2d52e1
--- /dev/null
+++ b/backends/telepathy/lib/tpf-persona-store-cache.vala
@@ -0,0 +1,154 @@
+/*
+ * 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;
+using Gee;
+using Folks;
+
+/**
+ * An object cache class which implements caching of sets of
+ * { link Tpf.Persona}s from a given { link Tpf.PersonaStore}.
+ *
+ * Each { link Tpf.Persona} is stored as a serialised { link Variant} which is
+ * a tuple containing the following fields:
+ *  # UID (`s`)
+ *  # IID (`s`)
+ *  # IM address (`s`)
+ *  # Protocol (`s`)
+ *  # Set of group names (`as`)
+ *  # Favourite? (`b`)
+ *  # Alias (`s`)
+ *  # In contact list? (`b`)
+ *  # Avatar file URI (`s`)
+ *
+ * @since UNRELEASED
+ */
+internal class Tpf.PersonaStoreCache : Folks.ObjectCache<Tpf.Persona>
+{
+  private weak PersonaStore _store;
+
+  /* Version number of the variant type returned by
+   * get_serialised_object_type(). This must be modified whenever that variant
+   * type or its semantics are changed, since that would necessitate a cache
+   * refresh. */
+  private static const uint8 _FILE_FORMAT_VERSION = 1;
+
+  internal PersonaStoreCache (PersonaStore store)
+    {
+      base ("tpf-persona-stores", store.id);
+
+      this._store = store;
+    }
+
+  protected override VariantType get_serialised_object_type ()
+    {
+      return new VariantType.tuple ({
+        VariantType.STRING, // UID
+        VariantType.STRING, // IID
+        VariantType.STRING, // ID
+        VariantType.STRING, // Protocol
+        new VariantType.array (VariantType.STRING), // Groups
+        VariantType.BOOLEAN, // Favourite?
+        VariantType.STRING, // Alias
+        VariantType.BOOLEAN, // In contact list?
+        VariantType.BOOLEAN, // Is user?
+        new VariantType.maybe (VariantType.STRING)  // Avatar
+      });
+    }
+
+  protected override uint8 get_serialised_object_version ()
+    {
+      return this._FILE_FORMAT_VERSION;
+    }
+
+  protected override Variant serialise_object (Tpf.Persona persona)
+    {
+      // Sort out the groups
+      Variant[] groups = new Variant[persona.groups.size];
+
+      uint i = 0;
+      foreach (var group in persona.groups)
+        {
+          groups[i++] = new Variant.string (group);
+        }
+
+      // Sort out the IM addresses (there's guaranteed to only be one)
+      string? im_protocol = null;
+
+      foreach (var protocol in persona.im_addresses.get_keys ())
+        {
+          im_protocol = protocol;
+          break;
+        }
+
+      // Avatar
+      var avatar_file = (persona.avatar != null && persona.avatar is FileIcon) ?
+          (persona.avatar as FileIcon).get_file () : null;
+      var avatar_variant = (avatar_file != null) ?
+          new Variant.string (avatar_file.get_uri ()) : null;
+
+      // Serialise the persona
+      return new Variant.tuple ({
+        new Variant.string (persona.uid),
+        new Variant.string (persona.iid),
+        new Variant.string (persona.display_id),
+        new Variant.string (im_protocol),
+        new Variant.array (VariantType.STRING, groups),
+        new Variant.boolean (persona.is_favourite),
+        new Variant.string (persona.alias),
+        new Variant.boolean (persona.is_in_contact_list),
+        new Variant.boolean (persona.is_user),
+        new Variant.maybe (VariantType.STRING, avatar_variant)
+      });
+    }
+
+  protected override Tpf.Persona deserialise_object (Variant variant)
+    {
+      // Deserialise the persona
+      var uid = variant.get_child_value (0).get_string ();
+      var iid = variant.get_child_value (1).get_string ();
+      var display_id = variant.get_child_value (2).get_string ();
+      var im_protocol = variant.get_child_value (3).get_string ();
+      var groups = variant.get_child_value (4);
+      var is_favourite = variant.get_child_value (5).get_boolean ();
+      var alias = variant.get_child_value (6).get_string ();
+      var is_in_contact_list = variant.get_child_value (7).get_boolean ();
+      var is_user = variant.get_child_value (8).get_boolean ();
+      var avatar_variant = variant.get_child_value (9).get_maybe ();
+
+      // Deserialise the groups
+      var group_set = new HashSet<string> ();
+      for (uint i = 0; i < groups.n_children (); i++)
+        {
+          group_set.add (groups.get_child_value (i).get_string ());
+        }
+
+      // Deserialise the avatar
+      var avatar = (avatar_variant != null) ?
+          new FileIcon (File.new_for_uri (avatar_variant.get_string ())) :
+          null;
+
+      return new Tpf.Persona.from_cache (this._store, uid, iid, display_id,
+          im_protocol, group_set, is_favourite, alias, is_in_contact_list,
+          is_user, avatar);
+    }
+}
+
+/* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */
diff --git a/backends/telepathy/lib/tpf-persona-store.vala b/backends/telepathy/lib/tpf-persona-store.vala
index fd5683f..b031c21 100644
--- a/backends/telepathy/lib/tpf-persona-store.vala
+++ b/backends/telepathy/lib/tpf-persona-store.vala
@@ -89,6 +89,9 @@ public class Tpf.PersonaStore : Folks.PersonaStore
   private MaybeBool _can_remove_personas = MaybeBool.UNSET;
   private bool _is_prepared = false;
   private Debug _debug;
+  private PersonaStoreCache _cache;
+  private Cancellable? _load_cache_cancellable = null;
+  private bool _cached = false;
 
   internal signal void group_members_changed (string group,
       GLib.List<Persona>? added, GLib.List<Persona>? removed);
@@ -195,6 +198,9 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       this._debug = Debug.dup ();
       this._debug.print_status.connect (this._debug_print_status);
 
+      // Set up the cache
+      this._cache = new PersonaStoreCache (this);
+
       this._reset ();
     }
 
@@ -498,8 +504,12 @@ public class Tpf.PersonaStore : Folks.PersonaStore
                 {
                   if (this.account == a)
                     {
-                      this._emit_personas_changed (null, this._persona_set);
-                      this.removed ();
+                      this._store_cache.begin ((o, r) =>
+                        {
+                          this._store_cache.end (r);
+                          this._emit_personas_changed (null, this._persona_set);
+                          this.removed ();
+                        });
                     }
                 });
               this._account_manager.account_removed.connect ((a) =>
@@ -507,6 +517,7 @@ public class Tpf.PersonaStore : Folks.PersonaStore
                   if (this.account == a)
                     {
                       this._emit_personas_changed (null, this._persona_set);
+                      this._cache.clear_cache ();
                       this.removed ();
                     }
                 });
@@ -516,6 +527,7 @@ public class Tpf.PersonaStore : Folks.PersonaStore
                       if (!valid && this.account == a)
                         {
                           this._emit_personas_changed (null, this._persona_set);
+                          this._cache.clear_cache ();
                           this.removed ();
                         }
                     });
@@ -533,6 +545,12 @@ public class Tpf.PersonaStore : Folks.PersonaStore
                       TelepathyGLib.ConnectionStatus.DISCONNECTED, status,
                       reason, null, null);
                 }
+              else
+                {
+                  /* If we're disconnected, advertise personas from the cache
+                   * instead. */
+                  yield this._load_cache ();
+                }
 
               try
                 {
@@ -713,14 +731,32 @@ public class Tpf.PersonaStore : Folks.PersonaStore
         {
           /* When disconnecting, we want the PersonaStore to remain alive, but
            * all its Personas to be removed. We do *not* want the PersonaStore
-           * to be destroyed, as that makes coming back online hard. */
-          this._emit_personas_changed (null, this._persona_set);
-          this._reset ();
+           * to be destroyed, as that makes coming back online hard.
+           *
+           * We have to start advertising personas from the cache instead.
+           * This will implicitly notify about removal of the existing persona
+           * set and call this._reset().
+           *
+           * Before we do this, we store the current set of personas to the
+           * cache. */
+          this._store_cache.begin ((o, r) =>
+            {
+              this._store_cache.end (r);
+
+              this._load_cache.begin ((o2, r2) =>
+                {
+                  this._load_cache.end (r2);
+                });
+            });
+
           return;
         }
       else if (new_status != TelepathyGLib.ConnectionStatus.CONNECTED)
         return;
 
+      // We're connected, so can stop advertising personas from the cache
+      this._unload_cache ();
+
       var conn = this.account.connection;
       conn.notify["connection-ready"].connect (this._connection_ready_cb);
 
@@ -836,6 +872,81 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       this._initialise_favourite_contacts.begin ();
     }
 
+  /**
+   * If our account is disconnected, we want to continue to export a static
+   * view of personas from the cache.
+   */
+  private async void _load_cache ()
+    {
+      var cancellable = new Cancellable ();
+
+      if (this._load_cache_cancellable != null)
+        {
+          this._load_cache_cancellable.cancel ();
+        }
+
+      this._load_cache_cancellable = cancellable;
+
+      // Load the persona set from the cache and notify of the change
+      var cached_personas = yield this._cache.load_objects (cancellable);
+      var old_personas = this._persona_set;
+
+      /* If the load operation was cancelled, don't change the state
+       * of the persona store at all. */
+      if (cancellable.is_cancelled () == true)
+        {
+          return;
+        }
+
+      this._reset ();
+      this._cached = true;
+
+      this._persona_set = new HashSet<Persona> ();
+      if (cached_personas != null)
+        {
+          foreach (var p in cached_personas)
+            {
+              this._persona_set.add (p);
+            }
+        }
+
+      this._emit_personas_changed (cached_personas, old_personas,
+          null, null, GroupDetails.ChangeReason.NONE);
+
+      this._can_add_personas = MaybeBool.FALSE;
+      this._can_alias_personas = MaybeBool.FALSE;
+      this._can_group_personas = MaybeBool.FALSE;
+      this._can_remove_personas = MaybeBool.FALSE;
+    }
+
+  /**
+   * When we're about to disconnect, store the current set of personas to the
+   * cache file so that we can access them once offline.
+   */
+  private async void _store_cache ()
+    {
+      yield this._cache.store_objects (this._persona_set);
+    }
+
+  /**
+   * When our account is connected again, we can unload the the personas which
+   * we're advertising from the cache.
+   */
+  private void _unload_cache ()
+    {
+      // If we're in the process of loading from the cache, cancel that
+      if (this._load_cache_cancellable != null)
+        {
+          this._load_cache_cancellable.cancel ();
+        }
+
+      this._emit_personas_changed (null, this._persona_set, null, null,
+          GroupDetails.ChangeReason.NONE);
+
+      this._reset ();
+      this._cached = false;
+    }
+
   private void _self_handle_changed_cb (Object s, ParamSpec? p)
     {
       var c = (Connection) s;
diff --git a/backends/telepathy/lib/tpf-persona.vala b/backends/telepathy/lib/tpf-persona.vala
index 18a06d9..5566379 100644
--- a/backends/telepathy/lib/tpf-persona.vala
+++ b/backends/telepathy/lib/tpf-persona.vala
@@ -236,8 +236,14 @@ public class Tpf.Persona : Folks.Persona,
 
   /**
    * The Telepathy contact represented by this persona.
+   *
+   * Note that this may be `null` if the { link PersonaStore} providing this
+   * { link Persona} isn't currently available (e.g. due to not being connected
+   * to the network). In this case, most other properties of the { link Persona}
+   * are being retrieved from a cache and may not be current (though there's no
+   * way to tell this).
    */
-  public Contact contact { get; construct; }
+  public Contact? contact { get; construct; }
 
   /**
    * Create a new persona.
@@ -341,12 +347,66 @@ public class Tpf.Persona : Folks.Persona,
                    error.code != TelepathyGLib.DBusError.OBJECT_REMOVED))
                 {
                   debug ("Group invalidated: %s", error.message);
+                  this._change_group (group, false);
                 }
-
-              this._change_group (group, false);
             });
     }
 
+  /**
+   * Create a new persona for the { link PersonaStore} `store`, representing
+   * a cached contact for which we currently have no Telepathy contact.
+   *
+   * @param store The persona store to place the persona in.
+   * @param uid The cached UID of the persona.
+   * @param iid The cached IID of the persona.
+   * @param im_address The cached IM address of the persona (excluding
+   * protocol).
+   * @param protocol The cached protocol of the persona.
+   * @param groups The cached set of groups the persona is in.
+   * @param is_favourite Whether the persona is a favourite.
+   * @param alias The cached alias for the persona.
+   * @param is_in_contact_list Whether the persona is in the user's contact
+   * list.
+   * @param is_user Whether the persona is the user.
+   * @param avatar The icon for the persona's cached avatar, or `null` if they
+   * have no avatar.
+   * @return A new { link Tpf.Persona} representing the cached persona.
+   *
+   * @since 0.5.UNRELEASED
+   */
+  internal Persona.from_cache (PersonaStore store, string uid, string iid,
+      string im_address, string protocol, HashSet<string> groups,
+      bool is_favourite, string alias, bool is_in_contact_list, bool is_user,
+      LoadableIcon? avatar)
+    {
+      Object (contact: null,
+              display_id: im_address,
+              iid: iid,
+              uid: uid,
+              store: store,
+              is_user: is_user);
+
+      debug ("Creating new Tpf.Persona '%s' from cache: %p", uid, this);
+
+      // IM addresses
+      this._im_addresses = new HashMultiMap<string, string> ();
+      this._im_addresses.set (protocol, im_address);
+
+      // Groups
+      this._groups = groups;
+      this._groups_ro = this._groups.read_only_view;
+
+      // Other properties
+      this._alias = alias;
+      this._is_favourite = is_favourite;
+      this.is_in_contact_list = is_in_contact_list;
+      this.avatar = avatar;
+
+      // Make the persona appear offline
+      this.presence_type = PresenceType.OFFLINE;
+      this.presence_message = "";
+    }
+
   ~Persona ()
     {
       debug ("Destroying Tpf.Persona '%s': %p", this.uid, this);



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