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