PIM Manager: implemented watching of folks changes

Adding, updating (via GObject "notify") and removing contacts works.
FullView needs access to the FolksIndividualAggregator for
this.

The order of instantiating was changed so that Manager creates the
FullView as part of starting, instead of having an idle FullView as
envisioned earlier. Seems easier that way.

The CompareFormattedName class (previously only used for unit testing)
is used for sorting in ascending "last, first" by default. It is
convenient for testing, because the sort criteria are plain strings
which can be dumped in a debugger (in contrast to boost::locale
transformed strings). Later this default will have to be replaced with
a true locale aware sorting.

Local changes are delayed and merged as much as reasonably possible in
the Manager before sending them out via D-Bus, to avoid excessive
traffic.

This is done until the underlying view is stable
("quiesent"). Detecting that when reading from libfolks is difficult
(see GNOME feature request #684766). The current approach is to flush
pending changes when the process goes idle. In practice this prevents
merging multiple "add" changes, because the process goes idle between
imports from EDS - probably need further work, either in libfolks or
SyncEvolution (use a timeout after all, despite the extra latency?).

Interaction between delaying change signals and reading contacts must
be handled carefully. The client must be up-to-date when it receives
the results of the read request, and "modified" signals must be
enabled again for the data that was sent.

Normally invalidating the same contacts multiple times is prevented,
because folks sends "modified" signals pretty often (GNOME bug
modified FolksIndividual instances until the process is idle and all
major changes to the instance are (hopefully) done.

There is a slight race condition (client reads contact that was
already modified, SyncEvolution processes "modified" signal later) but
because the result is only that the client is asked to re-read the
contact, that is not a problem.
This commit is contained in:
Patrick Ohly 2012-09-24 16:01:20 +02:00
parent 2fe69bc266
commit f6e979c2fd
3 changed files with 551 additions and 75 deletions

View file

@ -21,11 +21,59 @@
#include "folks.h"
#include <boost/bind.hpp>
#include "test.h"
#include <syncevo/BoostHelper.h>
#include <boost/ptr_container/ptr_vector.hpp>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
class CompareFormattedName : public IndividualCompare {
bool m_reversed;
bool m_firstLast;
public:
CompareFormattedName(bool reversed = false, bool firstLast = false) :
m_reversed(reversed),
m_firstLast(firstLast)
{
}
virtual void createCriteria(FolksIndividual *individual, Criteria_t &criteria) const {
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *family = folks_structured_name_get_family_name(fn);
const char *given = folks_structured_name_get_given_name(fn);
SE_LOG_DEBUG(NULL, NULL, "criteria: formatted name: %s, %s",
family, given);
if (m_firstLast) {
criteria.push_back(given ? given : "");
criteria.push_back(family ? family : "");
} else {
criteria.push_back(family ? family : "");
criteria.push_back(given ? given : "");
}
} else {
SE_LOG_DEBUG(NULL, NULL, "criteria: no formatted");
}
}
virtual bool compare(const Criteria_t &a, const Criteria_t &b) const {
return m_reversed ?
IndividualCompare::compare(b, a) :
IndividualCompare::compare(a, b);
}
};
void IndividualData::init(const boost::shared_ptr<IndividualCompare> &compare,
FolksIndividual *individual)
{
m_individual = individual;
m_criteria.clear();
compare->createCriteria(individual, m_criteria);
}
void IndividualView::readContacts(int start, int count, std::vector<FolksIndividualCXX> &contacts)
{
contacts.clear();
@ -70,21 +118,226 @@ bool IndividualCompare::compare(const Criteria_t &a, const Criteria_t &b) const
return false;
}
boost::shared_ptr<FullView> FullView::create()
FullView::FullView(const FolksIndividualAggregatorCXX &folks) :
m_folks(folks),
// Ensure that there is a sort criteria.
m_compare(new CompareFormattedName())
{
boost::shared_ptr<FullView> view(new FullView);
}
void FullView::init()
{
// Populate view from current set of data. Usually FullView
// gets instantiated when the aggregator is idle, in which
// case there won't be any contacts yet.
//
// Optimize the initial loading by filling a vector and sorting it
// more efficiently, then adding it all in one go.
// Use pointers in array, to speed up sorting.
boost::ptr_vector<IndividualData> individuals;
IndividualData data;
typedef GeeCollCXX< GeeMapEntryWrapper<const gchar *, FolksIndividual *> > Coll;
GeeMap *map = folks_individual_aggregator_get_individuals(m_folks);
Coll coll(map);
guint size = gee_map_get_size(map);
individuals.reserve(size);
SE_LOG_DEBUG(NULL, NULL, "starting with %u individuals", size);
BOOST_FOREACH (const Coll::value_type &entry, coll) {
data.init(m_compare, entry.value());
individuals.push_back(new IndividualData(data));
}
individuals.sort(IndividualDataCompare(m_compare));
// Copy the sorted data into the view. No slots are called,
// because nothing can be connected at the moment.
m_entries.insert(m_entries.begin(), individuals.begin(), individuals.end());
// Connect to changes. Aggregator might live longer than we do, so
// bind to weak pointer and check our existence at runtime.
m_folks.connectSignal<void (FolksIndividualAggregator *folks,
GeeSet *added,
GeeSet *removed,
gchar *message,
FolksPersona *actor,
FolksGroupDetailsChangeReason reason)>("individuals-changed",
boost::bind(&FullView::individualsChanged,
m_self,
_2, _3, _4, _5, _6));
m_folks.connectSignal<void (GObject *gobject,
GParamSpec *pspec)>("notify::is-quiescent",
boost::bind(&FullView::quiesenceChanged,
m_self));
}
boost::shared_ptr<FullView> FullView::create(const FolksIndividualAggregatorCXX &folks)
{
boost::shared_ptr<FullView> view(new FullView(folks));
view->m_self = view;
view->init();
return view;
}
void FullView::individualsChanged(GeeSet *added,
GeeSet *removed,
gchar *message,
FolksPersona *actor,
FolksGroupDetailsChangeReason reason)
{
SE_LOG_DEBUG(NULL, NULL, "individuals changed, %s, %d added, %d removed, message: %s",
actor ? folks_persona_get_display_id(actor) : "<<no actor>>",
added ? gee_collection_get_size(GEE_COLLECTION(added)) : 0,
removed ? gee_collection_get_size(GEE_COLLECTION(removed)) : 0,
message);
typedef GeeCollCXX<FolksIndividual *> Coll;
if (added) {
// TODO (?): Optimize adding many new individuals by pre-sorting them,
// then using that information to avoid comparisons in addIndividual().
BOOST_FOREACH (FolksIndividual *individual, Coll(added)) {
addIndividual(individual);
}
}
if (removed) {
BOOST_FOREACH (FolksIndividual *individual, Coll(removed)) {
removeIndividual(individual);
}
}
}
void FullView::individualModified(gpointer gobject,
GParamSpec *pspec)
{
SE_LOG_DEBUG(NULL, NULL, "individual %p modified",
gobject);
FolksIndividual *individual = FOLKS_INDIVIDUAL(gobject);
// Delay the expensive modification check until the process is
// idle, because in practice we get several change signals for
// each contact change in EDS.
//
// See https://bugzilla.gnome.org/show_bug.cgi?id=684764
// "too many FolksIndividual modification signals"
m_pendingModifications.insert(individual);
waitForIdle();
}
void FullView::quiesenceChanged()
{
bool quiesent = folks_individual_aggregator_get_is_quiescent(m_folks);
SE_LOG_DEBUG(NULL, NULL, "aggregator is %s", quiesent ? "quiesent" : "busy");
// In practice, libfolks only switches from "busy" to "quiesent"
// once. See https://bugzilla.gnome.org/show_bug.cgi?id=684766
// "enter and leave quiesence state".
if (quiesent) {
m_quiesenceSignal();
}
}
void FullView::doAddIndividual(const IndividualData &data)
{
// Binary search to find insertion point.
Entries_t::iterator it =
std::lower_bound(m_entries.begin(),
m_entries.end(),
data,
IndividualDataCompare(m_compare));
size_t index = it - m_entries.begin();
it = m_entries.insert(it, data);
SE_LOG_DEBUG(NULL, NULL, "full view: added at #%ld/%ld", index, m_entries.size());
m_addedSignal(index, it->m_individual);
waitForIdle();
// Monitor individual for changes.
it->m_individual.connectSignal<void (GObject *gobject,
GParamSpec *pspec)>("notify",
boost::bind(&FullView::individualModified,
m_self,
_1, _2));
}
void FullView::addIndividual(FolksIndividual *individual)
{
// TODO
IndividualData data;
data.init(m_compare, individual);
doAddIndividual(data);
}
void FullView::modifyIndividual(FolksIndividual *individual)
{
// Brute-force search for the individual. Pointer comparison is
// sufficient, libfolks will not change instances without
// announcing it.
for (Entries_t::iterator it = m_entries.begin();
it != m_entries.end();
++it) {
if (it->m_individual.get() == individual) {
size_t index = it - m_entries.begin();
IndividualData data;
data.init(m_compare, individual);
if (data.m_criteria != it->m_criteria &&
((it != m_entries.begin() && !m_compare->compare((it - 1)->m_criteria, data.m_criteria)) ||
(it + 1 != m_entries.end() && !m_compare->compare(data.m_criteria, (it + 1)->m_criteria)))) {
// Sort criteria changed in such a way that the old
// sorting became invalid => move the entry. Do it
// as simple as possible, because this is not expected
// to happen often.
SE_LOG_DEBUG(NULL, NULL, "full view: temporarily removed at #%ld/%ld", index, m_entries.size());
m_entries.erase(it);
m_removedSignal(index, individual);
doAddIndividual(data);
} else {
SE_LOG_DEBUG(NULL, NULL, "full view: modified at #%ld/%ld", index, m_entries.size());
m_modifiedSignal(index, individual);
waitForIdle();
}
return;
}
}
// Not a bug: individual might have been removed before we got
// around to processing the modification notification.
SE_LOG_DEBUG(NULL, NULL, "full view: modified individual not found");
}
void FullView::removeIndividual(FolksIndividual *individual)
{
// TODO
for (Entries_t::iterator it = m_entries.begin();
it != m_entries.end();
++it) {
if (it->m_individual.get() == individual) {
size_t index = it - m_entries.begin();
SE_LOG_DEBUG(NULL, NULL, "full view: removed at #%ld/%ld", index, m_entries.size());
m_entries.erase(it);
m_removedSignal(index, individual);
waitForIdle();
return;
}
}
// A bug?!
SE_LOG_DEBUG(NULL, NULL, "full view: individual to be removed not found");
}
void FullView::onIdle()
{
SE_LOG_DEBUG(NULL, NULL, "process is idle");
// Process delayed contact modifications.
BOOST_FOREACH (const FolksIndividualCXX &individual,
m_pendingModifications) {
modifyIndividual(const_cast<FolksIndividual *>(individual.get()));
}
m_pendingModifications.clear();
m_quiesenceSignal();
m_waitForIdle.deactivate();
}
void FullView::waitForIdle()
{
if (!m_waitForIdle) {
m_waitForIdle.runOnce(-1, boost::bind(&FullView::onIdle, this));
}
}
void FullView::setCompare(const boost::shared_ptr<IndividualCompare> &compare)
{
// TODO
@ -96,6 +349,7 @@ boost::shared_ptr<FilteredView> FilteredView::create(const boost::shared_ptr<Ind
boost::shared_ptr<FilteredView> view(new FilteredView);
view->m_self = view;
view->m_parent = parent;
parent->m_quiesenceSignal.connect(QuiesenceSignal_t::slot_type(boost::bind(boost::cref(view->m_quiesenceSignal))).track(view->m_self));
// TODO
return view;
}
@ -122,27 +376,9 @@ boost::shared_ptr<IndividualAggregator> IndividualAggregator::create()
aggregator->m_self = aggregator;
aggregator->m_folks =
FolksIndividualAggregatorCXX::steal(folks_individual_aggregator_new());
boost::shared_ptr<FullView> view(FullView::create());
aggregator->m_view = view;
aggregator->m_folks.connectSignal<void (FolksIndividualAggregator *,
GeeSet *added,
GeeSet *removed,
gchar *message,
FolksPersona *actor,
FolksGroupDetailsChangeReason)>("individuals-changed",
boost::bind(&IndividualAggregator::individualsChanged,
aggregator.get(),
_2, _3, _4));
return aggregator;
}
void IndividualAggregator::individualsChanged(GeeSet *added,
GeeSet *removed,
gchar *message) throw ()
{
// TODO
}
/**
* Generic error callback. There really isn't much that can be done if
* libfolks fails, except for logging the problem.
@ -158,8 +394,8 @@ static void logResult(const GError *gerror, const char *operation)
void IndividualAggregator::start()
{
if (!m_started) {
m_started = true;
if (!m_view) {
m_view = FullView::create(m_folks);
SYNCEVO_GLIB_CALL_ASYNC(folks_individual_aggregator_prepare,
boost::bind(logResult, _1,
"folks_individual_aggregator_prepare"),
@ -167,42 +403,16 @@ void IndividualAggregator::start()
}
}
boost::shared_ptr<IndividualView> IndividualAggregator::getMainView()
{
if (!m_view) {
start();
}
return m_view;
}
#ifdef ENABLE_UNIT_TESTS
class CompareFormattedName : public IndividualCompare {
bool m_reversed;
bool m_firstLast;
public:
CompareFormattedName(bool reversed = false, bool firstLast = false) :
m_reversed(reversed),
m_firstLast(firstLast)
{
}
virtual void createCriteria(FolksIndividual *individual, Criteria_t &criteria) const {
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *family = folks_structured_name_get_family_name(fn);
const char *given = folks_structured_name_get_given_name(fn);
if (m_firstLast) {
criteria.push_back(given ? given : "");
criteria.push_back(family ? family : "");
} else {
criteria.push_back(family ? family : "");
criteria.push_back(given ? given : "");
}
}
}
virtual bool compare(const Criteria_t &a, const Criteria_t &b) const {
return m_reversed ?
IndividualCompare::compare(b, a) :
IndividualCompare::compare(a, b);
}
};
class FolksTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(FolksTest);
CPPUNIT_TEST(open);

View file

@ -30,6 +30,8 @@
#include <folks/folks.h>
#include "../timeout.h"
#include <syncevo/GLibSupport.h>
#include <syncevo/GeeSupport.h>
#include <syncevo/GValueSupport.h>
@ -90,10 +92,38 @@ class IndividualCompare
*/
struct IndividualData
{
/**
* Sets all members to match the given individual, using the
* compare instance to compute values.
*/
void init(const boost::shared_ptr<IndividualCompare> &compare,
FolksIndividual *individual);
FolksIndividualCXX m_individual;
IndividualCompare::Criteria_t m_criteria;
};
/**
* wraps an IndividualCompare for std algorithms on IndividualData
*/
class IndividualDataCompare : public std::binary_function<IndividualData, IndividualData, bool>
{
boost::shared_ptr<IndividualCompare> m_compare;
public:
IndividualDataCompare(const boost::shared_ptr<IndividualCompare> &compare) :
m_compare(compare)
{}
IndividualDataCompare(const IndividualDataCompare &other) :
m_compare(other.m_compare)
{}
bool operator () (const IndividualData &a, const IndividualData &b) const
{
return m_compare->compare(a.m_criteria, b.m_criteria);
}
};
/**
* Abstract interface for filtering (aka searching) FolksIndividual
* instances.
@ -119,6 +149,13 @@ class IndividualView
{
public:
typedef boost::signals2::signal<void (int, FolksIndividual *)> ChangeSignal_t;
typedef boost::signals2::signal<void (void)> QuiesenceSignal_t;
/**
* Triggered each time the view reaches a quiesence state, meaning
* that its current content is stable, at least for now.
*/
QuiesenceSignal_t m_quiesenceSignal;
/**
* A new FolksIndividual was added at a specific index. This
@ -157,22 +194,69 @@ class IndividualView
*/
class FullView : public IndividualView
{
FolksIndividualAggregatorCXX m_folks;
boost::weak_ptr<FullView> m_self;
Timeout m_waitForIdle;
std::set<FolksIndividualCXX> m_pendingModifications;
/**
* Sorted vector. Sort order is maintained by this class.
*/
std::vector<IndividualData> m_entries;
typedef std::vector<IndividualData> Entries_t;
Entries_t m_entries;
/**
* The sort object to be used.
*/
boost::shared_ptr<IndividualCompare> m_compare;
FullView() {}
FullView(const FolksIndividualAggregatorCXX &folks);
void init();
/**
* Run via m_waitForIdle if (and only if) something
* changed.
*/
void onIdle();
/**
* Ensure that onIdle() gets invoked.
*/
void waitForIdle();
void doAddIndividual(const IndividualData &data);
public:
static boost::shared_ptr<FullView> create();
/**
* @param folks the aggregator to use, may be empty for testing
*/
static boost::shared_ptr<FullView> create(const FolksIndividualAggregatorCXX &folks =
FolksIndividualAggregatorCXX());
/** FolksIndividualAggregator "individuals-changed" slot */
void individualsChanged(GeeSet *added,
GeeSet *removed,
gchar *message,
FolksPersona *actor,
FolksGroupDetailsChangeReason reason = FOLKS_GROUP_DETAILS_CHANGE_REASON_NONE);
/** GObject "notify" slot */
void individualModified(gpointer gobject,
GParamSpec *pspec);
/**
* FolksIndividualAggregator "is-quiesent" property change slot.
*
* It turned out that "quiesence" is only set to true once in
* FolksIndividualAggregator. The code which watches that signal
* is still in place, but it will only get invoked once.
*
* Therefore the main mechanism for emitting m_quiesenceSignal in
* FullView is an idle callback which gets invoked each time the
* daemon has nothing to do, which implies that (at least for now)
* libfolks has no pending work to do.
*/
void quiesenceChanged();
/**
* Add a FolksIndividual. Starts monitoring it for changes.
@ -180,6 +264,11 @@ class FullView : public IndividualView
void addIndividual(FolksIndividual *individual);
/**
* Deal with FolksIndividual modification.
*/
void modifyIndividual(FolksIndividual *individual);
/**
* Remove a FolksIndividual.
*/
void removeIndividual(FolksIndividual *individual);
@ -255,17 +344,13 @@ class FilteredView : public IndividualView
*/
class IndividualAggregator
{
/** empty when not started yet */
boost::shared_ptr<FullView> m_view;
boost::weak_ptr<IndividualAggregator> m_self;
FolksIndividualAggregatorCXX m_folks;
Bool m_started;
IndividualAggregator() {}
void individualsChanged(GeeSet *added,
GeeSet *removed,
gchar *message) throw ();
public:
/**
* Creates an idle IndividualAggregator. Configure it and
@ -281,7 +366,7 @@ class IndividualAggregator
/**
* Starts pulling and sorting of contacts.
* Populates m_view and all other, derived views.
* Creates m_view and starts populating it.
* Can be called multiple times.
*
* See also org.01.pim.contacts.Manager.Start().
@ -292,9 +377,11 @@ class IndividualAggregator
/**
* Each aggregator has exactly one full view on the data. This
* method grants access to the change signals.
* method grants access to it and its change signals.
*
* @return never empty, start() will be called if necessary
*/
boost::shared_ptr<IndividualView> getMainView() const { return m_view; }
boost::shared_ptr<IndividualView> getMainView();
};

View file

@ -154,6 +154,12 @@ class ViewResource : public Resource, public GDBusCXX::DBusObjectHelper
GDBusCXX::DBusRemoteObject m_viewAgent;
boost::shared_ptr<IndividualView> m_view;
boost::weak_ptr<Client> m_owner;
struct Change {
Change() : m_start(0), m_count(0), m_call(NULL) {}
int m_start, m_count;
const GDBusCXX::DBusClientCall0 *m_call;
} m_pendingChange, m_lastChange;
GDBusCXX::DBusClientCall0 m_contactsModified,
m_contactsAdded,
m_contactsRemoved;
@ -172,6 +178,7 @@ class ViewResource : public Resource, public GDBusCXX::DBusObjectHelper
ID),
m_view(view),
m_owner(owner),
// use ViewAgent interface
m_contactsModified(m_viewAgent, "ContactsModified"),
m_contactsAdded(m_viewAgent, "ContactsAdded"),
@ -186,11 +193,165 @@ class ViewResource : public Resource, public GDBusCXX::DBusObjectHelper
void sendChange(const GDBusCXX::DBusClientCall0 &call,
int start, int count)
{
call.start(boost::bind(ViewResource::sendDone,
// Changes get aggregated inside sendChange().
call.start(getObject(),
start,
count,
boost::bind(ViewResource::sendDone,
m_self,
_1));
}
/**
* Merge local changes as much as possible, to avoid excessive
* D-Bus traffic. Pending changes get flushed each time the
* view reports that it is stable again or contact data
* needs to be sent back to the client. Flushing in the second
* case is necessary, because otherwise the client will not have
* an up-to-date view when the requested data arrives.
*/
void handleChange(const GDBusCXX::DBusClientCall0 &call,
int start, int count)
{
SE_LOG_DEBUG(NULL, NULL, "handle change: %s, #%d + %d",
&call == &m_contactsModified ? "modified" :
&call == &m_contactsAdded ? "added" :
&call == &m_contactsRemoved ? "remove" : "???",
start, count);
if (!count) {
// Nothing changed.
SE_LOG_DEBUG(NULL, NULL, "handle change: no change");
return;
}
if (m_pendingChange.m_count == 0) {
// Sending a "contact modified" twice for the same range is redundant.
if (m_lastChange.m_call == &m_contactsModified &&
&call == &m_contactsModified &&
start >= m_lastChange.m_start &&
start + count <= m_lastChange.m_start + m_lastChange.m_count) {
SE_LOG_DEBUG(NULL, NULL, "handle change: redundant 'modified' signal, ignore");
return;
}
// Nothing pending, delay sending.
m_pendingChange.m_call = &call;
m_pendingChange.m_start = start;
m_pendingChange.m_count = count;
SE_LOG_DEBUG(NULL, NULL, "handle change: stored as pending change");
return;
}
if (m_pendingChange.m_call == &call) {
// Same operation. Can we extend it?
if (&call == &m_contactsModified) {
// Modification, indices are unchanged. The union of
// old and new range must not have a gap between the
// original ranges, which can be checked by looking at
// the number of items.
int newStart = std::min(m_pendingChange.m_start, start);
int newEnd = std::max(m_pendingChange.m_start + m_pendingChange.m_count, start + count);
int newCount = newEnd - newStart;
if (newCount <= m_pendingChange.m_count + count) {
// Okay, no unrelated individuals were included in
// the tentative new range, we can use it.
SE_LOG_DEBUG(NULL, NULL, "handle change: modification, #%d + %d and #%d + %d => #%d + %d",
m_pendingChange.m_start, m_pendingChange.m_count,
start, count,
newStart, newCount);
m_pendingChange.m_start = newStart;
m_pendingChange.m_count = newCount;
return;
}
} else if (&call == &m_contactsAdded) {
// Added individuals. The new start index already includes
// the previously added individuals.
int newCount = m_pendingChange.m_count + count;
if (start >= m_pendingChange.m_start) {
// Adding in the middle or at the end?
if (start <= m_pendingChange.m_start + m_pendingChange.m_count) {
SE_LOG_DEBUG(NULL, NULL, "handle change: increase count of 'added' individuals, #%d + %d and #%d + %d new => #%d + %d",
m_pendingChange.m_start, m_pendingChange.m_count,
start, count,
m_pendingChange.m_start, newCount);
m_pendingChange.m_count = newCount;
return;
}
} else {
// Adding directly before the previous start?
if (start + count >= m_pendingChange.m_start) {
SE_LOG_DEBUG(NULL, NULL, "handle change: reduce start and increase count of 'added' individuals, #%d + %d and #%d + %d => #%d + %d",
m_pendingChange.m_start, m_pendingChange.m_count,
start, count,
start, newCount);
m_pendingChange.m_start = start;
m_pendingChange.m_count = newCount;
return;
}
}
} else {
// Removed individuals. The new start was already reduced by
// previously removed individuals.
int newCount = m_pendingChange.m_count + count;
if (start >= m_pendingChange.m_start) {
// Removing directly at end?
if (start == m_pendingChange.m_start) {
SE_LOG_DEBUG(NULL, NULL, "handle change: increase count of 'removed' individuals, #%d + %d and #%d + %d => #%d + %d",
m_pendingChange.m_start, m_pendingChange.m_count,
start, count,
m_pendingChange.m_start, newCount);
m_pendingChange.m_count = newCount;
return;
}
} else {
// Removing directly before the previous start or overlapping with it?
if (start + count >= m_pendingChange.m_start) {
SE_LOG_DEBUG(NULL, NULL, "handle change: reduce start and increase count of 'removed' individuals, #%d + %d and #%d + %d => #%d + %d",
m_pendingChange.m_start, m_pendingChange.m_count,
start, count,
start, newCount);
m_pendingChange.m_start = start;
m_pendingChange.m_count = newCount;
return;
}
}
}
}
// More complex merging is possible. For example, "removed 1
// at #10" and "added 1 at #10" could be turned into "modified
// 1 at #10". But it is uncertain how common and useful that
// would be, so not implemented...
// Cannot merge changes.
flushChanges();
// Now remember requested change.
m_pendingChange.m_call = &call;
m_pendingChange.m_start = start;
m_pendingChange.m_count = count;
}
/** Clear pending state and flush. */
void flushChanges()
{
int count = m_pendingChange.m_count;
if (count) {
SE_LOG_DEBUG(NULL, NULL, "send change: %s, #%d + %d",
m_pendingChange.m_call == &m_contactsModified ? "modified" :
m_pendingChange.m_call == &m_contactsAdded ? "added" :
m_pendingChange.m_call == &m_contactsRemoved ? "remove" : "???",
m_pendingChange.m_start, count);
m_lastChange = m_pendingChange;
m_pendingChange.m_count = 0;
sendChange(*m_pendingChange.m_call,
m_pendingChange.m_start,
count);
}
}
/**
* Used as callback for sending changes to the ViewAgent. Only
* holds weak references and thus does not prevent deleting view
@ -219,18 +380,19 @@ class ViewResource : public Resource, public GDBusCXX::DBusObjectHelper
add(this, &ViewResource::refineSearch, "RefineSearch");
activate();
// TODO: aggregate changes into batches
m_view->m_modifiedSignal.connect(IndividualView::ChangeSignal_t::slot_type(&ViewResource::sendChange,
m_view->m_quiesenceSignal.connect(IndividualView::QuiesenceSignal_t::slot_type(&ViewResource::flushChanges,
this).track(self));
m_view->m_modifiedSignal.connect(IndividualView::ChangeSignal_t::slot_type(&ViewResource::handleChange,
this,
boost::cref(m_contactsModified),
_1,
1).track(self));
m_view->m_addedSignal.connect(IndividualView::ChangeSignal_t::slot_type(&ViewResource::sendChange,
m_view->m_addedSignal.connect(IndividualView::ChangeSignal_t::slot_type(&ViewResource::handleChange,
this,
boost::cref(m_contactsAdded),
_1,
1).track(self));
m_view->m_removedSignal.connect(IndividualView::ChangeSignal_t::slot_type(&ViewResource::sendChange,
m_view->m_removedSignal.connect(IndividualView::ChangeSignal_t::slot_type(&ViewResource::handleChange,
this,
boost::cref(m_contactsRemoved),
_1,
@ -256,7 +418,22 @@ public:
/** ViewControl.ReadContacts() */
void readContacts(int start, int count, std::vector<FolksIndividualCXX> &contacts)
{
m_view->readContacts(start, count, contacts);
if (count) {
// Ensure that client's view is up-to-date, then prepare the
// data for it.
flushChanges();
m_view->readContacts(start, count, contacts);
// Discard the information about the previous 'modified' signal
// if the client now has data in that range. Necessary because
// otherwise future 'modified' signals for that range might get
// suppressed in handleChange().
if (m_lastChange.m_call == &m_contactsModified &&
m_lastChange.m_count &&
((m_lastChange.m_start >= start && m_lastChange.m_start < start + count) ||
(start >= m_lastChange.m_start && start < m_lastChange.m_start + m_lastChange.m_count))) {
m_lastChange.m_count = 0;
}
}
}
/** ViewControl.Close() */
@ -287,6 +464,8 @@ GDBusCXX::DBusObject_t Manager::search(const GDBusCXX::Caller_t &ID,
const StringMap &filter,
const GDBusCXX::DBusObject_t &agentPath)
{
start();
// Create and track view which is owned by the caller.
boost::shared_ptr<Client> client = m_server->addClient(ID, watch);