diff --git a/config.pri b/config.pri new file mode 100644 index 0000000..9ebb839 --- /dev/null +++ b/config.pri @@ -0,0 +1,17 @@ +include(package.pri) +include(contacts-namespace.pri) + +CONFIG += qt link_pkgconfig +QT -= gui + +equals(QT_MAJOR_VERSION, 4) { + CONFIG += mobility + MOBILITY += contacts versit + PKGCONFIG += qtcontacts-sqlite-extensions +} +equals(QT_MAJOR_VERSION, 5) { + PKGCONFIG += Qt5Contacts Qt5Versit qtcontacts-sqlite-qt5-extensions + + # Needed for qt4 moc, which can't handle numeric tests + DEFINES *= QT_VERSION_5 +} diff --git a/contacts-namespace.pri b/contacts-namespace.pri new file mode 100644 index 0000000..0a325b0 --- /dev/null +++ b/contacts-namespace.pri @@ -0,0 +1,18 @@ +# We need different macros depending on which Contacts we're using +equals(QT_MAJOR_VERSION, 4) { + DEFINES *= BEGIN_CONTACTS_NAMESPACE=QTM_BEGIN_NAMESPACE + DEFINES *= END_CONTACTS_NAMESPACE=QTM_END_NAMESPACE + DEFINES *= USE_CONTACTS_NAMESPACE=QTM_USE_NAMESPACE + DEFINES *= BEGIN_VERSIT_NAMESPACE= + DEFINES *= END_VERSIT_NAMESPACE= + DEFINES *= USE_VERSIT_NAMESPACE= +} +equals(QT_MAJOR_VERSION, 5) { + DEFINES *= USING_QTPIM + DEFINES *= BEGIN_CONTACTS_NAMESPACE=QT_BEGIN_NAMESPACE_CONTACTS + DEFINES *= END_CONTACTS_NAMESPACE=QT_END_NAMESPACE_CONTACTS + DEFINES *= USE_CONTACTS_NAMESPACE=QTCONTACTS_USE_NAMESPACE + DEFINES *= BEGIN_VERSIT_NAMESPACE=QT_BEGIN_NAMESPACE_VERSIT + DEFINES *= END_VERSIT_NAMESPACE=QT_END_NAMESPACE_VERSIT + DEFINES *= USE_VERSIT_NAMESPACE=QTVERSIT_USE_NAMESPACE +} diff --git a/libcontacts.pro b/libcontacts.pro new file mode 100644 index 0000000..0bae8a8 --- /dev/null +++ b/libcontacts.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs +SUBDIRS = src tests + +tests.depends = src diff --git a/package.pri b/package.pri new file mode 100644 index 0000000..83f5d33 --- /dev/null +++ b/package.pri @@ -0,0 +1,2 @@ +equals(QT_MAJOR_VERSION, 4): PACKAGENAME=contactcache +equals(QT_MAJOR_VERSION, 5): PACKAGENAME=contactcache-qt5 diff --git a/rpm/libcontacts-qt5.spec b/rpm/libcontacts-qt5.spec new file mode 100644 index 0000000..f179460 --- /dev/null +++ b/rpm/libcontacts-qt5.spec @@ -0,0 +1,64 @@ +Name: libcontacts-qt5 +Summary: Nemo contact cache library +Version: 0.0.0 +Release: 1 +Group: System/Libraries +License: BSD +URL: https://github.com/nemomobile/libcontacts +Source0: %{name}-%{version}.tar.bz2 +Requires: qtcontacts-sqlite-qt5 +BuildRequires: pkgconfig(Qt5Core) +BuildRequires: pkgconfig(Qt5Test) +BuildRequires: pkgconfig(Qt5Contacts) +BuildRequires: pkgconfig(Qt5Versit) +BuildRequires: pkgconfig(mlite5) +BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions) + +%description +%{summary}. + +%package tests +Summary: Nemo contact cache library tests +Group: System/Libraries +Requires: %{name} = %{version}-%{release} + +%description tests +%{summary}. + +%package devel +Summary: Nemo contact cache library headers +Group: System/Libraries +Requires: %{name} = %{version}-%{release} + +%description devel +%{summary}. + +%prep +%setup -q -n %{name}-%{version} + +%build + +%qmake5 + +make %{?jobs:-j%jobs} + +%install +rm -rf %{buildroot} +%qmake_install + +%post -p /sbin/ldconfig + +%postun -p /sbin/ldconfig + +%files +%defattr(-,root,root,-) +%{_libdir}/libcontactcache-qt5.so* + +%files tests +%defattr(-,root,root,-) +/opt/tests/contactcache-qt5/* + +%files devel +%defattr(-,root,root,-) +%{_includedir}/contactcache-qt5/* +%{_libdir}/pkgconfig/contactcache-qt5.pc diff --git a/rpm/libcontacts.spec b/rpm/libcontacts.spec new file mode 100644 index 0000000..eb3d731 --- /dev/null +++ b/rpm/libcontacts.spec @@ -0,0 +1,63 @@ +Name: libcontacts +Summary: Nemo contact cache library +Version: 0.0.0 +Release: 1 +Group: System/Libraries +License: BSD +URL: https://github.com/nemomobile/libcontacts +Source0: %{name}-%{version}.tar.bz2 +Requires: qtcontacts-sqlite +BuildRequires: pkgconfig(QtCore) +BuildRequires: pkgconfig(QtContacts) +BuildRequires: pkgconfig(QtVersit) +BuildRequires: pkgconfig(mlite) +BuildRequires: pkgconfig(qtcontacts-sqlite-extensions) + +%description +%{summary}. + +%package tests +Summary: Nemo contact cache library tests +Group: System/Libraries +Requires: %{name} = %{version}-%{release} + +%description tests +%{summary}. + +%package devel +Summary: Nemo contact cache library headers +Group: System/Libraries +Requires: %{name} = %{version}-%{release} + +%description devel +%{summary}. + +%prep +%setup -q -n %{name}-%{version} + +%build + +%qmake + +make %{?jobs:-j%jobs} + +%install +rm -rf %{buildroot} +%qmake_install + +%post -p /sbin/ldconfig + +%postun -p /sbin/ldconfig + +%files +%defattr(-,root,root,-) +%{_libdir}/libcontactcache.so* + +%files tests +%defattr(-,root,root,-) +/opt/tests/contactcache/* + +%files devel +%defattr(-,root,root,-) +%{_includedir}/contactcache/* +%{_libdir}/pkgconfig/contactcache.pc diff --git a/src/contactcacheexport.h b/src/contactcacheexport.h new file mode 100644 index 0000000..9779fb0 --- /dev/null +++ b/src/contactcacheexport.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 Jolla Mobile + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#ifndef CONTACTCACHEEXPORT_H +#define CONTACTCACHEEXPORT_H + +#include + +#ifdef CONTACTCACHE_BUILD +#define CONTACTCACHE_EXPORT Q_DECL_EXPORT +#else +#define CONTACTCACHE_EXPORT Q_DECL_IMPORT +#endif + +#endif diff --git a/src/normalization.cpp b/src/normalization.cpp new file mode 100644 index 0000000..93e37f0 --- /dev/null +++ b/src/normalization.cpp @@ -0,0 +1,50 @@ +/* + * libseaside - Library that provides an interface to the Contacts application + * Copyright (c) 2013, Matt Vogt + * + * This program is licensed under the terms and conditions of the + * Apache License, version 2.0. The full text of the Apache License is at + * http://www.apache.org/licenses/LICENSE-2.0 + * + */ + +#include "normalization_p.h" + +namespace Normalization { + +QString normalizePhoneNumber(const QString &input) +{ + // Use the same algorithm as maemo localNumber + + // Not actually the 'visual-separators' from RFC3966... + // This logic is derived from qtcontacts-tracker + static const QString separators(QString::fromLatin1(" .-()[]")); + static const QString dtmfChars(QString::fromLatin1("pPwWxX")); + + // TODO: possibly make this tunable? + static const int maxCharacters = 7; + + QString subset; + subset.reserve(input.length()); + + QString::const_iterator it = input.constBegin(), end = input.constEnd(); + for ( ; it != end; ++it) { + if ((*it).isDigit()) { + // Convert to ASCII, capturing unicode digit values + subset.append(QChar::fromLatin1('0' + (*it).digitValue())); + } else if (!separators.contains(*it) && + (*it).category() != QChar::Other_Format) { + // If this is a DTMF character, stop processing here + if (dtmfChars.contains(*it)) { + break; + } else { + subset.append(*it); + } + } + } + + return subset.right(maxCharacters); +} + +} + diff --git a/src/normalization_p.h b/src/normalization_p.h new file mode 100644 index 0000000..5bf202e --- /dev/null +++ b/src/normalization_p.h @@ -0,0 +1,22 @@ +/* + * libseaside - Library that provides an interface to the Contacts application + * Copyright (c) 2013, Matt Vogt + * + * This program is licensed under the terms and conditions of the + * Apache License, version 2.0. The full text of the Apache License is at + * http://www.apache.org/licenses/LICENSE-2.0 + * + */ + +#ifndef __NORMALIZATION_P_H__ +#define __NORMALIZATION_P_H__ + +#include + +namespace Normalization { + +QString normalizePhoneNumber(const QString &input); + +} + +#endif diff --git a/src/seasidecache.cpp b/src/seasidecache.cpp new file mode 100644 index 0000000..5882184 --- /dev/null +++ b/src/seasidecache.cpp @@ -0,0 +1,1735 @@ +/* + * Copyright (C) 2013 Jolla Mobile + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#include "seasidecache.h" + +#include "synchronizelists.h" +#include "normalization_p.h" + +#include +#ifdef USING_QTPIM +#include +#else +#include +#endif +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +USE_VERSIT_NAMESPACE + + +static QString aggregateRelationshipType = +#ifdef USING_QTPIM + QContactRelationship::Aggregates(); +#else + QContactRelationship::Aggregates; +#endif + +static const QString syncTargetLocal = QLatin1String("local"); +static const QString syncTargetWasLocal = QLatin1String("was_local"); + +static QList getAllContactNameGroups() +{ + QList groups; + groups << QLatin1Char('A') + << QLatin1Char('B') + << QLatin1Char('C') + << QLatin1Char('D') + << QLatin1Char('E') + << QLatin1Char('F') + << QLatin1Char('G') + << QLatin1Char('H') + << QLatin1Char('I') + << QLatin1Char('J') + << QLatin1Char('K') + << QLatin1Char('L') + << QLatin1Char('M') + << QLatin1Char('N') + << QLatin1Char('O') + << QLatin1Char('P') + << QLatin1Char('Q') + << QLatin1Char('R') + << QLatin1Char('S') + << QLatin1Char('T') + << QLatin1Char('U') + << QLatin1Char('V') + << QLatin1Char('W') + << QLatin1Char('X') + << QLatin1Char('Y') + << QLatin1Char('Z') + << QChar(0x00c5) // Å + << QChar(0x00c4) // Ä + << QChar(0x00d6) // Ö + << QLatin1Char('#'); + return groups; +} + +SeasideCache *SeasideCache::instancePtr = 0; +QList SeasideCache::allContactNameGroups = getAllContactNameGroups(); + +static QString managerName() +{ +#ifdef USING_QTPIM + // Temporary override until qtpim supports QTCONTACTS_MANAGER_OVERRIDE + return QStringLiteral("org.nemomobile.contacts.sqlite"); +#endif + QByteArray environmentManager = qgetenv("NEMO_CONTACT_MANAGER"); + return !environmentManager.isEmpty() + ? QString::fromLatin1(environmentManager, environmentManager.length()) + : QString(); +} + +template +void setDetailType(Filter &filter, Field field) +{ +#ifdef USING_QTPIM + filter.setDetailType(T::Type, field); +#else + filter.setDetailDefinitionName(T::DefinitionName, field); +#endif +} + +SeasideCache* SeasideCache::instance() +{ + return instancePtr; +} + +SeasideCache::ContactIdType SeasideCache::apiId(const QContact &contact) +{ +#ifdef USING_QTPIM + return contact.id(); +#else + return contact.id().localId(); +#endif +} + +SeasideCache::ContactIdType SeasideCache::apiId(quint32 iid) +{ + return QtContactsSqliteExtensions::apiContactId(iid); +} + +bool SeasideCache::validId(const ContactIdType &id) +{ +#ifdef USING_QTPIM + return !id.isNull(); +#else + return (id != 0); +#endif +} + +quint32 SeasideCache::internalId(const QContact &contact) +{ + return internalId(contact.id()); +} + +quint32 SeasideCache::internalId(const QContactId &id) +{ + return QtContactsSqliteExtensions::internalContactId(id); +} + +#ifndef USING_QTPIM +quint32 SeasideCache::internalId(QContactLocalId id) +{ + return QtContactsSqliteExtensions::internalContactId(id); +} +#endif + +SeasideCache::SeasideCache() + : m_manager(managerName()) +#ifdef HAS_MLITE + , m_displayLabelOrderConf(QLatin1String("/org/nemomobile/contacts/display_label_order")) +#endif + , m_resultsRead(0) + , m_populated(0) + , m_cacheIndex(0) + , m_queryIndex(0) + , m_appendIndex(0) + , m_fetchFilter(FilterNone) + , m_displayLabelOrder(FirstNameFirst) + , m_keepPopulated(false) + , m_updatesPending(false) + , m_refreshRequired(false) + , m_contactsUpdated(false) +{ + Q_ASSERT(!instancePtr); + instancePtr = this; + + m_timer.start(); + +#ifdef HAS_MLITE + connect(&m_displayLabelOrderConf, SIGNAL(valueChanged()), this, SLOT(displayLabelOrderChanged())); + QVariant displayLabelOrder = m_displayLabelOrderConf.value(); + if (displayLabelOrder.isValid()) + m_displayLabelOrder = static_cast(displayLabelOrder.toInt()); +#endif + +#ifdef USING_QTPIM + connect(&m_manager, SIGNAL(dataChanged()), this, SLOT(updateContacts())); + connect(&m_manager, SIGNAL(contactsAdded(QList)), + this, SLOT(contactsAdded(QList))); + connect(&m_manager, SIGNAL(contactsChanged(QList)), + this, SLOT(contactsChanged(QList))); + connect(&m_manager, SIGNAL(contactsRemoved(QList)), + this, SLOT(contactsRemoved(QList))); +#else + connect(&m_manager, SIGNAL(dataChanged()), this, SLOT(updateContacts())); + connect(&m_manager, SIGNAL(contactsAdded(QList)), + this, SLOT(contactsAdded(QList))); + connect(&m_manager, SIGNAL(contactsChanged(QList)), + this, SLOT(contactsChanged(QList))); + connect(&m_manager, SIGNAL(contactsRemoved(QList)), + this, SLOT(contactsRemoved(QList))); +#endif + + connect(&m_fetchRequest, SIGNAL(resultsAvailable()), this, SLOT(contactsAvailable())); + connect(&m_fetchByIdRequest, SIGNAL(resultsAvailable()), this, SLOT(contactsAvailable())); + connect(&m_contactIdRequest, SIGNAL(resultsAvailable()), this, SLOT(contactIdsAvailable())); + connect(&m_relationshipsFetchRequest, SIGNAL(resultsAvailable()), this, SLOT(relationshipsAvailable())); + + connect(&m_fetchRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + connect(&m_fetchByIdRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + connect(&m_contactIdRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + connect(&m_relationshipsFetchRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + connect(&m_removeRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + connect(&m_saveRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + connect(&m_relationshipSaveRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + connect(&m_relationshipRemoveRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(requestStateChanged(QContactAbstractRequest::State))); + + m_fetchRequest.setManager(&m_manager); + m_fetchByIdRequest.setManager(&m_manager); + m_contactIdRequest.setManager(&m_manager); + m_relationshipsFetchRequest.setManager(&m_manager); + m_removeRequest.setManager(&m_manager); + m_saveRequest.setManager(&m_manager); + m_relationshipSaveRequest.setManager(&m_manager); + m_relationshipRemoveRequest.setManager(&m_manager); + + QContactFetchHint fetchHint; + fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships + | QContactFetchHint::NoActionPreferences + | QContactFetchHint::NoBinaryBlobs); + + // Note: no restriction on detail definitions - the cache should contain the entire contact + + m_fetchRequest.setFetchHint(fetchHint); + + setSortOrder(m_displayLabelOrder); +} + +SeasideCache::~SeasideCache() +{ + if (instancePtr == this) + instancePtr = 0; +} + +void SeasideCache::checkForExpiry() +{ + if (instancePtr->m_users.isEmpty()) { + bool unused = true; + for (int i = 0; i < FilterTypesCount; ++i) { + unused &= instancePtr->m_models[i].isEmpty(); + } + if (unused) { + instancePtr->m_expiryTimer.start(30000, instancePtr); + } + } +} + +void SeasideCache::registerModel(ListModel *model, FilterType type) +{ + if (!instancePtr) { + new SeasideCache; + } else { + instancePtr->m_expiryTimer.stop(); + for (int i = 0; i < FilterTypesCount; ++i) + instancePtr->m_models[i].removeAll(model); + } + instancePtr->m_models[type].append(model); + instancePtr->keepPopulated(); +} + +void SeasideCache::unregisterModel(ListModel *model) +{ + for (int i = 0; i < FilterTypesCount; ++i) + instancePtr->m_models[i].removeAll(model); + + checkForExpiry(); +} + +void SeasideCache::registerUser(QObject *user) +{ + if (!instancePtr) { + new SeasideCache; + } else { + instancePtr->m_expiryTimer.stop(); + } + instancePtr->m_users.insert(user); +} + +void SeasideCache::unregisterUser(QObject *user) +{ + instancePtr->m_users.remove(user); + + checkForExpiry(); +} + +void SeasideCache::registerNameGroupChangeListener(SeasideNameGroupChangeListener *listener) +{ + if (!instancePtr) + new SeasideCache; + instancePtr->m_nameGroupChangeListeners.append(listener); +} + +void SeasideCache::unregisterNameGroupChangeListener(SeasideNameGroupChangeListener *listener) +{ + if (!instancePtr) + return; + instancePtr->m_nameGroupChangeListeners.removeAll(listener); +} + +QChar SeasideCache::nameGroupForCacheItem(CacheItem *cacheItem) +{ + if (!cacheItem) + return QChar(); + + QChar group; + QString first; + QString last; + QContactName nameDetail = cacheItem->contact.detail(); + if (SeasideCache::displayLabelOrder() == FirstNameFirst) { + first = nameDetail.firstName(); + last = nameDetail.lastName(); + } else { + first = nameDetail.lastName(); + last = nameDetail.firstName(); + } + if (!first.isEmpty()) { + group = first[0].toUpper(); + } else if (!last.isEmpty()) { + group = last[0].toUpper(); + } else { + QString displayLabel = (cacheItem->itemData) + ? cacheItem->itemData->getDisplayLabel() + : generateDisplayLabel(cacheItem->contact); + if (!displayLabel.isEmpty()) + group = displayLabel[0].toUpper(); + } + + // XXX temporary workaround for non-latin names: use non-name details to try to find a + // latin character group + if (!group.isNull() && group.toLatin1() != group) { + QString displayLabel = generateDisplayLabelFromNonNameDetails(cacheItem->contact); + if (!displayLabel.isEmpty()) + group = displayLabel[0].toUpper(); + } + + if (group.isNull() || !allContactNameGroups.contains(group)) { + group = QLatin1Char('#'); // 'other' group + } + return group; +} + +QList SeasideCache::allNameGroups() +{ + return allContactNameGroups; +} + +QHash SeasideCache::nameGroupCounts() +{ + if (instancePtr) + return instancePtr->m_contactNameGroups; + return QHash(); +} + +SeasideCache::DisplayLabelOrder SeasideCache::displayLabelOrder() +{ + return instancePtr->m_displayLabelOrder; +} + +int SeasideCache::contactId(const QContact &contact) +{ + quint32 internal = internalId(contact); + return static_cast(internal); +} + +SeasideCache::CacheItem *SeasideCache::itemById(const ContactIdType &id) +{ + if (!validId(id)) + return 0; + + quint32 iid = internalId(id); + + CacheItem *item = 0; + + QHash::iterator it = instancePtr->m_people.find(iid); + if (it != instancePtr->m_people.end()) { + item = &(*it); + } else { + // Insert a new item into the cache if the one doesn't exist. + item = &(instancePtr->m_people[iid]); +#ifdef USING_QTPIM + item->contact.setId(id); +#else + QContactId contactId; + contactId.setLocalId(id); + item->contact.setId(contactId); +#endif + } + + if (item->contactState == ContactAbsent) { + item->contactState = ContactRequested; + instancePtr->m_changedContacts.append(item->apiId()); + instancePtr->fetchContacts(); + } + + return item; +} + +#ifdef USING_QTPIM +SeasideCache::CacheItem *SeasideCache::itemById(int id) +{ + if (id != 0) { + QContactId contactId(apiId(static_cast(id))); + if (!contactId.isNull()) { + return itemById(contactId); + } + } + + return 0; +} +#endif + +SeasideCache::CacheItem *SeasideCache::existingItem(const ContactIdType &id) +{ + quint32 iid = internalId(id); + + QHash::iterator it = instancePtr->m_people.find(iid); + return it != instancePtr->m_people.end() + ? &(*it) + : 0; +} + +QContact SeasideCache::contactById(const ContactIdType &id) +{ + quint32 iid = internalId(id); + return instancePtr->m_people.value(iid, CacheItem()).contact; +} + +SeasideCache::CacheItem *SeasideCache::itemByPhoneNumber(const QString &msisdn) +{ + QString normalizedNumber = Normalization::normalizePhoneNumber(msisdn); + QHash::const_iterator it = instancePtr->m_phoneNumberIds.find(normalizedNumber); + if (it != instancePtr->m_phoneNumberIds.end()) + return itemById(*it); + return 0; +} + +SeasideCache::CacheItem *SeasideCache::itemByEmailAddress(const QString &email) +{ + QHash::const_iterator it = instancePtr->m_emailAddressIds.find(email.toLower()); + if (it != instancePtr->m_emailAddressIds.end()) + return itemById(*it); + return 0; +} + +SeasideCache::ContactIdType SeasideCache::selfContactId() +{ + return instancePtr->m_manager.selfContactId(); +} + +void SeasideCache::requestUpdate() +{ + if (!m_updatesPending) + QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); + m_updatesPending = true; +} + +bool SeasideCache::saveContact(const QContact &contact) +{ + ContactIdType id = apiId(contact); + if (validId(id)) { + instancePtr->m_contactsToSave[id] = contact; + + instancePtr->updateContactData(id, FilterFavorites); + instancePtr->updateContactData(id, FilterOnline); + instancePtr->updateContactData(id, FilterAll); + } else { + instancePtr->m_contactsToCreate.append(contact); + } + + instancePtr->requestUpdate(); + + return true; +} + +void SeasideCache::updateContactData( + const ContactIdType &contactId, FilterType filter) +{ + int row = m_contacts[filter].indexOf(contactId); + + QList &models = m_models[filter]; + for (int i = 0; row != -1 && i < models.count(); ++i) + models.at(i)->sourceDataChanged(row, row); +} + +void SeasideCache::removeContact(const QContact &contact) +{ + ContactIdType id = apiId(contact); + + instancePtr->m_contactsToRemove.append(id); + instancePtr->removeContactData(id, FilterFavorites); + instancePtr->removeContactData(id, FilterOnline); + instancePtr->removeContactData(id, FilterAll); + + instancePtr->requestUpdate(); +} + +void SeasideCache::removeContactData( + const ContactIdType &contactId, FilterType filter) +{ + int row = m_contacts[filter].indexOf(contactId); + if (row == -1) + return; + + QList &models = m_models[filter]; + for (int i = 0; i < models.count(); ++i) + models.at(i)->sourceAboutToRemoveItems(row, row); + + m_contacts[filter].remove(row); + + for (int i = 0; i < models.count(); ++i) + models.at(i)->sourceItemsRemoved(); +} + +void SeasideCache::fetchConstituents(const QContact &contact) +{ + QContactId personId(contact.id()); + + if (!instancePtr->m_contactsToFetchConstituents.contains(personId)) { + instancePtr->m_contactsToFetchConstituents.append(personId); + instancePtr->requestUpdate(); + } +} + +void SeasideCache::fetchMergeCandidates(const QContact &contact) +{ + QContactId personId(contact.id()); + + if (!instancePtr->m_contactsToFetchCandidates.contains(personId)) { + instancePtr->m_contactsToFetchCandidates.append(personId); + instancePtr->requestUpdate(); + } +} + +const QVector *SeasideCache::contacts(FilterType type) +{ + return &instancePtr->m_contacts[type]; +} + +bool SeasideCache::isPopulated(FilterType filterType) +{ + return instancePtr->m_populated & (1 << filterType); +} + +// small helper to avoid inconvenience +QString SeasideCache::generateDisplayLabel(const QContact &contact, DisplayLabelOrder order) +{ + QContactName name = contact.detail(); + +#ifdef USING_QTPIM + QString customLabel = name.value(QContactName__FieldCustomLabel); +#else + QString customLabel = name.customLabel(); +#endif + if (!customLabel.isNull()) + return customLabel; + + QString displayLabel; + + QString nameStr1; + QString nameStr2; + if (order == LastNameFirst) { + nameStr1 = name.lastName(); + nameStr2 = name.firstName(); + } else { + nameStr1 = name.firstName(); + nameStr2 = name.lastName(); + } + + if (!nameStr1.isNull()) + displayLabel.append(nameStr1); + + if (!nameStr2.isNull()) { + if (!displayLabel.isEmpty()) + displayLabel.append(" "); + displayLabel.append(nameStr2); + } + + if (!displayLabel.isEmpty()) { + return displayLabel; + } + + displayLabel = generateDisplayLabelFromNonNameDetails(contact); + if (!displayLabel.isEmpty()) { + return displayLabel; + } + + return "(Unnamed)"; // TODO: localisation +} + +QString SeasideCache::generateDisplayLabelFromNonNameDetails(const QContact &contact) +{ + foreach (const QContactNickname& nickname, contact.details()) { + if (!nickname.nickname().isNull()) { + return nickname.nickname(); + } + } + + foreach (const QContactGlobalPresence& gp, contact.details()) { + // should only be one of these, but qtct is strange, and doesn't list it as a unique detail in the schema... + if (!gp.nickname().isNull()) { + return gp.nickname(); + } + } + + foreach (const QContactPresence& presence, contact.details()) { + if (!presence.nickname().isNull()) { + return presence.nickname(); + } + } + + foreach (const QContactOnlineAccount& account, contact.details()) { + if (!account.accountUri().isNull()) { + return account.accountUri(); + } + } + + foreach (const QContactEmailAddress& email, contact.details()) { + if (!email.emailAddress().isNull()) { + return email.emailAddress(); + } + } + + QContactOrganization company = contact.detail(); + if (!company.name().isNull()) + return company.name(); + + foreach (const QContactPhoneNumber& phone, contact.details()) { + if (!phone.number().isNull()) + return phone.number(); + } + + return QString(); +} + +static QContactFilter filterForMergeCandidates(const QContact &contact) +{ + // Find any contacts that we might merge with the supplied contact + QContactFilter rv; + + QContactName name(contact.detail()); + QString firstName(name.firstName()); + QString lastName(name.lastName()); + + if (firstName.isEmpty() && lastName.isEmpty()) { + // Use the displayLabel to match with + QString label(contact.detail().label()); + + // Partial match to first name + QContactDetailFilter firstNameFilter; + setDetailType(firstNameFilter, QContactName::FieldFirstName); + firstNameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); + firstNameFilter.setValue(label); + rv = rv | firstNameFilter; + + // Partial match to last name + QContactDetailFilter lastNameFilter; + setDetailType(lastNameFilter, QContactName::FieldLastName); + lastNameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); + lastNameFilter.setValue(label); + rv = rv | lastNameFilter; + + // Partial match to nickname + QContactDetailFilter nicknameFilter; + setDetailType(nicknameFilter, QContactNickname::FieldNickname); + nicknameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); + nicknameFilter.setValue(label); + rv = rv | nicknameFilter; + } else { + if (!firstName.isEmpty()) { + // Partial match to first name + QContactDetailFilter nameFilter; + setDetailType(nameFilter, QContactName::FieldFirstName); + nameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); + nameFilter.setValue(firstName); + rv = rv | nameFilter; + + // Partial match to first name in the nickname + QContactDetailFilter nicknameFilter; + setDetailType(nicknameFilter, QContactNickname::FieldNickname); + nicknameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); + nicknameFilter.setValue(firstName); + rv = rv | nicknameFilter; + } + if (!lastName.isEmpty()) { + // Partial match to last name + QContactDetailFilter nameFilter; + setDetailType(nameFilter, QContactName::FieldLastName); + nameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); + nameFilter.setValue(lastName); + rv = rv | nameFilter; + + // Partial match to last name in the nickname + QContactDetailFilter nicknameFilter; + setDetailType(nicknameFilter, QContactNickname::FieldNickname); + nicknameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); + nicknameFilter.setValue(lastName); + rv = rv | nicknameFilter; + } + } + + // Phone number match + foreach (const QContactPhoneNumber &number, contact.details()) { + rv = rv | QContactPhoneNumber::match(number.number()); + } + + // Email address match + foreach (const QContactEmailAddress &emailAddress, contact.details()) { + QString address(emailAddress.emailAddress()); + int index = address.indexOf(QChar::fromLatin1('@')); + if (index > 0) { + // Match any address that is the same up to the @ symbol + address = address.left(index); + } + + QContactDetailFilter filter; + setDetailType(filter, QContactEmailAddress::FieldEmailAddress); + filter.setMatchFlags((index > 0 ? QContactFilter::MatchStartsWith : QContactFilter::MatchExactly) | QContactFilter::MatchFixedString); + filter.setValue(address); + rv = rv | filter; + } + + // Account URI match + foreach (const QContactOnlineAccount &account, contact.details()) { + QString uri(account.accountUri()); + int index = uri.indexOf(QChar::fromLatin1('@')); + if (index > 0) { + // Match any account URI that is the same up to the @ symbol + uri = uri.left(index); + } + + QContactDetailFilter filter; + setDetailType(filter, QContactOnlineAccount::FieldAccountUri); + filter.setMatchFlags((index > 0 ? QContactFilter::MatchStartsWith : QContactFilter::MatchExactly) | QContactFilter::MatchFixedString); + filter.setValue(uri); + rv = rv | filter; + } + + // Only return aggregate contact IDs + QContactDetailFilter syncTarget; + setDetailType(syncTarget, QContactSyncTarget::FieldSyncTarget); + syncTarget.setValue(QString::fromLatin1("aggregate")); + rv = rv & syncTarget; + + return rv; +} + +bool SeasideCache::event(QEvent *event) +{ + if (event->type() != QEvent::UpdateRequest) { + return QObject::event(event); + } else if (!m_relationshipsToSave.isEmpty() || !m_relationshipsToRemove.isEmpty()) { + // this has to be before contact saves are processed so that the disaggregation flow + // works properly + if (!m_relationshipsToSave.isEmpty()) { + m_relationshipSaveRequest.setRelationships(m_relationshipsToSave); + m_relationshipSaveRequest.start(); + m_relationshipsToSave.clear(); + } + if (!m_relationshipsToRemove.isEmpty()) { + m_relationshipRemoveRequest.setRelationships(m_relationshipsToRemove); + m_relationshipRemoveRequest.start(); + m_relationshipsToRemove.clear(); + } + + } else if (!m_contactsToRemove.isEmpty()) { + m_removeRequest.setContactIds(m_contactsToRemove); + m_removeRequest.start(); + + m_contactsToRemove.clear(); + } else if (!m_contactsToCreate.isEmpty() || !m_contactsToSave.isEmpty()) { + m_contactsToCreate.reserve(m_contactsToCreate.count() + m_contactsToSave.count()); + + typedef QHash::iterator iterator; + for (iterator it = m_contactsToSave.begin(); it != m_contactsToSave.end(); ++it) { + m_contactsToCreate.append(*it); + } + + m_saveRequest.setContacts(m_contactsToCreate); + m_saveRequest.start(); + + m_contactsToCreate.clear(); + m_contactsToSave.clear(); + } else if (!m_constituentIds.isEmpty()) { + // Fetch the constituent information (even if they're already in the + // cache, because we don't update non-aggregates on change notifications) +#ifdef USING_QTPIM + m_fetchByIdRequest.setIds(m_constituentIds); +#else + m_fetchByIdRequest.setLocalIds(m_constituentIds); +#endif + m_fetchByIdRequest.start(); + } else if (!m_contactsToFetchConstituents.isEmpty()) { + QContactId aggregateId = m_contactsToFetchConstituents.first(); + + // Find the constituents of this contact +#ifdef USING_QTPIM + QContact first; + first.setId(aggregateId); + m_relationshipsFetchRequest.setFirst(first); + m_relationshipsFetchRequest.setRelationshipType(QContactRelationship::Aggregates()); +#else + m_relationshipsFetchRequest.setFirst(aggregateId); + m_relationshipsFetchRequest.setRelationshipType(QContactRelationship::Aggregates); +#endif + + m_relationshipsFetchRequest.start(); + } else if (!m_contactsToFetchCandidates.isEmpty()) { +#ifdef USING_QTPIM + ContactIdType contactId(m_contactsToFetchCandidates.first()); +#else + ContactIdType contactId(m_contactsToFetchCandidates.first().localId()); +#endif + const QContact contact(contactById(contactId)); + + // Find candidates to merge with this contact + m_contactIdRequest.setFilter(filterForMergeCandidates(contact)); + m_contactIdRequest.start(); + } else if (!m_changedContacts.isEmpty()) { + m_resultsRead = 0; + +#ifdef USING_QTPIM + QContactIdFilter filter; +#else + QContactLocalIdFilter filter; +#endif + filter.setIds(m_changedContacts); + m_changedContacts.clear(); + + // A local ID filter will fetch all contacts, rather than just aggregates; + // we only want to retrieve aggregate contacts that have changed + QContactDetailFilter stFilter; + setDetailType(stFilter, QContactSyncTarget::FieldSyncTarget); + stFilter.setValue("aggregate"); + + m_appendIndex = 0; + m_fetchRequest.setFilter(filter & stFilter); + m_fetchRequest.start(); + } else if (m_refreshRequired) { + m_resultsRead = 0; + m_refreshRequired = false; + m_fetchFilter = FilterFavorites; + + m_contactIdRequest.setFilter(QContactFavorite::match()); + m_contactIdRequest.start(); + } else { + m_updatesPending = false; + + const QHash expiredContacts = m_expiredContacts; + m_expiredContacts.clear(); + + typedef QHash::const_iterator iterator; + for (iterator it = expiredContacts.begin(); it != expiredContacts.end(); ++it) { + if (*it >= 0) + continue; + + quint32 iid = internalId(it.key()); + QHash::iterator cacheItem = m_people.find(iid); + if (cacheItem != m_people.end()) { + delete cacheItem->itemData; + delete cacheItem->modelData; + m_people.erase(cacheItem); + } + } + } + return true; +} + +void SeasideCache::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_fetchTimer.timerId()) { + fetchContacts(); + } + + if (event->timerId() == m_expiryTimer.timerId()) { + m_expiryTimer.stop(); + instancePtr = 0; + deleteLater(); + } +} + +void SeasideCache::contactsAdded(const QList &ids) +{ + if (m_keepPopulated) { + updateContacts(ids); + } +} + +void SeasideCache::contactsChanged(const QList &ids) +{ + if (m_keepPopulated) { + updateContacts(ids); + } else { + // Update these contacts if they're already in the cache + QList presentIds; + foreach (const ContactIdType &id, ids) { + if (existingItem(id)) { + presentIds.append(id); + } + } + } +} + +void SeasideCache::contactsRemoved(const QList &ids) +{ + if (m_keepPopulated) { + m_refreshRequired = true; + requestUpdate(); + } else { + // Remove these contacts if they're already in the cache + bool present = false; + foreach (const ContactIdType &id, ids) { + if (existingItem(id)) { + present = true; + m_expiredContacts[id] += -1; + } + } + if (present) { + requestUpdate(); + } + } +} + +void SeasideCache::updateContacts() +{ + QList contactIds; + + typedef QHash::iterator iterator; + for (iterator it = m_people.begin(); it != m_people.end(); ++it) { + if (it->contactState != ContactAbsent) + contactIds.append(it->apiId()); + } + + updateContacts(contactIds); +} + +void SeasideCache::fetchContacts() +{ + static const int WaitIntervalMs = 250; + + if (m_fetchRequest.isActive()) { + // The current fetch is still active - we may as well continue to accumulate + m_fetchTimer.start(WaitIntervalMs , this); + } else { + m_fetchTimer.stop(); + m_fetchPostponed.invalidate(); + + // Fetch any changed contacts immediately + if (m_contactsUpdated) { + m_contactsUpdated = false; + if (m_keepPopulated) { + // Refresh our contact sets in case sorting has changed + m_refreshRequired = true; + } + } + requestUpdate(); + } +} + +void SeasideCache::updateContacts(const QList &contactIds) +{ + // Wait for new changes to be reported + static const int PostponementIntervalMs = 500; + + // Maximum wait until we fetch all changes previously reported + static const int MaxPostponementMs = 5000; + + if (!contactIds.isEmpty()) { + m_contactsUpdated = true; + m_changedContacts.append(contactIds); + + if (m_fetchPostponed.isValid()) { + // We are waiting to accumulate further changes + int remainder = MaxPostponementMs - m_fetchPostponed.elapsed(); + if (remainder > 0) { + // We can postpone further + m_fetchTimer.start(std::min(remainder, PostponementIntervalMs), this); + } + } else { + // Wait for further changes before we query for the ones we have now + m_fetchPostponed.restart(); + m_fetchTimer.start(PostponementIntervalMs, this); + } + } +} + +void SeasideCache::contactsAvailable() +{ + QContactAbstractRequest *request = static_cast(sender()); + + QList contacts; + if (request == &m_fetchByIdRequest) { + contacts = m_fetchByIdRequest.contacts(); + } else { + contacts = m_fetchRequest.contacts(); + } + + if (m_fetchFilter == FilterFavorites + || m_fetchFilter == FilterOnline + || m_fetchFilter == FilterAll) { + // Part of an initial query. + appendContacts(contacts); + } else { + // An update. + QList modifiedGroups; + + for (int i = m_resultsRead; i < contacts.count(); ++i) { + QContact contact = contacts.at(i); + ContactIdType apiId = SeasideCache::apiId(contact); + quint32 iid = internalId(contact); + + CacheItem &item = m_people[iid]; + QContactName oldName = item.contact.detail(); + QContactName newName = contact.detail(); + QChar oldNameGroup; + + if (m_fetchFilter == FilterAll) + oldNameGroup = nameGroupForCacheItem(&item); + +#ifdef USING_QTPIM + if (newName.value(QContactName__FieldCustomLabel).isEmpty()) { +#else + if (newName.customLabel().isEmpty()) { +#endif +#ifdef USING_QTPIM + newName.setValue(QContactName__FieldCustomLabel, oldName.value(QContactName__FieldCustomLabel)); +#else + newName.setCustomLabel(oldName.customLabel()); +#endif + contact.saveDetail(&newName); + } + + const bool roleDataChanged = newName != oldName + || contact.detail().imageUrl() != item.contact.detail().imageUrl(); + + if (item.modelData) { + item.modelData->contactChanged(contact); + } + if (item.itemData) { + item.itemData->updateContact(contact, &item.contact); + } else { + item.contact = contact; + } + item.contactState = ContactFetched; + + QList phoneNumbers = contact.details(); + for (int j = 0; j < phoneNumbers.count(); ++j) { + QString normalizedNumber = Normalization::normalizePhoneNumber(phoneNumbers.at(j).number()); + m_phoneNumberIds[normalizedNumber] = iid; + } + + QList emailAddresses = contact.details(); + for (int j = 0; j < emailAddresses.count(); ++j) { + m_emailAddressIds[emailAddresses.at(j).emailAddress().toLower()] = iid; + } + + if (m_fetchFilter == FilterAll) { + // do this even if !roleDataChanged as name groups are affected by other display label changes + QChar newNameGroup = nameGroupForCacheItem(&item); + if (newNameGroup != oldNameGroup) { + addToContactNameGroup(newNameGroup, &modifiedGroups); + removeFromContactNameGroup(oldNameGroup, &modifiedGroups); + } + } + + if (roleDataChanged) { + instancePtr->updateContactData(apiId, FilterFavorites); + instancePtr->updateContactData(apiId, FilterOnline); + instancePtr->updateContactData(apiId, FilterAll); + } + } + m_resultsRead = contacts.count(); + notifyNameGroupsChanged(modifiedGroups); + } +} + +void SeasideCache::addToContactNameGroup(const QChar &group, QList *modifiedGroups) +{ + if (!group.isNull()) { + m_contactNameGroups[group] += 1; + if (modifiedGroups && !m_nameGroupChangeListeners.isEmpty()) + modifiedGroups->append(group); + } +} + +void SeasideCache::removeFromContactNameGroup(const QChar &group, QList *modifiedGroups) +{ + if (!group.isNull() && m_contactNameGroups.contains(group)) { + m_contactNameGroups[group] -= 1; + if (modifiedGroups && !m_nameGroupChangeListeners.isEmpty()) + modifiedGroups->append(group); + } +} + +void SeasideCache::notifyNameGroupsChanged(const QList &groups) +{ + if (groups.isEmpty() || m_nameGroupChangeListeners.isEmpty()) + return; + + QHash updates; + for (int i = 0; i < groups.count(); ++i) + updates[groups[i]] = m_contactNameGroups[groups[i]]; + + for (int i = 0; i < m_nameGroupChangeListeners.count(); ++i) + m_nameGroupChangeListeners[i]->nameGroupsUpdated(updates); +} + +void SeasideCache::contactIdsAvailable() +{ + if (!m_contactsToFetchCandidates.isEmpty()) { + m_candidateIds.append(m_contactIdRequest.ids()); + return; + } + + synchronizeList( + this, + m_contacts[m_fetchFilter], + m_cacheIndex, + m_contactIdRequest.ids(), + m_queryIndex); +} + +void SeasideCache::relationshipsAvailable() +{ +#ifdef USING_QTPIM + static const QString aggregatesRelationship = QContactRelationship::Aggregates(); +#else + static const QString aggregatesRelationship = QContactRelationship::Aggregates; +#endif + + foreach (const QContactRelationship &rel, m_relationshipsFetchRequest.relationships()) { + if (rel.relationshipType() == aggregatesRelationship) { +#ifdef USING_QTPIM + m_constituentIds.append(apiId(rel.second())); +#else + m_constituentIds.append(rel.second().localId()); +#endif + } + } +} + +void SeasideCache::finalizeUpdate(FilterType filter) +{ + const QList queryIds = m_contactIdRequest.ids(); + QVector &cacheIds = m_contacts[filter]; + + if (m_cacheIndex < cacheIds.count()) + removeRange(filter, m_cacheIndex, cacheIds.count() - m_cacheIndex); + + if (m_queryIndex < queryIds.count()) { + const int count = queryIds.count() - m_queryIndex; + if (count) + insertRange(filter, cacheIds.count(), count, queryIds, m_queryIndex); + } + + m_cacheIndex = 0; + m_queryIndex = 0; +} + +void SeasideCache::removeRange( + FilterType filter, int index, int count) +{ + QVector &cacheIds = m_contacts[filter]; + QList &models = m_models[filter]; + QList modifiedNameGroups; + + for (int i = 0; i < models.count(); ++i) + models[i]->sourceAboutToRemoveItems(index, index + count - 1); + + for (int i = 0; i < count; ++i) { + if (filter == FilterAll) { + m_expiredContacts[cacheIds.at(index)] -= 1; + + removeFromContactNameGroup(nameGroupForCacheItem(existingItem(cacheIds.at(index))), &modifiedNameGroups); + } + + cacheIds.remove(index); + } + + for (int i = 0; i < models.count(); ++i) + models[i]->sourceItemsRemoved(); + + notifyNameGroupsChanged(modifiedNameGroups); +} + +int SeasideCache::insertRange( + FilterType filter, + int index, + int count, + const QList &queryIds, + int queryIndex) +{ + QVector &cacheIds = m_contacts[filter]; + QList &models = m_models[filter]; + QList modifiedNameGroups; + + const ContactIdType selfId = m_manager.selfContactId(); + + int end = index + count - 1; + for (int i = 0; i < models.count(); ++i) + models[i]->sourceAboutToInsertItems(index, end); + + for (int i = 0; i < count; ++i) { + if (queryIds.at(queryIndex + i) == selfId) + continue; + + if (filter == FilterAll) { + m_expiredContacts[queryIds.at(queryIndex + i)] += 1; + + addToContactNameGroup(nameGroupForCacheItem(existingItem(queryIds.at(queryIndex + i))), &modifiedNameGroups); + } + + cacheIds.insert(index + i, queryIds.at(queryIndex + i)); + } + + for (int i = 0; i < models.count(); ++i) + models[i]->sourceItemsInserted(index, end); + + notifyNameGroupsChanged(modifiedNameGroups); + + return end - index + 1; +} + +void SeasideCache::appendContacts(const QList &contacts) +{ + if (!contacts.isEmpty()) { + QVector &cacheIds = m_contacts[m_fetchFilter]; + QList &models = m_models[m_fetchFilter]; + + cacheIds.reserve(contacts.count()); + + const int begin = cacheIds.count(); + int end = cacheIds.count() + contacts.count() - m_appendIndex - 1; + + if (begin <= end) { + for (int i = 0; i < models.count(); ++i) + models.at(i)->sourceAboutToInsertItems(begin, end); + + for (; m_appendIndex < contacts.count(); ++m_appendIndex) { + QContact contact = contacts.at(m_appendIndex); + ContactIdType apiId = SeasideCache::apiId(contact); + quint32 iid = internalId(contact); + + cacheIds.append(apiId); + CacheItem &cacheItem = m_people[iid]; + cacheItem.contact = contact; + cacheItem.contactState = ContactFetched; + + if (m_fetchFilter == FilterAll) + addToContactNameGroup(nameGroupForCacheItem(&cacheItem), 0); + + foreach (const QContactPhoneNumber &phoneNumber, contact.details()) { + QString normalizedNumber = Normalization::normalizePhoneNumber(phoneNumber.number()); + m_phoneNumberIds[normalizedNumber] = iid; + } + + foreach (const QContactEmailAddress &emailAddress, contact.details()) { + m_emailAddressIds[emailAddress.emailAddress().toLower()] = iid; + } + } + + for (int i = 0; i < models.count(); ++i) + models.at(i)->sourceItemsInserted(begin, end); + + if (!m_nameGroupChangeListeners.isEmpty()) + notifyNameGroupsChanged(m_contactNameGroups.keys()); + } + } +} + +void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) +{ + if (state != QContactAbstractRequest::FinishedState) + return; + + QContactAbstractRequest *request = static_cast(sender()); + + if (request == &m_relationshipsFetchRequest) { + if (!m_contactsToFetchConstituents.isEmpty() && m_constituentIds.isEmpty()) { + // We didn't find any constituents - report the empty list + QContactId aggregateId = m_contactsToFetchConstituents.takeFirst(); +#ifdef USING_QTPIM + CacheItem *cacheItem = itemById(aggregateId); +#else + CacheItem *cacheItem = itemById(aggregateId.localId()); +#endif + if (cacheItem->itemData) { + cacheItem->itemData->constituentsFetched(QList()); + } + + updateConstituentAggregations(cacheItem->apiId()); + } + } else if (request == &m_fetchByIdRequest) { + if (!m_contactsToFetchConstituents.isEmpty()) { + // Report these results + QContactId aggregateId = m_contactsToFetchConstituents.takeFirst(); +#ifdef USING_QTPIM + CacheItem *cacheItem = itemById(aggregateId); +#else + CacheItem *cacheItem = itemById(aggregateId.localId()); +#endif + + QList constituentIds; + foreach (const ContactIdType &id, m_constituentIds) { + constituentIds.append(internalId(id)); + } + m_constituentIds.clear(); + + if (cacheItem->itemData) { + cacheItem->itemData->constituentsFetched(constituentIds); + } + + updateConstituentAggregations(cacheItem->apiId()); + } + } else if (request == &m_contactIdRequest) { + if (!m_contactsToFetchCandidates.isEmpty()) { + // Report these results + QContactId contactId = m_contactsToFetchCandidates.takeFirst(); +#ifdef USING_QTPIM + CacheItem *cacheItem = itemById(contactId); +#else + CacheItem *cacheItem = itemById(contactId.localId()); +#endif + + const quint32 contactIid = internalId(contactId); + + QList candidateIds; + foreach (const ContactIdType &id, m_candidateIds) { + // Exclude the original source contact + const quint32 iid = internalId(id); + if (iid != contactIid) { + candidateIds.append(iid); + } + } + m_candidateIds.clear(); + + if (cacheItem->itemData) { + cacheItem->itemData->mergeCandidatesFetched(candidateIds); + } + } + } else if (request == &m_relationshipSaveRequest || request == &m_relationshipRemoveRequest) { + QSet contactIds; + foreach (const QContactRelationship &relationship, m_relationshipSaveRequest.relationships() + + m_relationshipRemoveRequest.relationships()) { +#ifdef USING_QTPIM + contactIds.insert(SeasideCache::apiId(relationship.first())); +#else + contactIds.insert(relationship.first().localId()); +#endif + } + + foreach (const ContactIdType &contactId, contactIds) { + CacheItem *cacheItem = itemById(contactId); + if (cacheItem && cacheItem->itemData) + cacheItem->itemData->aggregationOperationCompleted(); + } + } + + if (m_fetchFilter == FilterFavorites) { + // Next, query for all contacts + m_fetchFilter = FilterAll; + + if (!isPopulated(FilterFavorites)) { + qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; + m_appendIndex = 0; + m_fetchRequest.setFilter(QContactFilter()); + m_fetchRequest.start(); + makePopulated(FilterFavorites); + } else { + finalizeUpdate(FilterFavorites); + m_contactIdRequest.setFilter(QContactFilter()); + m_contactIdRequest.start(); + } + } else if (m_fetchFilter == FilterAll) { + // Next, query for online contacts + m_fetchFilter = FilterOnline; + + if (!isPopulated(FilterAll)) { + qDebug() << "All queried in" << m_timer.elapsed() << "ms"; + // Not correct, but better than nothing... + m_appendIndex = 0; + m_fetchRequest.setFilter(QContactGlobalPresence::match(QContactPresence::PresenceAvailable)); + m_fetchRequest.start(); + makePopulated(FilterNone); + makePopulated(FilterAll); + } else { + finalizeUpdate(FilterAll); + m_contactIdRequest.setFilter(QContactGlobalPresence::match(QContactPresence::PresenceAvailable)); + m_contactIdRequest.start(); + } + } else if (m_fetchFilter == FilterOnline) { + m_fetchFilter = FilterNone; + + if (m_updatesPending) { + QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); + } + + if (!isPopulated(FilterOnline)) { + qDebug() << "Online queried in" << m_timer.elapsed() << "ms"; + m_fetchRequest.setFetchHint(QContactFetchHint()); + makePopulated(FilterOnline); + } else { + finalizeUpdate(FilterOnline); + } + } else if (m_fetchFilter == FilterNone) { + // Result of a specific query + if (m_updatesPending) { + QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); + } + } +} + +void SeasideCache::makePopulated(FilterType filter) +{ + m_populated |= (1 << filter); + + QList &models = m_models[filter]; + for (int i = 0; i < models.count(); ++i) + models.at(i)->makePopulated(); +} + +void SeasideCache::setSortOrder(DisplayLabelOrder order) +{ + QContactSortOrder firstNameOrder; + setDetailType(firstNameOrder, QContactName::FieldFirstName); + firstNameOrder.setCaseSensitivity(Qt::CaseInsensitive); + firstNameOrder.setDirection(Qt::AscendingOrder); + firstNameOrder.setBlankPolicy(QContactSortOrder::BlanksFirst); + + QContactSortOrder lastNameOrder; + setDetailType(lastNameOrder, QContactName::FieldLastName); + lastNameOrder.setCaseSensitivity(Qt::CaseInsensitive); + lastNameOrder.setDirection(Qt::AscendingOrder); + lastNameOrder.setBlankPolicy(QContactSortOrder::BlanksFirst); + + QList sorting = (order == FirstNameFirst) + ? (QList() << firstNameOrder << lastNameOrder) + : (QList() << lastNameOrder << firstNameOrder); + + m_fetchRequest.setSorting(sorting); + m_contactIdRequest.setSorting(sorting); +} + +void SeasideCache::displayLabelOrderChanged() +{ +#ifdef HAS_MLITE + QVariant displayLabelOrder = m_displayLabelOrderConf.value(); + if (displayLabelOrder.isValid() && displayLabelOrder.toInt() != m_displayLabelOrder) { + m_displayLabelOrder = static_cast(displayLabelOrder.toInt()); + + setSortOrder(m_displayLabelOrder); + + typedef QHash::iterator iterator; + for (iterator it = m_people.begin(); it != m_people.end(); ++it) { + if (it->itemData) { + it->itemData->displayLabelOrderChanged(m_displayLabelOrder); + } else { + QContactName name = it->contact.detail(); +#ifdef USING_QTPIM + name.setValue(QContactName__FieldCustomLabel, generateDisplayLabel(it->contact)); +#else + name.setCustomLabel(generateDisplayLabel(it->contact)); +#endif + it->contact.saveDetail(&name); + } + } + + for (int i = 0; i < FilterTypesCount; ++i) { + for (int j = 0; j < m_models[i].count(); ++j) + m_models[i].at(j)->updateDisplayLabelOrder(); + } + + m_refreshRequired = true; + requestUpdate(); + } +#endif +} + +int SeasideCache::importContacts(const QString &path) +{ + QFile vcf(path); + if (!vcf.open(QIODevice::ReadOnly)) { + qWarning() << Q_FUNC_INFO << "Cannot open " << path; + return 0; + } + + // TODO: thread + QVersitReader reader(&vcf); + reader.startReading(); + reader.waitForFinished(); + + QVersitContactImporter importer; + importer.importDocuments(reader.results()); + + QList newContacts = importer.contacts(); + + instancePtr->m_contactsToCreate += newContacts; + instancePtr->requestUpdate(); + + return newContacts.count(); +} + +QString SeasideCache::exportContacts() +{ + QVersitContactExporter exporter; + + QList contacts; + contacts.reserve(instancePtr->m_people.count()); + + QList contactsToFetch; + contactsToFetch.reserve(instancePtr->m_people.count()); + + const quint32 selfId = internalId(instancePtr->m_manager.selfContactId()); + + typedef QHash::iterator iterator; + for (iterator it = instancePtr->m_people.begin(); it != instancePtr->m_people.end(); ++it) { + if (it.key() == selfId) { + continue; + } else if (it->contactState == ContactFetched) { + contacts.append(it->contact); + } else { + contactsToFetch.append(apiId(it.key())); + } + } + + if (!contactsToFetch.isEmpty()) { + QList fetchedContacts = instancePtr->m_manager.contacts(contactsToFetch); + contacts.append(fetchedContacts); + } + + if (!exporter.exportContacts(contacts)) { + qWarning() << Q_FUNC_INFO << "Failed to export contacts: " << exporter.errorMap(); + return QString(); + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + QString baseDir; + foreach (const QString &loc, QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation)) { + baseDir = loc; + break; + } +#else + const QString baseDir = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); +#endif + QFile vcard(baseDir + + QDir::separator() + + QDateTime::currentDateTime().toString("ss_mm_hh_dd_mm_yyyy") + + ".vcf"); + + if (!vcard.open(QIODevice::WriteOnly)) { + qWarning() << "Cannot open " << vcard.fileName(); + return QString(); + } + + QVersitWriter writer(&vcard); + if (!writer.startWriting(exporter.documents())) { + qWarning() << Q_FUNC_INFO << "Can't start writing vcards " << writer.error(); + return QString(); + } + + // TODO: thread + writer.waitForFinished(); + return vcard.fileName(); +} + +void SeasideCache::keepPopulated() +{ + if (!m_keepPopulated) { + m_keepPopulated = true; + + // Start a query to fully populate the cache + m_refreshRequired = true; + requestUpdate(); + } +} + +// Aggregates contact2 into contact1. Aggregate relationships will be created between the first +// contact and the constituents of the second contact. +void SeasideCache::aggregateContacts(const QContact &contact1, const QContact &contact2) +{ + instancePtr->m_contactPairsToLink.append(qMakePair( + ContactLinkRequest(apiId(contact1)), + ContactLinkRequest(apiId(contact2)))); + instancePtr->fetchConstituents(contact1); + instancePtr->fetchConstituents(contact2); +} + +// Disaggregates contact2 (a non-aggregate constituent) from contact1 (an aggregate). This removes +// the existing aggregate relationships between the two contacts. +void SeasideCache::disaggregateContacts(const QContact &contact1, const QContact &contact2) +{ + instancePtr->m_relationshipsToRemove.append(makeRelationship(aggregateRelationshipType, contact1, contact2)); + instancePtr->m_relationshipsToSave.append(makeRelationship(QLatin1String("IsNot"), contact1, contact2)); + + if (contact2.detail().syncTarget() == syncTargetWasLocal) { + // restore the local sync target that was changed in a previous link creation operation + QContact c = contact2; + QContactSyncTarget syncTarget = c.detail(); + syncTarget.setSyncTarget(syncTargetLocal); + c.saveDetail(&syncTarget); + saveContact(c); + } + + QCoreApplication::postEvent(instancePtr, new QEvent(QEvent::UpdateRequest)); +} + +void SeasideCache::updateConstituentAggregations(const ContactIdType &contactId) +{ + typedef QList >::iterator iterator; + for (iterator it = m_contactPairsToLink.begin(); it != m_contactPairsToLink.end(); ) { + QPair &pair = *it; + if (pair.first.contactId == contactId) + pair.first.constituentsFetched = true; + if (pair.second.contactId == contactId) + pair.second.constituentsFetched = true; + if (pair.first.constituentsFetched && pair.second.constituentsFetched) { + completeContactAggregation(pair.first.contactId, pair.second.contactId); + it = m_contactPairsToLink.erase(it); + } else { + ++it; + } + } +} + +// Called once constituents have been fetched for both persons. +void SeasideCache::completeContactAggregation(const ContactIdType &contact1Id, const ContactIdType &contact2Id) +{ + CacheItem *cacheItem1 = itemById(contact1Id); + CacheItem *cacheItem2 = itemById(contact2Id); + if (!cacheItem1 || !cacheItem2 || !cacheItem1->itemData || !cacheItem2->itemData) + return; + + // Contact1 needs to be linked to each of person2's constituents. However, a local constituent + // cannot be linked to two aggregate contacts. So, if both contacts have local constituents, + // change contact2's local constitent's syncTarget to "was_local" and don't aggregate it with + // contact1. + const QList &constituents1 = cacheItem1->itemData->constituents(); + const QList &constituents2 = cacheItem2->itemData->constituents(); + QContact contact2Local; + bool bothHaveLocals = false; + foreach (int id, constituents1) { + QContact c = contactById(apiId(id)); + if (c.detail().syncTarget() == syncTargetLocal) { + foreach (int id, constituents2) { + QContact c = contactById(apiId(id)); + if (c.detail().syncTarget() == syncTargetLocal) { + contact2Local = c; + bothHaveLocals = true; + break; + } + } + break; + } + } + if (bothHaveLocals) { + QContactSyncTarget syncTarget = contact2Local.detail(); + syncTarget.setSyncTarget(syncTargetWasLocal); + contact2Local.saveDetail(&syncTarget); + saveContact(contact2Local); + } + + // For each constituent of contact2, add a relationship between it and contact1, and remove the + // relationship between it and contact2. + foreach (int id, constituents2) { + QContact c = contactById(apiId(id)); + m_relationshipsToSave.append(makeRelationship(aggregateRelationshipType, contactById(contact1Id), c)); + m_relationshipsToRemove.append(makeRelationship(aggregateRelationshipType, contactById(contact2Id), c)); + } + + if (!m_relationshipsToSave.isEmpty() || !m_relationshipsToRemove.isEmpty()) + requestUpdate(); +} + +QContactRelationship SeasideCache::makeRelationship(const QString &type, const QContact &contact1, const QContact &contact2) +{ + QContactRelationship relationship; + relationship.setRelationshipType(type); +#ifdef USING_QTPIM + relationship.setFirst(contact1); + relationship.setSecond(contact2); +#else + relationship.setFirst(contact1.id()); + relationship.setSecond(contact2.id()); +#endif + return relationship; +} + +// Instantiate the contact ID functions for qtcontacts-sqlite +#include diff --git a/src/seasidecache.h b/src/seasidecache.h new file mode 100644 index 0000000..ea4c7e5 --- /dev/null +++ b/src/seasidecache.h @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2013 Jolla Mobile + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#ifndef SEASIDECACHE_H +#define SEASIDECACHE_H + +#include "contactcacheexport.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef USING_QTPIM +#include +#include +#else +#include +#include +#endif + +#include +#include +#include + +#include +#include + +#ifdef HAS_MLITE +#include +#endif + +USE_CONTACTS_NAMESPACE + +class CONTACTCACHE_EXPORT SeasideNameGroupChangeListener +{ +public: + SeasideNameGroupChangeListener() {} + ~SeasideNameGroupChangeListener() {} + + virtual void nameGroupsUpdated(const QHash &groups) = 0; +}; + +class CONTACTCACHE_EXPORT SeasideCache : public QObject +{ + Q_OBJECT +public: + typedef QtContactsSqliteExtensions::ApiContactIdType ContactIdType; + + enum FilterType { + FilterNone, + FilterAll, + FilterFavorites, + FilterOnline, + FilterTypesCount + }; + + enum DisplayLabelOrder { + FirstNameFirst, + LastNameFirst + }; + + enum ContactState { + ContactAbsent, + ContactRequested, + ContactFetched + }; + + struct ItemData + { + virtual ~ItemData() {} + + virtual QString getDisplayLabel() const = 0; + virtual void displayLabelOrderChanged(DisplayLabelOrder order) = 0; + + virtual void updateContact(const QContact &newContact, QContact *oldContact) = 0; + + virtual void constituentsFetched(const QList &ids) = 0; + virtual void mergeCandidatesFetched(const QList &ids) = 0; + virtual void aggregationOperationCompleted() = 0; + + virtual QList constituents() const = 0; + }; + + struct ModelData + { + virtual ~ModelData() {} + + virtual void contactChanged(const QContact &newContact) = 0; + }; + + struct CacheItem + { + CacheItem() : itemData(0), modelData(0), contactState(ContactAbsent) {} + CacheItem(const QContact &contact) : contact(contact), itemData(0), modelData(0), contactState(ContactAbsent) {} + + ContactIdType apiId() const { return SeasideCache::apiId(contact); } + + QContact contact; + ItemData *itemData; + ModelData *modelData; + ContactState contactState; + }; + + struct ContactLinkRequest + { + ContactLinkRequest(const SeasideCache::ContactIdType &id) : contactId(id), constituentsFetched(false) {} + ContactLinkRequest(const ContactLinkRequest &req) : contactId(req.contactId), constituentsFetched(req.constituentsFetched) {} + + SeasideCache::ContactIdType contactId; + bool constituentsFetched; + }; + + class ListModel : public QAbstractListModel + { + public: + ListModel(QObject *parent = 0) : QAbstractListModel(parent) {} + virtual ~ListModel() {} + + virtual void sourceAboutToRemoveItems(int begin, int end) = 0; + virtual void sourceItemsRemoved() = 0; + + virtual void sourceAboutToInsertItems(int begin, int end) = 0; + virtual void sourceItemsInserted(int begin, int end) = 0; + + virtual void sourceDataChanged(int begin, int end) = 0; + + virtual void makePopulated() = 0; + virtual void updateDisplayLabelOrder() = 0; + }; + + static SeasideCache *instance(); + + static ContactIdType apiId(const QContact &contact); + static ContactIdType apiId(quint32 iid); + + static bool validId(const ContactIdType &id); + + static quint32 internalId(const QContact &contact); + static quint32 internalId(const QContactId &id); +#ifndef USING_QTPIM + static quint32 internalId(QContactLocalId id); +#endif + + static void registerModel(ListModel *model, FilterType type); + static void unregisterModel(ListModel *model); + + static void registerUser(QObject *user); + static void unregisterUser(QObject *user); + + static void registerNameGroupChangeListener(SeasideNameGroupChangeListener *listener); + static void unregisterNameGroupChangeListener(SeasideNameGroupChangeListener *listener); + + static DisplayLabelOrder displayLabelOrder(); + + static int contactId(const QContact &contact); + + static CacheItem *existingItem(const ContactIdType &id); + static CacheItem *itemById(const ContactIdType &id); +#ifdef USING_QTPIM + static CacheItem *itemById(int id); +#endif + static ContactIdType selfContactId(); + static QContact contactById(const ContactIdType &id); + static QChar nameGroupForCacheItem(CacheItem *cacheItem); + static QList allNameGroups(); + static QHash nameGroupCounts(); + + static CacheItem *itemByPhoneNumber(const QString &msisdn); + static CacheItem *itemByEmailAddress(const QString &email); + static bool saveContact(const QContact &contact); + static void removeContact(const QContact &contact); + + static void aggregateContacts(const QContact &contact1, const QContact &contact2); + static void disaggregateContacts(const QContact &contact1, const QContact &contact2); + + static void fetchConstituents(const QContact &contact); + static void fetchMergeCandidates(const QContact &contact); + + static int importContacts(const QString &path); + static QString exportContacts(); + + static const QVector *contacts(FilterType filterType); + static bool isPopulated(FilterType filterType); + + static QString generateDisplayLabel(const QContact &contact, DisplayLabelOrder order = FirstNameFirst); + static QString generateDisplayLabelFromNonNameDetails(const QContact &contact); + + bool event(QEvent *event); + + // For synchronizeLists() + int insertRange(int index, int count, const QList &source, int sourceIndex) { + return insertRange(m_fetchFilter, index, count, source, sourceIndex); } + int removeRange(int index, int count) { removeRange(m_fetchFilter, index, count); return 0; } + +protected: + void timerEvent(QTimerEvent *event); + void setSortOrder(DisplayLabelOrder order); + +private slots: + void contactsAvailable(); + void contactIdsAvailable(); + void relationshipsAvailable(); + void requestStateChanged(QContactAbstractRequest::State state); + void updateContacts(); +#ifdef USING_QTPIM + void contactsAdded(const QList &contactIds); + void contactsChanged(const QList &contactIds); + void contactsRemoved(const QList &contactIds); +#else + void contactsAdded(const QList &contactIds); + void contactsChanged(const QList &contactIds); + void contactsRemoved(const QList &contactIds); +#endif + void displayLabelOrderChanged(); + +private: + SeasideCache(); + ~SeasideCache(); + + static void checkForExpiry(); + + void keepPopulated(); + + void requestUpdate(); + void appendContacts(const QList &contacts); + void fetchContacts(); + void updateContacts(const QList &contactIds); + + void finalizeUpdate(FilterType filter); + void removeRange(FilterType filter, int index, int count); + int insertRange( + FilterType filter, + int index, + int count, + const QList &queryIds, + int queryIndex); + + void updateContactData(const ContactIdType &contactId, FilterType filter); + void removeContactData(const ContactIdType &contactId, FilterType filter); + void makePopulated(FilterType filter); + + void addToContactNameGroup(const QChar &group, QList *modifiedGroups = 0); + void removeFromContactNameGroup(const QChar &group, QList *modifiedGroups = 0); + void notifyNameGroupsChanged(const QList &groups); + + void updateConstituentAggregations(const ContactIdType &contactId); + void completeContactAggregation(const ContactIdType &contact1Id, const ContactIdType &contact2Id); + + static QContactRelationship makeRelationship(const QString &type, const QContact &contact1, const QContact &contact2); + + QBasicTimer m_expiryTimer; + QBasicTimer m_fetchTimer; + QHash m_people; + QHash m_phoneNumberIds; + QHash m_emailAddressIds; + QHash m_contactsToSave; + QHash m_contactNameGroups; + QList m_contactsToCreate; + QList m_contactsToRemove; + QList m_changedContacts; + QList m_contactsToFetchConstituents; + QList m_contactsToFetchCandidates; + QList > m_contactPairsToLink; + QList m_relationshipsToSave; + QList m_relationshipsToRemove; + QList m_nameGroupChangeListeners; + QVector m_contacts[FilterTypesCount]; + QList m_models[FilterTypesCount]; + QSet m_users; + QHash m_expiredContacts; + QContactManager m_manager; + QContactFetchRequest m_fetchRequest; + QContactFetchByIdRequest m_fetchByIdRequest; +#ifdef USING_QTPIM + QContactIdFetchRequest m_contactIdRequest; +#else + QContactLocalIdFetchRequest m_contactIdRequest; +#endif + QContactRelationshipFetchRequest m_relationshipsFetchRequest; + QContactRemoveRequest m_removeRequest; + QContactSaveRequest m_saveRequest; + QContactRelationshipSaveRequest m_relationshipSaveRequest; + QContactRelationshipRemoveRequest m_relationshipRemoveRequest; +#ifdef HAS_MLITE + MGConfItem m_displayLabelOrderConf; +#endif + int m_resultsRead; + int m_populated; + int m_cacheIndex; + int m_queryIndex; + int m_appendIndex; + FilterType m_fetchFilter; + DisplayLabelOrder m_displayLabelOrder; + bool m_keepPopulated; + bool m_updatesPending; + bool m_fetchActive; + bool m_refreshRequired; + bool m_contactsUpdated; + QList m_constituentIds; + QList m_candidateIds; + + QElapsedTimer m_timer; + QElapsedTimer m_fetchPostponed; + + static SeasideCache *instancePtr; + static QList allContactNameGroups; +}; + +#endif diff --git a/src/src.pro b/src/src.pro new file mode 100644 index 0000000..9c850c3 --- /dev/null +++ b/src/src.pro @@ -0,0 +1,51 @@ +include(../config.pri) + +TEMPLATE = lib +CONFIG += qt hide_symbols +CONFIG += create_pc create_prl no_install_prl + +# 'contacts' is too generic for the target name - use 'contactcache' +TARGET = $${PACKAGENAME} +target.path = $$PREFIX/lib +INSTALLS += target + +QMAKE_PKGCONFIG_INCDIR = $$PREFIX/include/$${PACKAGENAME} +QMAKE_PKGCONFIG_LIBDIR = $$PREFIX/lib +QMAKE_PKGCONFIG_DESTDIR = pkgconfig + +CONFIG += link_pkgconfig +equals(QT_MAJOR_VERSION, 4) { + packagesExist(mlite) { + PKGCONFIG += mlite + DEFINES += HAS_MLITE + } else { + warning("mlite not available. Some functionality may not work as expected.") + } +} +equals(QT_MAJOR_VERSION, 5) { + packagesExist(mlite5) { + PKGCONFIG += mlite5 + DEFINES += HAS_MLITE + } else { + warning("mlite not available. Some functionality may not work as expected.") + } +} + +DEFINES += CONTACTCACHE_BUILD + +SOURCES += \ + $$PWD/normalization.cpp \ + $$PWD/seasidecache.cpp + +HEADERS += \ + $$PWD/normalization_p.h \ + $$PWD/contactcacheexport.h \ + $$PWD/seasidecache.h \ + $$PWD/synchronizelists.h + +headers.files = \ + $$PWD/contactcacheexport.h \ + $$PWD/seasidecache.h \ + $$PWD/synchronizelists.h +headers.path = $$PREFIX/include/$$TARGET +INSTALLS += headers diff --git a/src/synchronizelists.h b/src/synchronizelists.h new file mode 100644 index 0000000..abef92e --- /dev/null +++ b/src/synchronizelists.h @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2013 Jolla Mobile + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#ifndef SYNCHRONIZELISTS_P_H +#define SYNCHRONIZELISTS_P_H + +// Helper utility to synchronize a cached list with some reference list with correct +// QAbstractItemModel signals and filtering. + +// If the reference list is populated incrementally this can be called multiple times with the +// same variables c and r to progressively synchronize the lists. If after the final call either +// the c or r index is not equal to the length of the cache or reference lists respectively then +// the remaining items can be synchronized manually by removing the remaining items from the +// cache list before (filtering and) appending the remaining reference items. + +template +class SynchronizeList +{ + typedef typename ValueList::value_type ValueType; + +public: + SynchronizeList( + Agent *agent, + const ValueList &cache, + int &c, + const ReferenceList &reference, + int &r) + : agent(agent), cache(cache), c(c), reference(reference), r(r) + { + while (c < cache.count() && r < reference.count()) { + if (cache.at(c) == reference.at(r)) { + ++c; + ++r; + continue; + } + + bool match = false; + + // Iterate through both the reference and cache lists in parallel looking for first + // point of commonality, when that is found resolve the differences and continue + // looking. + int count = 1; + for (; !match && c + count < cache.count() && r + count < reference.count(); ++count) { + const ValueType cacheValue = cache.at(c + count); + const ValueType referenceValue = reference.at(r + count); + + for (int i = 0; i <= count; ++i) { + if (cacheMatch(i, count, referenceValue) || referenceMatch(i, count, cacheValue)) { + match = true; + break; + } + } + } + + // Continue scanning the reference list if the cache has been exhausted. + for (int re = r + count; !match && re < reference.count(); ++re) { + const ValueType referenceValue = reference.at(re); + for (int i = 0; i < count; ++i) { + if (cacheMatch(i, re - r, referenceValue)) { + match = true; + break; + } + } + } + + // Continue scanning the cache if the reference list has been exhausted. + for (int ce = c + count; !match && ce < cache.count(); ++ce) { + const ValueType cacheValue = cache.at(ce); + for (int i = 0; i < count; ++i) { + if (referenceMatch(i, ce - c, cacheValue)) { + match = true; + break; + } + } + } + + if (!match) + return; + } + } + +private: + // Tests if the cached value at i matches a referenceValue. + // If there is a match removes all items traversed in the cache since the previous match + // and inserts any items in the reference set found to to not be in the cache. + bool cacheMatch(int i, int count, ValueType referenceValue) + { + if (cache.at(c + i) == referenceValue) { + if (i > 0) + c += agent->removeRange(c, i); + c += agent->insertRange(c, count, reference, r) + 1; + r += count + 1; + return true; + } else { + return false; + } + } + + // Tests if the reference value at i matches a cacheValue. + // If there is a match inserts all items traversed in the reference set since the + // previous match and removes any items from the cache that were not found in the + // reference list. + bool referenceMatch(int i, int count, ValueType cacheValue) + { + if (reference.at(r + i) == cacheValue) { + c += agent->removeRange(c, count); + if (i > 0) + c += agent->insertRange(c, i, reference, r); + c += 1; + r += i + 1; + return true; + } else { + return false; + } + } + + Agent * const agent; + const ValueList &cache; + int &c; + const ReferenceList &reference; + int &r; +}; + +template +void synchronizeList( + Agent *agent, + const ValueList &cache, + int &c, + const ReferenceList &reference, + int &r) +{ + SynchronizeList(agent, cache, c, reference, r); +} + +template +class SynchronizeFilteredList +{ + typedef typename ValueList::value_type ValueType; + +public: + SynchronizeFilteredList( + Agent *agent, + const ValueList &cache, + int &c, + const ReferenceList &reference, + int &r) + : cache(cache) + , agent(agent) + , previousIndex(0) + , removeCount(0) + { + synchronizeList(this, cache, c, reference, r); + + if (filteredValues.count() > 0) { + c += filteredValues.count(); + agent->insertRange(previousIndex, filteredValues.count(), filteredValues, 0); + } else if (removeCount > 0) { + c -= removeCount; + agent->removeRange(previousIndex, removeCount); + } + + for (; previousIndex < c; ++previousIndex) { + int filterCount = 0; + for (int i; (i = previousIndex + filterCount) < c; ++filterCount) { + if (agent->filterValue(cache.at(i))) + break; + } + if (filterCount > 0) { + agent->removeRange(previousIndex, filterCount); + c -= filterCount; + } + } + } + + int insertRange(int index, int count, const ValueList &source, int sourceIndex) + { + int adjustedIndex = index; + + if (removeCount > 0) { + adjustedIndex -= removeCount; + agent->removeRange(previousIndex, removeCount); + removeCount = 0; + } else if (filteredValues.count() > 0 && index > previousIndex) { + agent->insertRange(previousIndex, filteredValues.count(), filteredValues, 0); + adjustedIndex += filteredValues.count(); + previousIndex += filteredValues.count(); + filteredValues.resize(0); + } + + if (filteredValues.isEmpty()) { + for (; previousIndex < adjustedIndex;) { + int filterCount = 0; + for (int i; (i = previousIndex + filterCount) < adjustedIndex; ++filterCount) { + if (agent->filterValue(cache.at(i))) + break; + } + if (filterCount > 0) { + agent->removeRange(previousIndex, filterCount); + adjustedIndex -= filterCount; + } else { + ++previousIndex; + } + } + } + + for (int i = 0; i < count; ++i) { + const ValueType sourceValue = source.at(sourceIndex + i); + if (agent->filterValue(sourceValue)) + filteredValues.append(sourceValue); + } + + return adjustedIndex - index; + } + + int removeRange(int index, int count) + { + int adjustedIndex = index; + if (filteredValues.count() > 0) { + adjustedIndex += filteredValues.count(); + agent->insertRange(previousIndex, filteredValues.count(), filteredValues, 0); + filteredValues.resize(0); + } else if (removeCount > 0 && adjustedIndex > previousIndex + removeCount) { + adjustedIndex -= removeCount; + agent->removeRange(previousIndex, removeCount); + removeCount = 0; + } + + if (removeCount == 0) { + for (; previousIndex < adjustedIndex;) { + int filterCount = 0; + for (int i; (i = previousIndex + filterCount) < adjustedIndex; ++filterCount) { + if (agent->filterValue(cache.at(i))) + break; + } + if (previousIndex + filterCount == adjustedIndex) { + removeCount += filterCount; + break; + } else if (filterCount > 0) { + agent->removeRange(previousIndex, filterCount); + adjustedIndex -= filterCount; + } else { + ++previousIndex; + } + } + } + + removeCount += count; + + return adjustedIndex - index + count; + } + + ValueList filteredValues; + const ValueList &cache; + Agent *agent; + int previousIndex; + int removeCount; +}; + +template +void synchronizeFilteredList( + Agent *agent, + const ValueList &cache, + int &c, + const ReferenceList &reference, + int &r) +{ + SynchronizeFilteredList(agent, cache, c, reference, r); +} + +#endif diff --git a/tests/common.pri b/tests/common.pri new file mode 100644 index 0000000..2a4e92f --- /dev/null +++ b/tests/common.pri @@ -0,0 +1,13 @@ +include(../package.pri) + +SRCDIR = ../../src/ +INCLUDEPATH += $$SRCDIR +DEPENDPATH = $$INCLUDEPATH + +QT -= gui +QT += testlib +TEMPLATE = app +CONFIG -= app_bundle + +target.path = /opt/tests/$${PACKAGENAME} +INSTALLS += target diff --git a/tests/tests.pro b/tests/tests.pro new file mode 100644 index 0000000..8680be2 --- /dev/null +++ b/tests/tests.pro @@ -0,0 +1,20 @@ +include(../package.pri) + +TEMPLATE = subdirs +SUBDIRS = tst_synchronizelists + +tests_xml.target = tests.xml +tests_xml.depends = $$PWD/tests.xml.in +tests_xml.commands = sed -e "s:@PACKAGENAME@:$${PACKAGENAME}:g" $< > $@ +tests_xml.CONFIG += no_check_exist + +QMAKE_EXTRA_TARGETS = tests_xml +QMAKE_CLEAN += $$tests_xml.target +PRE_TARGETDEPS += $$tests_xml.target + +tests_install.path = /opt/tests/$${PACKAGENAME} +tests_install.files = $$tests_xml.target +tests_install.depends = tests_xml +tests_install.CONFIG += no_check_exist + +INSTALLS += tests_install diff --git a/tests/tests.xml.in b/tests/tests.xml.in new file mode 100644 index 0000000..a5faa91 --- /dev/null +++ b/tests/tests.xml.in @@ -0,0 +1,12 @@ + + + + Contact cache automatic tests + + Contact cache automatic tests + + /opt/tests/@PACKAGENAME@/tst_synchronizelists + + + + diff --git a/tests/tst_synchronizelists/tst_synchronizelists.cpp b/tests/tst_synchronizelists/tst_synchronizelists.cpp new file mode 100644 index 0000000..4ab5a67 --- /dev/null +++ b/tests/tst_synchronizelists/tst_synchronizelists.cpp @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2013 Jolla Mobile + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#include +#include + +#include "synchronizelists.h" + + +class tst_SynchronizeLists : public QObject +{ + Q_OBJECT + +public: + tst_SynchronizeLists(); + + bool m_filterEnabled; + QVector m_filter; + QVector m_cache; + + bool filterValue(quint32 contactId) const; + int insertRange(int index, int count, const QVector &source, int sourceIndex); + int removeRange(int index, int count); + +private slots: + void filtered_data(); + void filtered(); + void unfiltered_data(); + void unfiltered(); +}; + +typedef QVector List; + +Q_DECLARE_METATYPE(List) + + +tst_SynchronizeLists::tst_SynchronizeLists() +{ + qRegisterMetaType(); +} + +bool tst_SynchronizeLists::filterValue(quint32 contactId) const +{ + return !m_filterEnabled || m_filter.contains(contactId); +} + +int tst_SynchronizeLists::insertRange( + int index, int count, const QVector &source, int sourceIndex) +{ + for (int i = 0; i < count; ++i) + m_cache.insert(index + i, source.at(sourceIndex + i)); + + return count; +} + +int tst_SynchronizeLists::removeRange(int index, int count) +{ + m_cache.remove(index, count); + + return 0; +} + +void tst_SynchronizeLists::filtered_data() +{ + QTest::addColumn >("reference"); + QTest::addColumn >("original"); + QTest::addColumn >("expected"); + + { + const List reference = List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10; + const List original = List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10; + QTest::newRow("a0") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10); + QTest::newRow("a1") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5); + QTest::newRow("a2") + << reference + << original + << (List() << 6 << 7 << 8 << 9 << 10); + QTest::newRow("a3") + << reference + << original + << (List() << 1 << 3 << 5 << 7 << 9); + QTest::newRow("a4") + << reference + << original + << (List() << 0 << 2 << 4 << 6 << 8 << 10); + QTest::newRow("a5") + << reference + << original + << (List() << 0 << 1 << 3 << 8 << 9 << 10); + QTest::newRow("a6") + << reference + << original + << (List() << 4 << 5 << 6 << 10); + } { + const List reference = List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10; + const List original = List(); + + QTest::newRow("b0") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10); + QTest::newRow("b1") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5); + QTest::newRow("b2") + << reference + << original + << (List() << 6 << 7 << 8 << 9 << 10); + QTest::newRow("b3") + << reference + << original + << (List() << 1 << 3 << 5 << 7 << 9); + QTest::newRow("b4") + << reference + << original + << (List() << 0 << 2 << 4 << 6 << 8 << 10); + QTest::newRow("b5") + << reference + << original + << (List() << 0 << 1 << 3 << 8 << 9 << 10); + QTest::newRow("b6") + << reference + << original + << (List() << 4 << 5 << 6 << 10); + } { + const List reference = List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10; + const List original = List() << 10 << 9 << 8 << 7 << 6 << 5 << 4 << 3 << 2 << 1 << 0; + + QTest::newRow("c0") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10); + QTest::newRow("c1") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5); + QTest::newRow("c2") + << reference + << original + << (List() << 6 << 7 << 8 << 9 << 10); + QTest::newRow("c3") + << reference + << original + << (List() << 1 << 3 << 5 << 7 << 9); + QTest::newRow("c4") + << reference + << original + << (List() << 0 << 2 << 4 << 6 << 8 << 10); + QTest::newRow("c5") + << reference + << original + << (List() << 0 << 1 << 3 << 8 << 9 << 10); + QTest::newRow("c6") + << reference + << original + << (List() << 4 << 5 << 6 << 10); + } { + const List reference = List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10; + const List original = List() << 2 << 3 << 4 << 7 << 8; + + QTest::newRow("d0") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10); + QTest::newRow("d1") + << reference + << original + << (List() << 0 << 1 << 2 << 3 << 4 << 5); + QTest::newRow("d2") + << reference + << original + << (List() << 6 << 7 << 8 << 9 << 10); + QTest::newRow("d3") + << reference + << original + << (List() << 1 << 3 << 5 << 7 << 9); + QTest::newRow("d4") + << reference + << original + << (List() << 0 << 2 << 4 << 6 << 8 << 10); + QTest::newRow("d5") + << reference + << original + << (List() << 0 << 1 << 3 << 8 << 9 << 10); + QTest::newRow("d6") + << reference + << original + << (List() << 4 << 5 << 6 << 10); + } +} + +void tst_SynchronizeLists::filtered() +{ + QFETCH(QVector, reference); + QFETCH(QVector, original); + QFETCH(QVector, expected); + + m_filterEnabled = true; + m_cache = original; + m_filter = expected; + + int c = 0; + int r = 0; + + synchronizeFilteredList(this, m_cache, c, reference, r); + + if (c < m_cache.count()) + m_cache.remove(c, m_cache.count() - c); + for (; r < reference.count(); ++r) { + if (m_filter.contains(reference.at(r))) + m_cache.append(reference.at(r)); + } + + if (m_cache != expected) { + qDebug() << "expected" << expected; + qDebug() << "actual " << m_cache; + } + + QCOMPARE(m_cache, expected); +} + +void tst_SynchronizeLists::unfiltered_data() +{ + QTest::addColumn >("reference"); + QTest::addColumn >("original"); + + QTest::newRow("0") + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10) + << (List()); + QTest::newRow("1") + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10) + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10); + QTest::newRow("2") + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10) + << (List() << 0 << 1 << 2 << 3 << 4 << 5); + QTest::newRow("3") + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10) + << (List() << 6 << 7 << 8 << 9 << 10); + QTest::newRow("4") + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10) + << (List() << 10 << 9 << 8 << 7 << 6 << 5 << 4 << 3 << 2 << 1 << 0); + QTest::newRow("5") + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10) + << (List() << 5 << 4 << 3 << 2 << 1 << 0); + QTest::newRow("6") + << (List() << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10) + << (List() << 10 << 9 << 8 << 7 << 6); +} + +void tst_SynchronizeLists::unfiltered() +{ + QFETCH(QVector, reference); + QFETCH(QVector, original); + + m_filterEnabled = false; + m_cache = original; + + int c = 0; + int r = 0; + + synchronizeList(this, m_cache, c, reference, r); + + if (c < m_cache.count()) + m_cache.remove(c, m_cache.count() - c); + for (; r < reference.count(); ++r) + m_cache.append(reference.at(r)); + + if (m_cache != reference) { + qDebug() << "expected" << reference; + qDebug() << "actual " << m_cache; + } + + QCOMPARE(m_cache, reference); +} + +#include "tst_synchronizelists.moc" +QTEST_APPLESS_MAIN(tst_SynchronizeLists) diff --git a/tests/tst_synchronizelists/tst_synchronizelists.pro b/tests/tst_synchronizelists/tst_synchronizelists.pro new file mode 100644 index 0000000..7c8f44e --- /dev/null +++ b/tests/tst_synchronizelists/tst_synchronizelists.pro @@ -0,0 +1,4 @@ +include(../common.pri) +TARGET = tst_synchronizelists + +SOURCES += tst_synchronizelists.cpp