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