Commit 5b90ebe8 authored by Philip Withnall's avatar Philip Withnall
Browse files

Bug 652643 — Add PersonaStore cache

Use ObjectCache in Tpf.PersonaStore.

Closes: bgo#652643
parent 776adae7
......@@ -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
......
......@@ -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 = \
......
/*
* 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: */
......@@ -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;
......
......@@ -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);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment