Changes: get summary and date of a calendar entry in one go
[qtcontacts-tracker:contactsd.git] / plugins / birthday / cdbirthdaycontroller.cpp
1 /** This file is part of Contacts daemon
2  **
3  ** Copyright (c) 2010-2011 Nokia Corporation and/or its subsidiary(-ies).
4  **
5  ** Contact:  Nokia Corporation (info@qt.nokia.com)
6  **
7  ** GNU Lesser General Public License Usage
8  ** This file may be used under the terms of the GNU Lesser General Public License
9  ** version 2.1 as published by the Free Software Foundation and appearing in the
10  ** file LICENSE.LGPL included in the packaging of this file.  Please review the
11  ** following information to ensure the GNU Lesser General Public License version
12  ** 2.1 requirements will be met:
13  ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
14  **
15  ** In addition, as a special exception, Nokia gives you certain additional rights.
16  ** These rights are described in the Nokia Qt LGPL Exception version 1.1, included
17  ** in the file LGPL_EXCEPTION.txt in this package.
18  **
19  ** Other Usage
20  ** Alternatively, this file may be used in accordance with the terms and
21  ** conditions contained in a signed written agreement between you and Nokia.
22  **/
23
24 #include "cdbirthdaycontroller.h"
25 #include "cdbirthdaycalendar.h"
26 #include "cdbirthdayplugin.h"
27 #include "debug.h"
28
29 #include <QDir>
30 #include <QFile>
31
32 #include <cubi.h>
33 #include <ontologies.h>
34
35 #include <QContactBirthday>
36 #include <QContactDetailFilter>
37 #include <QContactDisplayLabel>
38 #include <QContactFetchRequest>
39 #include <QContactLocalIdFilter>
40
41
42 QTM_USE_NAMESPACE
43
44 CUBI_USE_NAMESPACE
45 CUBI_USE_NAMESPACE_RESOURCES
46
47 using namespace Contactsd;
48
49 // The logic behind this class was strongly influenced by QctTrackerChangeListener in
50 // qtcontacts-tracker.
51 CDBirthdayController::CDBirthdayController(QSparqlConnection &connection,
52                                            QObject *parent)
53     : QObject(parent)
54     , mSparqlConnection(connection)
55     , mCalendar(0)
56     , mManager(0)
57 {
58     const QLatin1String trackerManagerName = QLatin1String("tracker");
59
60     mManager = new QContactManager(trackerManagerName, QMap<QString, QString>(), this);
61
62     if (mManager->managerName() != trackerManagerName) {
63         debug() << Q_FUNC_INFO << "Tracker plugin not found";
64         return;
65     }
66
67     fetchTrackerIds();
68
69     if (not stampFileExists()) {
70         // Delete the calendar database.
71         mCalendar = new CDBirthdayCalendar(CDBirthdayCalendar::FullSync, this);
72
73         updateAllBirthdays();
74     } else {
75         // Use the existing calendar database.
76         mCalendar = new CDBirthdayCalendar(CDBirthdayCalendar::Incremental, this);
77     }
78 }
79
80 CDBirthdayController::~CDBirthdayController()
81 {
82 }
83
84 ///////////////////////////////////////////////////////////////////////////////////////////////////
85 // Tracker ID fetching
86 ///////////////////////////////////////////////////////////////////////////////////////////////////
87
88 void
89 CDBirthdayController::fetchTrackerIds()
90 {
91     // keep in sync with the enum in the header and NTrackerIds
92     const QList<ResourceValue> resources = QList<ResourceValue>()
93                                            << nco::birthDate::resource()
94                                            << rdf::type::resource()
95                                            << nco::PersonContact::resource()
96                                            << nco::ContactGroup::resource();
97
98     Select select;
99
100     foreach (const ResourceValue &value, resources) {
101         select.addProjection(Functions::trackerId.apply(value));
102     }
103
104     if (not mSparqlConnection.isValid()) {
105         debug() << Q_FUNC_INFO << "SPARQL connection is not valid";
106         return;
107     }
108
109     QScopedPointer<QSparqlResult> result(mSparqlConnection.exec(QSparqlQuery(select.sparql())));
110
111     if (result->hasError()) {
112         debug() << Q_FUNC_INFO << "Could not fetch Tracker IDs:" << result->lastError().message();
113         return;
114     }
115
116     connect(result.take(), SIGNAL(finished()), this, SLOT(onTrackerIdsFetched()));
117 }
118
119 void
120 CDBirthdayController::onTrackerIdsFetched()
121 {
122     QSparqlResult *result = qobject_cast<QSparqlResult*>(sender());
123
124     if (result == 0) {
125         debug() << Q_FUNC_INFO << "Invalid result";
126         return;
127     }
128
129     if (result->hasError()) {
130         warning() << Q_FUNC_INFO << "Could not fetch Tracker IDs:" << result->lastError().message();
131         result->deleteLater();
132         return;
133     }
134
135     if (not result->next()) {
136         warning() << Q_FUNC_INFO << "No results returned";
137         result->deleteLater();
138         return;
139     }
140
141     const QSparqlResultRow row = result->current();
142
143     for (int i = 0; i < NTrackerIds; ++i) {
144         mTrackerIds[i] = row.value(i).toInt();
145     }
146
147     // Provide hint we are done with this result.
148     result->deleteLater();
149
150     debug() << Q_FUNC_INFO << "Tracker IDs fetched, connecting the change notifier";
151
152     connectChangeNotifier();
153 }
154
155 ///////////////////////////////////////////////////////////////////////////////////////////////////
156 // Contact monitor
157 ///////////////////////////////////////////////////////////////////////////////////////////////////
158
159 void
160 CDBirthdayController::connectChangeNotifier()
161 {
162     const QStringList contactClassIris = QStringList()
163                                       << nco::PersonContact::iri()
164                                       << nco::ContactGroup::iri();
165
166     foreach (const QString &iri, contactClassIris) {
167         connect(new TrackerChangeNotifier(iri, this),
168             SIGNAL(changed(QList<TrackerChangeNotifier::Quad>,
169                            QList<TrackerChangeNotifier::Quad>)),
170             SLOT(onGraphChanged(QList<TrackerChangeNotifier::Quad>,
171                                 QList<TrackerChangeNotifier::Quad>)));
172     }
173 }
174
175 void
176 CDBirthdayController::onGraphChanged(const QList<TrackerChangeNotifier::Quad>& deletions,
177                                      const QList<TrackerChangeNotifier::Quad>& insertions)
178 {
179     mDeleteNotifications += deletions;
180     mInsertNotifications += insertions;
181
182     if (isDebugEnabled()) {
183         TrackerChangeNotifier const * const notifier = qobject_cast<TrackerChangeNotifier *>(sender());
184
185         if (notifier == 0) {
186             warning() << Q_FUNC_INFO << "Error casting birthday change notifier";
187             return;
188         }
189
190         debug() << notifier->watchedClass() << "birthday: deletions:" << deletions;
191         debug() << notifier->watchedClass() << "birthday: insertions:" << insertions;
192     }
193
194     processNotificationQueues();
195 }
196
197 void
198 CDBirthdayController::processNotifications(QList<TrackerChangeNotifier::Quad> &notifications,
199                                            QSet<QContactLocalId> &propertyChanges,
200                                            QSet<QContactLocalId> &resourceChanges)
201 {
202     foreach (const TrackerChangeNotifier::Quad &quad, notifications) {
203         if (quad.predicate == mTrackerIds[NcoBirthDate]) {
204             propertyChanges += quad.subject;
205             continue;
206         }
207
208         if (quad.predicate == mTrackerIds[RdfType]) {
209             if (quad.object == mTrackerIds[NcoPersonContact]
210              || quad.object == mTrackerIds[NcoContactGroup]) {
211                 resourceChanges += quad.subject;
212             }
213         }
214     }
215
216     // Remove the processed notifications.
217     notifications.clear();
218 }
219
220 void
221 CDBirthdayController::processNotificationQueues()
222 {
223     QSet<QContactLocalId> insertedContacts;
224     QSet<QContactLocalId> deletedContacts;
225     QSet<QContactLocalId> birthdayChangedIds;
226
227     // Process notification queues to determine contacts with changed birthdays.
228     processNotifications(mDeleteNotifications, birthdayChangedIds, deletedContacts);
229     processNotifications(mInsertNotifications, birthdayChangedIds, insertedContacts);
230
231     if (isDebugEnabled()) {
232         debug() << "changed birthdates: " << birthdayChangedIds.count() << birthdayChangedIds;
233     }
234
235     // Remove the birthdays for contacts that are not there anymore
236     foreach (QContactLocalId id, deletedContacts) {
237         mCalendar->deleteBirthday(id);
238     }
239
240     // Update the calendar with the birthday changes.
241     if (not birthdayChangedIds.isEmpty()) {
242         fetchContacts(birthdayChangedIds.toList());
243     }
244
245     // We save the calendar in processFetchRequest(), but if no contact needs
246     // to be fetched, then call save() now
247     if (not deletedContacts.isEmpty() && birthdayChangedIds.isEmpty()) {
248         mCalendar->save();
249     }
250 }
251
252 ///////////////////////////////////////////////////////////////////////////////////////////////////
253 // Full sync logic
254 ///////////////////////////////////////////////////////////////////////////////////////////////////
255
256 bool
257 CDBirthdayController::stampFileExists()
258 {
259     const QFile cacheFile(stampFilePath(), this);
260
261     return cacheFile.exists();
262 }
263
264 void
265 CDBirthdayController::createStampFile()
266 {
267     QFile cacheFile(stampFilePath(), this);
268
269     if (not cacheFile.exists()) {
270         if (not cacheFile.open(QIODevice::WriteOnly)) {
271             warning() << Q_FUNC_INFO << "Unable to create birthday plugin stamp file "
272                                      << cacheFile.fileName() << " with error " << cacheFile.errorString();
273         } else {
274             cacheFile.close();
275         }
276     }
277 }
278
279 QString
280 CDBirthdayController::stampFilePath() const
281 {
282     return BasePlugin::cacheFileName(QLatin1String("calendar.stamp"));
283 }
284
285 void
286 CDBirthdayController::updateAllBirthdays()
287 {
288     // Fetch any contact with a birthday.
289     QContactDetailFilter fetchFilter;
290     fetchFilter.setDetailDefinitionName(QContactBirthday::DefinitionName);
291
292     fetchContacts(fetchFilter, SLOT(onFullSyncRequestStateChanged(QContactAbstractRequest::State)));
293 }
294
295 void
296 CDBirthdayController::onFullSyncRequestStateChanged(QContactAbstractRequest::State newState)
297 {
298     if (processFetchRequest(qobject_cast<QContactFetchRequest*>(sender()), newState)) {
299         // Create the stamp file only after a successful full sync.
300         createStampFile();
301     }
302 }
303
304 ///////////////////////////////////////////////////////////////////////////////////////////////////
305 // Incremental sync logic
306 ///////////////////////////////////////////////////////////////////////////////////////////////////
307
308 void
309 CDBirthdayController::fetchContacts(const QList<QContactLocalId> &contactIds)
310 {
311     QContactLocalIdFilter fetchFilter;
312     fetchFilter.setIds(contactIds);
313
314     fetchContacts(fetchFilter, SLOT(onFetchRequestStateChanged(QContactAbstractRequest::State)));
315 }
316
317 void
318 CDBirthdayController::onFetchRequestStateChanged(QContactAbstractRequest::State newState)
319 {
320     processFetchRequest(qobject_cast<QContactFetchRequest*>(sender()), newState);
321 }
322
323 ///////////////////////////////////////////////////////////////////////////////////////////////////
324 // Common sync logic
325 ///////////////////////////////////////////////////////////////////////////////////////////////////
326
327 void
328 CDBirthdayController::fetchContacts(const QContactFilter &filter, const char *slot)
329 {
330     QContactFetchHint fetchHint;
331     static const QStringList detailDefinitions = QStringList() << QContactBirthday::DefinitionName
332                                                                << QContactDisplayLabel::DefinitionName;
333     fetchHint.setDetailDefinitionsHint(detailDefinitions);
334     fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships |
335                                    QContactFetchHint::NoActionPreferences |
336                                    QContactFetchHint::NoBinaryBlobs);
337
338     QContactFetchRequest * const fetchRequest = new QContactFetchRequest(this);
339     fetchRequest->setManager(mManager);
340     fetchRequest->setFetchHint(fetchHint);
341     fetchRequest->setFilter(filter);
342
343     connect(fetchRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), slot);
344
345     if (not fetchRequest->start()) {
346         warning() << Q_FUNC_INFO << "Unable to start birthday contact fetch request";
347         delete fetchRequest;
348         return;
349     }
350
351     debug() << "Birthday contacts fetch request started";
352 }
353
354 bool
355 CDBirthdayController::processFetchRequest(QContactFetchRequest *const fetchRequest,
356                                           QContactAbstractRequest::State newState)
357 {
358     if (fetchRequest == 0) {
359         warning() << Q_FUNC_INFO << "Invalid fetch request";
360         return false;
361     }
362
363     bool success = false;
364
365     switch (newState) {
366     case QContactAbstractRequest::FinishedState:
367         debug() << "Birthday contacts fetch request finished";
368
369         if (fetchRequest->error() != QContactManager::NoError) {
370             warning() << Q_FUNC_INFO << "Error during birthday contact fetch request, code: "
371                       << fetchRequest->error();
372         } else {
373             updateBirthdays(fetchRequest->contacts());
374             success = true;
375         }
376
377         break;
378
379     case QContactAbstractRequest::CanceledState:
380         break;
381
382     default:
383         return false;
384     }
385
386     // Save the calendar in any case (success or not), since this "save" call
387     // also applies for the deleteBirthday() calls in processNotificationQueues()
388     mCalendar->save();
389
390     // Provide hint we are done with this request.
391     fetchRequest->deleteLater();
392
393     return success;
394 }
395
396 void
397 CDBirthdayController::updateBirthdays(const QList<QContact> &changedBirthdays)
398 {
399     foreach (const QContact &contact, changedBirthdays) {
400         const QContactBirthday contactBirthday = contact.detail<QContactBirthday>();
401         const QContactDisplayLabel contactDisplayLabel = contact.detail<QContactDisplayLabel>();
402         const CalendarBirthday calendarBirthday = mCalendar->birthday(contact.localId());
403
404         // Display label or birthdate was removed from the contact, so delete it from the calendar.
405         if (contactDisplayLabel.label().isNull() || contactBirthday.date().isNull()) {
406             if (isDebugEnabled()) {
407                 debug() << "Contact: " << contact << " removed birthday or displayLabel, so delete the calendar event";
408             }
409
410             mCalendar->deleteBirthday(contact.localId());
411         // Display label or birthdate was changed on the contact, so update the calendar.
412         } else if ((contactDisplayLabel.label() != calendarBirthday.summary()) ||
413                    (contactBirthday.date() != calendarBirthday.date())) {
414             if (isDebugEnabled()) {
415                 debug() << "Contact with calendar birthday: " << contactBirthday.date()
416                         << " and calendar displayLabel: " << calendarBirthday.summary()
417                         << " changed details to: " << contact << ", so update the calendar event";
418             }
419
420             mCalendar->updateBirthday(contact);
421         }
422     }
423 }