Made contactlistener static.
[commhistory:libcommhistory.git] / src / queryresult.cpp
1 /******************************************************************************
2 **
3 ** This file is part of libcommhistory.
4 **
5 ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
6 ** Contact: Reto Zingg <reto.zingg@nokia.com>
7 **
8 ** This library is free software; you can redistribute it and/or modify it
9 ** under the terms of the GNU Lesser General Public License version 2.1 as
10 ** published by the Free Software Foundation.
11 **
12 ** This library is distributed in the hope that it will be useful, but
13 ** WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 ** or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
15 ** License for more details.
16 **
17 ** You should have received a copy of the GNU Lesser General Public License
18 ** along with this library; if not, write to the Free Software Foundation, Inc.,
19 ** 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20 **
21 ******************************************************************************/
22
23 #include "group.h"
24
25 #include "queryresult.h"
26 #include "contactlistener.h"
27
28 #include <QSettings>
29 using namespace CommHistory;
30
31 // used for filling data from tracker result rows
32 #define RESULT_INDEX(COL) result.result->value(result.columns[QLatin1String(COL)])
33 #define RESULT_INDEX2(COL) result->value(properties.indexOf(COL))
34
35 #define LAT(STR) QLatin1String(STR)
36
37 #define TELEPATHY_URI_PREFIX_LEN (sizeof("telepathy:") - 1)
38 #define IM_ADDRESS_SEPARATOR QLatin1Char('!')
39
40 #define NMO_ "http://www.semanticdesktop.org/ontologies/2007/03/22/nmo#"
41
42 QSharedPointer<ContactListener> QueryResult::contactListener;
43
44 namespace {
45
46 Event::EventStatus nmoStatusToEventStatus(const QString &status)
47 {
48     if (status == LAT(NMO_ "delivery-status-sent"))
49          return Event::SentStatus;
50     else if (status == LAT(NMO_ "delivery-status-delivered"))
51          return Event::DeliveredStatus;
52     else if (status == LAT(NMO_ "delivery-status-temporarily-failed"))
53         return Event::TemporarilyFailedStatus;
54     else if (status == LAT(NMO_ "delivery-status-temporarily-failed-offline"))
55         return Event::TemporarilyFailedOfflineStatus;
56     else if(status == LAT(NMO_ "delivery-status-permanently-failed"))
57             return Event::PermanentlyFailedStatus;
58
59     return Event::UnknownStatus;
60 }
61
62 // parse concatted & coalesced sip/tel/IM remote id column
63 QString parseRemoteUid(const QString &remoteUid)
64 {
65     QString result;
66
67     QStringList uids = remoteUid.split('\x1e', QString::SkipEmptyParts);
68     foreach (QString id, uids) {
69         if (id.startsWith(LAT("sip:")) || id.startsWith(LAT("sips:")))
70             return id;
71     }
72
73     if (!uids.isEmpty())
74         result = uids.first().section(IM_ADDRESS_SEPARATOR, -1);
75
76     return result;
77 }
78
79 QString getAddresbookNameOrder()
80 {
81     QSettings addressBookSettings(QSettings::IniFormat, QSettings::UserScope,
82                                   LAT("Nokia"), LAT("Contacts"));
83     return addressBookSettings.value(LAT("nameOrder")).toString();
84 }
85
86 bool isLastNameFirst = getAddresbookNameOrder() == LAT("last-first");
87
88 }
89
90 void QueryResult::fillEventFromModel(Event &event)
91 {
92     Event eventToFill;
93
94     // handle properties common to all events
95     foreach (Event::Property property, properties) {
96         switch (property) {
97         case Event::Id:
98             eventToFill.setId(Event::urlToId(RESULT_INDEX2(Event::Id).toString()));
99             break;
100         case Event::Type: {
101             QStringList types = RESULT_INDEX2(Event::Type).toString().split(QChar(','));
102             if (types.contains(LAT(NMO_ "MMSMessage"))) {
103                 eventToFill.setType(Event::MMSEvent);
104             } else if (types.contains(LAT(NMO_ "SMSMessage"))) {
105                 eventToFill.setType(Event::SMSEvent);
106             } else if (types.contains(LAT(NMO_ "IMMessage"))) {
107                 eventToFill.setType(Event::IMEvent);
108             } else if (types.contains(LAT(NMO_ "Call"))) {
109                 eventToFill.setType(Event::CallEvent);
110             }
111             break;
112         }
113         case Event::Direction:
114             if (RESULT_INDEX2(Event::Direction).toBool()) {
115                 eventToFill.setDirection(Event::Outbound);
116             } else {
117                 eventToFill.setDirection(Event::Inbound);
118             }
119             break;
120         case Event::MessageToken:
121             eventToFill.setMessageToken(RESULT_INDEX2(Event::MessageToken).toString());
122             break;
123         case Event::MmsId:
124             eventToFill.setMmsId(RESULT_INDEX2(Event::MmsId).toString());
125             break;
126         case Event::IsDraft:
127             eventToFill.setIsDraft(RESULT_INDEX2(Event::IsDraft).toBool());
128             break;
129         case Event::Subject:
130             eventToFill.setSubject(RESULT_INDEX2(Event::Subject).toString());
131             break;
132         case Event::FreeText:
133             eventToFill.setFreeText(RESULT_INDEX2(Event::FreeText).toString());
134             break;
135         case Event::ReportDelivery:
136             eventToFill.setReportDelivery(RESULT_INDEX2(Event::ReportDelivery).toBool());
137             break;
138         case Event::ReportRead:
139             eventToFill.setReportRead(RESULT_INDEX2(Event::ReportRead).toBool());
140             break;
141         case Event::ReportReadRequested:
142             eventToFill.setReportReadRequested(RESULT_INDEX2(Event::ReportReadRequested).toBool());
143             break;
144         case Event::BytesReceived:
145             eventToFill.setBytesReceived(RESULT_INDEX2(Event::BytesReceived).toInt());
146             break;
147         case Event::ContentLocation:
148             eventToFill.setContentLocation(RESULT_INDEX2(Event::ContentLocation).toString());
149             break;
150         case Event::GroupId: {
151             QString channel = RESULT_INDEX2(Event::GroupId).toString();
152             if (!channel.isEmpty())
153                 eventToFill.setGroupId(Group::urlToId(channel));
154             break;
155         }
156         case Event::StartTime:
157             eventToFill.setStartTime(RESULT_INDEX2(Event::StartTime).toDateTime());
158             break;
159         case Event::EndTime:
160             eventToFill.setEndTime(RESULT_INDEX2(Event::EndTime).toDateTime());
161             break;
162         case Event::IsRead:
163             eventToFill.setIsRead(RESULT_INDEX2(Event::IsRead).toBool());
164             break;
165         case Event::Status: {
166             QString status = RESULT_INDEX2(Event::Status).toString();
167             if (!status.isEmpty())
168                 eventToFill.setStatus(nmoStatusToEventStatus(status));
169             break;
170         }
171         case Event::ReadStatus: {
172             QString status = RESULT_INDEX2(Event::ReadStatus).toString();
173             if (!status.isEmpty()) {
174                 if (status == LAT(NMO_ "read-status-read")) {
175                     eventToFill.setReadStatus(Event::ReadStatusRead);
176                 } else if (status == LAT(NMO_ "read-status-deleted")) {
177                     eventToFill.setReadStatus(Event::ReadStatusDeleted);
178                 } else {
179                     eventToFill.setReadStatus(Event::UnknownReadStatus);
180                 }
181             }
182             break;
183         }
184         case Event::LastModified:
185             eventToFill.setLastModified(RESULT_INDEX2(Event::LastModified).toDateTime());
186             break;
187         case Event::IsMissedCall:
188             eventToFill.setIsMissedCall(!(RESULT_INDEX2(Event::IsMissedCall).toBool()));
189             break;
190         case Event::IsEmergencyCall:
191             eventToFill.setIsEmergencyCall(RESULT_INDEX2(Event::IsEmergencyCall).toBool());
192             break;
193         case Event::ParentId:
194             eventToFill.setParentId(RESULT_INDEX2(Event::ParentId).toInt());
195             break;
196         case Event::FromVCardFileName: { // handle Event::FromVCardLabel as well
197             QString filename = RESULT_INDEX2(Event::FromVCardFileName).toString();
198             if (!filename.isEmpty())
199                 eventToFill.setFromVCard(filename, RESULT_INDEX2(Event::FromVCardLabel).toString());
200             break;
201         }
202         case Event::Encoding:
203             eventToFill.setEncoding(RESULT_INDEX2(Event::Encoding).toString());
204             break;
205         case Event::CharacterSet:
206             eventToFill.setCharacterSet(RESULT_INDEX2(Event::CharacterSet).toString());
207             break;
208         case Event::IsDeleted:
209             eventToFill.setDeleted(RESULT_INDEX2(Event::IsDeleted).toBool());
210             break;
211         case Event::ValidityPeriod:
212             eventToFill.setValidityPeriod(RESULT_INDEX2(Event::ValidityPeriod).toInt());
213             break;
214         case Event::Cc:
215             eventToFill.setCcList(RESULT_INDEX2(Event::Cc).toString().split('\x1e', QString::SkipEmptyParts));
216             break;
217         case Event::Bcc:
218             eventToFill.setBccList(RESULT_INDEX2(Event::Bcc).toString().split('\x1e', QString::SkipEmptyParts));
219             break;
220         case Event::To:
221         case Event::Headers: {
222             QHash<QString, QString> headers;
223             parseHeaders(RESULT_INDEX2(Event::Headers).toString(), headers);
224             eventToFill.setHeaders(headers);
225             break;
226         }
227         default:
228             break;// handle below
229         }
230     }
231
232     // local/remote id and direction are common to all events
233     if (properties.contains(Event::LocalUid)
234         || properties.contains(Event::RemoteUid)) {
235         // local contact: <telepathy:/org/.../gabble/jabber/dut_40localhost0>
236         // remote contact: <telepathy:<account>!<imid>> or <tel:+35801234567>
237         QString fromId = RESULT_INDEX2(Event::LocalUid).toString();
238         QString toId = RESULT_INDEX2(Event::RemoteUid).toString();
239
240         if (eventToFill.direction() == Event::Outbound) {
241             eventToFill.setLocalUid(fromId.mid(TELEPATHY_URI_PREFIX_LEN));
242             eventToFill.setRemoteUid(parseRemoteUid(toId));
243         } else {
244             eventToFill.setLocalUid(toId.mid(TELEPATHY_URI_PREFIX_LEN));
245             eventToFill.setRemoteUid(parseRemoteUid(fromId));
246         }
247     }
248
249     if (eventToFill.status() == Event::UnknownStatus &&
250         (eventToFill.type() == Event::SMSEvent || eventToFill.type() == Event::MMSEvent) &&
251         !eventToFill.isDraft() &&
252         eventToFill.direction() == Event::Outbound) {
253         // treat missing status as sending for outbound SMS
254         eventToFill.setStatus(Event::SendingStatus);
255     }
256
257     if (eventToFill.type() == Event::IMEvent) {
258         eventToFill.setIsAction(RESULT_INDEX2(Event::IsAction).toBool());
259     }
260
261     // TODO: what to do with the contact id and nickname columns if
262     // Event::ContactId and Event::ContactName are replaced with
263     // Event::Contacts?
264     if (properties.contains(Event::ContactId)) {
265         QList<Event::Contact> contacts;
266         parseContacts(RESULT_INDEX2(Event::ContactId).toString(),
267                       eventToFill.localUid(), contacts);
268         eventToFill.setContacts(contacts);
269     }
270
271     // save data and give back as parameter
272     event = eventToFill;
273     event.resetModifiedProperties();
274 }
275
276 void QueryResult::fillGroupFromModel(Group &group)
277 {
278     Group groupToFill;
279
280     QStringList types = result->value(Group::LastEventType).toString().split(QChar(','));
281     if (types.contains(LAT(NMO_ "MMSMessage"))) {
282         groupToFill.setLastEventType(Event::MMSEvent);
283     } else if (types.contains(LAT(NMO_ "SMSMessage"))) {
284         groupToFill.setLastEventType(Event::SMSEvent);
285     } else if (types.contains(LAT(NMO_ "IMMessage"))) {
286         groupToFill.setLastEventType(Event::IMEvent);
287     }
288
289     QString status = result->value(Group::LastEventStatus).toString();
290     if (!status.isEmpty())
291         groupToFill.setLastEventStatus(nmoStatusToEventStatus(status));
292
293     groupToFill.setId(Group::urlToId(result->value(Group::Id).toString()));
294
295     groupToFill.setChatName(result->value(Group::ChatName).toString());
296
297     QString identifier = result->value(Group::Type).toString();
298     if (!identifier.isEmpty()) {
299         bool ok = false;
300         Group::ChatType chatType = (Group::ChatType)(identifier.toUInt(&ok));
301         if (ok)
302             groupToFill.setChatType(chatType);
303     }
304
305     groupToFill.setRemoteUids(QStringList() << result->value(Group::RemoteUids).toString());
306     groupToFill.setLocalUid(result->value(Group::LocalUid).toString());
307
308     QList<Event::Contact> contacts;
309     parseContacts(result->value(Group::ContactId).toString(),
310                   groupToFill.localUid(), contacts);
311     groupToFill.setContacts(contacts);
312
313     groupToFill.setTotalMessages(result->value(Group::TotalMessages).toInt());
314     groupToFill.setUnreadMessages(result->value(Group::UnreadMessages).toInt());
315     groupToFill.setSentMessages(result->value(Group::SentMessages).toInt());
316     groupToFill.setEndTime(result->value(Group::EndTime).toDateTime());
317     groupToFill.setLastEventId(Event::urlToId(result->value(Group::LastEventId).toString()));
318     groupToFill.setLastVCardFileName(result->value(Group::LastVCardFileName).toString());
319     groupToFill.setLastVCardLabel(result->value(Group::LastVCardLabel).toString());
320
321     QStringList text = result->value(Group::LastMessageText).toString().split("\x1e", QString::SkipEmptyParts);
322     if (!text.isEmpty())
323         groupToFill.setLastMessageText(text[0]);
324
325     // tracker query returns 0 for non-existing messages... make the
326     // value api-compatible
327     if (groupToFill.lastEventId() == 0)
328         groupToFill.setLastEventId(-1);
329
330     // we have to set nmo:sentTime for indexing, so consider time(0) as
331     // invalid
332     if (groupToFill.endTime() == QDateTime::fromTime_t(0))
333         groupToFill.setEndTime(QDateTime());
334
335     groupToFill.setLastModified(result->value(Group::LastModified).toDateTime());
336     groupToFill.setStartTime(result->value(Group::StartTime).toDateTime());
337
338     group = groupToFill;
339     group.resetModifiedProperties();
340 }
341
342 void QueryResult::fillMessagePartFromModel(MessagePart &messagePart)
343 {
344     MessagePart newPart;
345
346     if (!eventId) {
347         eventId = Event::urlToId(result->value(MessagePartColumnMessage).toString());
348     }
349     newPart.setUri(result->value(MessagePartColumnMessagePart).toString());
350     newPart.setContentId(result->value(MessagePartColumnContentId).toString());
351     newPart.setPlainTextContent(result->value(MessagePartColumnText).toString());
352     newPart.setContentType(result->value(MessagePartColumnMimeType).toString());
353     newPart.setCharacterSet(result->value(MessagePartColumnCharacterSet).toString());
354     newPart.setContentSize(result->value(MessagePartColumnContentSize).toInt());
355     newPart.setContentLocation(result->value(MessagePartColumnFileName).toString());
356
357     messagePart = newPart;
358 }
359
360 void QueryResult::fillCallGroupFromModel(Event &event)
361 {
362     Event eventToFill;
363
364     eventToFill.setType(Event::CallEvent);
365     eventToFill.setId(Event::urlToId(result->value(CallGroupColumnLastCall).toString()));
366     eventToFill.setStartTime(result->value(CallGroupColumnStartTime).toDateTime().toLocalTime());
367     eventToFill.setEndTime(result->value(CallGroupColumnEndTime).toDateTime().toLocalTime());
368     QString fromId = result->value(CallGroupColumnFrom).toString();
369     QString toId = result->value(CallGroupColumnTo).toString();
370
371     if (result->value(CallGroupColumnIsSent).toBool()) {
372         eventToFill.setDirection(Event::Outbound);
373         eventToFill.setLocalUid(fromId.mid(TELEPATHY_URI_PREFIX_LEN));
374         eventToFill.setRemoteUid(parseRemoteUid(toId));
375     } else {
376         eventToFill.setDirection(Event::Inbound);
377         eventToFill.setLocalUid(toId.mid(TELEPATHY_URI_PREFIX_LEN));
378         eventToFill.setRemoteUid(parseRemoteUid(fromId));
379     }
380
381     eventToFill.setIsMissedCall(!(result->value(CallGroupColumnIsAnswered).toBool()));
382     eventToFill.setIsEmergencyCall(result->value(CallGroupColumnIsEmergency).toBool());
383     eventToFill.setIsRead(result->value(CallGroupColumnIsRead).toBool());
384     eventToFill.setLastModified(result->value(CallGroupColumnLastModified).toDateTime().toLocalTime());
385
386     QList<Event::Contact> contacts;
387     parseContacts(result->value(CallGroupColumnContacts).toString(),
388                   eventToFill.localUid(), contacts);
389     eventToFill.setContacts(contacts);
390
391     eventToFill.setEventCount(result->value(CallGroupColumnMissedCount).toInt());
392
393     if (result->value(CallGroupColumnChannel).toString().endsWith("!video"))
394         eventToFill.setIsVideoCall(true);
395
396     event = eventToFill;
397 }
398
399 void QueryResult::parseHeaders(const QString &result,
400                                QHash<QString, QString> &headers)
401 {
402     /* Header format:
403      * key1 1D value1 1F key2 1D value2 1F ...
404      */
405
406     QStringList headerList = result.split("\x1f", QString::SkipEmptyParts);
407     foreach (QString header, headerList) {
408         QStringList keyValue = header.split("\x1d");
409         if (keyValue.isEmpty() || keyValue[0].isEmpty()) continue;
410         headers.insert(keyValue[0], keyValue[1]);
411     }
412 }
413
414 void QueryResult::parseContacts(const QString &result, const QString &localUid,
415                                 QList<Event::Contact> &contacts)
416 {
417     /*
418      * Query result format:
419      * result      ::= contact (1C contact)*
420      * contact     ::= namePart 1D contactNickname 1D imNickPart
421      * namePart    ::= contactID 1E firstName 1E lastName
422      * imNickPart  ::= 1E nickContact (1E nickContact)*
423      * nickContact ::= imAddress 1F nickname
424      * imAddress   ::= 'telepathy:' imAccountPath '!' remoteUid
425      */
426
427     // first split each contact to separate string
428     QStringList contactStringList = result.split('\x1c', QString::SkipEmptyParts);
429     foreach (QString contactString, contactStringList) {
430         // split contact to namePart and nickPart
431         QStringList contactPartList = contactString.split('\x1d');
432
433         // get nickname
434         QString contactNickname;
435         QString imNickname;
436         if (contactPartList.size() > 1) {
437             // nco:nickname
438             contactNickname = contactPartList[1];
439         }
440
441         if (contactPartList.size() > 2) {
442             // split nickPart to separate nickContacts
443             QStringList nickList = contactPartList[2].split('\x1e', QString::SkipEmptyParts);
444
445             foreach (QString nickContact, nickList) {
446                 // split nickContact to imAddress and nickname
447                 QStringList imPartList = nickContact.split('\x1f', QString::SkipEmptyParts);
448
449                 // get nickname from part that matches localUid
450                 if (imPartList[0].contains(localUid) && imPartList.size() > 1) {
451                     imNickname = imPartList[1];
452                     break;
453                 }
454
455                 // if localUid doesn't match to any imAddress (for example in call/SMS case),
456                 // first nickname in the list is used
457                 if (imNickname.isEmpty() && imPartList.size() > 1) {
458                     imNickname = imPartList[1];
459                 }
460             }
461         }
462
463         // create contact
464         Event::Contact contact;
465         // split namePart to contact id, first name, last name and nickname
466         QStringList namePartList = contactPartList[0].split('\x1e');
467         if (!namePartList.isEmpty()) {
468             contact.first = namePartList[0].toInt();
469
470             QString firstName, lastName;
471             if (namePartList.size() >= 2)
472                 firstName = namePartList[1];
473             if (namePartList.size() >= 3)
474                 lastName = namePartList[2];
475             contact.second = buildContactName(firstName, lastName, contactNickname, imNickname);
476
477             if (!contacts.contains(contact))
478                 contacts << contact;
479         }
480     }
481 }
482
483 QString QueryResult::buildContactName(const QString &firstName,
484                                       const QString &lastName,
485                                       const QString &contactNickname,
486                                       const QString &imNickname)
487 {
488     if (!contactListener) {
489         contactListener = ContactListener::instance();
490     }
491
492     QString name;
493
494     QString realName;
495     if (!firstName.isEmpty() || !lastName.isEmpty()) {
496         QString lname;
497         if (contactListener->isLastNameFirst())  {
498             realName = lastName;
499             lname = firstName;
500         } else {
501             realName = firstName;
502             lname = lastName;
503         }
504
505         if (!lname.isEmpty()) {
506             if (!realName.isEmpty())
507                 realName.append(' ');
508             realName.append(lname);
509         }
510     }
511
512     if (contactListener->preferNickname()) {
513         if (!contactNickname.isEmpty())
514             name = contactNickname;
515         else if (!realName.isEmpty())
516             name = realName;
517         else
518             name = imNickname;
519     } else {
520         if (!realName.isEmpty())
521             name = realName;
522         else if (!imNickname.isEmpty())
523             name = imNickname;
524         else
525             name = contactNickname;
526     }
527
528     return name;
529 }