diff --git a/src/seasidecache.cpp b/src/seasidecache.cpp index 737ac5d..fc59590 100644 --- a/src/seasidecache.cpp +++ b/src/seasidecache.cpp @@ -490,6 +490,7 @@ SeasideCache::SeasideCache() , m_fetchByIdProcessedCount(0) , m_keepPopulated(false) , m_populateProgress(Unpopulated) + , m_populating(0) , m_fetchTypes(0) , m_extraFetchTypes(0) , m_dataTypesFetched(0) @@ -497,7 +498,6 @@ SeasideCache::SeasideCache() , m_refreshRequired(false) , m_contactsUpdated(false) , m_displayOff(false) - , m_activeResolve(0) { Q_ASSERT(!instancePtr); instancePtr = this; @@ -663,26 +663,23 @@ void SeasideCache::unregisterResolveListener(ResolveListener *listener) if (!instancePtr) return; - // We might have outstanding resolve requests for this listener - if (instancePtr->m_activeResolve && (instancePtr->m_activeResolve->listener == listener)) { - instancePtr->m_activeResolve = 0; - } - - QList::iterator it = instancePtr->m_resolveAddresses.begin(); + QHash::iterator it = instancePtr->m_resolveAddresses.begin(); while (it != instancePtr->m_resolveAddresses.end()) { - if (it->listener == listener) { + if (it.value().listener == listener) { + it.key()->cancel(); + delete it.key(); it = instancePtr->m_resolveAddresses.erase(it); } else { ++it; } } - it = instancePtr->m_unknownAddresses.begin(); - while (it != instancePtr->m_unknownAddresses.end()) { - if (it->listener == listener) { - it = instancePtr->m_unknownAddresses.erase(it); + QList::iterator it2 = instancePtr->m_unknownAddresses.begin(); + while (it2 != instancePtr->m_unknownAddresses.end()) { + if (it2->listener == listener) { + it2 = instancePtr->m_unknownAddresses.erase(it2); } else { - ++it; + ++it2; } } } @@ -1414,23 +1411,54 @@ void SeasideCache::startRequest(bool *idleProcessing) // Test these conditions in priority order - if (m_keepPopulated && (m_populateProgress != Populated)) { - // We must populate the cache before we can do anything else + // Start by loading the favorites model, because it's so small and + // the user is likely to want to interact with it. + if (m_keepPopulated && (m_populateProgress == Unpopulated)) { if (m_fetchRequest.isActive()) { requestPending = true; } else { - if (m_populateProgress == Unpopulated) { - // Start a query to fully populate the cache, starting with favorites - m_fetchRequest.setFilter(favoriteFilter()); - m_fetchRequest.setFetchHint(favoriteFetchHint(m_fetchTypes)); - m_fetchRequest.setSorting(m_sortOrder); - m_fetchRequest.start(); + m_fetchRequest.setFilter(favoriteFilter()); + m_fetchRequest.setFetchHint(favoriteFetchHint(m_fetchTypes)); + m_fetchRequest.setSorting(m_sortOrder); + m_fetchRequest.start(); - m_fetchProcessedCount = 0; - m_populateProgress = FetchFavorites; - m_dataTypesFetched |= m_fetchTypes; - } else if (m_populateProgress == FetchMetadata) { - // Next, query for all contacts + m_fetchProcessedCount = 0; + m_populateProgress = FetchFavorites; + m_dataTypesFetched |= m_fetchTypes; + m_populating = true; + } + } + + const int maxPriorityIds = 20; + + // Next priority is refreshing small numbers of contacts, + // because these likely came from UI elements calling ensureCompletion() + if (!m_changedContacts.isEmpty() && m_changedContacts.count() < maxPriorityIds) { + if (m_fetchRequest.isActive()) { + requestPending = true; + } else { + QContactIdFilter filter; + 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 + m_fetchRequest.setFilter(filter & aggregateFilter()); + m_fetchRequest.setFetchHint(basicFetchHint()); + m_fetchRequest.setSorting(QList()); + m_fetchRequest.start(); + + m_fetchProcessedCount = 0; + } + } + + // Then populate the rest of the cache before doing anything else. + if (m_keepPopulated && (m_populateProgress != Populated)) { + if (m_fetchRequest.isActive()) { + requestPending = true; + } else { + if (m_populateProgress == FetchMetadata) { + // Query for all contacts // Request the metadata of all contacts (only data from the primary table, and any // other details required to determine whether the contacts matches the filter) m_fetchRequest.setFilter(allFilter()); @@ -1439,6 +1467,7 @@ void SeasideCache::startRequest(bool *idleProcessing) m_fetchRequest.start(); m_fetchProcessedCount = 0; + m_populating = true; } else if (m_populateProgress == FetchOnline) { // Now query for online contacts - fetch the account details, so we know if they're valid m_fetchRequest.setFilter(onlineFilter()); @@ -1447,6 +1476,7 @@ void SeasideCache::startRequest(bool *idleProcessing) m_fetchRequest.start(); m_fetchProcessedCount = 0; + m_populating = true; } } @@ -1608,48 +1638,6 @@ void SeasideCache::startRequest(bool *idleProcessing) } } - if (!m_resolveAddresses.isEmpty()) { - if (m_fetchRequest.isActive()) { - requestPending = true; - } else { - const ResolveData &resolve = m_resolveAddresses.first(); - - if (resolve.first.isEmpty()) { - // Search for phone number - m_fetchRequest.setFilter(QContactPhoneNumber::match(resolve.second)); - } else if (resolve.second.isEmpty()) { - // Search for email address - QContactDetailFilter detailFilter; - setDetailType(detailFilter, QContactEmailAddress::FieldEmailAddress); - detailFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive - detailFilter.setValue(resolve.first); - - m_fetchRequest.setFilter(detailFilter); - } else { - // Search for online account - QContactDetailFilter localFilter; - setDetailType(localFilter, QContactOnlineAccount__FieldAccountPath); - localFilter.setValue(resolve.first); - - QContactDetailFilter remoteFilter; - setDetailType(remoteFilter, QContactOnlineAccount::FieldAccountUri); - remoteFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive - remoteFilter.setValue(resolve.second); - - m_fetchRequest.setFilter(localFilter & remoteFilter); - } - - // If completion is not required, we need to at least retrieve as much detail - // as the favorites store, so we don't update any favorite with a smaller data subset - m_activeResolve = &resolve; - m_fetchRequest.setFetchHint(resolve.requireComplete ? basicFetchHint() : favoriteFetchHint(m_fetchTypes | m_extraFetchTypes)); - m_fetchRequest.setSorting(QList()); - m_fetchRequest.start(); - - m_fetchProcessedCount = 0; - } - } - if (!m_changedContacts.isEmpty()) { if (m_fetchRequest.isActive()) { requestPending = true; @@ -2199,14 +2187,10 @@ void SeasideCache::contactsAvailable() if (contacts.isEmpty()) return; - QSet queryDetailTypes; - foreach (const QContactDetail::DetailType &typeId, detailTypesHint(fetchHint)) { - queryDetailTypes.insert(typeId); - } - const bool partialFetch = !queryDetailTypes.isEmpty(); + QSet queryDetailTypes = detailTypesHint(fetchHint).toSet(); - if (m_populateProgress > Unpopulated && m_populateProgress < Populated) { - // We are populating the cache + if (request == &m_fetchRequest && m_populating) { + Q_ASSERT(m_populateProgress > Unpopulated && m_populateProgress < Populated); FilterType type(m_populateProgress == FetchFavorites ? FilterFavorites : (m_populateProgress == FetchMetadata ? FilterAll : FilterOnline)); @@ -2219,9 +2203,9 @@ void SeasideCache::contactsAvailable() } requestUpdate(); } else { - if (m_activeResolve || (request == &m_fetchByIdRequest)) { + if (contacts.count() == 1 || request == &m_fetchByIdRequest) { // Process these results immediately - applyContactUpdates(contacts, partialFetch, queryDetailTypes); + applyContactUpdates(contacts, queryDetailTypes); } else { // Add these contacts to the list to be progressively appended QList, QList > >::iterator it = m_contactsToUpdate.begin(), end = m_contactsToUpdate.end(); @@ -2291,12 +2275,11 @@ void SeasideCache::applyPendingContactUpdates() QList, QList > >::iterator it = m_contactsToUpdate.begin(); QSet &detailTypes((*it).first); - const bool partialFetch = !detailTypes.isEmpty(); // Update a single contact at a time; the update can cause numerous QML bindings // to be re-evaluated, so even a single contact update might be a slow operation QList &updatedContacts((*it).second); - applyContactUpdates(QList() << updatedContacts.takeFirst(), partialFetch, detailTypes); + applyContactUpdates(QList() << updatedContacts.takeFirst(), detailTypes); if (updatedContacts.isEmpty()) { m_contactsToUpdate.erase(it); @@ -2304,9 +2287,10 @@ void SeasideCache::applyPendingContactUpdates() } } -void SeasideCache::applyContactUpdates(const QList &contacts, bool partialFetch, const QSet &queryDetailTypes) +void SeasideCache::applyContactUpdates(const QList &contacts, const QSet &queryDetailTypes) { QSet modifiedGroups; + const bool partialFetch = !queryDetailTypes.isEmpty(); foreach (QContact contact, contacts) { quint32 iid = internalId(contact); @@ -2633,74 +2617,33 @@ void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) m_aggregatedContacts.clear(); } } else if (request == &m_fetchRequest) { - if (m_populateProgress == FetchFavorites) { - if (m_contactsToAppend.find(FilterFavorites) == m_contactsToAppend.end()) { - // No pending contacts, the models are now populated - makePopulated(FilterFavorites); - qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; - } - - m_populateProgress = FetchMetadata; - } else if (m_populateProgress == FetchMetadata) { - if (m_contactsToAppend.find(FilterAll) == m_contactsToAppend.end()) { - makePopulated(FilterNone); - makePopulated(FilterAll); - qDebug() << "All queried in" << m_timer.elapsed() << "ms"; - } - - m_populateProgress = FetchOnline; - } else if (m_populateProgress == FetchOnline) { - if (m_contactsToAppend.find(FilterOnline) == m_contactsToAppend.end()) { - makePopulated(FilterOnline); - qDebug() << "Online queried in" << m_timer.elapsed() << "ms"; - } - - m_populateProgress = Populated; - } else { - // Result of a specific query - if (m_activeResolve) { - if (m_activeResolve->first == QString()) { - // We have now queried this phone number - m_resolvedPhoneNumbers.insert(minimizePhoneNumber(m_activeResolve->second)); + if (m_populating) { + Q_ASSERT(m_populateProgress > Unpopulated && m_populateProgress < Populated); + if (m_populateProgress == FetchFavorites) { + if (m_contactsToAppend.find(FilterFavorites) == m_contactsToAppend.end()) { + // No pending contacts, the models are now populated + makePopulated(FilterFavorites); + qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; } - CacheItem *item = 0; - const QList &resolvedContacts(m_fetchRequest.contacts()); - if (!resolvedContacts.isEmpty()) { - if (resolvedContacts.count() == 1) { - item = itemById(apiId(resolvedContacts.first()), false); - } else { - // Lookup the result in our updated indexes - ResolveData data(*m_activeResolve); - if (data.first == QString()) { - item = itemByPhoneNumber(data.second, false); - } else if (data.second == QString()) { - item = itemByEmailAddress(data.first, false); - } else { - item = itemByOnlineAccount(data.first, data.second, false); - } - } - } else { - // This address is unknown - keep it for later resolution - ResolveData data(*m_activeResolve); - if (data.first == QString()) { - // Compare this phone number in minimized form - data.compare = minimizePhoneNumber(data.second); - } else if (data.second == QString()) { - // Compare this email address in lowercased form - data.compare = data.first.toLower(); - } else { - // Compare this account URI in lowercased form - data.compare = data.second.toLower(); - } + m_populateProgress = FetchMetadata; + } else if (m_populateProgress == FetchMetadata) { + if (m_contactsToAppend.find(FilterAll) == m_contactsToAppend.end()) { + makePopulated(FilterNone); + makePopulated(FilterAll); + qDebug() << "All queried in" << m_timer.elapsed() << "ms"; + } - m_unknownAddresses.append(data); + m_populateProgress = FetchOnline; + } else if (m_populateProgress == FetchOnline) { + if (m_contactsToAppend.find(FilterOnline) == m_contactsToAppend.end()) { + makePopulated(FilterOnline); + qDebug() << "Online queried in" << m_timer.elapsed() << "ms"; } - m_activeResolve->listener->addressResolved(m_activeResolve->first, m_activeResolve->second, item); - m_activeResolve = 0; - m_resolveAddresses.takeFirst(); + m_populateProgress = Populated; } + m_populating = false; } } @@ -2708,6 +2651,65 @@ void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) requestUpdate(); } +void SeasideCache::addressRequestStateChanged(QContactAbstractRequest::State state) +{ + if (state != QContactAbstractRequest::FinishedState) + return; + + // results are complete, so record them in the cache + QContactFetchRequest *request = static_cast(sender()); + QSet queryDetailTypes = detailTypesHint(request->fetchHint()).toSet(); + applyContactUpdates(request->contacts(), queryDetailTypes); + + // now figure out which address was being resolved and resolve it + QHash::iterator it = instancePtr->m_resolveAddresses.find(request); + if (it == instancePtr->m_resolveAddresses.end()) { + qWarning() << "Got stateChanged for unknown request"; + return; + } + + ResolveData data(it.value()); + if (data.first == QString()) { + // We have now queried this phone number + m_resolvedPhoneNumbers.insert(minimizePhoneNumber(data.second)); + } + + CacheItem *item = 0; + const QList &resolvedContacts = it.key()->contacts(); + + if (!resolvedContacts.isEmpty()) { + if (resolvedContacts.count() == 1) { + item = itemById(apiId(resolvedContacts.first()), false); + } else { + // Lookup the result in our updated indexes + if (data.first == QString()) { + item = itemByPhoneNumber(data.second, false); + } else if (data.second == QString()) { + item = itemByEmailAddress(data.first, false); + } else { + item = itemByOnlineAccount(data.first, data.second, false); + } + } + } else { + // This address is unknown - keep it for later resolution + if (data.first == QString()) { + // Compare this phone number in minimized form + data.compare = minimizePhoneNumber(data.second); + } else if (data.second == QString()) { + // Compare this email address in lowercased form + data.compare = data.first.toLower(); + } else { + // Compare this account URI in lowercased form + data.compare = data.second.toLower(); + } + + m_unknownAddresses.append(data); + } + data.listener->addressResolved(data.first, data.second, item); + delete it.key(); + m_resolveAddresses.erase(it); +} + void SeasideCache::makePopulated(FilterType filter) { m_populated |= (1 << filter); @@ -3088,11 +3090,44 @@ void SeasideCache::resolveAddress(ResolveListener *listener, const QString &firs if (knownUnknown) { m_unknownResolveAddresses.append(data); + requestUpdate(); } else { - m_resolveAddresses.append(data); - } + QContactFetchRequest *request = new QContactFetchRequest(this); + request->setManager(manager()); + + if (first.isEmpty()) { + // Search for phone number + request->setFilter(QContactPhoneNumber::match(second)); + } else if (second.isEmpty()) { + // Search for email address + QContactDetailFilter detailFilter; + setDetailType(detailFilter, QContactEmailAddress::FieldEmailAddress); + detailFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive + detailFilter.setValue(first); + + request->setFilter(detailFilter); + } else { + // Search for online account + QContactDetailFilter localFilter; + setDetailType(localFilter, QContactOnlineAccount__FieldAccountPath); + localFilter.setValue(first); - requestUpdate(); + QContactDetailFilter remoteFilter; + setDetailType(remoteFilter, QContactOnlineAccount::FieldAccountUri); + remoteFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive + remoteFilter.setValue(second); + + request->setFilter(localFilter & remoteFilter); + } + + // If completion is not required, we need to at least retrieve as much detail + // as the favorites store, so we don't update any favorite with a smaller data subset + request->setFetchHint(requireComplete ? basicFetchHint() : favoriteFetchHint(m_fetchTypes | m_extraFetchTypes)); + connect(request, SIGNAL(stateChanged(QContactAbstractRequest::State)), + this, SLOT(addressRequestStateChanged(QContactAbstractRequest::State))); + m_resolveAddresses[request] = data; + request->start(); + } } SeasideCache::CacheItem *SeasideCache::itemMatchingPhoneNumber(const QString &number, const QString &normalized, bool requireComplete) diff --git a/src/seasidecache.h b/src/seasidecache.h index 086ff0f..9e201bc 100644 --- a/src/seasidecache.h +++ b/src/seasidecache.h @@ -342,6 +342,7 @@ private slots: void contactIdsAvailable(); void relationshipsAvailable(); void requestStateChanged(QContactAbstractRequest::State state); + void addressRequestStateChanged(QContactAbstractRequest::State state); void updateContacts(); void contactsAdded(const QList &contactIds); void contactsChanged(const QList &contactIds); @@ -373,7 +374,7 @@ private slots: void fetchContacts(); void updateContacts(const QList &contactIds, QList *updateList); void applyPendingContactUpdates(); - void applyContactUpdates(const QList &contacts, bool partialFetch, const QSet &queryDetailTypes); + void applyContactUpdates(const QList &contacts, const QSet &queryDetailTypes); void resolveUnknownAddresses(const QString &first, const QString &second, CacheItem *item); bool updateContactIndexing(const QContact &oldContact, const QContact &contact, quint32 iid, const QSet &queryDetailTypes, CacheItem *item); @@ -454,6 +455,7 @@ private slots: QString m_groupProperty; bool m_keepPopulated; PopulateProgress m_populateProgress; + bool m_populating; // true if current m_fetchRequest makes progress quint32 m_fetchTypes; quint32 m_extraFetchTypes; quint32 m_dataTypesFetched; @@ -471,10 +473,9 @@ private slots: bool requireComplete; ResolveListener *listener; }; - QList m_resolveAddresses; + QHash m_resolveAddresses; QList m_unknownResolveAddresses; QList m_unknownAddresses; - const ResolveData *m_activeResolve; QSet m_resolvedPhoneNumbers; QElapsedTimer m_timer; diff --git a/tests/tests.pro b/tests/tests.pro index 1f97396..9db5aaa 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,7 +1,7 @@ include(../package.pri) TEMPLATE = subdirs -SUBDIRS = tst_synchronizelists tst_seasideimport +SUBDIRS = tst_synchronizelists tst_seasideimport tst_resolve tests_xml.target = tests.xml tests_xml.depends = $$PWD/tests.xml.in diff --git a/tests/tests.xml.in b/tests/tests.xml.in index 66a0251..4b58320 100644 --- a/tests/tests.xml.in +++ b/tests/tests.xml.in @@ -10,6 +10,9 @@ /opt/tests/@PACKAGENAME@/tst_seasideimport + + /opt/tests/@PACKAGENAME@/tst_resolve + diff --git a/tests/tst_resolve/tst_resolve.cpp b/tests/tst_resolve/tst_resolve.cpp new file mode 100644 index 0000000..52e57f7 --- /dev/null +++ b/tests/tst_resolve/tst_resolve.cpp @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2014 Jolla Ltd. + * Contact: Richard Braakman + * + * 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 + +#include +#include +#include +#include +#include + +#include "seasidecache.h" + +static const QString AccountPath(QString::fromLatin1("/example/jabber/0")); + +class tst_Resolve : public QObject +{ + Q_OBJECT + +public slots: + void initTestCase(); + void cleanupTestCase(); + +private: + // only firstname and lastname are mandatory + bool makeContact(QString firstname, QString lastname, QString phone, QString email, QString account); + void makeContacts(); + + QList m_createdContacts; + +private slots: + void resolveByPhone(); + void resolveByPhoneNotFound(); + void resolveByEmail(); + void resolveByEmailNotFound(); + void resolveByAccount(); + void resolveByAccountNotFound(); + + void resolveDuringContactLink(); +}; + +namespace { +struct TestResolveListener : public SeasideCache::ResolveListener { + TestResolveListener() + : m_resolved(false), m_item(0) + { } + + virtual void addressResolved(const QString &first, const QString &second, SeasideCache::CacheItem *item) + { m_resolved = true; m_item = item; } + + bool m_resolved; + SeasideCache::CacheItem *m_item; +}; + +}; // anonymous + +void tst_Resolve::initTestCase() +{ + makeContacts(); + SeasideCache::registerUser(this); +} + +void tst_Resolve::cleanupTestCase() +{ + SeasideCache::unregisterUser(this); + QVERIFY(SeasideCache::manager()->removeContacts(m_createdContacts)); + m_createdContacts.clear(); +} + +bool tst_Resolve::makeContact(QString firstname, QString lastname, QString phone, QString email, QString account) +{ + QContact contact; + + QContactName name; + name.setFirstName(firstname); + name.setLastName(lastname); + if (!contact.saveDetail(&name)) + return false; + + if (!phone.isEmpty()) { + QContactPhoneNumber pn; + pn.setNumber(phone); + if (!contact.saveDetail(&pn)) + return false; + } + + if (!email.isEmpty()) { + QContactEmailAddress e; + e.setEmailAddress(email); + if (!contact.saveDetail(&e)) + return false; + } + + if (!account.isEmpty()) { + QContactOnlineAccount acc; + acc.setAccountUri(account); + acc.setValue(QContactOnlineAccount__FieldAccountPath, AccountPath); + if (!contact.saveDetail(&acc)) + return false; + } + + if (!SeasideCache::manager()->saveContact(&contact)) + return false; + + m_createdContacts << contact.id(); + return true; +} + +void tst_Resolve::makeContacts() +{ + QVERIFY(makeContact("Alfred", "Alfredson", "+358474005000", "alfred@alfred.com", "")); + QVERIFY(makeContact("Berta", "Berenstain", "", "berta.b@geemail.com", "berta.b@geemail.com")); + QVERIFY(makeContact("Carlo", "Rizzi", "+358471112222", "", "")); + QVERIFY(makeContact("Daffy", "Duck", "+358470009955", "daffyd@example.com", "")); + QVERIFY(makeContact("Dafferd", "Duck", "", "daffy.d@example.com", "")); + QVERIFY(makeContact("Ernest", "Everest", "+358477758885", "", "")); +} + +void tst_Resolve::resolveByPhone() +{ + SeasideCache::CacheItem *item; + TestResolveListener listener; + QString number("+358470009955"); + + item = SeasideCache::resolvePhoneNumber(&listener, number, true); + if (!item) { + QTRY_VERIFY(listener.m_resolved); + item = listener.m_item; + } + + QContactName name = item->contact.detail(); + QCOMPARE(name.firstName(), QString::fromLatin1("Daffy")); +} + +void tst_Resolve::resolveByPhoneNotFound() +{ + SeasideCache::CacheItem *item; + TestResolveListener listener; + QString number("+358470000000"); + + item = SeasideCache::resolvePhoneNumber(&listener, number, true); + if (!item) { + QTRY_VERIFY(listener.m_resolved); + item = listener.m_item; + } + + QCOMPARE(item, (SeasideCache::CacheItem *)0); +} + +void tst_Resolve::resolveByEmail() +{ + SeasideCache::CacheItem *item; + TestResolveListener listener; + QString address("berta.b@geemail.com"); + + item = SeasideCache::resolveEmailAddress(&listener, address, true); + if (!item) { + QTRY_VERIFY(listener.m_resolved); + item = listener.m_item; + } + + QContactName name = item->contact.detail(); + QCOMPARE(name.firstName(), QString::fromLatin1("Berta")); +} + +void tst_Resolve::resolveByEmailNotFound() +{ + SeasideCache::CacheItem *item; + TestResolveListener listener; + QString address("example@example.com"); + + item = SeasideCache::resolveEmailAddress(&listener, address, true); + if (!item) { + QTRY_VERIFY(listener.m_resolved); + item = listener.m_item; + } + + QCOMPARE(item, (SeasideCache::CacheItem *)0); +} + +void tst_Resolve::resolveByAccount() +{ + SeasideCache::CacheItem *item; + TestResolveListener listener; + QString remoteUid("berta.b@geemail.com"); + + item = SeasideCache::resolveOnlineAccount(&listener, AccountPath, remoteUid, true); + if (!item) { + QTRY_VERIFY(listener.m_resolved); + item = listener.m_item; + } + + QContactName name = item->contact.detail(); + QCOMPARE(name.firstName(), QString::fromLatin1("Berta")); +} + +void tst_Resolve::resolveByAccountNotFound() +{ + SeasideCache::CacheItem *item; + TestResolveListener listener; + QString remoteUid("example@example.com"); + + item = SeasideCache::resolveOnlineAccount(&listener, AccountPath, remoteUid, true); + if (!item) { + QTRY_VERIFY(listener.m_resolved); + item = listener.m_item; + } + + QCOMPARE(item, (SeasideCache::CacheItem *)0); +} + +struct ItemWatcher : public SeasideCache::ItemData { + QList m_constituents; + bool m_aggregationComplete; + + ItemWatcher() : m_aggregationComplete(false) + { } + + virtual void displayLabelOrderChanged(SeasideCache::DisplayLabelOrder) + { } + virtual void updateContact(const QtContacts::QContact&, QtContacts::QContact*, SeasideCache::ContactState) + { } + virtual void mergeCandidatesFetched(const QList &) + { } + + virtual void aggregationOperationCompleted() + { m_aggregationComplete = true; } + + virtual void constituentsFetched(const QList &ids) + { m_constituents = ids; } + + virtual QList constituents() const + { return m_constituents; } +}; + +// Test that address resolutions don't interfere with contact linking +void tst_Resolve::resolveDuringContactLink() +{ + SeasideCache::CacheItem *item1; + TestResolveListener listener1; + QString address1("daffyd@example.com"); + SeasideCache::CacheItem *item2; + TestResolveListener listener2; + QString address2("daffy.d@example.com"); + + item1 = SeasideCache::resolveEmailAddress(&listener1, address1, true); + item2 = SeasideCache::resolveEmailAddress(&listener2, address2, true); + if (!item1) { + QTRY_VERIFY(listener1.m_resolved); + item1 = listener1.m_item; + } + QVERIFY(item1); + QCOMPARE(item1->displayLabel, QString::fromLatin1("Daffy Duck")); + if (!item2) { + QTRY_VERIFY(listener2.m_resolved); + item2 = listener2.m_item; + } + QVERIFY(item2); + QCOMPARE(item2->displayLabel, QString::fromLatin1("Dafferd Duck")); + + int iid = item1->iid; + item1->itemData = new ItemWatcher; + item2->itemData = new ItemWatcher; + SeasideCache::aggregateContacts(item1->contact, item2->contact); + + // Fire off an address resolution simultaneously + SeasideCache::CacheItem *item; + QString number("+358477758885"); + TestResolveListener listener; + + item = SeasideCache::resolvePhoneNumber(&listener, number, true); + if (!item) { + QTRY_VERIFY(listener.m_resolved); + item = listener.m_item; + } + + // Did the address resolution go ok? + QCOMPARE(item->displayLabel, QString::fromLatin1("Ernest Everest")); + + // wait for the aggregation + item1 = SeasideCache::existingItem(iid); + QVERIFY(item1); + QVERIFY(item1->itemData); + QTRY_VERIFY(((ItemWatcher *) item1->itemData)->m_aggregationComplete); + + // the aggregate's constituents are not updated in the cache, so they + // have to be reloaded before comparing. Is that a bug? + SeasideCache::fetchConstituents(item1->contact); + QTRY_COMPARE(item1->itemData->constituents().count(), 2); + + // Check that the expected two contacts are the constituents. + int iid1 = item1->itemData->constituents()[0]; + int iid2 = item1->itemData->constituents()[1]; + item1 = SeasideCache::itemById(iid1); + item2 = SeasideCache::itemById(iid2); + QTRY_COMPARE(item1->contactState, SeasideCache::ContactComplete); + QTRY_COMPARE(item2->contactState, SeasideCache::ContactComplete); + + QStringList names, expected; + names << item1->displayLabel << item2->displayLabel; + qSort(names); + expected << QString::fromLatin1("Dafferd Duck") << QString::fromLatin1("Daffy Duck"); + QCOMPARE(names, expected); +} + +#include "tst_resolve.moc" +QTEST_GUILESS_MAIN(tst_Resolve) diff --git a/tests/tst_resolve/tst_resolve.pro b/tests/tst_resolve/tst_resolve.pro new file mode 100644 index 0000000..eeee359 --- /dev/null +++ b/tests/tst_resolve/tst_resolve.pro @@ -0,0 +1,19 @@ +include(../common.pri) +include(../../config.pri) +TARGET = tst_resolve +QT += contacts-private dbus + +PKGCONFIG += mlocale5 + +# We need the moc output for ContactManagerEngine from sqlite-extensions +extensionsIncludePath = $$system(pkg-config --cflags-only-I qtcontacts-sqlite-qt5-extensions) +VPATH += $$replace(extensionsIncludePath, -I, ) +HEADERS += contactmanagerengine.h + +HEADERS += ../../src/seasidecache.h +SOURCES += ../../src/seasidecache.cpp + +HEADERS += ../../src/cacheconfiguration.h +SOURCES += ../../src/cacheconfiguration.cpp + +SOURCES += tst_resolve.cpp