WIP: CDTpStorage::syncAccount ported to qsparql. SaveAvatar still needs to be ported.
[qtcontacts-tracker:astojiljs-contactsd.git] / plugins / telepathy / cdtpstorage.cpp
1 /***************************************************************************
2 **
3 ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (people-users@projects.maemo.org)
6 **
7 ** This file is part of contactsd.
8 **
9 ** If you have questions regarding the use of this file, please contact
10 ** Nokia at people-users@projects.maemo.org.
11 **
12 ** This library is free software; you can redistribute it and/or
13 ** modify it under the terms of the GNU Lesser General Public
14 ** License version 2.1 as published by the Free Software Foundation
15 ** and appearing in the file LICENSE.LGPL included in the packaging
16 ** of this file.
17 **
18 ****************************************************************************/
19
20 #include <QtTracker/ontologies/nco.h>
21 #include <QtTracker/ontologies/nie.h>
22 #include <QtSparql>
23
24 #include <TelepathyQt4/AvatarData>
25 #include <TelepathyQt4/ContactCapabilities>
26 #include <TelepathyQt4/ConnectionCapabilities>
27
28 #include "cdtpstorage.h"
29 #include "sparqlconnectionmanager.h"
30
31 #define MAX_UPDATE_SIZE 999
32 #define MAX_REMOVE_SIZE 999
33
34 const QUrl CDTpStorage::defaultGraph = QUrl(
35         QLatin1String("urn:uuid:08070f5c-a334-4d19-a8b0-12a3071bfab9"));
36 const QUrl CDTpStorage::privateGraph = QUrl(
37         QLatin1String("urn:uuid:679293d4-60f0-49c7-8d63-f1528fe31f66"));
38
39 const QString defaultGenerator = "telepathy";
40
41 // cleanup - delete all telepathy contacts without affiliations (as affiliations are removed before)
42 const QLatin1String cleanupEmptyTelepathyContacts(
43         "DELETE {?c a rdfs:Resource.}\n"
44         "    WHERE {?c a nco:PersonContact; nie:generator 'telepathy'.\n"
45         "           OPTIONAL {?c nco:hasAffiliation ?af. ?af nco:hasIMAddress ?vExist.}\n"
46         "           FILTER(!bound(?vExist))}\n");
47
48 CDTpStorage::CDTpStorage(QObject *parent)
49     : QObject(parent)
50 {
51     //::tracker()->setServiceAttribute("tracker_access_method", QString("QSPARQL_DIRECT"));
52
53     mQueueTimer.setSingleShot(true);
54     mQueueTimer.setInterval(100);
55     connect(&mQueueTimer, SIGNAL(timeout()), SLOT(onQueueTimerTimeout()));
56 }
57
58 CDTpStorage::~CDTpStorage()
59 {
60 }
61
62 void CDTpStorage::syncAccountSet(const QList<QString> &accounts)
63 {
64     RDFVariable imAccount = RDFVariable::fromType<nco::IMAccount>();
65
66     RDFVariableList members;
67     Q_FOREACH (QString accountObjectPath, accounts) {
68         members << RDFVariable(QUrl(QString("telepathy:%1").arg(accountObjectPath)));
69     }
70     imAccount.isMemberOf(members).not_();
71
72     RDFSelect select;
73     select.addColumn("Accounts", imAccount);
74
75     CDTpSelectQuery *query = new CDTpSelectQuery(select, this);
76     connect(query,
77             SIGNAL(finished(CDTpSelectQuery *)),
78             SLOT(onAccountPurgeSelectQueryFinished(CDTpSelectQuery *)));
79 }
80
81 void CDTpStorage::onAccountPurgeSelectQueryFinished(CDTpSelectQuery *query)
82 {
83     LiveNodes result = query->reply();
84
85     for (int i = 0; i < result->rowCount(); i++) {
86         const QString accountUrl = result->index(i, 0).data().toString();
87         const QString accountObjectPath = accountUrl.mid(QString("telepathy:").length());
88
89         removeAccount(accountObjectPath);
90     }
91 }
92
93 void CDTpStorage::syncAccount(CDTpAccountPtr accountWrapper)
94 {
95     syncAccount(accountWrapper, CDTpAccount::All);
96
97     /* If contactsd leaves while account is still online, and get restarted
98      * when account is offline then contacts in tracker still have presence.
99      * This happens when rebooting the device. */
100     if (!accountWrapper->account()->connection()) {
101         setAccountContactsOffline(accountWrapper);
102     }
103 }
104
105 void CDTpStorage::syncAccount(CDTpAccountPtr accountWrapper,
106         CDTpAccount::Changes changes)
107 {
108     Tp::AccountPtr account = accountWrapper->account();
109     if (account->normalizedName().isEmpty()) {
110         return;
111     }
112
113     const QString accountObjectPath = account->objectPath();
114     const QString accountId = account->normalizedName();
115     const QDateTime datetime = QDateTime::currentDateTime();
116     const QString imAddressUri = contactImAddress(accountObjectPath, accountId).toString();
117
118     qDebug() << "Syncing account" << accountObjectPath << "to storage" << accountId;
119
120     // Create the IMAddress for this account's self contact
121     QString query = QString::fromLatin1("INSERT {<%1> a nco:IMAddress; nco:imID '%2'.}\n").
122                     arg(imAddressUri).arg(account->normalizedName());
123
124     if (changes & CDTpAccount::Nickname) {
125         qDebug() << "  nickname changed";
126         query += QString::fromLatin1("DELETE {<%1> nco:imNickname ?n.} WHERE {<%1> nco:imNickname ?n.}\n"
127                                      "INSERT {<%1> nco:imNickname '%2'.}\n").arg(imAddressUri).arg(account->nickname());
128     }
129
130     if (changes & CDTpAccount::Presence) {
131         qDebug() << "  presence changed";
132         const Tp::Presence presence(account->currentPresence());
133         query += QString::fromLatin1("DELETE {<%1> ?p ?v.} WHERE {<%1> ?p ?v. FILTER ((?p IN (nco:imPresence, nco:imStatusMessage, nco:presenceLastModified)))}\n"
134                                      "INSERT {<%1> nco:imPresence %2; nco:imStatusMessage '%3'; nco:presenceLastModified %4.}\n")
135                                      .arg(imAddressUri)
136                                      .arg(trackerStatusFromTpPresenceStatus(presence.status()))
137                                      .arg(presence.statusMessage())
138                                      .arg(sparqlRepresentation(datetime));
139     }
140
141     if (changes & CDTpAccount::Avatar) {
142         qWarning() << "  avatar changed not implemented. work in progress";
143         // const Tp::Avatar &avatar = account->avatar();
144         // FIXME: saving to disk needs to be removed here
145         // saveAccountAvatar(up, avatar.avatarData, avatar.MIMEType, imAddress,
146         //    inserts);
147     }
148
149     // Create an IMAccount
150     QString imAccount(QString::fromLatin1("telepathy:%1").arg(accountObjectPath));
151
152     // FIXME:
153     // remove redundant hasIMContact and use only imAccountAddress once it is removed from qtcontacts-tracker.
154     // accountType and protocol the same?
155     query += QString::fromLatin1("DELETE {<%1> ?p ?v.} WHERE {<%1> ?p ?v. FILTER ((?p IN (nco:imAccountType, nco:imProtocol, nco:imAccountAddress, nco:imDisplayName)))}\n"
156                                  "INSERT {<%1> a nco:IMAccount; nco:imAccountType '%2'; nco:imProtocol '%2'; nco:imAccountAddress <%3>; nco:hasIMContact <%3>; nco:imDisplayName '%4'.}\n")
157                                  .arg(imAccount)
158                                  .arg(account->protocolName())
159                                  .arg(imAddressUri)
160                                  .arg(account->displayName());
161
162     // link the IMAddress to me-contact via an affiliation
163     const QString imAffiliation(contactAffiliation(accountObjectPath, accountId).toString());
164
165     query += QString::fromLatin1("INSERT {<%1> a nco:Affiliation; rdfs:label 'Other'; nco:hasIMAddress <%2>.}\n")
166                                  .arg(imAffiliation)
167                                  .arg(imAddressUri);
168
169     query += QString::fromLatin1("DELETE {nco:default-contact-me nie:contentLastModified ?a.} WHERE {nco:default-contact-me nie:contentLastModified ?a.}\n"
170                                  "INSERT {nco:default-contact-me nie:contentLastModified %1; nco:hasAffiliation <%2>; nco:contactLocalUID '%3'.}\n")
171                                  .arg(sparqlRepresentation(datetime))
172                                  .arg(imAffiliation)
173                                  .arg(QString::number(0x7FFFFFFF));
174     executeQuery(QSparqlQuery(query, QSparqlQuery::InsertStatement));
175 }
176
177 void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper)
178 {
179     CDTpStorageSyncOperations &op = mSyncOperations[accountWrapper];
180     if (!op.active) {
181         op.active = true;
182         Q_EMIT syncStarted(accountWrapper);
183     }
184
185     syncAccountContacts(accountWrapper, accountWrapper->contacts(),
186             QList<CDTpContactPtr>());
187
188     /* We need to purge contacts that are in tracker but got removed from the
189      * account while contactsd was not running.
190      * We first query all contacts for that account, then will delete those that
191      * are not in the account anymore. We can't use NOT IN() in the query
192      * because with huge contact list it will hit SQL limit. */
193     op.nPendingOperations++;
194     CDTpAccountContactsSelectQuery *query = new CDTpAccountContactsSelectQuery(accountWrapper, this);
195     connect(query,
196             SIGNAL(finished(CDTpSelectQuery *)),
197             SLOT(onContactPurgeSelectQueryFinished(CDTpSelectQuery *)));
198 }
199
200 void CDTpStorage::onContactPurgeSelectQueryFinished(CDTpSelectQuery *query)
201 {
202     CDTpAccountContactsSelectQuery *contactsQuery =
203         qobject_cast<CDTpAccountContactsSelectQuery*>(query);
204     CDTpAccountPtr accountWrapper = contactsQuery->accountWrapper();
205
206     LiveNodes result = query->reply();
207     if (result->rowCount() <= 0) {
208         oneSyncOperationFinished(accountWrapper);
209         return;
210     }
211
212     RDFUpdate updateQuery;
213     RDFStatementList deletions;
214     RDFStatementList accountDeletions;
215     RDFStatementList inserts;
216
217     QList<QUrl> imAddressList;
218     Q_FOREACH (const CDTpContactPtr &contactWrapper, accountWrapper->contacts()) {
219         imAddressList << contactImAddress(contactWrapper);
220     }
221
222     bool foundOne = false;
223     CDTpStorageSyncOperations &op = mSyncOperations[accountWrapper];
224     Q_FOREACH (const CDTpContactsSelectItem &item, contactsQuery->items()) {
225         if (imAddressList.contains(item.imAddress)) {
226             continue;
227         }
228         foundOne = true;
229         op.nContactsRemoved++;
230         addRemoveContactToQuery(updateQuery, inserts, deletions, item);
231         addRemoveContactFromAccountToQuery(accountDeletions, item);
232     }
233
234     if (!foundOne) {
235         oneSyncOperationFinished(accountWrapper);
236         return;
237     }
238
239     updateQuery.addDeletion(deletions, defaultGraph);
240     updateQuery.addInsertion(inserts, defaultGraph);
241     updateQuery.addDeletion(accountDeletions, privateGraph);
242
243     QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
244     CDTpAccountsUpdateQuery *q = new CDTpAccountsUpdateQuery(accounts, updateQuery, this);
245     connect(q,
246             SIGNAL(finished(CDTpUpdateQuery *)),
247             SLOT(onAccountsUpdateQueryFinished(CDTpUpdateQuery *)));
248 }
249
250 void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper,
251         const QList<CDTpContactPtr> &contactsAdded,
252         const QList<CDTpContactPtr> &contactsRemoved)
253 {
254     Q_FOREACH (CDTpContactPtr contactWrapper, contactsAdded) {
255         queueUpdate(contactWrapper, CDTpContact::All);
256     }
257
258     removeContacts(accountWrapper, contactsRemoved);
259 }
260
261 void CDTpStorage::syncAccountContact(CDTpAccountPtr accountWrapper,
262         CDTpContactPtr contactWrapper, CDTpContact::Changes changes)
263 {
264     Q_UNUSED(accountWrapper);
265     queueUpdate(contactWrapper, changes);
266 }
267
268 void CDTpStorage::setAccountContactsOffline(CDTpAccountPtr accountWrapper)
269 {
270     qDebug() << "Setting presence to UNKNOWN for all contacts of account"
271              << accountWrapper->account()->objectPath();
272
273     CDTpAccountContactsSelectQuery *query = new CDTpAccountContactsSelectQuery(accountWrapper, this);
274     connect(query,
275             SIGNAL(finished(CDTpSelectQuery *)),
276             SLOT(onAccountOfflineSelectQueryFinished(CDTpSelectQuery *)));
277 }
278
279 void CDTpStorage::onAccountOfflineSelectQueryFinished(CDTpSelectQuery *query)
280 {
281     RDFVariable unknownState = QUrl(trackerStatusFromTpPresenceStatus(QLatin1String("unknown")));
282     CDTpAccountContactsSelectQuery *contactsQuery =
283         qobject_cast<CDTpAccountContactsSelectQuery*>(query);
284     CDTpAccountPtr accountWrapper = contactsQuery->accountWrapper();
285
286     RDFUpdate updateQuery;
287
288     Q_FOREACH (const CDTpContactsSelectItem &item, contactsQuery->items()) {
289         QUrl imContact(item.imContact);
290         QUrl imAddress(item.imAddress);
291
292         /* Update presence */
293         updateQuery.addDeletion(imAddress, nco::imPresence::iri(),
294             RDFVariable(), defaultGraph);
295         updateQuery.addDeletion(imAddress, nco::presenceLastModified::iri(),
296             RDFVariable(), defaultGraph);
297         updateQuery.addDeletion(imContact, nie::contentLastModified::iri(),
298             RDFVariable(), defaultGraph);
299
300         updateQuery.addInsertion(imAddress, nco::imPresence::iri(),
301             unknownState, defaultGraph);
302         updateQuery.addInsertion(imAddress, nco::presenceLastModified::iri(),
303             LiteralValue(QDateTime::currentDateTime()),defaultGraph);
304         updateQuery.addInsertion(imContact, nie::contentLastModified::iri(),
305             LiteralValue(QDateTime::currentDateTime()), defaultGraph);
306
307         /* Update capabilities */
308         RDFVariableList imAddressPropertyList;
309         RDFStatementList inserts;
310         addContactCapabilitiesInfoToQuery(inserts, imAddressPropertyList,
311             imAddress, accountWrapper->account()->capabilities());
312         Q_FOREACH (RDFVariable property, imAddressPropertyList) {
313             updateQuery.addDeletion(imAddress, property, RDFVariable(), defaultGraph);
314         }
315         updateQuery.addInsertion(inserts, defaultGraph);
316     }
317
318     new CDTpUpdateQuery(updateQuery);
319 }
320
321 /*!
322   * \brief Remove account and all contacts belonging to it
323   */
324 void CDTpStorage::removeAccount(const QString &accountObjectPath)
325 {
326     qDebug() << "Removing account" << accountObjectPath << "from storage";
327     const QString imAccountUri = QString("<telepathy:%1>").arg(accountObjectPath);
328
329     // remove all nco:Affiliations and all nco:IMAddresses related to this account
330     const QString removeContactsQuery = QString::fromLatin1(
331             "DELETE {?c nco:hasAffiliation ?affiliation. ?affiliation a rdfs:Resource. ?imAddress a rdfs:Resource}\n"
332             "    WHERE {?c nco:hasAffiliation ?affiliation. ?affiliation nco:hasIMAddress ?imAddress.\n"
333             "           %1 nco:hasIMContact ?imAddress.}\n").arg(imAccountUri);
334
335     // remove account and remove the account from me-contact
336     const QString updateAccountQuery = QString::fromLatin1(
337             "DELETE {nco:default-contact-me nco:hasIMAddress %1.}\n"
338             "DELETE {%1 a rdfs:Resource.}\n"
339             "DELETE {nco:default-contact-me nie:contentLastModified ?a.} WHERE {nco:default-contact-me nie:contentLastModified ?a.}"
340             "INSERT {nco:default-contact-me nie:contentLastModified %2.}")
341             .arg(imAccountUri).arg(sparqlRepresentation(QDateTime::currentDateTime()));
342
343     executeQuery(QSparqlQuery(removeContactsQuery + updateAccountQuery + cleanupEmptyTelepathyContacts,
344                               QSparqlQuery::DeleteStatement));
345 }
346
347 //DELETE {?affiliation a rdfs:Resource. ?imAddress a rdfs:Resource} WHERE {?affiliation a nco:Affiliation; nco:hasIMAddress ?imAddress. <telepathy:/org/freedesktop/fake/account> nco:hasIMContact ?imAddress.}
348
349 /*! \brief Returns string to be used in sparql */
350 QString CDTpStorage::sparqlRepresentation(const QDateTime &dateTime)
351 {
352     return QString::fromLatin1("\"%1\"").arg(dateTime.toString(Qt::ISODate));
353 }
354
355 void CDTpStorage::executeQuery(const QSparqlQuery &query)
356 {
357     QSparqlConnection &connection = com::nokia::contacts::SparqlConnectionManager::defaultConnection();
358     QSparqlResult *result = connection.exec(query);
359
360     if (not result) {
361         qWarning() << Q_FUNC_INFO << ":" << __LINE__ << " - QSparqlConnection::exec() == 0";
362         return;
363     }
364     if (result->hasError()) {
365         qWarning() << Q_FUNC_INFO << result->lastError().message();
366         delete result;
367         return;
368     }
369
370     result->setParent(this);
371     connect(result, SIGNAL(finished()), SLOT(onQueryFinished()), Qt::QueuedConnection);
372 }
373
374 void CDTpStorage::onQueryFinished()
375 {
376     QSparqlResult *const result = qobject_cast<QSparqlResult *>(sender());
377
378     if (0 == result) {
379         qWarning() << Q_FUNC_INFO << ("Ignoring signal from invalid sender.");
380         return;
381     }
382
383     if (result->hasError()) {
384         qWarning() << Q_FUNC_INFO << result->lastError().message();
385     }
386     result->deleteLater();
387 }
388
389 void CDTpStorage::removeContacts(CDTpAccountPtr accountWrapper,
390         const QList<CDTpContactPtr> &contacts)
391 {
392     if (contacts.isEmpty()) {
393         return;
394     }
395
396     /* Split the request into smaller batches if necessary */
397     if (contacts.size() > MAX_REMOVE_SIZE) {
398         QList<CDTpContactPtr> batch;
399         for (int i = 0; i < contacts.size(); i++) {
400             batch << contacts[i];
401             if (batch.size() == MAX_REMOVE_SIZE) {
402                 removeContacts(accountWrapper, batch);
403                 batch.clear();
404             }
405         }
406         if (!batch.isEmpty()) {
407             removeContacts(accountWrapper, batch);
408             batch.clear();
409         }
410         return;
411     }
412
413     QStringList allImUris;
414     Q_FOREACH (const CDTpContactPtr &contactWrapper, contacts) {
415         allImUris << QString::fromLatin1("<%1>").arg(contactImAddress(contactWrapper).toString());
416         qDebug() << "Contact Removed, cancel update:" << contactWrapper->contact()->id();
417         mUpdateQueue.remove(contactWrapper);
418     }
419
420     // remove all nco:Affiliations and all nco:IMAddresses,
421     const QString removeContactsQuery = QString::fromLatin1(
422             "DELETE {?affiliation a rdfs:Resource. ?imAddress a rdfs:Resource.}\n"
423             "    WHERE {?affiliation a nco:Affiliation; nco:hasIMAddress ?imAddress.\n"
424             "           FILTER (?imAddress IN (%1))}\n").arg(allImUris.join(QLatin1String(",")));
425
426     // remove all links ?imAccount nco:hasIMContact - doing it separatelly from previous, as account might be removed earlier
427     const QString removeFromAccountQuery = QString::fromLatin1(
428             "DELETE {?imAccount nco:hasIMContact ?imAddress.}\n"
429             "    WHERE {?imAccount nco:hasIMContact ?imAddress.\n"
430             "           FILTER (?imAddress IN (%1))}\n").arg(allImUris.join(QLatin1String(",")));
431
432     executeQuery(QSparqlQuery(removeContactsQuery + removeFromAccountQuery + cleanupEmptyTelepathyContacts,
433                               QSparqlQuery::DeleteStatement));
434 }
435
436 void CDTpStorage::addRemoveContactToQuery(RDFUpdate &query,
437         RDFStatementList &inserts,
438         RDFStatementList &deletions,
439         const CDTpContactsSelectItem &item)
440 {
441     const QUrl imContact(item.imContact);
442     const QUrl imAffiliation(item.imAffiliation);
443     const QUrl imAddress(item.imAddress);
444     bool deleteIMContact = (item.generator == defaultGenerator);
445
446     qDebug() << "Deleting" << imAddress << "from" << imContact;
447     qDebug() << "Also delete local contact:" << (deleteIMContact ? "Yes" : "No");
448
449     /* Drop the imAddress and its affiliation */
450     deletions << RDFStatement(imAffiliation, rdf::type::iri(), rdfs::Resource::iri());
451     deletions << RDFStatement(imAddress, rdf::type::iri(), rdfs::Resource::iri());
452
453     if (deleteIMContact) {
454         /* The PersonContact is still owned by contactsd, drop it entirely */
455         deletions << RDFStatement(imContact, rdf::type::iri(), rdfs::Resource::iri());
456     } else {
457         /* The PersonContact got modified by someone else, drop only the
458          * hasAffiliation and keep the local contact in case it contains
459          * additional info */
460         deletions << RDFStatement(imContact, nco::hasAffiliation::iri(), imAffiliation);
461
462         /* Update last modified time */
463         const QDateTime datetime = QDateTime::currentDateTime();
464         deletions << RDFStatement(imContact, nie::contentLastModified::iri(), RDFVariable());
465         inserts << RDFStatement(imContact, nie::contentLastModified::iri(), LiteralValue(datetime));
466     }
467
468     addRemoveContactInfoToQuery(query, imContact, imAddress);
469 }
470
471 void CDTpStorage::addRemoveContactFromAccountToQuery(RDFStatementList &deletions,
472         const CDTpContactsSelectItem &item)
473 {
474     const QUrl imAddress(item.imAddress);
475     const QUrl imAccount(QUrl(item.imAddress.left(item.imAddress.indexOf("!"))));
476     deletions << RDFStatement(imAccount, nco::hasIMContact::iri(), imAddress);
477 }
478
479 void CDTpStorage::onContactUpdateSelectQueryFinished(CDTpSelectQuery *query)
480 {
481     RDFUpdate updateQuery;
482     RDFStatementList inserts;
483     RDFStatementList imAccountInserts;
484     RDFUpdate contactInfoQuery;
485     QList<CDTpAccountPtr> accounts;
486
487     CDTpContactResolver *resolver = qobject_cast<CDTpContactResolver*>(query);
488     Q_FOREACH (CDTpContactPtr contactWrapper, resolver->remoteContacts()) {
489         CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();
490
491         /* Abort the update if the contact is not visible anymore. This could
492          * happen if the contact got removed/blocked/etc while we were
493          * resolving it. */
494         if (!contactWrapper->isVisible()) {
495             oneSyncOperationFinished(accountWrapper);
496             continue;
497         }
498
499         /* Build a list of accounts for which contacts are being updated.
500          * At this point nPendingOperations have one count per contact,
501          * keep only one per account */
502         if (!accounts.contains(accountWrapper)) {
503             accounts << accountWrapper;
504         } else {
505             mSyncOperations[accountWrapper].nPendingOperations--;
506         }
507
508         QString localId = resolver->storageIdForContact(contactWrapper);
509         bool resolved = !localId.isEmpty();
510         if (!resolved) {
511             localId = contactLocalId(contactWrapper);
512             mSyncOperations[accountWrapper].nContactsAdded++;
513         }
514
515         const QString accountObjectPath = accountWrapper->account()->objectPath();
516         const QString id = contactWrapper->contact()->id();
517
518         qDebug() << "Updating" << id << "(" << localId << ")";
519
520         RDFVariableList imAddressPropertyList;
521         RDFVariableList imContactPropertyList;
522         const RDFVariable imContact(contactIri(localId));
523         const RDFVariable imAddress(contactImAddress(contactWrapper));
524         const QDateTime datetime = QDateTime::currentDateTime();
525
526         /* Create an imContact if we couldn't resolve to an existing one.
527          * Otherwise just update its contentLastModified */
528         if (!resolved) {
529             inserts << RDFStatement(imAddress, rdf::type::iri(), nco::IMAddress::iri())
530                     << RDFStatement(imAddress, nco::imID::iri(), LiteralValue(id));
531
532             RDFVariable imAffiliation(contactAffiliation(contactWrapper));
533             inserts << RDFStatement(imAffiliation, rdf::type::iri(), nco::Affiliation::iri())
534                     << RDFStatement(imAffiliation, rdfs::label::iri(), LiteralValue("Other"))
535                     << RDFStatement(imAffiliation, nco::hasIMAddress::iri(), imAddress);
536
537             inserts << RDFStatement(imContact, rdf::type::iri(), nco::PersonContact::iri())
538                     << RDFStatement(imContact, nco::contactLocalUID::iri(), LiteralValue(localId))
539                     << RDFStatement(imContact, nco::contactUID::iri(), LiteralValue(localId))
540                     << RDFStatement(imContact, nie::contentCreated::iri(), LiteralValue(datetime))
541                     << RDFStatement(imContact, nie::contentLastModified::iri(), LiteralValue(datetime))
542                     << RDFStatement(imContact, nie::generator::iri(), LiteralValue(defaultGenerator))
543                     << RDFStatement(imContact, nco::hasAffiliation::iri(), imAffiliation);
544         } else {
545             imContactPropertyList << nie::contentLastModified::iri();
546             inserts << RDFStatement(imContact, nie::contentLastModified::iri(), LiteralValue(datetime));
547         }
548
549         const CDTpContact::Changes changes = resolver->contactChanges(contactWrapper);
550         if (changes & CDTpContact::Alias) {
551             qDebug() << "  alias changed";
552             addContactAliasInfoToQuery(inserts, imAddressPropertyList,
553                 imAddress, contactWrapper);
554         }
555         if (changes & CDTpContact::Presence) {
556             qDebug() << "  presence changed";
557             addContactPresenceInfoToQuery(inserts, imAddressPropertyList,
558                 imAddress, contactWrapper);
559         }
560         if (changes & CDTpContact::Capabilities) {
561             qDebug() << "  capabilities changed";
562             addContactCapabilitiesInfoToQuery(inserts, imAddressPropertyList,
563                 imAddress, contactWrapper->contact()->capabilities());
564         }
565         if (changes & CDTpContact::Avatar) {
566             qDebug() << "  avatar changed";
567             addContactAvatarInfoToQuery(updateQuery, inserts, imAddressPropertyList,
568                 imAddress, contactWrapper);
569         }
570         if (changes & CDTpContact::Authorization) {
571             qDebug() << "  authorization changed";
572             addContactAuthorizationInfoToQuery(inserts, imAddressPropertyList,
573                 imAddress, contactWrapper);
574         }
575         if (changes & CDTpContact::Infomation) {
576             qDebug() << "  vcard information changed";
577             /* Some ContactInfo insertions are made into per-contact graph. Also
578              * some must be done *after* imContact insertions, so we add them
579              * into another RDFUpdate, and we'll append it later. */
580             addContactInfoToQuery(contactInfoQuery, inserts, imContact, contactWrapper);
581         }
582
583         // Link the IMAccount to this IMAddress
584         const RDFVariable imAccount(QUrl(QString("telepathy:%1").arg(accountObjectPath)));
585         imAccountInserts << RDFStatement(imAccount, nco::hasIMContact::iri(), imAddress);
586
587         Q_FOREACH (RDFVariable property, imContactPropertyList) {
588             updateQuery.addDeletion(imContact, property, RDFVariable(), defaultGraph);
589         }
590
591         Q_FOREACH (RDFVariable property, imAddressPropertyList) {
592             updateQuery.addDeletion(imAddress, property, RDFVariable(), defaultGraph);
593         }
594     }
595
596     if (accounts.isEmpty()) {
597         return;
598     }
599
600     updateQuery.addInsertion(inserts, defaultGraph);
601     updateQuery.addInsertion(imAccountInserts, privateGraph);
602     updateQuery.appendUpdate(contactInfoQuery);
603
604     CDTpAccountsUpdateQuery *q = new CDTpAccountsUpdateQuery(accounts, updateQuery, this);
605     connect(q,
606             SIGNAL(finished(CDTpUpdateQuery *)),
607             SLOT(onAccountsUpdateQueryFinished(CDTpUpdateQuery *)));
608 }
609
610 void CDTpStorage::saveAccountAvatar(RDFUpdate &query, const QByteArray &data, const QString &mimeType,
611         const RDFVariable &imAddress, RDFStatementList &inserts)
612 {
613     Q_UNUSED(mimeType);
614
615     query.addDeletion(imAddress, nco::imAvatar::iri(), RDFVariable(), defaultGraph);
616
617     if (data.isEmpty()) {
618         return;
619     }
620
621     QString fileName = QString("%1/.contacts/avatars/%2")
622         .arg(QDir::homePath())
623         .arg(QString(QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex()));
624     qDebug() << "Saving account avatar to" << fileName;
625
626     QFile avatarFile(fileName);
627     if (!avatarFile.open(QIODevice::WriteOnly)) {
628         qWarning() << "Unable to save account avatar: error opening avatar "
629             "file" << fileName << "for writing";
630         return;
631     }
632     avatarFile.write(data);
633     avatarFile.close();
634
635     RDFVariable dataObject(QUrl::fromLocalFile(fileName));
636     query.addDeletion(dataObject, nie::url::iri(), RDFVariable(), defaultGraph);
637
638     inserts << RDFStatement(dataObject, rdf::type::iri(), nie::DataObject::iri())
639             << RDFStatement(dataObject, nie::url::iri(), dataObject)
640             << RDFStatement(imAddress, nco::imAvatar::iri(), dataObject);
641 }
642
643 void CDTpStorage::addContactAliasInfoToQuery(RDFStatementList &inserts,
644         RDFVariableList &properties,
645         const RDFVariable &imAddress,
646         CDTpContactPtr contactWrapper)
647 {
648     Tp::ContactPtr contact = contactWrapper->contact();
649     properties << nco::imNickname::iri();
650     inserts << RDFStatement(imAddress, nco::imNickname::iri(),
651                 LiteralValue(contact->alias()));
652 }
653
654 void CDTpStorage::addContactPresenceInfoToQuery(RDFStatementList &inserts,
655         RDFVariableList &properties,
656         const RDFVariable &imAddress,
657         CDTpContactPtr contactWrapper)
658 {
659     Tp::ContactPtr contact = contactWrapper->contact();
660
661     properties << nco::imPresence::iri() <<
662         nco::imStatusMessage::iri() <<
663         nco::presenceLastModified::iri();
664
665     inserts << RDFStatement(imAddress, nco::imStatusMessage::iri(),
666                 LiteralValue(contact->presence().statusMessage())) <<
667             RDFStatement(imAddress, nco::imPresence::iri(),
668                 trackerStatusFromTpPresenceType(contact->presence().type())) <<
669             RDFStatement(imAddress, nco::presenceLastModified::iri(),
670                 LiteralValue(QDateTime::currentDateTime()));
671 }
672
673 void CDTpStorage::addContactCapabilitiesInfoToQuery(RDFStatementList &inserts,
674         RDFVariableList &properties,
675         const RDFVariable &imAddress,
676         Tp::CapabilitiesBase capabilities)
677 {
678     properties << nco::imCapability::iri();
679
680     if (capabilities.textChats()) {
681         inserts << RDFStatement(imAddress, nco::imCapability::iri(),
682                     nco::im_capability_text_chat::iri());
683     }
684
685     if (capabilities.streamedMediaAudioCalls()) {
686         inserts << RDFStatement(imAddress, nco::imCapability::iri(),
687                     nco::im_capability_audio_calls::iri());
688     }
689
690     if (capabilities.streamedMediaVideoCalls()) {
691         inserts << RDFStatement(imAddress, nco::imCapability::iri(),
692                     nco::im_capability_video_calls::iri());
693     }
694 }
695
696 void CDTpStorage::addContactAvatarInfoToQuery(RDFUpdate &query,
697         RDFStatementList &inserts,
698         RDFVariableList &properties,
699         const RDFVariable &imAddress,
700         CDTpContactPtr contactWrapper)
701 {
702     Tp::ContactPtr contact = contactWrapper->contact();
703
704     /* If we don't know the avatar token, it is preferable to keep the old
705      * avatar until we get an update. */
706     if (!contact->isAvatarTokenKnown()) {
707         return;
708     }
709
710     /* If we have a token but not an avatar filename, that probably means the
711      * avatar data is being requested and we'll get an update later. */
712     if (!contact->avatarToken().isEmpty() &&
713         contact->avatarData().fileName.isEmpty()) {
714         return;
715     }
716
717     RDFVariable dataObject(QUrl::fromLocalFile(contact->avatarData().fileName));
718
719     properties << nco::imAvatar::iri();
720     query.addDeletion(dataObject, nie::url::iri(), RDFVariable(), defaultGraph);
721
722     if (!contact->avatarToken().isEmpty()) {
723         inserts << RDFStatement(dataObject, rdf::type::iri(), nie::DataObject::iri()) <<
724             RDFStatement(dataObject, nie::url::iri(), dataObject) <<
725             RDFStatement(imAddress, nco::imAvatar::iri(), dataObject);
726     }
727 }
728
729 QUrl CDTpStorage::authStatus(Tp::Contact::PresenceState state)
730 {
731     switch (state) {
732     case Tp::Contact::PresenceStateNo:
733         return nco::predefined_auth_status_no::iri();
734     case Tp::Contact::PresenceStateAsk:
735         return nco::predefined_auth_status_requested::iri();
736     case Tp::Contact::PresenceStateYes:
737         return nco::predefined_auth_status_yes::iri();
738     }
739
740     qWarning() << "Unknown telepathy presence state:" << state;
741     return nco::predefined_auth_status_no::iri();
742 }
743
744 void CDTpStorage::addContactAuthorizationInfoToQuery(RDFStatementList &inserts,
745         RDFVariableList &properties,
746         const RDFVariable &imAddress,
747         CDTpContactPtr contactWrapper)
748 {
749     Tp::ContactPtr contact = contactWrapper->contact();
750
751     properties << nco::imAddressAuthStatusFrom::iri() <<
752         nco::imAddressAuthStatusTo::iri();
753     inserts << RDFStatement(imAddress, nco::imAddressAuthStatusFrom::iri(),
754         RDFVariable(authStatus(contact->subscriptionState())));
755     inserts << RDFStatement(imAddress, nco::imAddressAuthStatusTo::iri(),
756         RDFVariable(authStatus(contact->publishState())));
757 }
758
759 void CDTpStorage::addRemoveContactInfoToQuery(RDFUpdate &query,
760         const RDFVariable &imContact,
761         const QUrl &graph)
762 {
763     query.addDeletion(RDFVariable(), rdf::type::iri(), rdfs::Resource::iri(), graph);
764     query.addDeletion(imContact, nco::hasAffiliation::iri(), RDFVariable(), graph);
765     query.addDeletion(imContact, nco::birthDate::iri(), RDFVariable(), graph);
766     query.addDeletion(imContact, nco::note::iri(), RDFVariable(), graph);
767 }
768
769 QString CDTpStorage::safeStringListAt(const QStringList &list, int i)
770 {
771     if (i >= list.size()) {
772         return QString();
773     }
774
775     return list.at(i);
776 }
777
778 void CDTpStorage::addContactInfoToQuery(RDFUpdate &query,
779         RDFStatementList &inserts,
780         const RDFVariable &imContact,
781         CDTpContactPtr contactWrapper)
782 {
783     /* Use the IMAddress URI as Graph URI for all its ContactInfo. This makes
784      * easy to know which im contact those entities/properties belongs to */
785     const QUrl graph = contactImAddress(contactWrapper);
786
787     /* Drop current info */
788     addRemoveContactInfoToQuery(query, imContact, graph);
789
790     Tp::ContactPtr contact = contactWrapper->contact();
791     Tp::ContactInfoFieldList listContactInfo = contact->infoFields().allFields();
792
793     if (listContactInfo.count() == 0) {
794         qDebug() << "No contact info present";
795         return;
796     }
797
798     QHash<QString, RDFVariable> affiliationsMap;
799     RDFStatementList graphInserts;
800
801     Q_FOREACH (const Tp::ContactInfoField &field, listContactInfo) {
802         if (field.fieldValue.count() == 0) {
803             continue;
804         }
805
806         /* FIXME:
807          *  - Do we care about "fn" and "nickname" ?
808          *  - How do we write affiliation for "org" ?
809          */
810         if (!field.fieldName.compare("tel")) {
811             addContactVoicePhoneNumberToQuery(graphInserts, inserts,
812                     ensureAffiliation(affiliationsMap, graphInserts, imContact, field),
813                     safeStringListAt(field.fieldValue, 0));
814         } else if (!field.fieldName.compare("adr")) {
815             addContactAddressToQuery(graphInserts,
816                     ensureAffiliation(affiliationsMap, graphInserts, imContact, field),
817                     safeStringListAt(field.fieldValue, 0),
818                     safeStringListAt(field.fieldValue, 1),
819                     safeStringListAt(field.fieldValue, 2),
820                     safeStringListAt(field.fieldValue, 3),
821                     safeStringListAt(field.fieldValue, 4),
822                     safeStringListAt(field.fieldValue, 5),
823                     safeStringListAt(field.fieldValue, 6));
824         } else if (!field.fieldName.compare("email")) {
825             addContactEmailToQuery(graphInserts, inserts,
826                     ensureAffiliation(affiliationsMap, graphInserts, imContact, field),
827                     safeStringListAt(field.fieldValue, 0));
828         } else if (!field.fieldName.compare("url")) {
829             RDFVariable affiliation = ensureAffiliation(affiliationsMap, graphInserts, imContact, field);
830             graphInserts << RDFStatement(affiliation, nco::url::iri(), LiteralValue(safeStringListAt(field.fieldValue, 0)));
831         } else if (!field.fieldName.compare("title")) {
832             RDFVariable affiliation = ensureAffiliation(affiliationsMap, graphInserts, imContact, field);
833             graphInserts << RDFStatement(affiliation, nco::title::iri(), LiteralValue(safeStringListAt(field.fieldValue, 0)));
834         } else if (!field.fieldName.compare("role")) {
835             RDFVariable affiliation = ensureAffiliation(affiliationsMap, graphInserts, imContact, field);
836             graphInserts << RDFStatement(affiliation, nco::role::iri(), LiteralValue(safeStringListAt(field.fieldValue, 0)));
837         } else if (!field.fieldName.compare("note") || !field.fieldName.compare("desc")) {
838             graphInserts << RDFStatement(imContact, nco::note::iri(), LiteralValue(safeStringListAt(field.fieldValue, 0)));
839         } else if (!field.fieldName.compare("bday")) {
840             /* Tracker will reject anything not [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]
841              * VCard spec allows only ISO 8601, but most IM clients allows
842              * any string. */
843             /* FIXME: support more date format for compatibility */
844             QDate date = QDate::fromString(safeStringListAt(field.fieldValue, 0), "yyyy-MM-dd");
845             if (!date.isValid()) {
846                 date = QDate::fromString(safeStringListAt(field.fieldValue, 0), "yyyyMMdd");
847             }
848
849             if (date.isValid()) {
850                 graphInserts << RDFStatement(imContact, nco::birthDate::iri(), LiteralValue(date));
851             } else {
852                 qDebug() << "Unsupported bday format:" << safeStringListAt(field.fieldValue, 0);
853             }
854         } else {
855             qDebug() << "Unsupported VCard field" << field.fieldName;
856         }
857     }
858
859     query.addInsertion(graphInserts, graph);
860 }
861
862 RDFVariable CDTpStorage::ensureAffiliation(QHash<QString, RDFVariable> &map,
863         RDFStatementList &graphInserts,
864         const RDFVariable &imContact,
865         const Tp::ContactInfoField &field)
866 {
867     /* FIXME: Do we support more types? */
868     static QHash<QString, QString> knownTypes;
869     if (knownTypes.isEmpty()) {
870         knownTypes.insert ("work", "Work");
871         knownTypes.insert ("home", "Home");
872     }
873
874     QString type = "Other";
875     Q_FOREACH (const QString &parameter, field.parameters) {
876         if (!parameter.startsWith("type=")) {
877             continue;
878         }
879
880         const QString str = parameter.mid(5);
881         if (knownTypes.contains(str)) {
882             type = knownTypes[str];
883             break;
884         }
885     }
886
887     if (!map.contains(type)) {
888         static uint counter = 0;
889         RDFVariable affiliation = RDFVariable(QString("affiliation%1").arg(++counter));
890         graphInserts << RDFStatement(affiliation, rdf::type::iri(), nco::Affiliation::iri())
891                      << RDFStatement(affiliation, rdfs::label::iri(), LiteralValue(type))
892                      << RDFStatement(imContact, nco::hasAffiliation::iri(), affiliation);
893         map.insert(type, affiliation);
894     }
895
896     return map[type];
897 }
898
899 void CDTpStorage::addContactVoicePhoneNumberToQuery(RDFStatementList &graphInserts,
900         RDFStatementList &inserts,
901         const RDFVariable &affiliation,
902         const QString &phoneNumber)
903 {
904     RDFVariable voicePhoneNumber = QUrl(QString("tel:%1").arg(phoneNumber));
905     inserts << RDFStatement(voicePhoneNumber, rdf::type::iri(), nco::VoicePhoneNumber::iri())
906             << RDFStatement(voicePhoneNumber, maemo::localPhoneNumber::iri(), LiteralValue(phoneNumber))
907             << RDFStatement(voicePhoneNumber, nco::phoneNumber::iri(), LiteralValue(phoneNumber));
908
909     graphInserts << RDFStatement(affiliation, nco::hasPhoneNumber::iri(), voicePhoneNumber);
910 }
911
912 void CDTpStorage::addContactAddressToQuery(RDFStatementList &graphInserts,
913         const RDFVariable &affiliation,
914         const QString &pobox,
915         const QString &extendedAddress,
916         const QString &streetAddress,
917         const QString &locality,
918         const QString &region,
919         const QString &postalcode,
920         const QString &country)
921 {
922     static uint counter = 0;
923     RDFVariable imPostalAddress = RDFVariable(QString("address%1").arg(++counter));
924     graphInserts << RDFStatement(imPostalAddress, rdf::type::iri(), nco::PostalAddress::iri())
925                  << RDFStatement(imPostalAddress, nco::pobox::iri(), LiteralValue(pobox))
926                  << RDFStatement(imPostalAddress, nco::extendedAddress::iri(), LiteralValue(extendedAddress))
927                  << RDFStatement(imPostalAddress, nco::streetAddress::iri(), LiteralValue(streetAddress))
928                  << RDFStatement(imPostalAddress, nco::locality::iri(), LiteralValue(locality))
929                  << RDFStatement(imPostalAddress, nco::region::iri(), LiteralValue(region))
930                  << RDFStatement(imPostalAddress, nco::postalcode::iri(), LiteralValue(postalcode))
931                  << RDFStatement(imPostalAddress, nco::country::iri(), LiteralValue(country));
932
933     graphInserts << RDFStatement(affiliation, nco::hasPostalAddress::iri(), imPostalAddress);
934 }
935
936 void CDTpStorage::addContactEmailToQuery(RDFStatementList &graphInserts,
937         RDFStatementList &inserts,
938         const RDFVariable &affiliation,
939         const QString &email)
940 {
941     RDFVariable emailAddress = QUrl(QString("mailto:%1").arg(email));
942     inserts << RDFStatement(emailAddress, rdf::type::iri(), nco::EmailAddress::iri())
943             << RDFStatement(emailAddress, nco::emailAddress::iri(), LiteralValue(email));
944
945     graphInserts << RDFStatement(affiliation, nco::hasEmailAddress::iri(), emailAddress);
946 }
947
948 QString CDTpStorage::contactLocalId(const QString &contactAccountObjectPath,
949         const QString &contactId)
950 {
951     return QString::number(qHash(QString("%1!%2")
952                 .arg(contactAccountObjectPath)
953                 .arg(contactId)));
954 }
955
956 QString CDTpStorage::contactLocalId(CDTpContactPtr contactWrapper)
957 {
958     CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();
959     Tp::AccountPtr account = accountWrapper->account();
960     Tp::ContactPtr contact = contactWrapper->contact();
961     return contactLocalId(account->objectPath(), contact->id());
962 }
963
964 QUrl CDTpStorage::contactIri(const QString &contactLocalId)
965 {
966     return QUrl(QString("contact:%1").arg(contactLocalId));
967 }
968
969 QUrl CDTpStorage::contactIri(CDTpContactPtr contactWrapper)
970 {
971     return contactIri(contactLocalId(contactWrapper));
972 }
973
974 QUrl CDTpStorage::contactImAddress(const QString &contactAccountObjectPath,
975         const QString &contactId)
976 {
977     return QUrl(QString("telepathy:%1!%2")
978             .arg(contactAccountObjectPath)
979             .arg(contactId));
980 }
981
982 QUrl CDTpStorage::contactImAddress(CDTpContactPtr contactWrapper)
983 {
984     CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();
985     Tp::AccountPtr account = accountWrapper->account();
986     Tp::ContactPtr contact = contactWrapper->contact();
987     return contactImAddress(account->objectPath(), contact->id());
988 }
989
990 QUrl CDTpStorage::contactAffiliation(const QString &contactAccountObjectPath,
991         const QString &contactId)
992 {
993     return QUrl(QString("affiliationtelepathy:%1!%2")
994             .arg(contactAccountObjectPath)
995             .arg(contactId));
996 }
997
998 QUrl CDTpStorage::contactAffiliation(CDTpContactPtr contactWrapper)
999 {
1000     CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();
1001     Tp::AccountPtr account = accountWrapper->account();
1002     Tp::ContactPtr contact = contactWrapper->contact();
1003     return contactAffiliation(account->objectPath(), contact->id());
1004 }
1005
1006 QUrl CDTpStorage::trackerStatusFromTpPresenceType(uint tpPresenceType)
1007 {
1008     switch (tpPresenceType) {
1009     case Tp::ConnectionPresenceTypeUnset:
1010         return nco::presence_status_unknown::iri();
1011     case Tp::ConnectionPresenceTypeOffline:
1012         return nco::presence_status_offline::iri();
1013     case Tp::ConnectionPresenceTypeAvailable:
1014         return nco::presence_status_available::iri();
1015     case Tp::ConnectionPresenceTypeAway:
1016         return nco::presence_status_away::iri();
1017     case Tp::ConnectionPresenceTypeExtendedAway:
1018         return nco::presence_status_extended_away::iri();
1019     case Tp::ConnectionPresenceTypeHidden:
1020         return nco::presence_status_hidden::iri();
1021     case Tp::ConnectionPresenceTypeBusy:
1022         return nco::presence_status_busy::iri();
1023     case Tp::ConnectionPresenceTypeUnknown:
1024         return nco::presence_status_unknown::iri();
1025     case Tp::ConnectionPresenceTypeError:
1026         return nco::presence_status_error::iri();
1027     default:
1028         qWarning() << "Unknown telepathy presence status" << tpPresenceType;
1029     }
1030
1031     return nco::presence_status_error::iri();
1032 }
1033
1034 QString CDTpStorage::trackerStatusFromTpPresenceStatus(
1035         const QString &tpPresenceStatus)
1036 {
1037     static QHash<QString, QLatin1String> mapping;
1038     if (mapping.isEmpty()) {
1039         mapping.insert(QString::fromLatin1("offline"),  QLatin1String("nco:presence_status_offline"));
1040         mapping.insert(QString::fromLatin1("available"),QLatin1String("nco:presence_status_available"));
1041         mapping.insert(QString::fromLatin1("away"),     QLatin1String("nco:presence_status_away"));
1042         mapping.insert(QString::fromLatin1("xa"),       QLatin1String("nco:presence_status_extended_away"));
1043         mapping.insert(QString::fromLatin1("dnd"),      QLatin1String("nco:presence_status_busy"));
1044         mapping.insert(QString::fromLatin1("busy"),     QLatin1String("nco:presence_status_busy"));
1045         mapping.insert(QString::fromLatin1("hidden"),   QLatin1String("nco:presence_status_hidden"));
1046         mapping.insert(QString::fromLatin1("unknown"),  QLatin1String("nco:presence_status_unknown"));
1047     }
1048
1049     QHash<QString, QLatin1String>::const_iterator i(mapping.constFind(tpPresenceStatus));
1050     if (i != mapping.end()) {
1051         return *i;
1052     }
1053     return QString::fromLatin1("nco:presence_status_error");
1054 }
1055
1056 void CDTpStorage::queueUpdate(CDTpContactPtr contactWrapper, CDTpContact::Changes changes)
1057 {
1058     if (!mUpdateQueue.contains(contactWrapper)) {
1059         qDebug() << "queue update for" << contactWrapper->contact()->id();
1060         mUpdateQueue.insert(contactWrapper, changes);
1061         mSyncOperations[contactWrapper->accountWrapper()].nPendingOperations++;
1062     } else {
1063         mUpdateQueue[contactWrapper] |= changes;
1064     }
1065
1066     /* If the queue is too big, flush it now to avoid hitting query size limit */
1067     if (mUpdateQueue.size() >= MAX_UPDATE_SIZE) {
1068         onQueueTimerTimeout();
1069     } else if (!mQueueTimer.isActive()) {
1070         mQueueTimer.start();
1071     }
1072 }
1073
1074 void CDTpStorage::onQueueTimerTimeout()
1075 {
1076     if (mUpdateQueue.isEmpty()) {
1077         return;
1078     }
1079
1080     CDTpContactResolver *query = new CDTpContactResolver(mUpdateQueue, this);
1081     connect(query,
1082             SIGNAL(finished(CDTpSelectQuery *)),
1083             SLOT(onContactUpdateSelectQueryFinished(CDTpSelectQuery *)));
1084
1085     mUpdateQueue.clear();
1086 }
1087
1088 void CDTpStorage::oneSyncOperationFinished(CDTpAccountPtr accountWrapper)
1089 {
1090     CDTpStorageSyncOperations &op = mSyncOperations[accountWrapper];
1091     op.nPendingOperations--;
1092
1093     if (op.nPendingOperations == 0) {
1094         if (op.active) {
1095             Q_EMIT syncEnded(accountWrapper, op.nContactsAdded, op.nContactsRemoved);
1096         }
1097         mSyncOperations.remove(accountWrapper);
1098     }
1099 }
1100
1101 void CDTpStorage::onAccountsUpdateQueryFinished(CDTpUpdateQuery *query)
1102 {
1103     CDTpAccountsUpdateQuery *accountsQuery = qobject_cast<CDTpAccountsUpdateQuery *>(query);
1104
1105     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accountsQuery->accounts()) {
1106         oneSyncOperationFinished(accountWrapper);
1107     }
1108 }
1109
1110 CDTpStorageSyncOperations::CDTpStorageSyncOperations() : active(false),
1111     nPendingOperations(0), nContactsAdded(0), nContactsRemoved(0)
1112 {
1113 }
1114