New: Add omit-presence-changes parameter
[qtcontacts-tracker:hasselmms-qtcontacts-tracker.git] / src / engine / trackerchangelistener.cpp
1 /*********************************************************************************
2  ** This file is part of QtContacts tracker storage plugin
3  **
4  ** Copyright (c) 2009-2011 Nokia Corporation and/or its subsidiary(-ies).
5  **
6  ** Contact:  Nokia Corporation (info@qt.nokia.com)
7  **
8  ** GNU Lesser General Public License Usage
9  ** This file may be used under the terms of the GNU Lesser General Public License
10  ** version 2.1 as published by the Free Software Foundation and appearing in the
11  ** file LICENSE.LGPL included in the packaging of this file.  Please review the
12  ** following information to ensure the GNU Lesser General Public License version
13  ** 2.1 requirements will be met:
14  ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
15  **
16  ** In addition, as a special exception, Nokia gives you certain additional rights.
17  ** These rights are described in the Nokia Qt LGPL Exception version 1.1, included
18  ** in the file LGPL_EXCEPTION.txt in this package.
19  **
20  ** Other Usage
21  ** Alternatively, this file may be used in accordance with the terms and
22  ** conditions contained in a signed written agreement between you and Nokia.
23  *********************************************************************************/
24
25 #include "trackerchangelistener.h"
26
27 #include <dao/contactdetailschema.h>
28 #include <dao/sparqlresolver.h>
29 #include <engine/engine_p.h>
30 #include <engine/tasks.h>
31 #include <lib/logger.h>
32
33 #include <ontologies/nco.h>
34
35 ////////////////////////////////////////////////////////////////////////////////////////////////////
36
37 CUBI_USE_NAMESPACE
38 CUBI_USE_NAMESPACE_RESOURCES
39
40 ////////////////////////////////////////////////////////////////////////////////////////////////////
41
42 QctTrackerChangeListener::QctTrackerChangeListener(const QContactTrackerEngineParameters &params,
43                                                    QObject *parent)
44     : QObject(parent)
45     , m_debugSignals(params.m_debugFlags.testFlag(QContactTrackerEngine::ShowSignals))
46     , m_omitPresenceChanges(params.m_omitPresenceChanges)
47 {
48     // setup m_signalCoalescingTimer
49     m_signalCoalescingTimer.setInterval(params.m_coalescingDelay);
50     m_signalCoalescingTimer.setSingleShot(true);
51     m_signalCoalescingTimer.stop();
52
53     connect(&m_signalCoalescingTimer, SIGNAL(timeout()), SLOT(emitQueuedNotifications()));
54
55     // setup change notification listeners
56     QSet<QString> contactClassIris;
57
58     foreach(const QTrackerContactDetailSchema &schema, params.m_detailSchemas) {
59         // Must monitor individual contact classes instead of nco:Contact
60         // to avoid bogus notifications for:
61         //
62         //  - QContactOrganization details, implemented via nco:OrganizationContact
63         //  - incoming calls which cause call-ui to create temporary(?) nco:Contact instances
64         //
65         // Additionally, nco:Contact does not have the tracker:notify property set to true, whereas
66         // nco:PersonContact and nco:ContactGroup do have the property, so only those classes will
67         // receive notifications of changes.
68         foreach(const QString &iri, schema.contactClassIris()) {
69             if (iri != nco::Contact::iri()) {
70                 contactClassIris += iri;
71             }
72         }
73     }
74
75     // Connecting to those signals early should prevent losing signals while
76     // we wait for the tracker:id() resolver.
77     foreach(const QString &iri, contactClassIris) {
78         connect(new TrackerChangeNotifier(iri, this),
79                 SIGNAL(changed(QList<TrackerChangeNotifier::Quad>,
80                                QList<TrackerChangeNotifier::Quad>)),
81                 SLOT(onGraphChanged(QList<TrackerChangeNotifier::Quad>,
82                                     QList<TrackerChangeNotifier::Quad>)));
83     }
84
85     // Store some frequently needed resource ids.
86     QStringList resourceIris = QStringList() << nco::belongsToGroup::iri()
87                                              << nco::contactLocalUID::iri()
88                                              << rdf::type::iri()
89                                              << nie::contentLastModified::iri()
90                                              << params.m_graphIri
91                                              << contactClassIris.toList();
92
93     // Keep ownership of the object, so that we can reliably access it in onTrackerIdsResolved().
94     m_resolver = new QctTrackerIdResolver(resourceIris, this);
95     QScopedPointer<QctResolverTask> task(new QctResolverTask(m_resolver));
96
97     // The task will run in a different thread, therefore this signal will be delivered per
98     // queued connection. Note that the task might have been deleted already in its thread,
99     // when the listener finally processes its finished() signal.
100     connect(task.data(), SIGNAL(finished()),
101             this, SLOT(onTrackerIdsResolved()));
102
103     // Cannot use the engine's task queue because change listeners are attached to their thread,
104     // not their engine.
105     m_taskQueue = new QctQueue(this);
106     m_taskQueue->enqueue(task.take());
107 }
108
109 QctTrackerChangeListener::~QctTrackerChangeListener()
110 {
111     // Basically to make sure we wait for pending tasks: We cannot rely on QObject's children
112     // deletion here because its behavior is too random for our purposes here. For instance,
113     // depending on object initialization order, m_resolver could get deleted by that mechanism
114     // before m_taskQueue, leading to m_taskQueue operating with a dead resolver. Note that
115     // QScopedPointer suffers from some similiar problem: There we'd depend on order of
116     // declaration.
117     resetTaskQueue();
118 }
119
120 void
121 QctTrackerChangeListener::onTrackerIdsResolved()
122 {
123     // This slot really must run in the listener's own thread. Direct connection would be harmful.
124     Q_ASSERT(QThread::currentThread() == thread());
125
126     if (0 == m_resolver) {
127         qctWarn("the change listener's resolver has been deleted already");
128         return;
129     }
130
131     if (m_resolver->resourceIris().count() != m_resolver->trackerIds().count()) {
132         qctWarn("the change listener's resolver task failed");
133         return;
134     }
135
136     QList<uint> trackerIds = m_resolver->trackerIds();
137     m_trackerIds.belongsToGroup = trackerIds.takeFirst();
138     m_trackerIds.contactLocalUID = trackerIds.takeFirst();
139     m_trackerIds.rdfType = trackerIds.takeFirst();
140     m_trackerIds.contentLastModified = trackerIds.takeFirst();
141     m_trackerIds.graph = trackerIds.takeFirst();
142     m_trackerIds.contactClasses = trackerIds;
143
144     emitQueuedNotifications();
145
146     resetTaskQueue();
147 }
148
149 void
150 QctTrackerChangeListener::resetTaskQueue()
151 {
152     // Delete the task queue before we delete the resolver to avoid the risk of operating
153     // on a dead resolver. The destructor will block if currently some task is running.
154
155     delete m_taskQueue;
156     m_taskQueue = 0;
157
158     delete m_resolver;
159     m_resolver = 0;
160 }
161
162 void
163 QctTrackerChangeListener::processNotifications(QList<TrackerChangeNotifier::Quad> &notifications,
164                                                QSet<QContactLocalId> &additionsOrRemovals,
165                                                QSet<QContactLocalId> &relationshipChanges,
166                                                QSet<QContactLocalId> &propertyChanges,
167                                                bool matchTaggedSignals)
168 {
169     QSet<QContactLocalId> ignoredIds;
170
171     foreach(const TrackerChangeNotifier::Quad &quad, notifications) {
172         // Contactsd will always "tag" the updates on nco:PersonContact with its
173         // own graph if the update is not significant for clients not interested
174         // in IM stuff (presence change would be tagged, adding a birthday/phone
175         // number would not be tagged). If we detect an update on a contact that
176         // is outside our graph (and not in the default one), then we don't emit
177         // a change signal for that contact.
178         // The tagging of the signal is done by contactsd by inserting a "dummy"
179         // nie:contentLastModified statement in contactsd's graph that does not
180         // change the final inserted data. So the data present in Tracker in the
181         // end does not change, but the GraphUpdated signal mentions this dummy
182         // insert in contactsd graph.
183         if (matchTaggedSignals) {
184             if (quad.graph != 0 && quad.graph != m_trackerIds.graph
185              && quad.predicate == m_trackerIds.contentLastModified) {
186                 ignoredIds += quad.subject;
187                 continue;
188             }
189         }
190
191         // Changes to rdf:type are the most reliable indication of resource creation and removal.
192         if (m_trackerIds.rdfType == uint(quad.predicate)) {
193             if (m_trackerIds.contactClasses.contains(quad.object)) {
194                 additionsOrRemovals += quad.subject;
195             }
196             continue;
197         }
198
199         // Changes to nco:belongsToGroup indicate group membership changes.
200         if (m_trackerIds.belongsToGroup == uint(quad.predicate)) {
201             relationshipChanges += quad.subject;
202             relationshipChanges += quad.object;
203             continue;
204         }
205
206         // All other property changes indicate a contact change,
207         // except for changes to nco:contactLocalUID which happen
208         // when that property is updated for backward compatiblity.
209         if (m_trackerIds.contactLocalUID != uint(quad.predicate)) {
210             propertyChanges += quad.subject;
211         }
212     }
213
214     // We only ignore IDs for property changes, contact creation/deletion is
215     // always significant, and relationships are handled only through contacts
216     // application
217     propertyChanges -= ignoredIds;
218
219     // Hopefully not bogus optimization:
220     // Avoid change notifications wich overlap with addition or removal notifications.
221     propertyChanges -= additionsOrRemovals;
222
223     // Clear processed notifcation queue.
224     notifications.clear();
225 }
226
227 void
228 QctTrackerChangeListener::onGraphChanged(const QList<TrackerChangeNotifier::Quad>& deletes,
229                                          const QList<TrackerChangeNotifier::Quad>& inserts)
230 {
231     if (m_debugSignals) {
232         TrackerChangeNotifier *const notifier = static_cast<TrackerChangeNotifier *>(sender());
233
234         qDebug() << notifier->watchedClass() << "- deletes:" << deletes;
235         qDebug() << notifier->watchedClass() << "- inserts:" << inserts;
236     }
237
238     m_deleteNotifications += deletes;
239     m_insertNotifications += inserts;
240
241     // minimally delay signal emission to give a chance for signals getting coalesced
242     if (not m_trackerIds.contactClasses.isEmpty() && not m_signalCoalescingTimer.isActive()) {
243         m_signalCoalescingTimer.start();
244     }
245 }
246
247 void
248 QctTrackerChangeListener::emitQueuedNotifications()
249 {
250     QSet<QContactLocalId> contactsAddedIds, contactsRemovedIds, contactsChangedIds;
251     QSet<QContactLocalId> relationshipsAddedIds, relationshipsRemovedIds;
252
253     // process notification queues...
254     // We never match tagged signals on DELETEs, since the graph information is
255     // not reliable there (see GB#659936)
256     processNotifications(m_deleteNotifications, contactsRemovedIds,
257                          relationshipsRemovedIds, contactsChangedIds,
258                          false);
259     processNotifications(m_insertNotifications, contactsAddedIds,
260                          relationshipsAddedIds, contactsChangedIds,
261                          m_omitPresenceChanges);
262
263     // ...report identified changes when requested...
264     if (m_debugSignals) {
265         qDebug() << "added contacts:" << contactsAddedIds.count() << contactsAddedIds;
266         qDebug() << "changed contacts:" << contactsChangedIds.count() << contactsChangedIds;
267         qDebug() << "removed contacts:" << contactsRemovedIds.count() << contactsRemovedIds;
268         qDebug() << "added relationships:" << relationshipsAddedIds.count() << relationshipsAddedIds;
269         qDebug() << "removed relationships:" << relationshipsRemovedIds.count() << relationshipsRemovedIds;
270     }
271
272     // ...and finally emit the signals.
273     if (not contactsAddedIds.isEmpty()) {
274         emit contactsAdded(contactsAddedIds.toList());
275     }
276
277     if (not contactsChangedIds.isEmpty()) {
278         emit contactsChanged(contactsChangedIds.toList());
279     }
280
281     if (not contactsRemovedIds.isEmpty()) {
282         emit contactsRemoved(contactsRemovedIds.toList());
283     }
284
285     if (not relationshipsAddedIds.isEmpty()) {
286         emit relationshipsAdded(relationshipsAddedIds.toList());
287     }
288
289     if (not relationshipsRemovedIds.isEmpty()) {
290         emit relationshipsRemoved(relationshipsRemovedIds.toList());
291     }
292 }