telepathy: Don't keep a ref to the CDTpContact in CDTpAvatarUpdate
[qtcontacts-tracker:contactsd.git] / plugins / telepathy / cdtpstorage.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 <tracker-sparql.h>
25
26 #include <TelepathyQt4/AvatarData>
27 #include <TelepathyQt4/ContactCapabilities>
28 #include <TelepathyQt4/ContactManager>
29 #include <TelepathyQt4/ConnectionCapabilities>
30 #include <qtcontacts-tracker/phoneutils.h>
31 #include <qtcontacts-tracker/garbagecollector.h>
32 #include <ontologies.h>
33
34 #include <importstateconst.h>
35
36 #include "cdtpstorage.h"
37 #include "cdtpavatarupdate.h"
38 #include "debug.h"
39
40 CUBI_USE_NAMESPACE_RESOURCES
41 using namespace Contactsd;
42
43 static const int UPDATE_TIMEOUT = 150; // ms
44 static const int UPDATE_THRESHOLD = 50; // contacts
45
46 static const LiteralValue defaultGenerator = LiteralValue(QString::fromLatin1("telepathy"));
47 static const ResourceValue defaultGraph = ResourceValue(QString::fromLatin1("urn:uuid:08070f5c-a334-4d19-a8b0-12a3071bfab9"));
48 static const ResourceValue privateGraph = ResourceValue(QString::fromLatin1("urn:uuid:679293d4-60f0-49c7-8d63-f1528fe31f66"));
49 static const ResourceValue aValue = ResourceValue(QString::fromLatin1("a"), ResourceValue::PrefixedName);
50 static const Variable imAddressVar = Variable(QString::fromLatin1("imAddress"));
51 static const Variable imContactVar = Variable(QString::fromLatin1("imContact"));
52 static const Variable imAccountVar = Variable(QString::fromLatin1("imAccount"));
53 static const ValueChain imAddressChain = ValueChain() << nco::hasAffiliation::resource() << nco::hasIMAddress::resource();
54
55 CDTpStorage::CDTpStorage(QObject *parent) : QObject(parent),
56     mUpdateRunning(false), mDirectGC(false)
57 {
58     mUpdateTimer.setInterval(UPDATE_TIMEOUT);
59     mUpdateTimer.setSingleShot(true);
60     connect(&mUpdateTimer, SIGNAL(timeout()), SLOT(onUpdateQueueTimeout()));
61
62     if (!qgetenv("CONTACTSD_DIRECT_GC").isEmpty())
63         mDirectGC = true;
64 }
65
66 CDTpStorage::~CDTpStorage()
67 {
68 }
69
70 static void deletePropertyWithGraph(Delete &d, const Value &s, const Value &p, const Variable &g)
71 {
72     Variable o;
73     d.addData(s, p, o);
74
75     Graph graph(g);
76     graph.addPattern(s, p, o);
77
78     PatternGroup optional;
79     optional.setOptional(true);
80     optional.addPattern(graph);
81
82     d.addRestriction(optional);
83 }
84
85 static void deleteProperty(Delete &d, const Value &s, const Value &p)
86 {
87     Variable o;
88     d.addData(s, p, o);
89
90     PatternGroup optional;
91     optional.setOptional(true);
92     optional.addPattern(s, p, o);
93
94     d.addRestriction(optional);
95 }
96
97 static ResourceValue presenceType(Tp::ConnectionPresenceType presenceType)
98 {
99     switch (presenceType) {
100     case Tp::ConnectionPresenceTypeUnset:
101         return nco::presence_status_unknown::resource();
102     case Tp::ConnectionPresenceTypeOffline:
103         return nco::presence_status_offline::resource();
104     case Tp::ConnectionPresenceTypeAvailable:
105         return nco::presence_status_available::resource();
106     case Tp::ConnectionPresenceTypeAway:
107         return nco::presence_status_away::resource();
108     case Tp::ConnectionPresenceTypeExtendedAway:
109         return nco::presence_status_extended_away::resource();
110     case Tp::ConnectionPresenceTypeHidden:
111         return nco::presence_status_hidden::resource();
112     case Tp::ConnectionPresenceTypeBusy:
113         return nco::presence_status_busy::resource();
114     case Tp::ConnectionPresenceTypeUnknown:
115         return nco::presence_status_unknown::resource();
116     case Tp::ConnectionPresenceTypeError:
117         return nco::presence_status_error::resource();
118     default:
119         break;
120     }
121
122     warning() << "Unknown telepathy presence status" << presenceType;
123
124     return nco::presence_status_error::resource();
125 }
126
127 static ResourceValue presenceState(Tp::Contact::PresenceState presenceState)
128 {
129     switch (presenceState) {
130     case Tp::Contact::PresenceStateNo:
131         return nco::predefined_auth_status_no::resource();
132     case Tp::Contact::PresenceStateAsk:
133         return nco::predefined_auth_status_requested::resource();
134     case Tp::Contact::PresenceStateYes:
135         return nco::predefined_auth_status_yes::resource();
136     }
137
138     warning() << "Unknown telepathy presence state:" << presenceState;
139
140     return nco::predefined_auth_status_no::resource();
141 }
142
143 static LiteralValue literalTimeStamp()
144 {
145     return LiteralValue(QDateTime::currentDateTime());
146 }
147
148 static QString imAddress(const QString &accountPath, const QString &contactId)
149 {
150     static const QString tmpl = QString::fromLatin1("telepathy:%1!%2");
151     return tmpl.arg(accountPath, contactId);
152 }
153
154 static QString imAddress(const QString &accountPath)
155 {
156     static const QString tmpl = QString::fromLatin1("telepathy:%1!self");
157     return tmpl.arg(accountPath);
158 }
159
160 static QString imAddress(const CDTpContactPtr &contactWrapper)
161 {
162     const QString accountPath = contactWrapper->accountWrapper()->account()->objectPath();
163     const QString contactId = contactWrapper->contact()->id();
164     return imAddress(accountPath, contactId);
165 }
166
167 static QString imAccount(const QString &accountPath)
168 {
169     static const QString tmpl = QString::fromLatin1("telepathy:%1");
170     return tmpl.arg(accountPath);
171 }
172
173 static ResourceValue literalIMAddress(const QString &accountPath, const QString &contactId)
174 {
175     return ResourceValue(imAddress(accountPath, contactId));
176 }
177
178 static ResourceValue literalIMAddress(const CDTpContactPtr &contactWrapper)
179 {
180     return ResourceValue(imAddress(contactWrapper));
181 }
182
183 static ResourceValue literalIMAddress(const CDTpAccountPtr &accountWrapper)
184 {
185     return ResourceValue(imAddress(accountWrapper->account()->objectPath()));
186 }
187
188 static ValueList literalIMAddressList(const QList<CDTpContactPtr> &contacts)
189 {
190     ValueList list;
191     Q_FOREACH (const CDTpContactPtr &contactWrapper, contacts) {
192         const QString accountPath = contactWrapper->accountWrapper()->account()->objectPath();
193         const QString contactId = contactWrapper->contact()->id();
194         list.addValue(LiteralValue(imAddress(accountPath, contactId)));
195     }
196     return list;
197 }
198
199 static ValueList literalIMAddressList(const QList<CDTpAccountPtr> &accounts)
200 {
201     ValueList list;
202     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
203         list.addValue(LiteralValue(imAddress(accountWrapper->account()->objectPath())));
204     }
205     return list;
206 }
207
208 static ResourceValue literalIMAccount(const QString &accountPath)
209 {
210     return ResourceValue(imAccount(accountPath));
211 }
212
213 static ResourceValue literalIMAccount(const CDTpAccountPtr &accountWrapper)
214 {
215     return ResourceValue(imAccount(accountWrapper->account()->objectPath()));
216 }
217
218 static ValueList literalIMAccountList(const QList<CDTpAccountPtr> &accounts)
219 {
220     ValueList list;
221     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
222         list.addValue(LiteralValue(imAccount(accountWrapper->account()->objectPath())));
223     }
224     return list;
225 }
226
227 static LiteralValue literalContactInfo(const Tp::ContactInfoField &field, int i)
228 {
229     if (i >= field.fieldValue.count()) {
230         return LiteralValue(QLatin1String(""));
231     }
232
233     return LiteralValue(field.fieldValue[i]);
234 }
235
236 static void addPresence(PatternGroup &g,
237         const Value &imAddress,
238         const Tp::Presence &presence)
239 {
240     g.addPattern(imAddress, nco::imPresence::resource(), presenceType(presence.type()));
241     g.addPattern(imAddress, nco::presenceLastModified::resource(), literalTimeStamp());
242     g.addPattern(imAddress, nco::imStatusMessage::resource(), LiteralValue(presence.statusMessage()));
243 }
244
245 static void addCapabilities(PatternGroup &g,
246         const Value &imAddress,
247         Tp::CapabilitiesBase capabilities)
248 {
249     /* FIXME: We could also add im_capability_stream_tubes and
250      * im_capability_dbus_tubes */
251
252     if (capabilities.textChats()) {
253         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_text_chat::resource());
254     }
255     if (capabilities.streamedMediaCalls()) {
256         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_media_calls::resource());
257     }
258     if (capabilities.streamedMediaAudioCalls()) {
259         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_audio_calls::resource());
260     }
261     if (capabilities.streamedMediaVideoCalls()) {
262         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_video_calls::resource());
263     }
264     if (capabilities.upgradingStreamedMediaCalls()) {
265         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_upgrading_calls::resource());
266     }
267     if (capabilities.fileTransfers()) {
268         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_file_transfers::resource());
269     }
270 }
271
272 static void addAvatars(PatternGroup &g, const Value &imAddress,
273                        const QString &defaultAvatarPath,
274                        const QString &largeAvatarPath)
275 {
276     typedef QPair<QString, QString> PairOfStrings;
277     QList<PairOfStrings> avatarPaths;
278
279     if (not defaultAvatarPath.isEmpty()) {
280         static const QString label = QLatin1String("Default");
281         avatarPaths += qMakePair(defaultAvatarPath, label);
282     }
283
284     if (not largeAvatarPath.isEmpty()) {
285         static const QString label = QLatin1String("Large");
286         avatarPaths += qMakePair(largeAvatarPath, label);
287     }
288
289     if (avatarPaths.isEmpty()) {
290         g.addPattern(imAddress, nco::imAvatar::resource(), NullValue());
291         return;
292     }
293
294     // use fixed IRI to reduce amount of leaked nfo:Image resources
295     ResourceValue img;
296
297     foreach(const PairOfStrings &p, avatarPaths) {
298         const QUrl url = QUrl::fromLocalFile(p.first);
299         const ResourceValue fdo(url);
300
301         g.addPattern(fdo, rdf::type::resource(), nfo::FileDataObject::resource());
302         g.addPattern(fdo, rdf::type::resource(), nfo::Image::resource());
303         g.addPattern(fdo, rdfs::label::resource(), LiteralValue(p.second));
304         g.addPattern(fdo, nie::url::resource(), LiteralValue(url));
305
306         if (not img.isValid()) {
307             // im address links to first avatar file,
308             // which then is used to link to all other avatar files
309             g.addPattern(imAddress, nco::imAvatar::resource(), img = fdo);
310         } else {
311             // first iteration of the loop initialize img
312             g.addPattern(img, nie::relatedTo::resource(), fdo);
313             g.addPattern(fdo, nie::isPartOf::resource(), img);
314         }
315     }
316 }
317
318 static Value ensureContactAffiliation(PatternGroup &g,
319         QHash<QString, Value> &affiliations,
320         QString affiliationLabel,
321         const Value &imContact)
322 {
323     if (!affiliations.contains(affiliationLabel)) {
324         const BlankValue affiliation;
325         g.addPattern(affiliation, aValue, nco::Affiliation::resource());
326         g.addPattern(affiliation, rdfs::label::resource(), LiteralValue(affiliationLabel));
327         g.addPattern(imContact, nco::hasAffiliation::resource(), affiliation);
328         affiliations.insert(affiliationLabel, affiliation);
329     }
330
331     return affiliations[affiliationLabel];
332 }
333
334 static const ResourceValue & genderInstance(const QString &gender)
335 {
336     const QString lowerGender = gender.toLower();
337
338     if (lowerGender == QLatin1String("male") ||
339         lowerGender == QLatin1String("m")) {
340         return nco::gender_male::resource();
341     }
342
343     if (lowerGender == QLatin1String("female") ||
344         lowerGender == QLatin1String("f")) {
345         return nco::gender_female::resource();
346     }
347
348     // simply declaring all other strings to belong to gender_other
349     return nco::gender_other::resource();
350 }
351
352 static CDTpQueryBuilder createContactInfoBuilder(CDTpContactPtr contactWrapper)
353 {
354     if (!contactWrapper->isInformationKnown()) {
355         debug() << "contact information is unknown for contact" << contactWrapper->contact()->id();
356         return CDTpQueryBuilder();
357     }
358
359     Tp::ContactInfoFieldList listContactInfo = contactWrapper->contact()->infoFields().allFields();
360     if (listContactInfo.count() == 0) {
361         debug() << "contact information is empty";
362         return CDTpQueryBuilder();
363     }
364
365     /* Create a builder with ?imContact bound to this contact.
366      * Use the imAddress as graph for ContactInfo fields, so we can easilly
367      * know from which contact it comes from */
368     CDTpQueryBuilder builder;
369     Insert i(Insert::Replace);
370     Graph g(literalIMAddress(contactWrapper));
371     i.addRestriction(imContactVar, imAddressChain, literalIMAddress(contactWrapper));
372
373     QHash<QString, Value> affiliations;
374     Q_FOREACH (const Tp::ContactInfoField &field, listContactInfo) {
375         if (field.fieldValue.count() == 0) {
376             continue;
377         }
378
379         /* Extract field types */
380         QStringList subTypes;
381         QString affiliationLabel = QLatin1String("Other");
382         Q_FOREACH (const QString &param, field.parameters) {
383             if (!param.startsWith(QLatin1String("type="))) {
384                 continue;
385             }
386             const QString type = param.mid(5);
387             if (type == QLatin1String("home")) {
388                 affiliationLabel = QLatin1String("Home");
389             } else if (type == QLatin1String("work")) {
390                 affiliationLabel = QLatin1String("Work");
391             } else if (!subTypes.contains(type)){
392                 subTypes << type;
393             }
394         }
395
396         /* FIXME: do we care about "fn" and "nickname" ? */
397         if (!field.fieldName.compare(QLatin1String("tel"))) {
398             static QHash<QString, Value> knownTypes;
399             if (knownTypes.isEmpty()) {
400                 knownTypes.insert(QLatin1String("bbsl"), nco::BbsNumber::resource());
401                 knownTypes.insert(QLatin1String("car"), nco::CarPhoneNumber::resource());
402                 knownTypes.insert(QLatin1String("cell"), nco::CellPhoneNumber::resource());
403                 knownTypes.insert(QLatin1String("fax"), nco::FaxNumber::resource());
404                 knownTypes.insert(QLatin1String("isdn"), nco::IsdnNumber::resource());
405                 knownTypes.insert(QLatin1String("modem"), nco::ModemNumber::resource());
406                 knownTypes.insert(QLatin1String("pager"), nco::PagerNumber::resource());
407                 knownTypes.insert(QLatin1String("pcs"), nco::PcsNumber::resource());
408                 knownTypes.insert(QLatin1String("video"), nco::VideoTelephoneNumber::resource());
409                 knownTypes.insert(QLatin1String("voice"), nco::VoicePhoneNumber::resource());
410             }
411
412             QStringList realSubTypes;
413             Q_FOREACH (const QString &type, subTypes) {
414                 if (knownTypes.contains(type)) {
415                     realSubTypes << type;
416                 }
417             }
418
419             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
420             const Value phoneNumber = qctMakePhoneNumberResource(field.fieldValue[0], realSubTypes);
421             g.addPattern(phoneNumber, aValue, nco::PhoneNumber::resource());
422             Q_FOREACH (const QString &type, realSubTypes) {
423                 g.addPattern(phoneNumber, aValue, knownTypes[type]);
424             }
425             g.addPattern(phoneNumber, nco::phoneNumber::resource(), literalContactInfo(field, 0));
426             g.addPattern(phoneNumber, maemo::localPhoneNumber::resource(),
427                     LiteralValue(qctMakeLocalPhoneNumber(field.fieldValue[0])));
428             g.addPattern(affiliation, nco::hasPhoneNumber::resource(), phoneNumber);
429         }
430
431         else if (!field.fieldName.compare(QLatin1String("adr"))) {
432             static QHash<QString, Value> knownTypes;
433             if (knownTypes.isEmpty()) {
434                 knownTypes.insert(QLatin1String("dom"), nco::DomesticDeliveryAddress::resource());
435                 knownTypes.insert(QLatin1String("intl"), nco::InternationalDeliveryAddress::resource());
436                 knownTypes.insert(QLatin1String("parcel"), nco::ParcelDeliveryAddress::resource());
437                 knownTypes.insert(QLatin1String("postal"), maemo::PostalAddress::resource());
438             }
439
440             QStringList realSubTypes;
441             Q_FOREACH (const QString &type, subTypes) {
442                 if (knownTypes.contains(type)) {
443                     realSubTypes << type;
444                 }
445             }
446
447             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
448             const BlankValue postalAddress;
449             g.addPattern(postalAddress, aValue, nco::PostalAddress::resource());
450             Q_FOREACH (const QString &type, realSubTypes) {
451                 g.addPattern(postalAddress, aValue, knownTypes[type]);
452             }
453             g.addPattern(postalAddress, nco::pobox::resource(),           literalContactInfo(field, 0));
454             g.addPattern(postalAddress, nco::extendedAddress::resource(), literalContactInfo(field, 1));
455             g.addPattern(postalAddress, nco::streetAddress::resource(),   literalContactInfo(field, 2));
456             g.addPattern(postalAddress, nco::locality::resource(),        literalContactInfo(field, 3));
457             g.addPattern(postalAddress, nco::region::resource(),          literalContactInfo(field, 4));
458             g.addPattern(postalAddress, nco::postalcode::resource(),      literalContactInfo(field, 5));
459             g.addPattern(postalAddress, nco::country::resource(),         literalContactInfo(field, 6));
460             g.addPattern(affiliation, nco::hasPostalAddress::resource(), postalAddress);
461         }
462
463         else if (!field.fieldName.compare(QLatin1String("email"))) {
464             static const QString tmpl = QString::fromLatin1("mailto:%1");
465             const Value emailAddress = ResourceValue(tmpl.arg(field.fieldValue[0]));
466             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
467             g.addPattern(emailAddress, aValue, nco::EmailAddress::resource());
468             g.addPattern(emailAddress, nco::emailAddress::resource(), literalContactInfo(field, 0));
469             g.addPattern(affiliation, nco::hasEmailAddress::resource(), emailAddress);
470         }
471
472         else if (!field.fieldName.compare(QLatin1String("url"))) {
473             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
474             g.addPattern(affiliation, nco::url::resource(), literalContactInfo(field, 0));
475         }
476
477         else if (!field.fieldName.compare(QLatin1String("title"))) {
478             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
479             g.addPattern(affiliation, nco::title::resource(), literalContactInfo(field, 0));
480         }
481
482         else if (!field.fieldName.compare(QLatin1String("role"))) {
483             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
484             g.addPattern(affiliation, nco::role::resource(), literalContactInfo(field, 0));
485         }
486
487         else if (!field.fieldName.compare(QLatin1String("org"))) {
488             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
489             const BlankValue organizationContact;
490             g.addPattern(organizationContact, aValue, nco::OrganizationContact::resource());
491             g.addPattern(organizationContact, nco::fullname::resource(), literalContactInfo(field, 0));
492             g.addPattern(affiliation, nco::department::resource(), literalContactInfo(field, 1));
493             g.addPattern(affiliation, nco::org::resource(), organizationContact);
494         }
495
496         else if (!field.fieldName.compare(QLatin1String("note")) || !field.fieldName.compare(QLatin1String("desc"))) {
497             g.addPattern(imContactVar, nco::note::resource(), literalContactInfo(field, 0));
498         }
499
500         else if (!field.fieldName.compare(QLatin1String("bday"))) {
501             /* Tracker will reject anything not [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]
502              * VCard spec allows only ISO 8601, but most IM clients allows
503              * any string. */
504             /* FIXME: support more date format for compatibility */
505             QDate date = QDate::fromString(field.fieldValue[0], QLatin1String("yyyy-MM-dd"));
506             if (!date.isValid()) {
507                 date = QDate::fromString(field.fieldValue[0], QLatin1String("yyyyMMdd"));
508             }
509
510             if (date.isValid()) {
511                 g.addPattern(imContactVar, nco::birthDate::resource(), LiteralValue(QDateTime(date)));
512             } else {
513                 debug() << "Unsupported bday format:" << field.fieldValue[0];
514             }
515         }
516
517         else if (!field.fieldName.compare(QLatin1String("x-gender"))) {
518             const QString gender = field.fieldValue.at(0);
519
520             g.addPattern(imContactVar, nco::gender::resource(), genderInstance(gender));
521         }
522
523         else {
524             debug() << "Unsupported VCard field" << field.fieldName;
525         }
526     }
527     i.addData(g);
528     builder.append(i);
529
530     return builder;
531 }
532
533 static void addRemoveContactInfo(Delete &d,
534         const Variable &imAddress,
535         const Value &imContact)
536 {
537     /* Remove all triples on imContact and in graph. All sub-resources will be
538      * GCed by qct sometimes.
539      * imAddress is used as graph for properties on the imContact */
540     deletePropertyWithGraph(d, imContact, nco::birthDate::resource(), imAddress);
541     deletePropertyWithGraph(d, imContact, nco::gender::resource(), imAddress);
542     deletePropertyWithGraph(d, imContact, nco::note::resource(), imAddress);
543     deletePropertyWithGraph(d, imContact, nco::hasAffiliation::resource(), imAddress);
544 }
545
546 static QString saveAccountAvatar(CDTpAccountPtr accountWrapper)
547 {
548     const Tp::Avatar &avatar = accountWrapper->account()->avatar();
549
550     if (avatar.avatarData.isEmpty()) {
551         return QString();
552     }
553
554     static const QString tmpl = QString::fromLatin1("%1/.contacts/avatars/%2");
555     QString fileName = tmpl.arg(QDir::homePath())
556         .arg(QLatin1String(QCryptographicHash::hash(avatar.avatarData, QCryptographicHash::Sha1).toHex()));
557     debug() << "Saving account avatar to" << fileName;
558
559     QFile avatarFile(fileName);
560     if (!avatarFile.open(QIODevice::WriteOnly)) {
561         warning() << "Unable to save account avatar: error opening avatar "
562             "file" << fileName << "for writing";
563         return QString();
564     }
565     avatarFile.write(avatar.avatarData);
566     avatarFile.close();
567
568     return fileName;
569 }
570
571 static void addAccountChanges(PatternGroup &g,
572         CDTpAccountPtr accountWrapper,
573         CDTpAccount::Changes changes)
574 {
575     Tp::AccountPtr account = accountWrapper->account();
576     const Value imAccount = literalIMAccount(accountWrapper);
577     const Value imAddress = literalIMAddress(accountWrapper);
578
579     if (changes & CDTpAccount::Presence) {
580         debug() << "  presence changed";
581         addPresence(g, imAddress, account->currentPresence());
582     }
583     if (changes & CDTpAccount::Avatar) {
584         debug() << "  avatar changed";
585         addAvatars(g, imAddress, saveAccountAvatar(accountWrapper), QString());
586     }
587     if (changes & CDTpAccount::Nickname) {
588         debug() << "  nickname changed";
589         g.addPattern(imAddress, nco::imNickname::resource(), LiteralValue(account->nickname()));
590     }
591     if (changes & CDTpAccount::DisplayName) {
592         debug() << "  display name changed";
593         g.addPattern(imAccount, nco::imDisplayName::resource(), LiteralValue(account->displayName()));
594     }
595     if (changes & CDTpAccount::Enabled) {
596         debug() << "  enabled changed";
597         g.addPattern(imAccount, nco::imEnabled::resource(), LiteralValue(account->isEnabled()));
598     }
599 }
600
601 static void addContactChanges(PatternGroup &g,
602         const Value &imAddress,
603         CDTpContact::Changes changes,
604         CDTpContactPtr contactWrapper)
605 {
606     debug() << "Update contact" << imAddress.sparql();
607     Tp::ContactPtr contact = contactWrapper->contact();
608
609     // Apply changes
610     if (changes & CDTpContact::Alias) {
611         debug() << "  alias changed";
612         g.addPattern(imAddress, nco::imNickname::resource(), LiteralValue(contact->alias().trimmed()));
613     }
614     if (changes & CDTpContact::Presence) {
615         debug() << "  presence changed";
616         addPresence(g, imAddress, contact->presence());
617     }
618     if (changes & CDTpContact::Capabilities) {
619         debug() << "  capabilities changed";
620         addCapabilities(g, imAddress, contact->capabilities());
621     }
622     if (changes & CDTpContact::Avatar) {
623         debug() << "  avatar changed";
624
625         QString defaultAvatarPath = contact->avatarData().fileName;
626
627         if (defaultAvatarPath.isEmpty()) {
628             defaultAvatarPath = contactWrapper->squareAvatarPath();
629         }
630
631         addAvatars(g, imAddress, defaultAvatarPath, contactWrapper->largeAvatarPath());
632     }
633     if (changes & CDTpContact::Authorization) {
634         debug() << "  authorization changed";
635         g.addPattern(imAddress, nco::imAddressAuthStatusFrom::resource(),
636                 presenceState(contact->subscriptionState()));
637         g.addPattern(imAddress, nco::imAddressAuthStatusTo::resource(),
638                 presenceState(contact->publishState()));
639     }
640 }
641
642 /* "Tagging" the signal means that we insert the timestamp first in contactsd
643  * and then replace it right away in the default graph. That way, the final
644  * data in Tracker is the same, but the GraphUpdated signal will contain the
645  * additional update in contactsd graph, which can be matched by other
646  * processes listening to change signals.
647  * Timestamp updates are "tagged" if they only concern a presence/capability
648  * update
649  */
650
651 enum UpdateTimestampMode {
652     UntaggedSignalUpdate,
653     TaggedSignalUpdate
654 };
655
656 static void updateTimestamp(Insert &i, const Value &subject, UpdateTimestampMode mode = UntaggedSignalUpdate)
657 {
658     const Value timestamp = literalTimeStamp();
659
660     Graph g(defaultGraph);
661
662     if (mode == TaggedSignalUpdate) {
663         g = Graph(privateGraph);
664         g.addPattern(subject, nie::contentLastModified::resource(), timestamp);
665         i.addData(g);
666
667         g = Graph(defaultGraph);
668     }
669
670     g.addPattern(subject, nie::contentLastModified::resource(), timestamp);
671     i.addData(g);
672 }
673
674 static Insert updateTimestampOnIMAddresses(const QStringList &imAddresses, UpdateTimestampMode mode = UntaggedSignalUpdate)
675 {
676     Insert i(Insert::Replace);
677     updateTimestamp(i, imContactVar, mode);
678     i.addRestriction(imContactVar, imAddressChain, imAddressVar);
679     i.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), LiteralValue(imAddresses))));
680
681     return i;
682 }
683
684 static bool contactChangedPresenceOrCapsOnly(CDTpContact::Changes changes)
685 {
686     // We mask out only Presence and Capabilities
687     static const CDTpContact::Changes significantChangeMask =
688             (CDTpContact::Changes)(CDTpContact::All ^ (CDTpContact::Presence | CDTpContact::Capabilities));
689
690     return ((changes & significantChangeMask) == 0);
691 }
692
693 static CDTpQueryBuilder createAccountsBuilder(const QList<CDTpAccountPtr> &accounts)
694 {
695     CDTpQueryBuilder builder;
696     Insert i(Insert::Replace);
697
698     updateTimestamp(i, nco::default_contact_me::resource());
699
700     Graph g(privateGraph);
701     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
702         Tp::AccountPtr account = accountWrapper->account();
703         const Value imAccount = literalIMAccount(accountWrapper);
704         const Value imAddress = literalIMAddress(accountWrapper);
705
706         debug() << "Create account" << imAddress.sparql();
707
708         // Ensure the IMAccount exists
709         g.addPattern(imAccount, aValue, nco::IMAccount::resource());
710         g.addPattern(imAccount, nco::imAccountType::resource(), LiteralValue(account->protocolName()));
711         g.addPattern(imAccount, nco::imAccountAddress::resource(), imAddress);
712         g.addPattern(imAccount, nco::hasIMContact::resource(), imAddress);
713
714         // Ensure the self contact has an IMAddress
715         g.addPattern(imAddress, aValue, nco::IMAddress::resource());
716         g.addPattern(imAddress, nco::imID::resource(), LiteralValue(account->normalizedName()));
717         g.addPattern(imAddress, nco::imProtocol::resource(), LiteralValue(account->protocolName()));
718
719         // Add all mutable properties
720         addAccountChanges(g, accountWrapper, CDTpAccount::All);
721     }
722     i.addData(g);
723     builder.append(i);
724
725     // Ensure the IMAddresses are bound to the default-contact-me via an affiliation
726     const Value affiliation = BlankValue(QString::fromLatin1("affiliation"));
727     Exists e;
728     i = Insert(Insert::Replace);
729     g = Graph(privateGraph);
730     g.addPattern(nco::default_contact_me::resource(), nco::hasAffiliation::resource(), affiliation);
731     g.addPattern(affiliation, aValue, nco::Affiliation::resource());
732     g.addPattern(affiliation, nco::hasIMAddress::resource(), imAddressVar);
733     g.addPattern(affiliation, rdfs::label::resource(), LiteralValue(QString::fromLatin1("Other")));
734     i.addData(g);
735     i.addRestriction(imAddressVar, aValue, nco::IMAddress::resource());
736     e.addPattern(nco::default_contact_me::resource(), imAddressChain, imAddressVar);
737     i.setFilter(Filter(Functions::and_.apply(
738             Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)),
739             Functions::not_.apply(Filter(e)))));
740     builder.append(i);
741
742     return builder;
743 }
744
745 static CDTpQueryBuilder updateContactsInfoBuilder(const QList<CDTpContactPtr> &contacts,
746                                                   const QHash<QString, CDTpContact::Changes> &changes = QHash<QString, CDTpContact::Changes>())
747 {
748     CDTpQueryBuilder builder;
749
750     QStringList deleteInfoAddresses;
751     QList<CDTpContactPtr> updateInfoContacts;
752
753     Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
754         const QString address = imAddress(contactWrapper);
755         const QHash<QString, CDTpContact::Changes>::ConstIterator contactChangesIter = changes.find(address);
756         CDTpContact::Changes contactChanges = 0;
757
758         if (contactChangesIter != changes.constEnd()) {
759             contactChanges = contactChangesIter.value();
760         } else {
761             warning() << "Internal error: unknown changes for" << address;
762             contactChanges = CDTpContact::Added;
763         }
764
765         // Contact info needs to be deleted for existing contacts that had it changed
766         if (contactChanges != CDTpContact::Added && (contactChanges & CDTpContact::Information)) {
767             deleteInfoAddresses.append(address);
768         }
769
770         // and it needs to be updated for new contacts, or existing contacts that had it changed (CDTpContact::Added
771         // sets CDTpContact::Information to 1)
772         if (contactChanges & CDTpContact::Information) {
773             updateInfoContacts.append(contactWrapper);
774         }
775     }
776
777     // Delete ContactInfo for the contacts that had it updated
778     if (!deleteInfoAddresses.isEmpty()) {
779         Delete d;
780         d.addRestriction(imContactVar, imAddressChain, imAddressVar);
781         d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar),
782                                                LiteralValue(deleteInfoAddresses))));
783         addRemoveContactInfo(d, imAddressVar, imContactVar);
784         builder.append(d);
785     }
786
787     Q_FOREACH (const CDTpContactPtr &contactWrapper, updateInfoContacts) {
788         builder.append(createContactInfoBuilder(contactWrapper));
789     }
790
791     return builder;
792 }
793
794 static CDTpQueryBuilder createContactsBuilder(const QList<CDTpContactPtr> &contacts,
795                                               const QHash<QString, CDTpContact::Changes> &changes = QHash<QString, CDTpContact::Changes>())
796 {
797     CDTpQueryBuilder builder;
798
799     // nco:imCapability is multi valued, so we have to explicitely delete before
800     // updating it (at least until we don't have null support in Tracker)
801     QStringList capsDeleteAddresses;
802
803     QStringList allAddresses;
804
805     // Ensure all imAddress exist and are linked from imAccount for new contacts
806     Insert i(Insert::Replace);
807     Graph g(privateGraph);
808     Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
809         CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();
810         const QString contactAddress = imAddress(contactWrapper);
811         const QHash<QString, CDTpContact::Changes>::ConstIterator contactChangesIter = changes.find(contactAddress);
812         CDTpContact::Changes contactChanges = 0;
813
814         allAddresses.append(contactAddress);
815
816         if (contactChangesIter != changes.constEnd()) {
817             contactChanges = contactChangesIter.value();
818         } else {
819             warning() << "Internal error: Unknown changes for" << contactAddress;
820             contactChanges = CDTpContact::Added;
821         }
822
823         const Value imAddress = literalIMAddress(contactWrapper);
824
825         // If it's a new contact in the roster, we need to create an IMAddress for it
826         if (contactChanges == CDTpContact::Added) {
827             g.addPattern(imAddress, aValue, nco::IMAddress::resource());
828             g.addPattern(imAddress, nco::imID::resource(), LiteralValue(contactWrapper->contact()->id()));
829             g.addPattern(imAddress, nco::imProtocol::resource(), LiteralValue(accountWrapper->account()->protocolName()));
830
831             const Value imAccount = literalIMAccount(accountWrapper);
832             g.addPattern(imAccount, nco::hasIMContact::resource(), imAddress);
833         } else {
834             // Else, if the capabilities changed we need to first delete the old one
835             // explicitly
836             if ((contactChanges & CDTpContact::Capabilities) && (contactChanges != CDTpContact::Added)) {
837                 capsDeleteAddresses.append(contactAddress);
838             }
839         }
840
841         // Add mutable properties except for ContactInfo
842         addContactChanges(g, imAddress, contactChanges, contactWrapper);
843     }
844     i.addData(g);
845
846     // Delete the nco:imCapability where needed
847     if (!capsDeleteAddresses.isEmpty()) {
848         Delete d;
849         deleteProperty(d, imAddressVar, nco::imCapability::resource());
850         d.setFilter(Functions::in.apply(imAddressVar, LiteralValue(capsDeleteAddresses)));
851         builder.append(d);
852     }
853
854     builder.append(i);
855
856     // Ensure all imAddresses are bound to a PersonContact
857     i = Insert();
858     g = Graph(defaultGraph);
859     BlankValue imContact(QString::fromLatin1("contact"));
860     g.addPattern(imContact, aValue, nco::PersonContact::resource());
861     g.addPattern(imContact, nie::contentCreated::resource(), literalTimeStamp());
862     g.addPattern(imContact, nie::generator::resource(), defaultGenerator);
863     i.addData(g);
864     updateTimestamp(i, imContact);
865     g = Graph(privateGraph);
866     BlankValue affiliation(QString::fromLatin1("affiliation"));
867     g.addPattern(imContact, nco::hasAffiliation::resource(), affiliation);
868     g.addPattern(affiliation, aValue, nco::Affiliation::resource());
869     g.addPattern(affiliation, nco::hasIMAddress::resource(), imAddressVar);
870     g.addPattern(affiliation, rdfs::label::resource(), LiteralValue(QString::fromLatin1("Other")));
871     i.addData(g);
872     g = Graph(privateGraph);
873     Variable imId;
874     g.addPattern(imAddressVar, nco::imID::resource(), imId);
875     i.addRestriction(g);
876     Exists e;
877     e.addPattern(imContactVar, imAddressChain, imAddressVar);
878     i.setFilter(Functions::and_.apply(Functions::not_.apply(Filter(e)),
879                                       Functions::in.apply(Functions::str.apply(imAddressVar), LiteralValue(allAddresses))));
880     builder.append(i);
881
882     return builder;
883 }
884
885 static CDTpQueryBuilder purgeContactsBuilder()
886 {
887     static const Function equalsMeContact = Functions::equal.apply(imContactVar,
888                                                                    nco::default_contact_me::resource());
889     /* Purge nco:IMAddress not bound from an nco:IMAccount */
890
891     CDTpQueryBuilder builder;
892
893     /* Step 1 - Clean nco:PersonContact from all info imported from the imAddress.
894      * Note: We don't delete the affiliation because it could contain other
895      * info (See NB#239973) */
896     Delete d;
897     Variable affiliationVar;
898     d.addData(affiliationVar, nco::hasIMAddress::resource(), imAddressVar);
899     Graph g(privateGraph);
900     Variable imId;
901     g.addPattern(imAddressVar, nco::imID::resource(), imId);
902     d.addRestriction(g);
903     d.addRestriction(imContactVar, nco::hasAffiliation::resource(), affiliationVar);
904     d.addRestriction(affiliationVar, nco::hasIMAddress::resource(), imAddressVar);
905     Exists e;
906     e.addPattern(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
907     Exists e2;
908     e2.addPattern(imAddressVar, nco::imAddressAuthStatusFrom::resource(), Variable());
909     /* we delete addresses that are not connected to an IMAccount, but spare the ones
910        that don't have their AuthStatus set (those are failed invitations)
911        The IMAddress on me-contact has no AuthStatus set, but we delete it anyway*/
912     d.setFilter(Filter(Functions::and_.apply(Functions::not_.apply(Filter(e)),
913                                              Functions::or_.apply(Filter(e2),
914                                                                   equalsMeContact))));
915     deleteProperty(d, imContactVar, nie::contentLastModified::resource());
916     addRemoveContactInfo(d, imAddressVar, imContactVar);
917     builder.append(d);
918
919     /* Step 1.1 - Remove the nco:IMAddress resource.
920      * This must be done in a separate query because of NB#242979 */
921     d = Delete();
922     d.addData(imAddressVar, aValue, rdfs::Resource::resource());
923     g = Graph(privateGraph);
924     imId = Variable();
925     g.addPattern(imAddressVar, nco::imID::resource(), imId);
926     d.addRestriction(g);
927     e = Exists();
928     e.addPattern(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
929     d.setFilter(Filter(Functions::not_.apply(Filter(e))));
930     builder.append(d);
931
932     /* Step 2 - Purge nco:PersonContact with generator "telepathy" but with no
933      * nco:IMAddress bound anymore */
934     d = Delete();
935     d.addData(imContactVar, aValue, rdfs::Resource::resource());
936     d.addRestriction(imContactVar, nie::generator::resource(), defaultGenerator);
937     e = Exists();
938     Variable v;
939     e.addPattern(imContactVar, imAddressChain, v);
940     d.setFilter(Filter(Functions::not_.apply(Filter(e))));
941     builder.append(d);
942
943     /* Step 3 - Add back nie:contentLastModified for nco:PersonContact missing one */
944     Insert i(Insert::Replace);
945     updateTimestamp(i, imContactVar);
946     i.addRestriction(imContactVar, aValue, nco::PersonContact::resource());
947     e = Exists();
948     v = Variable();
949     e.addPattern(imContactVar, nie::contentLastModified::resource(), v);
950     i.setFilter(Filter(Functions::not_.apply(Filter(e))));
951     builder.append(i);
952
953     return builder;
954 }
955
956 static Tp::Presence unknownPresence()
957 {
958     static const Tp::Presence unknownPresence(Tp::ConnectionPresenceTypeUnknown, QLatin1String("unknown"), QString());
959     return unknownPresence;
960 }
961
962 static CDTpQueryBuilder syncNoRosterAccountsContactsBuilder(const QList<CDTpAccountPtr> accounts)
963 {
964     CDTpQueryBuilder builder;
965
966     // Set presence to UNKNOWN for all contacts, except for self contact because
967     // its presence is OFFLINE and will be set in updateAccount()
968     Insert i(Insert::Replace);
969     Graph g(privateGraph);
970     addPresence(g, imAddressVar, unknownPresence());
971     i.addData(g);
972     // We only update the contact presence, so tag the signal
973     updateTimestamp(i, imContactVar, TaggedSignalUpdate);
974     i.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
975     i.addRestriction(imContactVar, imAddressChain, imAddressVar);
976     i.setFilter(Filter(Functions::and_.apply(
977             Functions::notIn.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)),
978             Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts)))));
979     builder.append(i);
980
981     // Add capabilities on all contacts for each account
982     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
983         Insert i;
984         Graph g(privateGraph);
985         addCapabilities(g, imAddressVar, accountWrapper->account()->capabilities());
986         i.addData(g);
987         i.addRestriction(literalIMAccount(accountWrapper), nco::hasIMContact::resource(), imAddressVar);
988         builder.append(i);
989     }
990
991     return builder;
992 }
993
994 static void updateFacebookAvatar(QNetworkAccessManager &network, CDTpContactPtr contactWrapper,
995                                  const QString &facebookId, const QString &avatarType)
996 {
997     const QUrl avatarUrl(QLatin1String("http://graph.facebook.com/") % facebookId %
998                          QLatin1String("/picture?type=") % avatarType);
999
1000     // CDTpAvatarUpdate keeps a weak reference to CDTpContact, since the contact is
1001     // also its parent. If we'd pass a CDTpContactPtr to the update, it'd keep a ref that
1002     // keeps the CDTpContact alive. Then, if the update is the last object to hold
1003     // a ref to the contact, the refcount of the contact will go to 0 when the update
1004     // dtor is called (for example from deleteLater). At this point, the update will
1005     // already be being deleted, but the dtor of CDTpContact will try to delete the
1006     // update a second time, causing a double free.
1007     QObject *const update = new CDTpAvatarUpdate(network.get(QNetworkRequest(avatarUrl)),
1008                                                  contactWrapper.data(),
1009                                                  avatarType,
1010                                                  contactWrapper.data());
1011
1012     QObject::connect(update, SIGNAL(finished()), update, SLOT(deleteLater()));
1013 }
1014
1015 static void updateSocialAvatars(QNetworkAccessManager &network, CDTpContactPtr contactWrapper)
1016 {
1017     if (network.networkAccessible() == QNetworkAccessManager::NotAccessible) {
1018         return;
1019     }
1020
1021     QRegExp facebookIdPattern(QLatin1String("-(\\d+)@chat\\.facebook\\.com"));
1022
1023     if (not facebookIdPattern.exactMatch(contactWrapper->contact()->id())) {
1024         return; // only supporting Facebook avatars right now
1025     }
1026
1027     const QString socialId = facebookIdPattern.cap(1);
1028
1029     updateFacebookAvatar(network, contactWrapper, socialId, CDTpAvatarUpdate::Large);
1030     updateFacebookAvatar(network, contactWrapper, socialId, CDTpAvatarUpdate::Square);
1031 }
1032
1033 static CDTpQueryBuilder syncRosterAccountsContactsBuilder(QNetworkAccessManager &network,
1034                                                           const QList<CDTpAccountPtr> &accounts,
1035                                                           bool purgeContacts = false)
1036 {
1037     QList<CDTpContactPtr> allContacts;
1038     QHash<QString, CDTpContact::Changes> allChanges;
1039
1040     // The two following lists partition the list of updated IMAddresses in two:
1041     // the addresses for which only the presence/capabilities got updated, and
1042     // the addresses for which more information was updated
1043     QStringList onlyPresenceChangedAddresses;
1044     QStringList infoChangedAddresses;
1045
1046     // The obsolete contacts are the one we have cached, but that are not in
1047     // the roster anymore.
1048     QStringList obsoleteAddresses;
1049
1050     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
1051         const QString accountPath = accountWrapper->account()->objectPath();
1052         const QHash<QString, CDTpContact::Changes> changes = accountWrapper->rosterChanges();
1053
1054         allContacts << accountWrapper->contacts();
1055
1056         for (QHash<QString, CDTpContact::Changes>::ConstIterator it = changes.constBegin();
1057              it != changes.constEnd(); ++it) {
1058             const QString address = imAddress(accountPath, it.key());
1059
1060             // If the contact was deleted, we just mark it for deletion in Tracker
1061             // and don't need to add it to the global changes hash, since it's not
1062             // in allContacts anyway
1063             if (it.value() == CDTpContact::Deleted) {
1064                 obsoleteAddresses.append(address);
1065                 continue;
1066             }
1067
1068             if (contactChangedPresenceOrCapsOnly(it.value())) {
1069                 onlyPresenceChangedAddresses.append(address);
1070             } else {
1071                 infoChangedAddresses.append(address);
1072             }
1073
1074             // We always update contact presence since this method is called after
1075             // a presence change
1076             allChanges.insert(address, it.value() | CDTpContact::Presence);
1077         }
1078
1079         foreach(CDTpContactPtr contactWrapper, accountWrapper->contacts()) {
1080             const QString address = imAddress(accountPath, contactWrapper->contact()->id());
1081
1082             if (allChanges.value(address) & CDTpContact::DefaultAvatar) {
1083                 updateSocialAvatars(network, contactWrapper);
1084             }
1085         }
1086     }
1087
1088     CDTpQueryBuilder builder;
1089     // Remove the contacts that are not in the roster anymore (we actually just
1090     // "disconnect" them from the IMAccount and they'll get garbage collected)
1091     if (!obsoleteAddresses.isEmpty()) {
1092         Delete d;
1093         d.addData(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
1094         d.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
1095         d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar),
1096                                                LiteralValue(QStringList(obsoleteAddresses)))));
1097         builder.append(d);
1098     }
1099
1100     /* Now create all contacts */
1101     if (!allContacts.isEmpty()) {
1102         builder.append(createContactsBuilder(allContacts, allChanges));
1103         builder.append(updateContactsInfoBuilder(allContacts, allChanges));
1104
1105         /* Update timestamp on all nco:PersonContact bound to this account */
1106         if (not onlyPresenceChangedAddresses.isEmpty()) {
1107             builder.append(updateTimestampOnIMAddresses(onlyPresenceChangedAddresses, TaggedSignalUpdate));
1108         }
1109
1110         if (not infoChangedAddresses.isEmpty()) {
1111             builder.append(updateTimestampOnIMAddresses(infoChangedAddresses));
1112         }
1113     }
1114
1115     /* Purge IMAddresses not bound from an account anymore */
1116     if (purgeContacts && !obsoleteAddresses.isEmpty()) {
1117         builder.append(purgeContactsBuilder());
1118     }
1119
1120     return builder;
1121 }
1122
1123 static CDTpQueryBuilder syncDisabledAccountsContactsBuilder(const QList<CDTpAccountPtr> &accounts)
1124 {
1125     CDTpQueryBuilder builder;
1126
1127     /* Disabled account: We want remove pure-IM contacts, but for merged/edited
1128      * contacts we want to keep only its IMAddress as local information, so it
1129      * will be merged again when we re-enable the account. */
1130
1131     /* Step 1 - Unlink pure-IM IMAddress from its IMAccount */
1132     Delete d;
1133     d.addData(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
1134     d.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
1135     d.addRestriction(imContactVar, imAddressChain, imAddressVar);
1136     d.addRestriction(imContactVar, nie::generator::resource(), defaultGenerator);
1137     d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts))));
1138     builder.append(d);
1139
1140     /* Step 2 - Delete pure-IM contacts since they are now unbound */
1141     builder.append(purgeContactsBuilder());
1142
1143     /* Step 3 - remove all imported info from merged/edited contacts (those remaining) */
1144     d = Delete();
1145     d.addRestriction(imContactVar, imAddressChain, imAddressVar);
1146     d.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
1147     addRemoveContactInfo(d, imAddressVar, imContactVar);
1148     deleteProperty(d, imAddressVar, nco::imPresence::resource());
1149     deleteProperty(d, imAddressVar, nco::presenceLastModified::resource());
1150     deleteProperty(d, imAddressVar, nco::imStatusMessage::resource());
1151     deleteProperty(d, imAddressVar, nco::imCapability::resource());
1152     deleteProperty(d, imAddressVar, nco::imAvatar::resource());
1153     deleteProperty(d, imAddressVar, nco::imNickname::resource());
1154     deleteProperty(d, imAddressVar, nco::imAddressAuthStatusFrom::resource());
1155     deleteProperty(d, imAddressVar, nco::imAddressAuthStatusTo::resource());
1156     d.setFilter(Filter(Functions::and_.apply(
1157         Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts)),
1158         Functions::notIn.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)))));
1159     builder.append(d);
1160
1161     /* Step 4 - move merged/edited IMAddress to qct's graph and update timestamp */
1162     Insert i(Insert::Replace);
1163     Graph g(defaultGraph);
1164     Variable imId;
1165     g.addPattern(imAddressVar, nco::imID::resource(), imId);
1166     i.addData(g);
1167     updateTimestamp(i, imContactVar);
1168     g = Graph(privateGraph);
1169     g.addPattern(imAddressVar, nco::imID::resource(), imId);
1170     i.addRestriction(g);
1171     i.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
1172     i.addRestriction(imContactVar, imAddressChain, imAddressVar);
1173     i.setFilter(Filter(Functions::and_.apply(
1174         Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts)),
1175         Functions::notIn.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)))));
1176     builder.append(i);
1177
1178     return builder;
1179 }
1180
1181 static CDTpQueryBuilder removeContactsBuilder(const QString &accountPath,
1182         const QStringList &contactIds)
1183 {
1184     // delete all nco:hasIMContact link from the nco:IMAccount
1185     CDTpQueryBuilder builder;
1186     Delete d;
1187     const Value imAccount = literalIMAccount(accountPath);
1188     Q_FOREACH (const QString &contactId, contactIds) {
1189         d.addData(imAccount, nco::hasIMContact::resource(), literalIMAddress(accountPath, contactId));
1190     }
1191     builder.append(d);
1192
1193     // Now purge contacts not linked from an IMAccount
1194     builder.append(purgeContactsBuilder());
1195
1196     return builder;
1197 }
1198
1199 static CDTpQueryBuilder removeContactsBuilder(CDTpAccountPtr accountWrapper,
1200         const QList<CDTpContactPtr> &contacts)
1201 {
1202     QStringList contactIds;
1203     Q_FOREACH (const CDTpContactPtr &contactWrapper, contacts) {
1204         contactIds << contactWrapper->contact()->id();
1205     }
1206
1207     return removeContactsBuilder(accountWrapper->account()->objectPath(), contactIds);
1208 }
1209
1210 static CDTpQueryBuilder createIMAddressBuilder(const QString &accountPath,
1211                                                const QStringList &imIds,
1212                                                uint localId)
1213 {
1214     CDTpQueryBuilder builder;
1215
1216     Insert i(Insert::Replace);
1217     Graph g(privateGraph);
1218     const ResourceValue imAccount = literalIMAccount(accountPath);
1219     PatternGroup restrictions;
1220
1221     Q_FOREACH (const QString &imId, imIds) {
1222         const ResourceValue imAddress = literalIMAddress(accountPath, imId);
1223         const BlankValue affiliation;
1224
1225         g.addPattern(imAddress, rdf::type::resource(), nco::IMAddress::resource());
1226         g.addPattern(imAddress, nco::imID::resource(), LiteralValue(imId));
1227         g.addPattern(affiliation, rdf::type::resource(), nco::Affiliation::resource());
1228         g.addPattern(affiliation, nco::hasIMAddress::resource(), imAddress);
1229         g.addPattern(imContactVar, nco::hasAffiliation::resource(), affiliation);
1230         g.addPattern(imAccount, nco::hasIMContact::resource(), imAddress);
1231     }
1232
1233     i.addData(g);
1234
1235     restrictions.addPattern(imContactVar, rdf::type::resource(), nco::PersonContact::resource());
1236     restrictions.setFilter(Functions::equal.apply(Functions::trackerId.apply(imContactVar),
1237                                                    LiteralValue(localId)));
1238     i.addRestriction(restrictions);
1239
1240     builder.append(i);
1241
1242     return builder;
1243 }
1244
1245 static CDTpQueryBuilder createGarbageCollectorBuilder()
1246 {
1247     CDTpQueryBuilder builder;
1248
1249     /* Resources we leak in the various queries:
1250      *
1251      *  - nfo:FileDataObject in privateGraph for each avatar update.
1252      *  - nco:Affiliation in privateGraph for each deleted IMAddress.
1253      *  - nco:Affiliationn nco:PhoneNumber, nco:PostalAddress, nco:EmailAddress
1254      *    and nco:OrganizationContact in the IMAddress graph for each
1255      *    ContactInfo update.
1256      */
1257
1258     /* Each avatar update leaks the previous nfo:FileDataObject in privateGraph */
1259     {
1260         Delete d;
1261         Exists e1, e2;
1262         Graph g(privateGraph);
1263         Variable fdo, img;
1264         g.addPattern(fdo, aValue, nfo::FileDataObject::resource());
1265         e1.addPattern(imAddressVar, nco::imAvatar::resource(), fdo);
1266         e2.addPattern(imAddressVar, nco::imAvatar::resource(), img);
1267         e2.addPattern(img, nie::relatedTo::resource(), fdo);
1268         d.addData(fdo, aValue, rdfs::Resource::resource());
1269         d.addRestriction(g);
1270         d.setFilter(Functions::not_.apply(Functions::or_.apply(Filter(e1), Filter(e2))));
1271         builder.append(d);
1272     }
1273
1274     /* Affiliations used to link to the IMAddress are leaked when IMAddress is
1275      * deleted. If affiliation is in privateGraph and has no hasIMAddress we
1276      * can purge it and unlink from PersonContact.
1277      *
1278      * FIXME: Because of NB#242979 we need to split the unlink and actually
1279      * deletion of the affiliation
1280      */
1281
1282     /* Part 1. Delete the affiliation */
1283     {
1284         Delete d;
1285         Graph g(privateGraph);
1286         Exists e;
1287         Variable affiliation;
1288         d.addData(affiliation, aValue, rdfs::Resource::resource());
1289         g.addPattern(affiliation, aValue, nco::Affiliation::resource());
1290         d.addRestriction(g);
1291         e.addPattern(affiliation, nco::hasIMAddress::resource(), Variable());
1292         d.setFilter(Functions::not_.apply(Filter(e)));
1293         builder.append(d);
1294     }
1295
1296     /* Part 2. Delete hasAffiliation if linked resource does not exist anymore  */
1297     {
1298         Delete d;
1299         Graph g(privateGraph);
1300         Exists e;
1301         Variable affiliation;
1302         d.addData(imContactVar, nco::hasAffiliation::resource(), affiliation);
1303         g.addPattern(imContactVar, nco::hasAffiliation::resource(), affiliation);
1304         d.addRestriction(g);
1305         e.addPattern(affiliation, aValue, rdfs::Resource::resource());
1306         d.setFilter(Functions::not_.apply(Filter(e)));
1307         builder.append(d);
1308     }
1309
1310     /* Each ContactInfo update leaks various resources in IMAddress graph. But
1311      * the IMAddress could even not exist anymore... so drop everything from
1312      * a telepathy: graph not linked anymore
1313      */
1314
1315     typedef QPair<Value, Value> ValuePair;
1316     static QList<ValuePair> contactInfoResources;
1317     if (contactInfoResources.isEmpty()) {
1318         contactInfoResources << ValuePair(nco::Affiliation::resource(), nco::hasAffiliation::resource());
1319         contactInfoResources << ValuePair(nco::PhoneNumber::resource(), nco::hasPhoneNumber::resource());
1320         contactInfoResources << ValuePair(nco::PostalAddress::resource(), nco::hasPostalAddress::resource());
1321         contactInfoResources << ValuePair(nco::EmailAddress::resource(), nco::hasEmailAddress::resource());
1322         contactInfoResources << ValuePair(nco::OrganizationContact::resource(), nco::org::resource());
1323     }
1324
1325     Q_FOREACH (const ValuePair &pair, contactInfoResources) {
1326         Variable graph;
1327         Delete d;
1328         Exists e;
1329         Graph g(graph);
1330         Variable resource;
1331         d.addData(resource, aValue, rdfs::Resource::resource());
1332         g.addPattern(resource, aValue, pair.first);
1333         e.addPattern(Variable(), pair.second, resource);
1334         d.addRestriction(g);
1335         d.setFilter(Functions::and_.apply(
1336                 Functions::startsWith.apply(graph, LiteralValue(QLatin1String("telepathy:"))),
1337                 Functions::not_.apply(Filter(e))));
1338         builder.append(d);
1339     }
1340
1341     return builder;
1342 }
1343
1344 void CDTpStorage::triggerGarbageCollector(CDTpQueryBuilder &builder, uint nContacts)
1345 {
1346     if (mDirectGC) {
1347         builder.append(createGarbageCollectorBuilder());
1348         return;
1349     }
1350
1351     static bool registered = false;
1352     if (!registered) {
1353         registered = true;
1354         QctGarbageCollector::registerQuery(QLatin1String("com.nokia.contactsd"),
1355                 createGarbageCollectorBuilder().sparql());
1356     }
1357
1358     QctGarbageCollector::trigger(QLatin1String("com.nokia.contactsd"),
1359             ((double)nContacts)/1000);
1360 }
1361
1362 void CDTpStorage::syncAccounts(const QList<CDTpAccountPtr> &accounts)
1363 {
1364     /* Remove IMAccount that does not exist anymore */
1365     CDTpQueryBuilder builder;
1366     Delete d;
1367     d.addData(imAccountVar, aValue, rdfs::Resource::resource());
1368     d.addRestriction(imAccountVar, aValue, nco::IMAccount::resource());
1369     if (!accounts.isEmpty()) {
1370         d.setFilter(Filter(Functions::notIn.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts))));
1371     }
1372     builder.append(d);
1373
1374     /* Sync accounts and their contacts */
1375     uint nContacts = 0;
1376     if (!accounts.isEmpty()) {
1377         builder.append(createAccountsBuilder(accounts));
1378
1379         // Split accounts that have their roster, and those who don't
1380         QList<CDTpAccountPtr> disabledAccounts;
1381         QList<CDTpAccountPtr> rosterAccounts;
1382         QList<CDTpAccountPtr> noRosterAccounts;
1383         Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
1384             if (!accountWrapper->isEnabled()) {
1385                 disabledAccounts << accountWrapper;
1386             } else if (accountWrapper->hasRoster()) {
1387                 rosterAccounts << accountWrapper;
1388             } else {
1389                 noRosterAccounts << accountWrapper;
1390             }
1391             nContacts += accountWrapper->contacts().count();
1392         }
1393
1394         // Sync contacts
1395         if (!disabledAccounts.isEmpty()) {
1396             builder.append(syncDisabledAccountsContactsBuilder(disabledAccounts));
1397         }
1398
1399         if (!rosterAccounts.isEmpty()) {
1400             builder.append(syncRosterAccountsContactsBuilder(mNetwork, rosterAccounts));
1401         }
1402
1403         if (!noRosterAccounts.isEmpty()) {
1404             builder.append(syncNoRosterAccountsContactsBuilder(noRosterAccounts));
1405         }
1406     }
1407
1408     /* Purge IMAddresses not bound from an account anymore, this include the
1409      * self IMAddress and the default-contact-me as well */
1410     builder.append(purgeContactsBuilder());
1411
1412     triggerGarbageCollector(builder, nContacts);
1413
1414     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1415     connect(query,
1416             SIGNAL(finished(CDTpSparqlQuery *)),
1417             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1418 }
1419
1420 void CDTpStorage::createAccount(CDTpAccountPtr accountWrapper)
1421 {
1422     CDTpQueryBuilder builder;
1423
1424     /* Create account */
1425     QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
1426     builder.append(createAccountsBuilder(accounts));
1427
1428     /* if account has no contacts, we are done */
1429     if (not accountWrapper->contacts().isEmpty()) {
1430         /* Create account's contacts */
1431         builder.append(createContactsBuilder(accountWrapper->contacts()));
1432         builder.append(updateContactsInfoBuilder(accountWrapper->contacts()));
1433
1434         /* Update timestamp on all nco:PersonContact bound to this account */
1435         Insert i(Insert::Replace);
1436         updateTimestamp(i, imContactVar);
1437         i.addRestriction(literalIMAccount(accountWrapper), nco::hasIMContact::resource(), imAddressVar);
1438         i.addRestriction(imContactVar, imAddressChain, imAddressVar);
1439         builder.append(i);
1440     }
1441
1442     CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
1443
1444     const Tp::ConnectionPtr accountConnection = accountWrapper->account()->connection();
1445
1446     // We will only get the contacts now if the roster is ready. If the roster is not ready,
1447     // we should not emit syncEnded when the query ends since we won't have saved any contact
1448     if (not accountConnection.isNull()
1449      && (accountConnection->actualFeatures().contains(Tp::Connection::FeatureRoster))
1450      && (accountConnection->contactManager()->state() == Tp::ContactListStateSuccess)) {
1451         connect(query,
1452                 SIGNAL(finished(CDTpSparqlQuery *)),
1453                 SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
1454     } else {
1455         connect(query,
1456                 SIGNAL(finished(CDTpSparqlQuery *)),
1457                 SLOT(onSparqlQueryFinished(CDTpSparqlQuery*)));
1458     }
1459 }
1460
1461 void CDTpStorage::updateAccount(CDTpAccountPtr accountWrapper,
1462         CDTpAccount::Changes changes)
1463 {
1464     debug() << "Update account" << literalIMAddress(accountWrapper).sparql();
1465
1466     CDTpQueryBuilder builder;
1467
1468     Insert i(Insert::Replace);
1469     updateTimestamp(i, nco::default_contact_me::resource());
1470
1471     Graph g(privateGraph);
1472     addAccountChanges(g, accountWrapper, changes);
1473     i.addData(g);
1474
1475     builder.append(i);
1476
1477     /* If account got disabled, we have special threatment for its contacts */
1478     if ((changes & CDTpAccount::Enabled) != 0 && !accountWrapper->isEnabled()) {
1479         cancelQueuedUpdates(accountWrapper->contacts());
1480         QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
1481         builder.append(syncDisabledAccountsContactsBuilder(accounts));
1482         triggerGarbageCollector(builder, accountWrapper->contacts().count());
1483     }
1484
1485     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1486     connect(query,
1487             SIGNAL(finished(CDTpSparqlQuery *)),
1488             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1489 }
1490
1491 void CDTpStorage::removeAccount(CDTpAccountPtr accountWrapper)
1492 {
1493     cancelQueuedUpdates(accountWrapper->contacts());
1494
1495     const Value imAccount = literalIMAccount(accountWrapper);
1496     debug() << "Remove account" << imAccount.sparql();
1497
1498     /* Remove account */
1499     CDTpQueryBuilder builder;
1500     Delete d;
1501     d.addData(imAccount, aValue, rdfs::Resource::resource());
1502     builder.append(d);
1503
1504     /* Purge IMAddresses not bound from an account anymore.
1505      * This will at least remove the IMAddress of the self contact and update
1506      * default-contact-me */
1507     builder.append(purgeContactsBuilder());
1508
1509     triggerGarbageCollector(builder, 200);
1510
1511     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1512     connect(query,
1513             SIGNAL(finished(CDTpSparqlQuery *)),
1514             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1515 }
1516
1517 // This is called when account goes online/offline
1518 void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper)
1519 {
1520     CDTpQueryBuilder builder;
1521
1522     QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
1523     if (accountWrapper->hasRoster()) {
1524         builder = syncRosterAccountsContactsBuilder(mNetwork, accounts, true);
1525         triggerGarbageCollector(builder, accountWrapper->contacts().count());
1526     } else {
1527         builder = syncNoRosterAccountsContactsBuilder(accounts);
1528     }
1529
1530     /* If it is not the first time account gets a roster, execute query without
1531        notifying import progress */
1532     if (!accountWrapper->isNewAccount()) {
1533         CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1534         connect(query,
1535                 SIGNAL(finished(CDTpSparqlQuery *)),
1536                 SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1537         return;
1538     }
1539
1540     CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
1541     connect(query,
1542             SIGNAL(finished(CDTpSparqlQuery *)),
1543             SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
1544 }
1545
1546 void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper,
1547         const QList<CDTpContactPtr> &contactsAdded,
1548         const QList<CDTpContactPtr> &contactsRemoved)
1549 {
1550     CDTpQueryBuilder builder;
1551
1552     if (!contactsAdded.isEmpty()) {
1553         builder.append(createContactsBuilder(contactsAdded));
1554         builder.append(updateContactsInfoBuilder(contactsAdded));
1555
1556         // Update nie:contentLastModified on all nco:PersonContact bound to contacts
1557         Insert i(Insert::Replace);
1558         updateTimestamp(i, imContactVar);
1559         i.addRestriction(imContactVar, imAddressChain, imAddressVar);
1560         i.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(contactsAdded))));
1561         builder.append(i);
1562     }
1563
1564     if (!contactsRemoved.isEmpty()) {
1565         cancelQueuedUpdates(contactsRemoved);
1566         builder.append(removeContactsBuilder(accountWrapper, contactsRemoved));
1567         triggerGarbageCollector(builder, contactsRemoved.count());
1568     }
1569
1570     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1571     connect(query,
1572             SIGNAL(finished(CDTpSparqlQuery *)),
1573             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1574 }
1575
1576 void CDTpStorage::createAccountContacts(const QString &accountPath, const QStringList &imIds, uint localId)
1577 {
1578     CDTpSparqlQuery *query = new CDTpSparqlQuery(createIMAddressBuilder(accountPath, imIds, localId),
1579                                                  this);
1580     connect(query,
1581             SIGNAL(finished(CDTpSparqlQuery *)),
1582             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1583 }
1584
1585 /* Use this only in offline mode - use syncAccountContacts in online mode */
1586 void CDTpStorage::removeAccountContacts(const QString &accountPath, const QStringList &contactIds)
1587 {
1588     CDTpQueryBuilder builder;
1589
1590     builder = removeContactsBuilder(accountPath, contactIds);
1591     triggerGarbageCollector(builder, 200);
1592
1593     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1594     connect(query,
1595             SIGNAL(finished(CDTpSparqlQuery *)),
1596             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1597 }
1598
1599 void CDTpStorage::updateContact(CDTpContactPtr contactWrapper, CDTpContact::Changes changes)
1600 {
1601     mUpdateQueue[contactWrapper] |= changes;
1602
1603     if (not mUpdateRunning) {
1604         // Only update IM contacts in tracker after queuing 50 contacts or after
1605         // not receiving an update notifiction for 150 ms. This dramatically reduces
1606         // system but also keeps update latency within acceptable bounds.
1607         if (not mUpdateTimer.isActive() || mUpdateQueue.count() < UPDATE_THRESHOLD) {
1608             mUpdateTimer.start();
1609         }
1610     }
1611 }
1612
1613 void CDTpStorage::onUpdateQueueTimeout()
1614 {
1615     debug() << "Update" << mUpdateQueue.count() << "contacts";
1616
1617     CDTpQueryBuilder builder;
1618     Graph g(privateGraph);
1619
1620     QList<CDTpContactPtr> allContacts;
1621     QList<CDTpContactPtr> infoContacts;
1622     QList<CDTpContactPtr> capsContacts;
1623
1624     // Separate contacts for which we can emit a "tagged" signal and the others
1625     QStringList onlyPresenceChangedAddresses;
1626     QStringList infoChangedAddresses;
1627
1628     QHash<CDTpContactPtr, CDTpContact::Changes>::const_iterator iter;
1629
1630     for (iter = mUpdateQueue.constBegin(); iter != mUpdateQueue.constEnd(); iter++) {
1631         CDTpContactPtr contactWrapper = iter.key();
1632
1633         // Skip the contact in case its account was deleted before this function
1634         // was invoked
1635         if (contactWrapper->accountWrapper().isNull()) {
1636             continue;
1637         }
1638
1639         if (!contactWrapper->isVisible()) {
1640             continue;
1641         }
1642
1643         const QString address = imAddress(contactWrapper);
1644         CDTpContact::Changes changes = iter.value();
1645
1646         if (contactChangedPresenceOrCapsOnly(changes)) {
1647             onlyPresenceChangedAddresses.append(address);
1648         } else {
1649             infoChangedAddresses.append(address);
1650         }
1651
1652         allContacts << contactWrapper;
1653
1654         // Special case for Capabilities and Information changes
1655         if (changes & CDTpContact::Capabilities) {
1656             capsContacts << contactWrapper;
1657         }
1658         if (changes & CDTpContact::Information) {
1659             infoContacts << contactWrapper;
1660         }
1661
1662         // Update social avatars if needed
1663         if (changes & CDTpContact::DefaultAvatar) {
1664             updateSocialAvatars(mNetwork, contactWrapper);
1665         }
1666
1667         // Add IMAddress changes
1668         addContactChanges(g, literalIMAddress(contactWrapper), changes, contactWrapper);
1669     }
1670     mUpdateQueue.clear();
1671
1672     if (allContacts.isEmpty()) {
1673         debug() << "  None needs update";
1674         return;
1675     }
1676
1677     Insert i(Insert::Replace);
1678     i.addData(g);
1679     builder.append(i);
1680
1681     // prepend delete nco:imCapability for contacts who's caps changed
1682     if (!capsContacts.isEmpty()) {
1683         Delete d;
1684         Variable caps;
1685         d.addData(imAddressVar, nco::imCapability::resource(), caps);
1686         d.addRestriction(imAddressVar, nco::imCapability::resource(), caps);
1687         d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(capsContacts))));
1688         builder.prepend(d);
1689     }
1690
1691     // delete ContactInfo for contacts who's info changed
1692     if (!infoContacts.isEmpty()) {
1693         Delete d;
1694         d.addRestriction(imContactVar, imAddressChain, imAddressVar);
1695         d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(infoContacts))));
1696         addRemoveContactInfo(d, imAddressVar, imContactVar);
1697         builder.append(d);
1698     }
1699
1700     // Create ContactInfo for each contact who's info changed
1701     Q_FOREACH (const CDTpContactPtr contactWrapper, infoContacts) {
1702         builder.append(createContactInfoBuilder(contactWrapper));
1703     }
1704
1705     // Update nie:contentLastModified on all nco:PersonContact bound to contacts
1706     if (not onlyPresenceChangedAddresses.isEmpty()) {
1707         builder.append(updateTimestampOnIMAddresses(onlyPresenceChangedAddresses, TaggedSignalUpdate));
1708     }
1709
1710     if (not infoChangedAddresses.isEmpty()) {
1711         builder.append(updateTimestampOnIMAddresses(infoChangedAddresses));
1712     }
1713
1714     triggerGarbageCollector(builder, allContacts.count());
1715
1716     // Launch the query
1717     mUpdateRunning = true;
1718     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1719     connect(query,
1720             SIGNAL(finished(CDTpSparqlQuery *)),
1721             SLOT(onUpdateFinished(CDTpSparqlQuery *)));
1722 }
1723
1724 void CDTpStorage::onUpdateFinished(CDTpSparqlQuery *query)
1725 {
1726     onSparqlQueryFinished(query);
1727
1728     mUpdateRunning = false;
1729
1730     if (not mUpdateQueue.isEmpty() && not mUpdateTimer.isActive()) {
1731         mUpdateTimer.start();
1732     }
1733 }
1734
1735 void CDTpStorage::cancelQueuedUpdates(const QList<CDTpContactPtr> &contacts)
1736 {
1737     Q_FOREACH (const CDTpContactPtr &contactWrapper, contacts) {
1738         mUpdateQueue.remove(contactWrapper);
1739     }
1740 }
1741
1742 void CDTpStorage::onSyncOperationEnded(CDTpSparqlQuery *query)
1743 {
1744     onSparqlQueryFinished(query);
1745
1746     CDTpAccountsSparqlQuery *accountsQuery = qobject_cast<CDTpAccountsSparqlQuery*>(query);
1747     QList<CDTpAccountPtr> accounts = accountsQuery->accounts();
1748
1749     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
1750         accountWrapper->emitSyncEnded(accountWrapper->contacts().count(), 0);
1751     }
1752 }
1753
1754 void CDTpStorage::onSparqlQueryFinished(CDTpSparqlQuery *query)
1755 {
1756     if (query->hasError()) {
1757         QSparqlError e = query->error();
1758         ErrorCode code = ErrorUnknown;
1759         if (e.type() == QSparqlError::BackendError && e.number() == TRACKER_SPARQL_ERROR_NO_SPACE) {
1760             code = ErrorNoSpace;
1761         }
1762         Q_EMIT error(code, e.message());
1763     }
1764 }
1765