/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QTVERSIT_USE_NAMESPACE namespace { Q_GLOBAL_STATIC(CacheConfiguration, cacheConfig) ML10N::MLocale mLocale; const QString aggregateRelationshipType = QContactRelationship::Aggregates(); const QString isNotRelationshipType = QString::fromLatin1("IsNot"); const QString syncTargetLocal = QLatin1String("local"); const QString syncTargetWasLocal = QLatin1String("was_local"); // Find the script that all letters in the name belong to, else yield Unknown QChar::Script nameScript(const QString &name) { QChar::Script script(QChar::Script_Unknown); if (!name.isEmpty()) { QString::const_iterator it = name.begin(), end = name.end(); for ( ; it != end; ++it) { const QChar::Category charCategory((*it).category()); if (charCategory >= QChar::Letter_Uppercase && charCategory <= QChar::Letter_Other) { const QChar::Script charScript((*it).script()); if (script == QChar::Script_Unknown) { script = charScript; } else if (charScript != script) { return QChar::Script_Unknown; } } } } return script; } QChar::Script nameScript(const QString &firstName, const QString &lastName) { if (firstName.isEmpty()) { return nameScript(lastName); } else if (lastName.isEmpty()) { return nameScript(firstName); } QChar::Script firstScript(nameScript(firstName)); if (firstScript != QChar::Script_Unknown) { QChar::Script lastScript(nameScript(lastName)); if (lastScript == firstScript) { return lastScript; } } return QChar::Script_Unknown; } bool nameScriptImpliesFamilyFirst(const QString &firstName, const QString &lastName) { switch (nameScript(firstName, lastName)) { // These scripts are used by cultures that conform to the family-name-first nameing convention: case QChar::Script_Han: case QChar::Script_Lao: case QChar::Script_Hangul: case QChar::Script_Khmer: case QChar::Script_Mongolian: case QChar::Script_Hiragana: case QChar::Script_Katakana: case QChar::Script_Bopomofo: case QChar::Script_Yi: return true; default: return false; } } QString managerName() { return QString::fromLatin1("org.nemomobile.contacts.sqlite"); } QMap managerParameters() { QMap rv; // Report presence changes independently from other contact changes rv.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("false")); if (!qgetenv("LIBCONTACTS_TEST_MODE").isEmpty()) { rv.insert(QString::fromLatin1("autoTest"), QString::fromLatin1("true")); } return rv; } Q_GLOBAL_STATIC_WITH_ARGS(QContactManager, manager, (managerName(), managerParameters())) typedef QList DetailList; template QContactDetail::DetailType detailType() { return T::Type; } QContactDetail::DetailType detailType(const QContactDetail &detail) { return detail.type(); } template void setDetailType(Filter &filter, Field field) { filter.setDetailType(T::Type, field); } DetailList detailTypesHint(const QContactFetchHint &hint) { return hint.detailTypesHint(); } void setDetailTypesHint(QContactFetchHint &hint, const DetailList &types) { hint.setDetailTypesHint(types); } QContactFetchHint basicFetchHint() { QContactFetchHint fetchHint; // We generally have no use for these things: fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships | QContactFetchHint::NoActionPreferences | QContactFetchHint::NoBinaryBlobs); return fetchHint; } QContactFetchHint presenceFetchHint() { QContactFetchHint fetchHint(basicFetchHint()); setDetailTypesHint(fetchHint, DetailList() << detailType() << detailType() << detailType()); return fetchHint; } DetailList contactsTableDetails() { DetailList types; // These details are reported in every query types << detailType() << detailType() << detailType() << detailType() << detailType() << detailType() << detailType(); return types; } QContactFetchHint metadataFetchHint(quint32 fetchTypes = 0) { QContactFetchHint fetchHint(basicFetchHint()); // Include all detail types which come from the main contacts table DetailList types(contactsTableDetails()); // Include nickname, as some contacts have no other name types << detailType(); if (fetchTypes & SeasideCache::FetchAccountUri) { types << detailType(); } if (fetchTypes & SeasideCache::FetchPhoneNumber) { types << detailType(); } if (fetchTypes & SeasideCache::FetchEmailAddress) { types << detailType(); } if (fetchTypes & SeasideCache::FetchOrganization) { types << detailType(); } setDetailTypesHint(fetchHint, types); return fetchHint; } QContactFetchHint onlineFetchHint(quint32 fetchTypes = 0) { QContactFetchHint fetchHint(metadataFetchHint(fetchTypes)); // We also need global presence state setDetailTypesHint(fetchHint, detailTypesHint(fetchHint) << detailType()); return fetchHint; } QContactFetchHint favoriteFetchHint(quint32 fetchTypes = 0) { QContactFetchHint fetchHint(onlineFetchHint(fetchTypes)); // We also need avatar info setDetailTypesHint(fetchHint, detailTypesHint(fetchHint) << detailType()); return fetchHint; } QContactFetchHint extendedMetadataFetchHint(quint32 fetchTypes) { QContactFetchHint fetchHint(basicFetchHint()); DetailList types; // Only query for the specific types we need if (fetchTypes & SeasideCache::FetchAccountUri) { types << detailType(); } if (fetchTypes & SeasideCache::FetchPhoneNumber) { types << detailType(); } if (fetchTypes & SeasideCache::FetchEmailAddress) { types << detailType(); } if (fetchTypes & SeasideCache::FetchOrganization) { types << detailType(); } setDetailTypesHint(fetchHint, types); return fetchHint; } QContactFilter allFilter() { return QContactFilter(); } QContactFilter favoriteFilter() { return QContactFavorite::match(); } QContactFilter nonfavoriteFilter() { QContactDetailFilter filter; setDetailType(filter, QContactFavorite::FieldFavorite); filter.setMatchFlags(QContactFilter::MatchExactly); filter.setValue(false); return filter; } QContactFilter onlineFilter() { return QContactStatusFlags::matchFlag(QContactStatusFlags::IsOnline); } QContactFilter aggregateFilter() { static const QString aggregate(QString::fromLatin1("aggregate")); QContactDetailFilter filter; setDetailType(filter, QContactSyncTarget::FieldSyncTarget); filter.setValue(aggregate); return filter; } typedef QPair StringPair; QList addressPairs(const QContactPhoneNumber &phoneNumber) { QList rv; const QString normalized(SeasideCache::normalizePhoneNumber(phoneNumber.number())); if (!normalized.isEmpty()) { const QChar plus(QChar::fromLatin1('+')); if (normalized.startsWith(plus)) { // Also index the complete form of this number rv.append(qMakePair(QString(), normalized)); } // Always index the minimized form of the number const QString minimized(SeasideCache::minimizePhoneNumber(normalized)); rv.append(qMakePair(QString(), minimized)); } return rv; } StringPair addressPair(const QContactEmailAddress &emailAddress) { return qMakePair(emailAddress.emailAddress().toLower(), QString()); } StringPair addressPair(const QContactOnlineAccount &account) { StringPair address = qMakePair(account.value(QContactOnlineAccount__FieldAccountPath), account.accountUri().toLower()); return !address.first.isEmpty() && !address.second.isEmpty() ? address : StringPair(); } bool validAddressPair(const StringPair &address) { return (!address.first.isEmpty() || !address.second.isEmpty()); } bool ignoreContactForDisplayLabelGroups(const QContact &contact) { static const QString aggregate(QString::fromLatin1("aggregate")); // Don't include the self contact in name groups if (SeasideCache::apiId(contact) == SeasideCache::selfContactId()) { return true; } // Also ignore non-aggregate contacts QContactSyncTarget syncTarget = contact.detail(); return (syncTarget.syncTarget() != aggregate); } QList internalIds(const QList &ids) { QList rv; rv.reserve(ids.count()); foreach (const QContactId &id, ids) { rv.append(SeasideCache::internalId(id)); } return rv; } QString::const_iterator firstDtmfChar(QString::const_iterator it, QString::const_iterator end) { static const QString dtmfChars(QString::fromLatin1("pPwWxX#*")); for ( ; it != end; ++it) { if (dtmfChars.contains(*it)) return it; } return end; } const int ExactMatch = 100; int matchLength(const QString &lhs, const QString &rhs) { if (lhs.isEmpty() || rhs.isEmpty()) return 0; QString::const_iterator lbegin = lhs.constBegin(), lend = lhs.constEnd(); QString::const_iterator rbegin = rhs.constBegin(), rend = rhs.constEnd(); // Do these numbers contain DTMF elements? QString::const_iterator ldtmf = firstDtmfChar(lbegin, lend); QString::const_iterator rdtmf = firstDtmfChar(rbegin, rend); QString::const_iterator lit, rit; bool processDtmf = false; int matchLength = 0; if ((ldtmf != lbegin) && (rdtmf != rbegin)) { // Start match length calculation at the last non-DTMF digit lit = ldtmf - 1; rit = rdtmf - 1; while (*lit == *rit) { ++matchLength; --lit; --rit; if ((lit == lbegin) || (rit == rbegin)) { if (*lit == *rit) { ++matchLength; if ((lit == lbegin) && (rit == rbegin)) { // We have a complete, exact match - this must be the best match return ExactMatch; } else { // We matched all of one number - continue looking in the DTMF part processDtmf = true; } } break; } } } else { // Process the DTMF section for a match processDtmf = true; } // Have we got a match? if ((matchLength >= QtContactsSqliteExtensions::DefaultMaximumPhoneNumberCharacters) || processDtmf) { // See if the match continues into the DTMF area QString::const_iterator lit = ldtmf; QString::const_iterator rit = rdtmf; for ( ; (lit != lend) && (rit != rend); ++lit, ++rit) { if ((*lit).toLower() != (*rit).toLower()) break; ++matchLength; } } return matchLength; } int bestPhoneNumberMatchLength(const QContact &contact, const QString &match) { int bestMatchLength = 0; foreach (const QContactPhoneNumber& phone, contact.details()) { bestMatchLength = qMax(bestMatchLength, matchLength(SeasideCache::normalizePhoneNumber(phone.number()), match)); if (bestMatchLength == ExactMatch) { return ExactMatch; } } return bestMatchLength; } } SeasideCache *SeasideCache::instancePtr = 0; int SeasideCache::contactDisplayLabelGroupCount = 0; QStringList SeasideCache::allContactDisplayLabelGroups = QStringList(); QTranslator *SeasideCache::engEnTranslator = 0; QTranslator *SeasideCache::translator = 0; QContactManager* SeasideCache::manager() { return ::manager(); } SeasideCache* SeasideCache::instance() { if (!instancePtr) { instancePtr = new SeasideCache; } return instancePtr; } QContactId SeasideCache::apiId(const QContact &contact) { return contact.id(); } QContactId SeasideCache::apiId(quint32 iid) { return QtContactsSqliteExtensions::apiContactId(iid); } bool SeasideCache::validId(const QContactId &id) { return !id.isNull(); } quint32 SeasideCache::internalId(const QContact &contact) { return internalId(contact.id()); } quint32 SeasideCache::internalId(const QContactId &id) { return QtContactsSqliteExtensions::internalContactId(id); } SeasideCache::SeasideCache() : m_syncFilter(FilterNone) , m_populated(0) , m_cacheIndex(0) , m_queryIndex(0) , m_fetchProcessedCount(0) , m_fetchByIdProcessedCount(0) , m_keepPopulated(false) , m_populateProgress(Unpopulated) , m_populating(0) , m_fetchTypes(0) , m_extraFetchTypes(0) , m_dataTypesFetched(0) , m_updatesPending(false) , m_refreshRequired(false) , m_contactsUpdated(false) , m_displayOff(false) { m_timer.start(); m_fetchPostponed.invalidate(); CacheConfiguration *config(cacheConfig()); connect(config, SIGNAL(displayLabelOrderChanged(CacheConfiguration::DisplayLabelOrder)), this, SLOT(displayLabelOrderChanged(CacheConfiguration::DisplayLabelOrder))); connect(config, SIGNAL(sortPropertyChanged(QString)), this, SLOT(sortPropertyChanged(QString))); // Is this a GUI application? If so, we want to defer some processing when the display is off if (qApp && qApp->property("applicationDisplayName").isValid()) { // Only QGuiApplication has this property if (!QDBusConnection::systemBus().connect(MCE_SERVICE, MCE_SIGNAL_PATH, MCE_SIGNAL_IF, MCE_DISPLAY_SIG, this, SLOT(displayStatusChanged(QString)))) { qWarning() << "Unable to connect to MCE displayStatusChanged signal"; } } QContactManager *mgr(manager()); // The contactsPresenceChanged signal is not exported by QContactManager, so we // need to find it from the manager's engine object typedef QtContactsSqliteExtensions::ContactManagerEngine EngineType; EngineType *cme = dynamic_cast(QContactManagerData::managerData(mgr)->m_engine); connect(cme, SIGNAL(displayLabelGroupsChanged(QStringList)), this, SLOT(displayLabelGroupsChanged(QStringList))); displayLabelGroupsChanged(cme->displayLabelGroups()); connect(mgr, SIGNAL(dataChanged()), this, SLOT(dataChanged())); connect(mgr, SIGNAL(contactsAdded(QList)), this, SLOT(contactsAdded(QList))); connect(mgr, SIGNAL(contactsChanged(QList)), this, SLOT(contactsChanged(QList))); connect(cme, SIGNAL(contactsPresenceChanged(QList)), this, SLOT(contactsPresenceChanged(QList))); connect(mgr, SIGNAL(contactsRemoved(QList)), this, SLOT(contactsRemoved(QList))); 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(mgr); m_fetchByIdRequest.setManager(mgr); m_contactIdRequest.setManager(mgr); m_relationshipsFetchRequest.setManager(mgr); m_removeRequest.setManager(mgr); m_saveRequest.setManager(mgr); m_relationshipSaveRequest.setManager(mgr); m_relationshipRemoveRequest.setManager(mgr); setSortOrder(sortProperty()); } SeasideCache::~SeasideCache() { if (instancePtr == this) instancePtr = 0; } void SeasideCache::checkForExpiry() { if (instancePtr->m_users.isEmpty() && !QCoreApplication::closingDown()) { 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, FetchDataType requiredTypes, FetchDataType extraTypes) { // Ensure the cache has been instantiated instance(); 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(requiredTypes & SeasideCache::FetchTypesMask, extraTypes & SeasideCache::FetchTypesMask); if (requiredTypes & SeasideCache::FetchTypesMask) { // If we have filtered models, they will need a contact ID refresh after the cache is populated instancePtr->m_refreshRequired = true; } } void SeasideCache::unregisterModel(ListModel *model) { for (int i = 0; i < FilterTypesCount; ++i) instancePtr->m_models[i].removeAll(model); checkForExpiry(); } void SeasideCache::registerUser(QObject *user) { // Ensure the cache has been instantiated instance(); instancePtr->m_expiryTimer.stop(); instancePtr->m_users.insert(user); } void SeasideCache::unregisterUser(QObject *user) { instancePtr->m_users.remove(user); checkForExpiry(); } void SeasideCache::registerDisplayLabelGroupChangeListener(SeasideDisplayLabelGroupChangeListener *listener) { // Ensure the cache has been instantiated instance(); instancePtr->m_displayLabelGroupChangeListeners.append(listener); } void SeasideCache::unregisterDisplayLabelGroupChangeListener(SeasideDisplayLabelGroupChangeListener *listener) { if (!instancePtr) return; instancePtr->m_displayLabelGroupChangeListeners.removeAll(listener); } void SeasideCache::registerChangeListener(ChangeListener *listener) { // Ensure the cache has been instantiated instance(); instancePtr->m_changeListeners.append(listener); } void SeasideCache::unregisterChangeListener(ChangeListener *listener) { if (!instancePtr) return; instancePtr->m_changeListeners.removeAll(listener); } void SeasideCache::unregisterResolveListener(ResolveListener *listener) { if (!instancePtr) return; QHash::iterator it = instancePtr->m_resolveAddresses.begin(); while (it != instancePtr->m_resolveAddresses.end()) { if (it.value().listener == listener) { it.key()->cancel(); delete it.key(); it = instancePtr->m_resolveAddresses.erase(it); } else { ++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 { ++it2; } } QList::iterator it3 = instancePtr->m_unknownResolveAddresses.begin(); while (it3 != instancePtr->m_unknownResolveAddresses.end()) { if (it3->listener == listener) { it3 = instancePtr->m_unknownResolveAddresses.erase(it3); } else { ++it3; } } QSet::iterator it4 = instancePtr->m_pendingResolve.begin(); while (it4 != instancePtr->m_pendingResolve.end()) { if (it4->listener == listener) { it4 = instancePtr->m_pendingResolve.erase(it4); } else { ++it4; } } } QString SeasideCache::displayLabelGroup(const CacheItem *cacheItem) { if (!cacheItem) return QString(); return cacheItem->displayLabelGroup; } QStringList SeasideCache::allDisplayLabelGroups() { // Ensure the cache has been instantiated instance(); return allContactDisplayLabelGroups; } QHash > SeasideCache::displayLabelGroupMembers() { if (instancePtr) return instancePtr->m_contactDisplayLabelGroups; return QHash >(); } SeasideCache::DisplayLabelOrder SeasideCache::displayLabelOrder() { return static_cast(cacheConfig()->displayLabelOrder()); } QString SeasideCache::sortProperty() { return cacheConfig()->sortProperty(); } QString SeasideCache::groupProperty() { return cacheConfig()->groupProperty(); } int SeasideCache::contactId(const QContact &contact) { quint32 internal = internalId(contact); return static_cast(internal); } SeasideCache::CacheItem *SeasideCache::itemById(const QContactId &id, bool requireComplete) { 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]); item->iid = iid; item->contactState = ContactAbsent; item->contact.setId(id); } if (requireComplete) { ensureCompletion(item); } return item; } SeasideCache::CacheItem *SeasideCache::itemById(int id, bool requireComplete) { if (id != 0) { QContactId contactId(apiId(static_cast(id))); if (!contactId.isNull()) { return itemById(contactId, requireComplete); } } return 0; } SeasideCache::CacheItem *SeasideCache::existingItem(const QContactId &id) { return existingItem(internalId(id)); } SeasideCache::CacheItem *SeasideCache::existingItem(quint32 iid) { QHash::iterator it = instancePtr->m_people.find(iid); return it != instancePtr->m_people.end() ? &(*it) : 0; } QContact SeasideCache::contactById(const QContactId &id) { quint32 iid = internalId(id); return instancePtr->m_people.value(iid, CacheItem()).contact; } void SeasideCache::ensureCompletion(CacheItem *cacheItem) { if (cacheItem->contactState < ContactRequested) { refreshContact(cacheItem); } } void SeasideCache::refreshContact(CacheItem *cacheItem) { cacheItem->contactState = ContactRequested; instancePtr->m_changedContacts.append(cacheItem->apiId()); instancePtr->fetchContacts(); } SeasideCache::CacheItem *SeasideCache::itemByPhoneNumber(const QString &number, bool requireComplete) { const QString normalized(normalizePhoneNumber(number)); if (normalized.isEmpty()) return 0; const QChar plus(QChar::fromLatin1('+')); if (normalized.startsWith(plus)) { // See if there is a match for the complete form of this number if (CacheItem *item = instancePtr->itemMatchingPhoneNumber(normalized, normalized, requireComplete)) { return item; } } const QString minimized(minimizePhoneNumber(normalized)); if (((instancePtr->m_fetchTypes & SeasideCache::FetchPhoneNumber) == 0) && !instancePtr->m_resolvedPhoneNumbers.contains(minimized)) { // We haven't previously queried this number, so there may be more matches than any // that we already have cached; return 0 to force a query return 0; } return instancePtr->itemMatchingPhoneNumber(minimized, normalized, requireComplete); } SeasideCache::CacheItem *SeasideCache::itemByEmailAddress(const QString &email, bool requireComplete) { if (email.trimmed().isEmpty()) return 0; QHash::const_iterator it = instancePtr->m_emailAddressIds.find(email.toLower()); if (it != instancePtr->m_emailAddressIds.end()) return itemById(*it, requireComplete); return 0; } SeasideCache::CacheItem *SeasideCache::itemByOnlineAccount(const QString &localUid, const QString &remoteUid, bool requireComplete) { if (localUid.trimmed().isEmpty() || remoteUid.trimmed().isEmpty()) return 0; QPair address = qMakePair(localUid, remoteUid.toLower()); QHash, quint32>::const_iterator it = instancePtr->m_onlineAccountIds.find(address); if (it != instancePtr->m_onlineAccountIds.end()) return itemById(*it, requireComplete); return 0; } SeasideCache::CacheItem *SeasideCache::resolvePhoneNumber(ResolveListener *listener, const QString &number, bool requireComplete) { // Ensure the cache has been instantiated instance(); CacheItem *item = itemByPhoneNumber(number, requireComplete); if (!item) { // Don't bother trying to resolve an invalid number const QString normalized(normalizePhoneNumber(number)); if (!normalized.isEmpty()) { instancePtr->resolveAddress(listener, QString(), number, requireComplete); } else { // Report this address is unknown ResolveData data; data.second = number; data.listener = listener; instancePtr->m_unknownResolveAddresses.append(data); instancePtr->requestUpdate(); } } else if (requireComplete) { ensureCompletion(item); } return item; } SeasideCache::CacheItem *SeasideCache::resolveEmailAddress(ResolveListener *listener, const QString &address, bool requireComplete) { // Ensure the cache has been instantiated instance(); CacheItem *item = itemByEmailAddress(address, requireComplete); if (!item) { instancePtr->resolveAddress(listener, address, QString(), requireComplete); } else if (requireComplete) { ensureCompletion(item); } return item; } SeasideCache::CacheItem *SeasideCache::resolveOnlineAccount(ResolveListener *listener, const QString &localUid, const QString &remoteUid, bool requireComplete) { // Ensure the cache has been instantiated instance(); CacheItem *item = itemByOnlineAccount(localUid, remoteUid, requireComplete); if (!item) { instancePtr->resolveAddress(listener, localUid, remoteUid, requireComplete); } else if (requireComplete) { ensureCompletion(item); } return item; } QContactId SeasideCache::selfContactId() { return manager()->selfContactId(); } void SeasideCache::requestUpdate() { if (!m_updatesPending) { QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); m_updatesPending = true; } } bool SeasideCache::saveContact(const QContact &contact) { QContactId id = apiId(contact); if (validId(id)) { instancePtr->m_contactsToSave[id] = contact; instancePtr->contactDataChanged(internalId(id)); } else { instancePtr->m_contactsToCreate.append(contact); } instancePtr->requestUpdate(); return true; } void SeasideCache::contactDataChanged(quint32 iid) { instancePtr->contactDataChanged(iid, FilterFavorites); instancePtr->contactDataChanged(iid, FilterOnline); instancePtr->contactDataChanged(iid, FilterAll); } void SeasideCache::contactDataChanged(quint32 iid, FilterType filter) { int row = contactIndex(iid, filter); if (row != -1) { QList &models = m_models[filter]; for (int i = 0; i < models.count(); ++i) { models.at(i)->sourceDataChanged(row, row); } } } bool SeasideCache::removeContact(const QContact &contact) { QContactId id = apiId(contact); if (!validId(id)) return false; instancePtr->m_contactsToRemove.append(id); quint32 iid = internalId(id); instancePtr->removeContactData(iid, FilterFavorites); instancePtr->removeContactData(iid, FilterOnline); instancePtr->removeContactData(iid, FilterAll); instancePtr->requestUpdate(); return true; } void SeasideCache::removeContactData(quint32 iid, FilterType filter) { int row = contactIndex(iid, filter); 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].removeAt(row); if (filter == FilterAll) { const QString group(displayLabelGroup(existingItem(iid))); QSet modifiedDisplayLabelGroups; removeFromContactDisplayLabelGroup(iid, group, &modifiedDisplayLabelGroups); notifyDisplayLabelGroupsChanged(modifiedDisplayLabelGroups); } for (int i = 0; i < models.count(); ++i) models.at(i)->sourceItemsRemoved(); } bool SeasideCache::fetchConstituents(const QContact &contact) { QContactId personId(contact.id()); if (!validId(personId)) return false; if (!instancePtr->m_contactsToFetchConstituents.contains(personId)) { instancePtr->m_contactsToFetchConstituents.append(personId); instancePtr->requestUpdate(); } return true; } bool SeasideCache::fetchMergeCandidates(const QContact &contact) { QContactId personId(contact.id()); if (!validId(personId)) return false; if (!instancePtr->m_contactsToFetchCandidates.contains(personId)) { instancePtr->m_contactsToFetchCandidates.append(personId); instancePtr->requestUpdate(); } return true; } const QList *SeasideCache::contacts(FilterType type) { return &instancePtr->m_contacts[type]; } bool SeasideCache::isPopulated(FilterType filterType) { return instancePtr->m_populated & (1 << filterType); } QString SeasideCache::primaryName(const QString &firstName, const QString &lastName) { if (firstName.isEmpty() && lastName.isEmpty()) { return QString(); } const bool familyNameFirst(displayLabelOrder() == LastNameFirst || nameScriptImpliesFamilyFirst(firstName, lastName)); return familyNameFirst ? lastName : firstName; } QString SeasideCache::secondaryName(const QString &firstName, const QString &lastName) { const bool familyNameFirst(displayLabelOrder() == LastNameFirst || nameScriptImpliesFamilyFirst(firstName, lastName)); return familyNameFirst ? firstName : lastName; } static bool needsSpaceBetweenNames(const QString &first, const QString &second) { if (first.isEmpty() || second.isEmpty()) { return false; } return first[first.length()-1].script() != QChar::Script_Han || second[0].script() != QChar::Script_Han; } template void updateNameDetail(F1 getter, F2 setter, QContactName *nameDetail, const QString &value) { QString existing((nameDetail->*getter)()); if (!existing.isEmpty()) { existing.append(QChar::fromLatin1(' ')); } (nameDetail->*setter)(existing + value); } void SeasideCache::decomposeDisplayLabel(const QString &formattedDisplayLabel, QContactName *nameDetail) { if (!translator) { engEnTranslator = new QTranslator(qApp); engEnTranslator->load(QString::fromLatin1("libcontacts_eng_en"), QString::fromLatin1("/usr/share/translations")); qApp->installTranslator(engEnTranslator); translator = new QTranslator(qApp); translator->load(QLocale(), QString::fromLatin1("libcontacts"), QString::fromLatin1("-"), QString::fromLatin1("/usr/share/translations")); qApp->installTranslator(translator); } // Try to parse the structure from the formatted name // TODO: Use MBreakIterator for localized splitting QStringList tokens(formattedDisplayLabel.split(QChar::fromLatin1(' '), QString::SkipEmptyParts)); if (tokens.count() >= 2) { QString format; if (tokens.count() == 2) { //: Format string for allocating 2 tokens to name parts - 2 characters from the set [FMLPS] //% "FL" format = qtTrId("libcontacts_name_structure_2_tokens"); } else if (tokens.count() == 3) { //: Format string for allocating 3 tokens to name parts - 3 characters from the set [FMLPS] //% "FML" format = qtTrId("libcontacts_name_structure_3_tokens"); } else if (tokens.count() > 3) { //: Format string for allocating 4 tokens to name parts - 4 characters from the set [FMLPS] //% "FFML" format = qtTrId("libcontacts_name_structure_4_tokens"); // Coalesce the leading tokens together to limit the possibilities int excess = tokens.count() - 4; if (excess > 0) { QString first(tokens.takeFirst()); while (--excess >= 0) { QString nextNamePart = tokens.takeFirst(); first += (needsSpaceBetweenNames(first, nextNamePart) ? QChar::fromLatin1(' ') : QString()) + nextNamePart; } tokens.prepend(first); } } if (format.length() != tokens.length()) { qWarning() << "Invalid structure format for" << tokens.count() << "tokens:" << format; } else { foreach (const QChar &part, format) { const QString token(tokens.takeFirst()); switch (part.toUpper().toLatin1()) { case 'F': updateNameDetail(&QContactName::firstName, &QContactName::setFirstName, nameDetail, token); break; case 'M': updateNameDetail(&QContactName::middleName, &QContactName::setMiddleName, nameDetail, token); break; case 'L': updateNameDetail(&QContactName::lastName, &QContactName::setLastName, nameDetail, token); break; case 'P': updateNameDetail(&QContactName::prefix, &QContactName::setPrefix, nameDetail, token); break; case 'S': updateNameDetail(&QContactName::suffix, &QContactName::setSuffix, nameDetail, token); break; default: qWarning() << "Invalid structure format character:" << part; } } } } } // small helper to avoid inconvenience QString SeasideCache::generateDisplayLabel(const QContact &contact, DisplayLabelOrder order) { QContactName name = contact.detail(); QString displayLabel; QString nameStr1(name.firstName()); QString nameStr2(name.lastName()); const bool familyNameFirst(order == LastNameFirst || nameScriptImpliesFamilyFirst(nameStr1, nameStr2)); if (familyNameFirst) { nameStr1 = name.lastName(); nameStr2 = name.firstName(); } if (!nameStr1.isEmpty()) displayLabel.append(nameStr1); if (!nameStr2.isEmpty()) { if (needsSpaceBetweenNames(nameStr1, nameStr2)) { displayLabel.append(" "); } displayLabel.append(nameStr2); } if (!displayLabel.isEmpty()) { return displayLabel; } // Try to generate a label from the contact details, in our preferred order 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().isEmpty()) { 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().isEmpty()) { return gp.nickname(); } } foreach (const QContactPresence& presence, contact.details()) { if (!presence.nickname().isEmpty()) { return presence.nickname(); } } // If this contact has organization details but no name, it probably represents that organization directly QContactOrganization company = contact.detail(); if (!company.name().isEmpty()) { return company.name(); } // If none of the detail fields provides a label, fallback to the backend's label string, in // preference to using any of the addressing details directly const QString displayLabel = contact.detail().label(); if (!displayLabel.isEmpty()) { return displayLabel; } foreach (const QContactOnlineAccount& account, contact.details()) { if (!account.accountUri().isEmpty()) { return account.accountUri(); } } foreach (const QContactEmailAddress& email, contact.details()) { if (!email.emailAddress().isEmpty()) { return email.emailAddress(); } } foreach (const QContactPhoneNumber& phone, contact.details()) { if (!phone.number().isEmpty()) return phone.number(); } return QString(); } static bool avatarUrlWithMetadata(const QContact &contact, QUrl &matchingUrl, const QString &metadataFragment = QString()) { static const QString coverMetadata(QString::fromLatin1("cover")); static const QString localMetadata(QString::fromLatin1("local")); static const QString fileScheme(QString::fromLatin1("file")); int fallbackScore = 0; QUrl fallbackUrl; QList avatarDetails = contact.details(); for (int i = 0; i < avatarDetails.size(); ++i) { const QContactAvatar &av(avatarDetails[i]); const QString metadata(av.value(QContactAvatar__FieldAvatarMetadata).toString()); if (!metadataFragment.isEmpty() && !metadata.startsWith(metadataFragment)) { // this avatar doesn't match the metadata requirement. ignore it. continue; } const QUrl avatarImageUrl = av.imageUrl(); if (metadata == localMetadata) { // We have a local avatar record - use the image it specifies matchingUrl = avatarImageUrl; return true; } else { // queue it as fallback if its score is better than the best fallback seen so far. // prefer local file system images over remote urls, and prefer normal avatars // over "cover" (background image) type avatars. const bool remote(!avatarImageUrl.scheme().isEmpty() && avatarImageUrl.scheme() != fileScheme); int score = remote ? 3 : 4; if (metadata == coverMetadata) { score -= 2; } if (score > fallbackScore) { fallbackUrl = avatarImageUrl; fallbackScore = score; } } } if (!fallbackUrl.isEmpty()) { matchingUrl = fallbackUrl; return true; } // no matching avatar image. return false; } QUrl SeasideCache::filteredAvatarUrl(const QContact &contact, const QStringList &metadataFragments) { QUrl matchingUrl; if (metadataFragments.isEmpty()) { if (avatarUrlWithMetadata(contact, matchingUrl)) { return matchingUrl; } } foreach (const QString &metadataFragment, metadataFragments) { if (avatarUrlWithMetadata(contact, matchingUrl, metadataFragment)) { return matchingUrl; } } return QUrl(); } QString SeasideCache::normalizePhoneNumber(const QString &input, bool validate) { QtContactsSqliteExtensions::NormalizePhoneNumberFlags normalizeFlags(QtContactsSqliteExtensions::KeepPhoneNumberDialString); if (validate) { // If the number if not valid, return empty normalizeFlags |= QtContactsSqliteExtensions::ValidatePhoneNumber; } return QtContactsSqliteExtensions::normalizePhoneNumber(input, normalizeFlags); } QString SeasideCache::minimizePhoneNumber(const QString &input, bool validate) { // TODO: use a configuration variable to make this configurable const int maxCharacters = QtContactsSqliteExtensions::DefaultMaximumPhoneNumberCharacters; QString validated(normalizePhoneNumber(input, validate)); if (validated.isEmpty()) return validated; return QtContactsSqliteExtensions::minimizePhoneNumber(validated, maxCharacters); } static QContactFilter filterForMergeCandidates(const QContact &contact) { // Find any contacts that we might merge with the supplied contact QContactFilter rv; QContactName name(contact.detail()); const QString firstName(name.firstName().trimmed()); const QString lastName(name.lastName().trimmed()); if (firstName.isEmpty() && lastName.isEmpty()) { // Use the displayLabel to match with const QString label(contact.detail().label().trimmed()); if (!label.isEmpty()) { // 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 (firstName.length() > 3) { // Also look for shortened forms of this name, such as 'Timothy' => 'Tim' QContactDetailFilter shortFilter; setDetailType(shortFilter, QContactName::FieldFirstName); shortFilter.setMatchFlags(QContactFilter::MatchStartsWith | QContactFilter::MatchFixedString); shortFilter.setValue(firstName.left(3)); rv = rv | shortFilter; } } 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 &phoneNumber, contact.details()) { const QString number(phoneNumber.number().trimmed()); if (number.isEmpty()) continue; rv = rv | QContactPhoneNumber::match(number); } // Email address match foreach (const QContactEmailAddress &emailAddress, contact.details()) { QString address(emailAddress.emailAddress().trimmed()); int index = address.indexOf(QChar::fromLatin1('@')); if (index > 0) { // Match any address that is the same up to the @ symbol address = address.left(index).trimmed(); } if (address.isEmpty()) continue; 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().trimmed()); 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).trimmed(); } if (uri.isEmpty()) continue; QContactDetailFilter filter; setDetailType(filter, QContactOnlineAccount::FieldAccountUri); filter.setMatchFlags((index > 0 ? QContactFilter::MatchStartsWith : QContactFilter::MatchExactly) | QContactFilter::MatchFixedString); filter.setValue(uri); rv = rv | filter; } // If we know the contact gender rule out mismatches QContactGender gender(contact.detail()); if (gender.gender() != QContactGender::GenderUnspecified) { QContactDetailFilter matchFilter; setDetailType(matchFilter, QContactGender::FieldGender); matchFilter.setValue(gender.gender()); QContactDetailFilter unknownFilter; setDetailType(unknownFilter, QContactGender::FieldGender); unknownFilter.setValue(QContactGender::GenderUnspecified); rv = rv & (matchFilter | unknownFilter); } // Only return aggregate contact IDs return rv & aggregateFilter(); } void SeasideCache::startRequest(bool *idleProcessing) { bool requestPending = false; // Test these conditions in priority order // 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 { m_fetchRequest.setFilter(favoriteFilter()); m_fetchRequest.setFetchHint(favoriteFetchHint(m_fetchTypes)); m_fetchRequest.setSorting(m_sortOrder); qDebug() << "Starting favorites query at" << m_timer.elapsed() << "ms"; m_fetchRequest.start(); 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()); m_fetchRequest.setFetchHint(metadataFetchHint(m_fetchTypes)); m_fetchRequest.setSorting(m_sortOrder); qDebug() << "Starting metadata query at" << m_timer.elapsed() << "ms"; 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()); m_fetchRequest.setFetchHint(onlineFetchHint(m_fetchTypes | SeasideCache::FetchAccountUri)); m_fetchRequest.setSorting(m_onlineSortOrder); qDebug() << "Starting online query at" << m_timer.elapsed() << "ms"; m_fetchRequest.start(); m_fetchProcessedCount = 0; m_populating = true; } } // Do nothing else until the cache is populated return; } if (m_refreshRequired) { // We can't refresh the IDs til all contacts have been appended if (m_contactsToAppend.isEmpty()) { if (m_contactIdRequest.isActive()) { requestPending = true; } else { m_refreshRequired = false; m_syncFilter = FilterFavorites; m_contactIdRequest.setFilter(favoriteFilter()); m_contactIdRequest.setSorting(m_sortOrder); m_contactIdRequest.start(); } } } else if (m_syncFilter == FilterAll || m_syncFilter == FilterOnline) { if (m_contactIdRequest.isActive()) { requestPending = true; } else { if (m_syncFilter == FilterAll) { m_contactIdRequest.setFilter(allFilter()); m_contactIdRequest.setSorting(m_sortOrder); } else if (m_syncFilter == FilterOnline) { m_contactIdRequest.setFilter(onlineFilter()); m_contactIdRequest.setSorting(m_onlineSortOrder); } m_contactIdRequest.start(); } } 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()) { if (!m_relationshipSaveRequest.isActive()) { m_relationshipSaveRequest.setRelationships(m_relationshipsToSave); m_relationshipSaveRequest.start(); m_relationshipsToSave.clear(); } } if (!m_relationshipsToRemove.isEmpty()) { if (!m_relationshipRemoveRequest.isActive()) { m_relationshipRemoveRequest.setRelationships(m_relationshipsToRemove); m_relationshipRemoveRequest.start(); m_relationshipsToRemove.clear(); } } // do not proceed with other tasks, even if we couldn't start a new request return; } if (!m_contactsToRemove.isEmpty()) { if (m_removeRequest.isActive()) { requestPending = true; } else { m_removeRequest.setContactIds(m_contactsToRemove); m_removeRequest.start(); m_contactsToRemove.clear(); } } if (!m_contactsToCreate.isEmpty() || !m_contactsToSave.isEmpty()) { if (m_saveRequest.isActive()) { requestPending = true; } else { 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(); } } if (!m_constituentIds.isEmpty()) { if (m_fetchByIdRequest.isActive()) { requestPending = true; } else { // Fetch the constituent information (even if they're already in the // cache, because we don't update non-aggregates on change notifications) m_fetchByIdRequest.setIds(m_constituentIds.toList()); m_fetchByIdRequest.start(); m_fetchByIdProcessedCount = 0; } } if (!m_contactsToFetchConstituents.isEmpty()) { if (m_relationshipsFetchRequest.isActive()) { requestPending = true; } else { QContactId aggregateId = m_contactsToFetchConstituents.first(); // Find the constituents of this contact QContact first; first.setId(aggregateId); m_relationshipsFetchRequest.setFirst(first); m_relationshipsFetchRequest.setRelationshipType(QContactRelationship::Aggregates()); m_relationshipsFetchRequest.start(); } } if (!m_contactsToFetchCandidates.isEmpty()) { if (m_contactIdRequest.isActive()) { requestPending = true; } else { QContactId contactId(m_contactsToFetchCandidates.first()); const QContact contact(contactById(contactId)); // Find candidates to merge with this contact m_contactIdRequest.setFilter(filterForMergeCandidates(contact)); m_contactIdRequest.setSorting(m_sortOrder); m_contactIdRequest.start(); } } if (m_fetchTypes) { quint32 unfetchedTypes = m_fetchTypes & ~m_dataTypesFetched & SeasideCache::FetchTypesMask; if (unfetchedTypes) { if (m_fetchRequest.isActive()) { requestPending = true; } else { // Fetch the missing data types for whichever contacts need them m_fetchRequest.setSorting(m_sortOrder); if (unfetchedTypes == SeasideCache::FetchPhoneNumber) { m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasPhoneNumber, QContactFilter::MatchContains)); } else if (unfetchedTypes == SeasideCache::FetchEmailAddress) { m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasEmailAddress, QContactFilter::MatchContains)); } else if (unfetchedTypes == SeasideCache::FetchAccountUri) { m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasOnlineAccount, QContactFilter::MatchContains)); m_fetchRequest.setSorting(m_onlineSortOrder); } else { m_fetchRequest.setFilter(allFilter()); } m_fetchRequest.setFetchHint(extendedMetadataFetchHint(unfetchedTypes)); m_fetchRequest.start(); m_fetchProcessedCount = 0; m_dataTypesFetched |= unfetchedTypes; } } } if (!m_changedContacts.isEmpty()) { if (m_fetchRequest.isActive()) { requestPending = true; } else if (!m_displayOff) { // If we request too many IDs we will exceed the SQLite bound variables limit // The actual limit is over 800, but we should reduce further to increase interactivity const int maxRequestIds = 200; QContactIdFilter filter; if (m_changedContacts.count() > maxRequestIds) { filter.setIds(m_changedContacts.mid(0, maxRequestIds)); m_changedContacts = m_changedContacts.mid(maxRequestIds); } else { 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; } } if (!m_presenceChangedContacts.isEmpty()) { if (m_fetchRequest.isActive()) { requestPending = true; } else if (!m_displayOff) { const int maxRequestIds = 200; QContactIdFilter filter; if (m_presenceChangedContacts.count() > maxRequestIds) { filter.setIds(m_presenceChangedContacts.mid(0, maxRequestIds)); m_presenceChangedContacts = m_presenceChangedContacts.mid(maxRequestIds); } else { filter.setIds(m_presenceChangedContacts); m_presenceChangedContacts.clear(); } m_fetchRequest.setFilter(filter & aggregateFilter()); m_fetchRequest.setFetchHint(presenceFetchHint()); m_fetchRequest.setSorting(QList()); m_fetchRequest.start(); m_fetchProcessedCount = 0; } } if (requestPending) { // Don't proceed if we were unable to start one of the above requests return; } // No remaining work is pending - do we have any background task requests? if (m_extraFetchTypes) { quint32 unfetchedTypes = m_extraFetchTypes & ~m_dataTypesFetched & SeasideCache::FetchTypesMask; if (unfetchedTypes) { if (m_fetchRequest.isActive()) { requestPending = true; } else { quint32 fetchType = 0; // Load extra data items that we want to be able to search on, if not already fetched if (unfetchedTypes & SeasideCache::FetchOrganization) { // since this uses allFilter(), might as well grab // all the missing detail types fetchType = unfetchedTypes; m_fetchRequest.setFilter(allFilter()); } else if (unfetchedTypes & SeasideCache::FetchPhoneNumber) { fetchType = SeasideCache::FetchPhoneNumber; m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasPhoneNumber, QContactFilter::MatchContains)); } else if (unfetchedTypes & SeasideCache::FetchEmailAddress) { fetchType = SeasideCache::FetchEmailAddress; m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasEmailAddress, QContactFilter::MatchContains)); } else { fetchType = SeasideCache::FetchAccountUri; m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasOnlineAccount, QContactFilter::MatchContains)); } m_fetchRequest.setFetchHint(extendedMetadataFetchHint(fetchType)); m_fetchRequest.start(); m_fetchProcessedCount = 0; m_dataTypesFetched |= fetchType; } } } if (!requestPending) { // Nothing to do - proceeed with idle processing *idleProcessing = true; } } bool SeasideCache::event(QEvent *event) { if (event->type() != QEvent::UpdateRequest) return QObject::event(event); m_updatesPending = false; bool idleProcessing = false; startRequest(&idleProcessing); // Report any unknown addresses while (!m_unknownResolveAddresses.isEmpty()) { const ResolveData &resolve = m_unknownResolveAddresses.takeFirst(); resolve.listener->addressResolved(resolve.first, resolve.second, 0); } if (!m_contactsToAppend.isEmpty() || !m_contactsToUpdate.isEmpty()) { applyPendingContactUpdates(); // Send another event to trigger further processing requestUpdate(); return true; } if (idleProcessing) { // Remove expired contacts when all other activity has been processed if (!m_expiredContacts.isEmpty()) { QList removeIds; QHash::const_iterator it = m_expiredContacts.constBegin(), end = m_expiredContacts.constEnd(); for ( ; it != end; ++it) { if (it.value() < 0) { quint32 iid = internalId(it.key()); removeIds.append(iid); } } m_expiredContacts.clear(); QSet modifiedGroups; // Before removal, ensure none of these contacts are in name groups foreach (quint32 iid, removeIds) { if (CacheItem *item = existingItem(iid)) { removeFromContactDisplayLabelGroup(item->iid, item->displayLabelGroup, &modifiedGroups); } } notifyDisplayLabelGroupsChanged(modifiedGroups); // Remove the contacts from the cache foreach (quint32 iid, removeIds) { QHash::iterator cacheItem = m_people.find(iid); if (cacheItem != m_people.end()) { delete cacheItem->itemData; m_people.erase(cacheItem); } } } } return true; } void SeasideCache::timerEvent(QTimerEvent *event) { if (event->timerId() == m_fetchTimer.timerId()) { // If the display is off, defer these fetches until they can be seen if (!m_displayOff) { fetchContacts(); } } if (event->timerId() == m_expiryTimer.timerId()) { m_expiryTimer.stop(); instancePtr = 0; deleteLater(); } } void SeasideCache::contactsAdded(const QList &ids) { // These additions may change address resolutions, so we may need to process them const bool relevant(m_keepPopulated || !instancePtr->m_changeListeners.isEmpty()); if (relevant) { updateContacts(ids, &m_changedContacts); } } void SeasideCache::contactsChanged(const QList &ids) { if (m_keepPopulated) { updateContacts(ids, &m_changedContacts); } else { // Update these contacts if they're already in the cache QList presentIds; foreach (const QContactId &id, ids) { if (existingItem(id)) { presentIds.append(id); } } updateContacts(presentIds, &m_changedContacts); } } void SeasideCache::contactsPresenceChanged(const QList &ids) { if (m_keepPopulated) { updateContacts(ids, &m_presenceChangedContacts); } else { // Update these contacts if they're already in the cache QList presentIds; foreach (const QContactId &id, ids) { if (existingItem(id)) { presentIds.append(id); } } updateContacts(presentIds, &m_presenceChangedContacts); } } void SeasideCache::contactsRemoved(const QList &ids) { QList presentIds; foreach (const QContactId &id, ids) { if (CacheItem *item = existingItem(id)) { // Report this item is about to be removed foreach (ChangeListener *listener, m_changeListeners) { listener->itemAboutToBeRemoved(item); } ItemListener *listener = item->listeners; while (listener) { ItemListener *next = listener->next; listener->itemAboutToBeRemoved(item); listener = next; } item->listeners = 0; // Remove the links to addressible details updateContactIndexing(item->contact, QContact(), item->iid, QSet(), item); if (!m_keepPopulated) { presentIds.append(id); } } } if (m_keepPopulated) { m_refreshRequired = true; } else { // Remove these contacts if they're already in the cache; they won't be removed by syncing foreach (const QContactId &id, presentIds) { m_expiredContacts[id] += -1; } } requestUpdate(); } void SeasideCache::dataChanged() { 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, &m_changedContacts); // The backend will automatically update, but notify the models of the change. for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (int j = 0; j < models.count(); ++j) { ListModel *model = models.at(j); model->updateGroupProperty(); model->sourceItemsChanged(); model->sourceDataChanged(0, m_contacts[i].size()); } } // Update the sorted list order m_refreshRequired = true; requestUpdate(); } 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, QList *updateList) { // 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; updateList->append(contactIds); // If the display is off, defer fetching these changes if (!m_displayOff) { 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::updateCache(CacheItem *item, const QContact &contact, bool partialFetch, bool initialInsert) { if (item->contactState < ContactRequested) { item->contactState = partialFetch ? ContactPartial : ContactComplete; } else if (!partialFetch) { // Don't set a complete contact back after a partial update item->contactState = ContactComplete; } // Preserve the value of HasValidOnlineAccount, which is held only in the cache const int hasValidFlagValue = item->statusFlags & HasValidOnlineAccount; item->statusFlags = contact.detail().flagsValue() | hasValidFlagValue; if (item->itemData) { item->itemData->updateContact(contact, &item->contact, item->contactState); } else { item->contact = contact; } item->displayLabel = generateDisplayLabel(item->contact, displayLabelOrder()); item->displayLabelGroup = contact.detail().value(QContactDisplayLabel__FieldLabelGroup).toString(); if (!initialInsert) { reportItemUpdated(item); } } void SeasideCache::reportItemUpdated(CacheItem *item) { // Report the change to this contact ItemListener *listener = item->listeners; while (listener) { listener->itemUpdated(item); listener = listener->next; } foreach (ChangeListener *listener, m_changeListeners) { listener->itemUpdated(item); } } void SeasideCache::resolveUnknownAddresses(const QString &first, const QString &second, CacheItem *item) { QList::iterator it = instancePtr->m_unknownAddresses.begin(); while (it != instancePtr->m_unknownAddresses.end()) { bool resolved = false; if (first == QString()) { // This is a phone number - test in normalized form resolved = (it->first == QString()) && (it->compare == second); } else if (second == QString()) { // Email address - compare in lowercased form resolved = (it->compare == first) && (it->second == QString()); } else { // Online account - compare URI in lowercased form resolved = (it->first == first) && (it->compare == second); } if (resolved) { // Inform the listener of resolution it->listener->addressResolved(it->first, it->second, item); // Do we need to request completion as well? if (it->requireComplete) { ensureCompletion(item); } it = instancePtr->m_unknownAddresses.erase(it); } else { ++it; } } } bool SeasideCache::updateContactIndexing(const QContact &oldContact, const QContact &contact, quint32 iid, const QSet &queryDetailTypes, CacheItem *item) { bool modified = false; QSet oldAddresses; if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { // Addresses which are no longer in the contact should be de-indexed foreach (const QContactPhoneNumber &phoneNumber, oldContact.details()) { foreach (const StringPair &address, addressPairs(phoneNumber)) { if (validAddressPair(address)) oldAddresses.insert(address); } } // Update our address indexes for any address details in this contact foreach (const QContactPhoneNumber &phoneNumber, contact.details()) { foreach (const StringPair &address, addressPairs(phoneNumber)) { if (!validAddressPair(address)) continue; if (!oldAddresses.remove(address)) { // This address was not previously recorded modified = true; resolveUnknownAddresses(address.first, address.second, item); } if (!m_phoneNumberIds.contains(address.second, iid)) m_phoneNumberIds.insert(address.second, iid); } } // Remove any addresses no longer available for this contact if (!oldAddresses.isEmpty()) { modified = true; foreach (const StringPair &address, oldAddresses) { m_phoneNumberIds.remove(address.second, iid); } oldAddresses.clear(); } } if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { foreach (const QContactEmailAddress &emailAddress, oldContact.details()) { const StringPair address(addressPair(emailAddress)); if (validAddressPair(address)) oldAddresses.insert(address); } foreach (const QContactEmailAddress &emailAddress, contact.details()) { const StringPair address(addressPair(emailAddress)); if (!validAddressPair(address)) continue; if (!oldAddresses.remove(address)) { modified = true; resolveUnknownAddresses(address.first, address.second, item); } m_emailAddressIds[address.first] = iid; } if (!oldAddresses.isEmpty()) { modified = true; foreach (const StringPair &address, oldAddresses) { m_emailAddressIds.remove(address.first); } oldAddresses.clear(); } } if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { foreach (const QContactOnlineAccount &account, oldContact.details()) { const StringPair address(addressPair(account)); if (validAddressPair(address)) oldAddresses.insert(address); } // Keep track of whether this contact has any valid IM accounts bool hasValid = false; foreach (const QContactOnlineAccount &account, contact.details()) { const StringPair address(addressPair(account)); if (!validAddressPair(address)) continue; if (!oldAddresses.remove(address)) { modified = true; resolveUnknownAddresses(address.first, address.second, item); } m_onlineAccountIds[address] = iid; hasValid = true; } if (hasValid) { item->statusFlags |= HasValidOnlineAccount; } else { item->statusFlags &= ~HasValidOnlineAccount; } if (!oldAddresses.isEmpty()) { modified = true; foreach (const StringPair &address, oldAddresses) { m_onlineAccountIds.remove(address); } oldAddresses.clear(); } } return modified; } void updateDetailsFromCache(QContact &contact, SeasideCache::CacheItem *item, const QSet &queryDetailTypes) { // Copy any existing detail types that are in the current record to the new instance foreach (const QContactDetail &existing, item->contact.details()) { const QContactDetail::DetailType existingType(detailType(existing)); static const DetailList contactsTableTypes(contactsTableDetails()); // The queried contact already contains any types in the contacts table, and those // types explicitly fetched by the query if (!queryDetailTypes.contains(existingType) && !contactsTableTypes.contains(existingType)) { QContactDetail copy(existing); contact.saveDetail(©); } } } void SeasideCache::contactsAvailable() { QContactAbstractRequest *request = static_cast(sender()); QList contacts; QContactFetchHint fetchHint; if (request == &m_fetchByIdRequest) { contacts = m_fetchByIdRequest.contacts(); if (m_fetchByIdProcessedCount) { contacts = contacts.mid(m_fetchByIdProcessedCount); } m_fetchByIdProcessedCount += contacts.count(); fetchHint = m_fetchByIdRequest.fetchHint(); } else { contacts = m_fetchRequest.contacts(); if (m_fetchProcessedCount) { contacts = contacts.mid(m_fetchProcessedCount); } m_fetchProcessedCount += contacts.count(); fetchHint = m_fetchRequest.fetchHint(); } if (contacts.isEmpty()) return; QSet queryDetailTypes = detailTypesHint(fetchHint).toSet(); 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)); QHash, QList > >::iterator it = m_contactsToAppend.find(type); if (it != m_contactsToAppend.end()) { // All populate queries have the same detail types, so we can append this list to the existing one it.value().second.append(contacts); } else { m_contactsToAppend.insert(type, qMakePair(queryDetailTypes, contacts)); } requestUpdate(); } else { if (contacts.count() == 1 || request == &m_fetchByIdRequest) { // Process these results immediately 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(); for ( ; it != end; ++it) { if ((*it).first == queryDetailTypes) { (*it).second.append(contacts); break; } } if (it == end) { m_contactsToUpdate.append(qMakePair(queryDetailTypes, contacts)); } requestUpdate(); } } } void SeasideCache::applyPendingContactUpdates() { if (!m_contactsToAppend.isEmpty()) { // Insert the contacts in the order they're requested QHash, QList > >::iterator end = m_contactsToAppend.end(), it = end; if ((it = m_contactsToAppend.find(FilterFavorites)) != end) { } else if ((it = m_contactsToAppend.find(FilterAll)) != end) { } else { it = m_contactsToAppend.find(FilterOnline); } Q_ASSERT(it != end); FilterType type = it.key(); QSet &detailTypes((*it).first); const bool partialFetch = !detailTypes.isEmpty(); QList &appendedContacts((*it).second); const int maxBatchSize = 200; const int minBatchSize = 50; if (appendedContacts.count() < maxBatchSize) { // For a small number of contacts, append all at once appendContacts(appendedContacts, type, partialFetch, detailTypes); appendedContacts.clear(); } else { // Append progressively in batches appendContacts(appendedContacts.mid(0, minBatchSize), type, partialFetch, detailTypes); appendedContacts = appendedContacts.mid(minBatchSize); } if (appendedContacts.isEmpty()) { m_contactsToAppend.erase(it); // This list has been processed - have we finished populating the group? if (type == FilterFavorites && (m_populateProgress != FetchFavorites)) { makePopulated(FilterFavorites); qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; } else if (type == FilterAll && (m_populateProgress != FetchMetadata)) { makePopulated(FilterNone); makePopulated(FilterAll); qDebug() << "All queried in" << m_timer.elapsed() << "ms"; } else if (type == FilterOnline && (m_populateProgress != FetchOnline)) { makePopulated(FilterOnline); qDebug() << "Online queried in" << m_timer.elapsed() << "ms"; } } } else { QList, QList > >::iterator it = m_contactsToUpdate.begin(); QSet &detailTypes((*it).first); // 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(), detailTypes); if (updatedContacts.isEmpty()) { m_contactsToUpdate.erase(it); } } } void SeasideCache::applyContactUpdates(const QList &contacts, const QSet &queryDetailTypes) { QSet modifiedGroups; const bool partialFetch = !queryDetailTypes.isEmpty(); foreach (QContact contact, contacts) { quint32 iid = internalId(contact); QString oldDisplayLabelGroup; QString oldDisplayLabel; CacheItem *item = existingItem(iid); if (!item) { // We haven't seen this contact before item = &(m_people[iid]); item->iid = iid; } else { oldDisplayLabelGroup = item->displayLabelGroup; oldDisplayLabel = item->displayLabel; if (partialFetch) { // Update our new instance with any details not returned by the current query updateDetailsFromCache(contact, item, queryDetailTypes); } } bool roleDataChanged = false; // This is a simplification of reality, should we test more changes? if (!partialFetch || queryDetailTypes.contains(detailType())) { roleDataChanged |= (contact.details() != item->contact.details()); } if (!partialFetch || queryDetailTypes.contains(detailType())) { roleDataChanged |= (contact.detail() != item->contact.detail()); } roleDataChanged |= updateContactIndexing(item->contact, contact, iid, queryDetailTypes, item); updateCache(item, contact, partialFetch, false); roleDataChanged |= (item->displayLabel != oldDisplayLabel); // do this even if !roleDataChanged as name groups are affected by other display label changes if (item->displayLabelGroup != oldDisplayLabelGroup) { if (!ignoreContactForDisplayLabelGroups(item->contact)) { addToContactDisplayLabelGroup(item->iid, item->displayLabelGroup, &modifiedGroups); removeFromContactDisplayLabelGroup(item->iid, oldDisplayLabelGroup, &modifiedGroups); } } if (roleDataChanged) { instancePtr->contactDataChanged(item->iid); } } notifyDisplayLabelGroupsChanged(modifiedGroups); } void SeasideCache::addToContactDisplayLabelGroup(quint32 iid, const QString &group, QSet *modifiedGroups) { if (!group.isEmpty()) { QSet &set(m_contactDisplayLabelGroups[group]); if (!set.contains(iid)) { set.insert(iid); if (modifiedGroups && !m_displayLabelGroupChangeListeners.isEmpty()) { modifiedGroups->insert(group); } } } } void SeasideCache::removeFromContactDisplayLabelGroup(quint32 iid, const QString &group, QSet *modifiedGroups) { if (!group.isEmpty()) { QSet &set(m_contactDisplayLabelGroups[group]); if (set.remove(iid)) { if (modifiedGroups && !m_displayLabelGroupChangeListeners.isEmpty()) { modifiedGroups->insert(group); } } } } void SeasideCache::notifyDisplayLabelGroupsChanged(const QSet &groups) { if (groups.isEmpty() || m_displayLabelGroupChangeListeners.isEmpty()) return; QHash > updates; foreach (const QString &group, groups) updates.insert(group, m_contactDisplayLabelGroups[group]); for (int i = 0; i < m_displayLabelGroupChangeListeners.count(); ++i) m_displayLabelGroupChangeListeners[i]->displayLabelGroupsUpdated(updates); } void SeasideCache::contactIdsAvailable() { if (m_syncFilter == FilterNone) { if (!m_contactsToFetchCandidates.isEmpty()) { foreach (const QContactId &id, m_contactIdRequest.ids()) { m_candidateIds.insert(id); } } } else { synchronizeList(this, m_contacts[m_syncFilter], m_cacheIndex, internalIds(m_contactIdRequest.ids()), m_queryIndex); } } void SeasideCache::relationshipsAvailable() { static const QString aggregatesRelationship = QContactRelationship::Aggregates(); foreach (const QContactRelationship &rel, m_relationshipsFetchRequest.relationships()) { if (rel.relationshipType() == aggregatesRelationship) { m_constituentIds.insert(apiId(rel.second())); } } } void SeasideCache::removeRange(FilterType filter, int index, int count) { QList &cacheIds = m_contacts[filter]; QList &models = m_models[filter]; 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) { const quint32 iid = cacheIds.at(index); m_expiredContacts[apiId(iid)] -= 1; } cacheIds.removeAt(index); } for (int i = 0; i < models.count(); ++i) models[i]->sourceItemsRemoved(); } int SeasideCache::insertRange(FilterType filter, int index, int count, const QList &queryIds, int queryIndex) { QList &cacheIds = m_contacts[filter]; QList &models = m_models[filter]; const quint32 selfId = internalId(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) { quint32 iid = queryIds.at(queryIndex + i); if (iid == selfId) continue; if (filter == FilterAll) { const QContactId apiId = SeasideCache::apiId(iid); m_expiredContacts[apiId] += 1; } cacheIds.insert(index + i, iid); } for (int i = 0; i < models.count(); ++i) models[i]->sourceItemsInserted(index, end); return end - index + 1; } void SeasideCache::appendContacts(const QList &contacts, FilterType filterType, bool partialFetch, const QSet &queryDetailTypes) { if (!contacts.isEmpty()) { QList &cacheIds = m_contacts[filterType]; QList &models = m_models[filterType]; cacheIds.reserve(contacts.count()); const int begin = cacheIds.count(); int end = cacheIds.count() + contacts.count() - 1; if (begin <= end) { QSet modifiedGroups; for (int i = 0; i < models.count(); ++i) models.at(i)->sourceAboutToInsertItems(begin, end); foreach (QContact contact, contacts) { quint32 iid = internalId(contact); cacheIds.append(iid); CacheItem *item = existingItem(iid); if (!item) { item = &(m_people[iid]); item->iid = iid; } else { if (partialFetch) { // Update our new instance with any details not returned by the current query updateDetailsFromCache(contact, item, queryDetailTypes); } } updateContactIndexing(item->contact, contact, iid, queryDetailTypes, item); updateCache(item, contact, partialFetch, true); if (filterType == FilterAll) { addToContactDisplayLabelGroup(iid, displayLabelGroup(item), &modifiedGroups); } } for (int i = 0; i < models.count(); ++i) models.at(i)->sourceItemsInserted(begin, end); notifyDisplayLabelGroupsChanged(modifiedGroups); } } } void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) { if (state != QContactAbstractRequest::FinishedState) return; QContactAbstractRequest *request = static_cast(sender()); if (request == &m_relationshipsFetchRequest) { if (!m_contactsToFetchConstituents.isEmpty()) { QContactId aggregateId = m_contactsToFetchConstituents.takeFirst(); if (!m_constituentIds.isEmpty()) { m_contactsToLinkTo.append(aggregateId); } else { // We didn't find any constituents - report the empty list CacheItem *cacheItem = itemById(aggregateId); if (cacheItem->itemData) { cacheItem->itemData->constituentsFetched(QList()); } updateConstituentAggregations(cacheItem->apiId()); } } } else if (request == &m_fetchByIdRequest) { if (!m_contactsToLinkTo.isEmpty()) { // Report these results QContactId aggregateId = m_contactsToLinkTo.takeFirst(); CacheItem *cacheItem = itemById(aggregateId); QList constituentIds; foreach (const QContactId &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_syncFilter != FilterNone) { // We have completed fetching this filter set completeSynchronizeList(this, m_contacts[m_syncFilter], m_cacheIndex, internalIds(m_contactIdRequest.ids()), m_queryIndex); // Notify models of completed updates QList &models = m_models[m_syncFilter]; for (int i = 0; i < models.count(); ++i) models.at(i)->sourceItemsChanged(); if (m_syncFilter == FilterFavorites) { // Next, query for all contacts (including favorites) m_syncFilter = FilterAll; } else if (m_syncFilter == FilterAll) { // Next, query for online contacts m_syncFilter = FilterOnline; } else if (m_syncFilter == FilterOnline) { m_syncFilter = FilterNone; } } else if (!m_contactsToFetchCandidates.isEmpty()) { // Report these results QContactId contactId = m_contactsToFetchCandidates.takeFirst(); CacheItem *cacheItem = itemById(contactId); const quint32 contactIid = internalId(contactId); QList candidateIds; foreach (const QContactId &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 { qWarning() << "ID fetch completed with no filter?"; } } else if (request == &m_relationshipSaveRequest || request == &m_relationshipRemoveRequest) { bool completed = false; QList relationships; if (request == &m_relationshipSaveRequest) { relationships = m_relationshipSaveRequest.relationships(); completed = !m_relationshipRemoveRequest.isActive(); } else { relationships = m_relationshipRemoveRequest.relationships(); completed = !m_relationshipSaveRequest.isActive(); } foreach (const QContactRelationship &relationship, relationships) { m_aggregatedContacts.insert(SeasideCache::apiId(relationship.first())); } if (completed) { foreach (const QContactId &contactId, m_aggregatedContacts) { CacheItem *cacheItem = itemById(contactId); if (cacheItem && cacheItem->itemData) cacheItem->itemData->aggregationOperationCompleted(); } // We need to update these modified contacts immediately foreach (const QContactId &id, m_aggregatedContacts) m_changedContacts.append(id); fetchContacts(); m_aggregatedContacts.clear(); } } else if (request == &m_fetchRequest) { 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"; } 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; } m_populating = false; } } // See if there are any more requests to dispatch 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); } m_pendingResolve.remove(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); QList &models = m_models[filter]; for (int i = 0; i < models.count(); ++i) models.at(i)->makePopulated(); } void SeasideCache::setSortOrder(const QString &property) { bool firstNameFirst = (property == QString::fromLatin1("firstName")); 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); QContactSortOrder displayLabelGroupOrder; setDetailType(displayLabelGroupOrder, QContactDisplayLabel__FieldLabelGroup); m_sortOrder = firstNameFirst ? (QList() << displayLabelGroupOrder << firstNameOrder << lastNameOrder) : (QList() << displayLabelGroupOrder << lastNameOrder << firstNameOrder); m_onlineSortOrder = m_sortOrder; QContactSortOrder onlineOrder; setDetailType(onlineOrder, QContactGlobalPresence::FieldPresenceState); onlineOrder.setDirection(Qt::AscendingOrder); m_onlineSortOrder.prepend(onlineOrder); } void SeasideCache::displayLabelOrderChanged(CacheConfiguration::DisplayLabelOrder order) { // Update the display labels typedef QHash::iterator iterator; for (iterator it = m_people.begin(); it != m_people.end(); ++it) { // Regenerate the display label QString newLabel = generateDisplayLabel(it->contact, static_cast(order)); if (newLabel != it->displayLabel) { it->displayLabel = newLabel; contactDataChanged(it->iid); reportItemUpdated(&*it); } if (it->itemData) { it->itemData->displayLabelOrderChanged(static_cast(order)); } } for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (int j = 0; j < models.count(); ++j) { ListModel *model = models.at(j); model->updateDisplayLabelOrder(); model->sourceItemsChanged(); } } } void SeasideCache::displayLabelGroupsChanged(const QStringList &groups) { allContactDisplayLabelGroups = groups; contactDisplayLabelGroupCount = groups.count(); } void SeasideCache::sortPropertyChanged(const QString &sortProperty) { setSortOrder(sortProperty); for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (int j = 0; j < models.count(); ++j) { models.at(j)->updateSortProperty(); // No need for sourceItemsChanged, as the sorted list update will cause that } } // Update the sorted list order m_refreshRequired = true; requestUpdate(); } void SeasideCache::displayStatusChanged(const QString &status) { const bool off = (status == QLatin1String(MCE_DISPLAY_OFF_STRING)); if (m_displayOff != off) { m_displayOff = off; if (!m_displayOff) { // The display has been enabled; check for pending fetches requestUpdate(); } } } 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(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 == ContactComplete) { contacts.append(it->contact); } else { contactsToFetch.append(apiId(it.key())); } } if (!contactsToFetch.isEmpty()) { QList fetchedContacts = manager()->contacts(contactsToFetch); contacts.append(fetchedContacts); } if (!exporter.exportContacts(contacts)) { qWarning() << Q_FUNC_INFO << "Failed to export contacts: " << exporter.errorMap(); return QString(); } QString baseDir; foreach (const QString &loc, QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation)) { baseDir = loc; break; } QFile vcard(baseDir + QDir::separator() + QLocale::c().toString(QDateTime::currentDateTime(), QStringLiteral("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(quint32 requiredTypes, quint32 extraTypes) { bool updateRequired(false); // If these types are required, we will fetch them immediately quint32 unfetchedTypes = requiredTypes & ~m_fetchTypes & SeasideCache::FetchTypesMask; if (unfetchedTypes) { m_fetchTypes |= requiredTypes; updateRequired = true; } // Otherwise, we can fetch them when idle unfetchedTypes = extraTypes & ~m_extraFetchTypes & SeasideCache::FetchTypesMask; if (unfetchedTypes) { m_extraFetchTypes |= extraTypes; updateRequired = true; } if (((requiredTypes | extraTypes) & SeasideCache::FetchPhoneNumber) != 0) { // We won't need to check resolved numbers any further m_resolvedPhoneNumbers.clear(); } if (!m_keepPopulated) { m_keepPopulated = true; updateRequired = true; } if (updateRequired) { 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(isNotRelationshipType, 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); } instancePtr->requestUpdate(); } void SeasideCache::updateConstituentAggregations(const QContactId &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 QContactId &contact1Id, const QContactId &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) { const QContactId constituentId(apiId(id)); m_relationshipsToSave.append(makeRelationship(aggregateRelationshipType, contact1Id, constituentId)); m_relationshipsToRemove.append(makeRelationship(aggregateRelationshipType, contact2Id, constituentId)); // If there is an existing IsNot relationship, it would be better to remove it; // unfortunately, we can't be certain that it exists, and if it does not, the // relationship removal will fail - hence, better to ignore the possibility: //m_relationshipsToRemove.append(makeRelationship(isNotRelationshipType, contact1Id, constituentId)); } if (!m_relationshipsToSave.isEmpty() || !m_relationshipsToRemove.isEmpty()) requestUpdate(); } void SeasideCache::resolveAddress(ResolveListener *listener, const QString &first, const QString &second, bool requireComplete) { ResolveData data; data.first = first; data.second = second; data.requireComplete = requireComplete; data.listener = listener; // filter out duplicate requests if (m_pendingResolve.find(data) != m_pendingResolve.end()) return; // Is this address a known-unknown? bool knownUnknown = false; QList::const_iterator it = instancePtr->m_unknownAddresses.constBegin(), end = m_unknownAddresses.constEnd(); for ( ; it != end; ++it) { if (it->first == first && it->second == second) { knownUnknown = true; break; } } if (knownUnknown) { m_unknownResolveAddresses.append(data); requestUpdate(); } else { 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); 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, at least include the contact endpoint details (since resolving is obviously being used) const quint32 detailFetchTypes(SeasideCache::FetchAccountUri | SeasideCache::FetchPhoneNumber | SeasideCache::FetchEmailAddress); request->setFetchHint(requireComplete ? basicFetchHint() : onlineFetchHint(m_fetchTypes | m_extraFetchTypes | detailFetchTypes)); connect(request, SIGNAL(stateChanged(QContactAbstractRequest::State)), this, SLOT(addressRequestStateChanged(QContactAbstractRequest::State))); m_resolveAddresses[request] = data; m_pendingResolve.insert(data); request->start(); } } SeasideCache::CacheItem *SeasideCache::itemMatchingPhoneNumber(const QString &number, const QString &normalized, bool requireComplete) { QMultiHash::const_iterator it = m_phoneNumberIds.find(number), end = m_phoneNumberIds.constEnd(); if (it != end) { // How many matches are there for this number? int matchCount = 1; QMultiHash::const_iterator matchingIt = it + 1; while ((matchingIt != end) && (matchingIt.key() == number)) { ++matchCount; ++matchingIt; } if (matchCount == 1) return itemById(*it, requireComplete); // Choose the best match from these contacts int bestMatchLength = 0; CacheItem *matchItem = 0; for ( ; matchCount > 0; ++it, --matchCount) { if (CacheItem *item = existingItem(*it)) { int matchLength = bestPhoneNumberMatchLength(item->contact, normalized); if (matchLength > bestMatchLength) { bestMatchLength = matchLength; matchItem = item; if (bestMatchLength == ExactMatch) break; } } } if (matchItem != 0) { if (requireComplete) { ensureCompletion(matchItem); } return matchItem; } } return 0; } int SeasideCache::contactIndex(quint32 iid, FilterType filterType) { const QList &cacheIds(m_contacts[filterType]); return cacheIds.indexOf(iid); } QContactRelationship SeasideCache::makeRelationship(const QString &type, const QContactId &id1, const QContactId &id2) { QContact first, second; first.setId(id1); second.setId(id2); QContactRelationship relationship; relationship.setRelationshipType(type); relationship.setFirst(first); relationship.setSecond(second); return relationship; } QContactRelationship SeasideCache::makeRelationship(const QString &type, const QContact &contact1, const QContact &contact2) { return makeRelationship(type, contact1.id(), contact2.id()); } bool operator==(const SeasideCache::ResolveData &lhs, const SeasideCache::ResolveData &rhs) { // .listener and .requireComplete first because they are the cheapest comparisons // then .second before .first because .second is most likely to be unequal // .compare is derived from first and second so it does not need to be checked return lhs.listener == rhs.listener && lhs.requireComplete == rhs.requireComplete && lhs.second == rhs.second && lhs.first == rhs.first; } uint qHash(const SeasideCache::ResolveData &key, uint seed) { uint h1 = qHash(key.first, seed); uint h2 = qHash(key.second, seed); uint h3 = key.requireComplete; uint h4 = qHash(key.listener, seed); // Don't xor with seed here because h4 already did that return h1 ^ h2 ^ h3 ^ h4; } // Instantiate the contact ID functions for qtcontacts-sqlite #include