From 6114837afc7be114c78fb7857283fda5b9867748 Mon Sep 17 00:00:00 2001 From: Matt Vogt Date: Fri, 2 Aug 2013 08:10:27 +1000 Subject: [PATCH] [libcontacts] Make contact browsing faster Pre-fill the cache with contact metadata rather than entire contact records. Where a particular address type is required for selection by an attached model, include that address type in the queried data. Also allow partially loaded contacts to be completed, and contacts to be queried by address, whether already present in the cache or otherwise. --- src/seasidecache.cpp | 735 ++++++++++++++++++++++++++++++++----------- src/seasidecache.h | 88 +++++- 2 files changed, 617 insertions(+), 206 deletions(-) diff --git a/src/seasidecache.cpp b/src/seasidecache.cpp index d764971..17e9500 100644 --- a/src/seasidecache.cpp +++ b/src/seasidecache.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -68,18 +69,19 @@ USE_VERSIT_NAMESPACE +namespace { -static QString aggregateRelationshipType = +const QString aggregateRelationshipType = #ifdef USING_QTPIM QContactRelationship::Aggregates(); #else QContactRelationship::Aggregates; #endif -static const QString syncTargetLocal = QLatin1String("local"); -static const QString syncTargetWasLocal = QLatin1String("was_local"); +const QString syncTargetLocal = QLatin1String("local"); +const QString syncTargetWasLocal = QLatin1String("was_local"); -static QList getAllContactNameGroups() +QList getAllContactNameGroups() { QList groups; groups << QLatin1Char('A') @@ -115,10 +117,7 @@ static QList getAllContactNameGroups() return groups; } -SeasideCache *SeasideCache::instancePtr = 0; -QList SeasideCache::allContactNameGroups = getAllContactNameGroups(); - -static QString managerName() +QString managerName() { #ifdef USING_QTPIM // Temporary override until qtpim supports QTCONTACTS_MANAGER_OVERRIDE @@ -130,6 +129,27 @@ static QString managerName() : QString(); } +typedef QList DetailList; + +template +DetailList::value_type detailType() +{ +#ifdef USING_QTPIM + return T::Type; +#else + return T::DefinitionName; +#endif +} + +DetailList::value_type detailType(const QContactDetail &detail) +{ +#ifdef USING_QTPIM + return detail.type(); +#else + return detail.definitionName(); +#endif +} + template void setDetailType(Filter &filter, Field field) { @@ -140,6 +160,105 @@ void setDetailType(Filter &filter, Field field) #endif } +QContactFetchHint basicFetchHint() +{ + QContactFetchHint fetchHint; + + // We generally have no use for these things: + fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships | + QContactFetchHint::NoActionPreferences | + QContactFetchHint::NoBinaryBlobs); + + return fetchHint; +} + +QContactFetchHint metadataFetchHint(quint32 fetchTypes = 0) +{ + QContactFetchHint fetchHint(basicFetchHint()); + + // Include all detail types which come from the main contacts table + DetailList types; + types << detailType() << + detailType() << + detailType() << + detailType() << + detailType() << + detailType(); + + if (fetchTypes & SeasideCache::FetchAccountUri) { + types << detailType(); + } + if (fetchTypes & SeasideCache::FetchPhoneNumber) { + types << detailType(); + } + if (fetchTypes & SeasideCache::FetchEmailAddress) { + types << detailType(); + } + + fetchHint.setDetailTypesHint(types); + return fetchHint; +} + +QContactFetchHint onlineFetchHint(quint32 fetchTypes = 0) +{ + QContactFetchHint fetchHint(metadataFetchHint(fetchTypes)); + + // We also need global presence state + fetchHint.setDetailTypesHint(fetchHint.detailTypesHint() << detailType()); + + return fetchHint; +} + +QContactFetchHint favoriteFetchHint(quint32 fetchTypes = 0) +{ + QContactFetchHint fetchHint(onlineFetchHint(fetchTypes)); + + // We also need avatar info + fetchHint.setDetailTypesHint(fetchHint.detailTypesHint() << detailType()); + + 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() +{ + // Where presence is available + return QContactGlobalPresence::match(QContactPresence::PresenceAvailable); +} + +QContactFilter aggregateFilter() +{ + QContactDetailFilter filter; + setDetailType(filter, QContactSyncTarget::FieldSyncTarget); + filter.setValue("aggregate"); + + return filter; +} + +} + +SeasideCache *SeasideCache::instancePtr = 0; +QList SeasideCache::allContactNameGroups = getAllContactNameGroups(); + SeasideCache* SeasideCache::instance() { return instancePtr; @@ -202,12 +321,16 @@ SeasideCache::SeasideCache() , m_cacheIndex(0) , m_queryIndex(0) , m_appendIndex(0) - , m_fetchFilter(FilterNone) + , m_syncFilter(FilterNone) , m_displayLabelOrder(FirstNameFirst) , m_keepPopulated(false) + , m_populateProgress(Unpopulated) + , m_fetchTypes(0) + , m_fetchTypesChanged(false) , m_updatesPending(false) , m_refreshRequired(false) , m_contactsUpdated(false) + , m_activeResolve(0) { Q_ASSERT(!instancePtr); instancePtr = this; @@ -271,15 +394,6 @@ SeasideCache::SeasideCache() m_relationshipSaveRequest.setManager(&m_manager); m_relationshipRemoveRequest.setManager(&m_manager); - QContactFetchHint fetchHint; - fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships - | QContactFetchHint::NoActionPreferences - | QContactFetchHint::NoBinaryBlobs); - - // Note: no restriction on detail definitions - the cache should contain the entire contact - - m_fetchRequest.setFetchHint(fetchHint); - setSortOrder(m_displayLabelOrder); } @@ -302,7 +416,7 @@ void SeasideCache::checkForExpiry() } } -void SeasideCache::registerModel(ListModel *model, FilterType type) +void SeasideCache::registerModel(ListModel *model, FilterType type, FetchDataType fetchTypes) { if (!instancePtr) { new SeasideCache; @@ -311,8 +425,9 @@ void SeasideCache::registerModel(ListModel *model, FilterType type) for (int i = 0; i < FilterTypesCount; ++i) instancePtr->m_models[i].removeAll(model); } + instancePtr->m_models[type].append(model); - instancePtr->keepPopulated(); + instancePtr->keepPopulated(fetchTypes); } void SeasideCache::unregisterModel(ListModel *model) @@ -419,7 +534,7 @@ int SeasideCache::contactId(const QContact &contact) return static_cast(internal); } -SeasideCache::CacheItem *SeasideCache::itemById(const ContactIdType &id) +SeasideCache::CacheItem *SeasideCache::itemById(const ContactIdType &id, bool requireComplete) { if (!validId(id)) return 0; @@ -443,22 +558,19 @@ SeasideCache::CacheItem *SeasideCache::itemById(const ContactIdType &id) #endif } - if (item->contactState == ContactAbsent) { - item->contactState = ContactRequested; - instancePtr->m_changedContacts.append(item->apiId()); - instancePtr->fetchContacts(); + if (requireComplete) { + ensureCompletion(item); } - return item; } #ifdef USING_QTPIM -SeasideCache::CacheItem *SeasideCache::itemById(int id) +SeasideCache::CacheItem *SeasideCache::itemById(int id, bool requireComplete) { if (id != 0) { QContactId contactId(apiId(static_cast(id))); if (!contactId.isNull()) { - return itemById(contactId); + return itemById(contactId, requireComplete); } } @@ -494,23 +606,73 @@ QContact SeasideCache::contactById(const ContactIdType &id) return instancePtr->m_people.value(iid, CacheItem()).contact; } -SeasideCache::CacheItem *SeasideCache::itemByPhoneNumber(const QString &msisdn) +void SeasideCache::ensureCompletion(CacheItem *cacheItem) { - QString normalizedNumber = Normalization::normalizePhoneNumber(msisdn); + if (cacheItem->contactState < ContactRequested) { + cacheItem->contactState = ContactRequested; + instancePtr->m_changedContacts.append(cacheItem->apiId()); + instancePtr->fetchContacts(); + } +} + +SeasideCache::CacheItem *SeasideCache::itemByPhoneNumber(const QString &number, bool requireComplete) +{ + QString normalizedNumber = Normalization::normalizePhoneNumber(number); QHash::const_iterator it = instancePtr->m_phoneNumberIds.find(normalizedNumber); if (it != instancePtr->m_phoneNumberIds.end()) - return itemById(*it); + return itemById(*it, requireComplete); + return 0; } -SeasideCache::CacheItem *SeasideCache::itemByEmailAddress(const QString &email) +SeasideCache::CacheItem *SeasideCache::itemByEmailAddress(const QString &email, bool requireComplete) { QHash::const_iterator it = instancePtr->m_emailAddressIds.find(email.toLower()); if (it != instancePtr->m_emailAddressIds.end()) - return itemById(*it); + return itemById(*it, requireComplete); + return 0; } +SeasideCache::CacheItem *SeasideCache::itemByOnlineAccount(const QString &localUid, const QString &remoteUid, bool requireComplete) +{ + 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) +{ + CacheItem *item = itemByPhoneNumber(number, requireComplete); + if (!item) { + instancePtr->resolveAddress(listener, QString(), number, requireComplete); + } + + return item; +} + +SeasideCache::CacheItem *SeasideCache::resolveEmailAddress(ResolveListener *listener, const QString &address, bool requireComplete) +{ + CacheItem *item = itemByEmailAddress(address, requireComplete); + if (!item) { + instancePtr->resolveAddress(listener, address, QString(), requireComplete); + } + return item; +} + +SeasideCache::CacheItem *SeasideCache::resolveOnlineAccount(ResolveListener *listener, const QString &localUid, const QString &remoteUid, bool requireComplete) +{ + CacheItem *item = itemByOnlineAccount(localUid, remoteUid, requireComplete); + if (!item) { + instancePtr->resolveAddress(listener, localUid, remoteUid, requireComplete); + } + return item; +} + SeasideCache::ContactIdType SeasideCache::selfContactId() { return instancePtr->m_manager.selfContactId(); @@ -528,10 +690,7 @@ bool SeasideCache::saveContact(const QContact &contact) ContactIdType id = apiId(contact); if (validId(id)) { instancePtr->m_contactsToSave[id] = contact; - - instancePtr->updateContactData(id, FilterFavorites); - instancePtr->updateContactData(id, FilterOnline); - instancePtr->updateContactData(id, FilterAll); + instancePtr->contactDataChanged(id); } else { instancePtr->m_contactsToCreate.append(contact); } @@ -541,14 +700,22 @@ bool SeasideCache::saveContact(const QContact &contact) return true; } -void SeasideCache::updateContactData( - const ContactIdType &contactId, FilterType filter) +void SeasideCache::contactDataChanged(const ContactIdType &contactId) { - int row = m_contacts[filter].indexOf(contactId); + instancePtr->contactDataChanged(contactId, FilterFavorites); + instancePtr->contactDataChanged(contactId, FilterOnline); + instancePtr->contactDataChanged(contactId, FilterAll); +} - QList &models = m_models[filter]; - for (int i = 0; row != -1 && i < models.count(); ++i) - models.at(i)->sourceDataChanged(row, row); +void SeasideCache::contactDataChanged(const ContactIdType &contactId, FilterType filter) +{ + int row = m_contacts[filter].indexOf(contactId); + 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) @@ -814,19 +981,17 @@ static QContactFilter filterForMergeCandidates(const QContact &contact) } // Only return aggregate contact IDs - QContactDetailFilter syncTarget; - setDetailType(syncTarget, QContactSyncTarget::FieldSyncTarget); - syncTarget.setValue(QString::fromLatin1("aggregate")); - rv = rv & syncTarget; - - return rv; + return rv & aggregateFilter(); } bool SeasideCache::event(QEvent *event) { - if (event->type() != QEvent::UpdateRequest) { + if (event->type() != QEvent::UpdateRequest) return QObject::event(event); - } else if (!m_relationshipsToSave.isEmpty() || !m_relationshipsToRemove.isEmpty()) { + + // Test these conditions in priority order + if ((!m_relationshipsToSave.isEmpty() && !m_relationshipSaveRequest.isActive()) || + (!m_relationshipsToRemove.isEmpty() && !m_relationshipRemoveRequest.isActive())) { // this has to be before contact saves are processed so that the disaggregation flow // works properly if (!m_relationshipsToSave.isEmpty()) { @@ -840,12 +1005,12 @@ bool SeasideCache::event(QEvent *event) m_relationshipsToRemove.clear(); } - } else if (!m_contactsToRemove.isEmpty()) { + } else if (!m_contactsToRemove.isEmpty() && !m_removeRequest.isActive()) { m_removeRequest.setContactIds(m_contactsToRemove); m_removeRequest.start(); m_contactsToRemove.clear(); - } else if (!m_contactsToCreate.isEmpty() || !m_contactsToSave.isEmpty()) { + } else if ((!m_contactsToCreate.isEmpty() || !m_contactsToSave.isEmpty()) && !m_saveRequest.isActive()) { m_contactsToCreate.reserve(m_contactsToCreate.count() + m_contactsToSave.count()); typedef QHash::iterator iterator; @@ -858,7 +1023,7 @@ bool SeasideCache::event(QEvent *event) m_contactsToCreate.clear(); m_contactsToSave.clear(); - } else if (!m_constituentIds.isEmpty()) { + } else if (!m_constituentIds.isEmpty() && !m_fetchByIdRequest.isActive()) { // Fetch the constituent information (even if they're already in the // cache, because we don't update non-aggregates on change notifications) #ifdef USING_QTPIM @@ -867,7 +1032,7 @@ bool SeasideCache::event(QEvent *event) m_fetchByIdRequest.setLocalIds(m_constituentIds); #endif m_fetchByIdRequest.start(); - } else if (!m_contactsToFetchConstituents.isEmpty()) { + } else if (!m_contactsToFetchConstituents.isEmpty() && !m_relationshipsFetchRequest.isActive()) { QContactId aggregateId = m_contactsToFetchConstituents.first(); // Find the constituents of this contact @@ -882,7 +1047,7 @@ bool SeasideCache::event(QEvent *event) #endif m_relationshipsFetchRequest.start(); - } else if (!m_contactsToFetchCandidates.isEmpty()) { + } else if (!m_contactsToFetchCandidates.isEmpty() && !m_contactIdRequest.isActive()) { #ifdef USING_QTPIM ContactIdType contactId(m_contactsToFetchCandidates.first()); #else @@ -893,7 +1058,23 @@ bool SeasideCache::event(QEvent *event) // Find candidates to merge with this contact m_contactIdRequest.setFilter(filterForMergeCandidates(contact)); m_contactIdRequest.start(); - } else if (!m_changedContacts.isEmpty()) { + } else if ((m_populateProgress == Unpopulated) && m_keepPopulated && !m_fetchRequest.isActive()) { + // Start a query to fully populate the cache, starting with favorites + m_fetchRequest.setFilter(favoriteFilter()); + m_fetchRequest.setFetchHint(favoriteFetchHint(m_fetchTypes)); + m_fetchRequest.start(); + + m_appendIndex = 0; + m_populateProgress = FetchFavorites; + } else if ((m_populateProgress == Populated) && m_fetchTypesChanged && !m_fetchRequest.isActive()) { + // We need to refetch the metadata for all contacts (because the required data changed) + m_fetchRequest.setFilter(favoriteFilter()); + m_fetchRequest.setFetchHint(favoriteFetchHint(m_fetchTypes)); + m_fetchRequest.start(); + + m_fetchTypesChanged = false; + m_populateProgress = RefetchFavorites; + } else if (!m_changedContacts.isEmpty() && !m_fetchRequest.isActive()) { m_resultsRead = 0; #ifdef USING_QTPIM @@ -906,19 +1087,48 @@ bool SeasideCache::event(QEvent *event) // A local ID filter will fetch all contacts, rather than just aggregates; // we only want to retrieve aggregate contacts that have changed - QContactDetailFilter stFilter; - setDetailType(stFilter, QContactSyncTarget::FieldSyncTarget); - stFilter.setValue("aggregate"); + m_fetchRequest.setFilter(filter & aggregateFilter()); + m_fetchRequest.setFetchHint(basicFetchHint()); + m_fetchRequest.start(); + } else if (!m_resolveAddresses.isEmpty() && !m_fetchRequest.isActive()) { + const ResolveData &resolve = m_resolveAddresses.first(); + + if (resolve.first.isEmpty()) { + // Search for phone number + m_fetchRequest.setFilter(QContactPhoneNumber::match(resolve.second)); + } else if (resolve.second.isEmpty()) { + // Search for email address + QContactDetailFilter detailFilter; + setDetailType(detailFilter, QContactEmailAddress::FieldEmailAddress); + detailFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive + detailFilter.setValue(resolve.first); + + m_fetchRequest.setFilter(detailFilter); + } else { + // Search for online account + QContactDetailFilter localFilter; + setDetailType(localFilter, QContactOnlineAccount__FieldAccountPath); + localFilter.setValue(resolve.first); - m_appendIndex = 0; - m_fetchRequest.setFilter(filter & stFilter); + QContactDetailFilter remoteFilter; + setDetailType(remoteFilter, QContactOnlineAccount::FieldAccountUri); + remoteFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive + remoteFilter.setValue(resolve.second); + + m_fetchRequest.setFilter(localFilter | remoteFilter); + } + + // If completion is not required, we need to at least retrieve as much detail + // as the favorites store, so we don't update any favorite with a smaller data subset + m_activeResolve = &resolve; + m_fetchRequest.setFetchHint(resolve.requireComplete ? basicFetchHint() : favoriteFetchHint(m_fetchTypes)); m_fetchRequest.start(); - } else if (m_refreshRequired) { - m_resultsRead = 0; + } else if (m_refreshRequired && !m_contactIdRequest.isActive()) { m_refreshRequired = false; - m_fetchFilter = FilterFavorites; - m_contactIdRequest.setFilter(QContactFavorite::match()); + m_resultsRead = 0; + m_syncFilter = FilterFavorites; + m_contactIdRequest.setFilter(favoriteFilter()); m_contactIdRequest.start(); } else { m_updatesPending = false; @@ -975,6 +1185,7 @@ void SeasideCache::contactsChanged(const QList &ids) presentIds.append(id); } } + updateContacts(presentIds); } } @@ -1017,7 +1228,7 @@ void SeasideCache::fetchContacts() if (m_fetchRequest.isActive()) { // The current fetch is still active - we may as well continue to accumulate - m_fetchTimer.start(WaitIntervalMs , this); + m_fetchTimer.start(WaitIntervalMs, this); } else { m_fetchTimer.stop(); m_fetchPostponed.invalidate(); @@ -1061,22 +1272,124 @@ void SeasideCache::updateContacts(const QList &contactIds) } } +void SeasideCache::updateCache(CacheItem *item, const QContact &contact, bool partialFetch) +{ + 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; + } + + item->statusFlags = contact.detail().flagsValue(); + if (item->modelData) { + item->modelData->contactChanged(contact, item->contactState); + } + if (item->itemData) { + item->itemData->updateContact(contact, &item->contact, item->contactState); + } else { + item->contact = contact; + } +} + +bool SeasideCache::updateContactIndexing(const QContact &oldContact, const QContact &contact, quint32 iid, const QSet &queryDetailTypes) +{ + bool modified = false; + + if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { + // Addresses which are no longer in the contact should be de-indexed + QSet oldPhoneNumbers; + foreach (const QContactPhoneNumber &phoneNumber, oldContact.details()) { + oldPhoneNumbers.insert(Normalization::normalizePhoneNumber(phoneNumber.number())); + } + + // Update our address indexes for any address details in this contact + foreach (const QContactPhoneNumber &phoneNumber, contact.details()) { + QString normalizedNumber = Normalization::normalizePhoneNumber(phoneNumber.number()); + m_phoneNumberIds[normalizedNumber] = iid; + modified |= !oldPhoneNumbers.remove(normalizedNumber); + } + + // Remove any addresses no longer available for this contact + modified |= !oldPhoneNumbers.isEmpty(); + foreach (const QString &phoneNumber, oldPhoneNumbers) { + m_phoneNumberIds.remove(phoneNumber); + } + } + + if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { + QSet oldEmailAddresses; + foreach (const QContactEmailAddress &emailAddress, oldContact.details()) { + oldEmailAddresses.insert(emailAddress.emailAddress().toLower()); + } + + foreach (const QContactEmailAddress &emailAddress, contact.details()) { + QString address = emailAddress.emailAddress().toLower(); + m_emailAddressIds[address] = iid; + modified |= !oldEmailAddresses.remove(address); + } + + modified |= !oldEmailAddresses.isEmpty(); + foreach (const QString &emailAddress, oldEmailAddresses) { + m_emailAddressIds.remove(emailAddress); + } + } + + if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { + typedef QPair StringPair; + QSet oldOnlineAccounts; + foreach (const QContactOnlineAccount &account, oldContact.details()) { + oldOnlineAccounts.insert(qMakePair(account.value(QContactOnlineAccount__FieldAccountPath), account.accountUri().toLower())); + } + + foreach (const QContactOnlineAccount &account, contact.details()) { + QString path = account.value(QContactOnlineAccount__FieldAccountPath); + StringPair address = qMakePair(path, account.accountUri()); + m_onlineAccountIds[address] = iid; + modified |= !oldOnlineAccounts.remove(address); + } + + modified |= !oldOnlineAccounts.isEmpty(); + foreach (const StringPair &address, oldOnlineAccounts) { + m_onlineAccountIds.remove(address); + } + } + + return modified; +} + void SeasideCache::contactsAvailable() { QContactAbstractRequest *request = static_cast(sender()); QList contacts; + QContactFetchHint fetchHint; if (request == &m_fetchByIdRequest) { contacts = m_fetchByIdRequest.contacts(); + fetchHint = m_fetchByIdRequest.fetchHint(); } else { contacts = m_fetchRequest.contacts(); + fetchHint = m_fetchRequest.fetchHint(); } - if (m_fetchFilter == FilterFavorites - || m_fetchFilter == FilterOnline - || m_fetchFilter == FilterAll) { - // Part of an initial query. - appendContacts(contacts); + QSet queryDetailTypes; + foreach (const DetailTypeId &typeId, +#ifdef USING_QTPIM + fetchHint.detailTypesHint() +#else + fetchHint.detailDefinitionsHint() +#endif + ) { + queryDetailTypes.insert(typeId); + } + const bool partialFetch = !queryDetailTypes.isEmpty(); + + if (m_populateProgress > Unpopulated && m_populateProgress < Populated) { + // We are populating the cache + FilterType type(m_populateProgress == FetchFavorites ? FilterFavorites + : (m_populateProgress == FetchMetadata ? FilterAll + : FilterOnline)); + appendContacts(contacts, type, partialFetch); } else { // An update. QList modifiedGroups; @@ -1086,19 +1399,41 @@ void SeasideCache::contactsAvailable() ContactIdType apiId = SeasideCache::apiId(contact); quint32 iid = internalId(contact); - CacheItem &item = m_people[iid]; - QContactName oldName = item.contact.detail(); - QContactName newName = contact.detail(); + CacheItem *item = existingItem(iid); + + const bool preexisting = (item != 0); + if (!item) { + // We haven't seen this contact before + item = &(m_people[iid]); + item->iid = iid; + } + QChar oldNameGroup; + QContactName oldName; + + if (preexisting) { + oldNameGroup = nameGroupForCacheItem(item); + oldName = item->contact.detail(); + + if (partialFetch) { + // Copy any existing detail types that are in the current record to the new instance + foreach (const QContactDetail &existing, item->contact.details()) { + if (!queryDetailTypes.contains(detailType(existing))) { + QContactDetail copy(existing); + contact.saveDetail(©); + } + } + } + } - if (m_fetchFilter == FilterAll) - oldNameGroup = nameGroupForCacheItem(&item); + QContactName newName = contact.detail(); #ifdef USING_QTPIM if (newName.value(QContactName__FieldCustomLabel).isEmpty()) { #else if (newName.customLabel().isEmpty()) { #endif + // Maintain the existing custom label value if we have set it #ifdef USING_QTPIM newName.setValue(QContactName__FieldCustomLabel, oldName.value(QContactName__FieldCustomLabel)); #else @@ -1107,45 +1442,25 @@ void SeasideCache::contactsAvailable() contact.saveDetail(&newName); } - const bool roleDataChanged = newName != oldName - || contact.detail().imageUrl() != item.contact.detail().imageUrl(); + // This is a simplification of reality, should we test more changes? + bool roleDataChanged = (newName != oldName) || + contact.detail().imageUrl() != item->contact.detail().imageUrl(); + + roleDataChanged |= updateContactIndexing(item->contact, contact, iid, queryDetailTypes); + updateCache(item, contact, partialFetch); - if (item.modelData) { - item.modelData->contactChanged(contact); + // do this even if !roleDataChanged as name groups are affected by other display label changes + QChar newNameGroup = nameGroupForCacheItem(item); + if (newNameGroup != oldNameGroup) { + addToContactNameGroup(item->iid, newNameGroup, &modifiedGroups); + if (preexisting) { + removeFromContactNameGroup(item->iid, oldNameGroup, &modifiedGroups); + } } - if (item.itemData) { - item.itemData->updateContact(contact, &item.contact); - } else { - item.contact = contact; + + if (roleDataChanged) { + instancePtr->contactDataChanged(apiId); } - item.statusFlags = contact.detail().flagsValue(); - item.contactState = ContactFetched; - - QList phoneNumbers = contact.details(); - for (int j = 0; j < phoneNumbers.count(); ++j) { - QString normalizedNumber = Normalization::normalizePhoneNumber(phoneNumbers.at(j).number()); - m_phoneNumberIds[normalizedNumber] = iid; - } - - QList emailAddresses = contact.details(); - for (int j = 0; j < emailAddresses.count(); ++j) { - m_emailAddressIds[emailAddresses.at(j).emailAddress().toLower()] = iid; - } - - if (m_fetchFilter == FilterAll) { - // do this even if !roleDataChanged as name groups are affected by other display label changes - QChar newNameGroup = nameGroupForCacheItem(&item); - if (newNameGroup != oldNameGroup) { - addToContactNameGroup(item.iid, newNameGroup, &modifiedGroups); - removeFromContactNameGroup(item.iid, oldNameGroup, &modifiedGroups); - } - } - - if (roleDataChanged) { - instancePtr->updateContactData(apiId, FilterFavorites); - instancePtr->updateContactData(apiId, FilterOnline); - instancePtr->updateContactData(apiId, FilterAll); - } } m_resultsRead = contacts.count(); notifyNameGroupsChanged(modifiedGroups); @@ -1176,8 +1491,8 @@ void SeasideCache::notifyNameGroupsChanged(const QList &groups) return; QHash > updates; - for (int i = 0; i < groups.count(); ++i) - updates[groups[i]] = m_contactNameGroups[groups[i]]; + foreach (const QChar &group, groups) + updates.insert(group, m_contactNameGroups[group]); for (int i = 0; i < m_nameGroupChangeListeners.count(); ++i) m_nameGroupChangeListeners[i]->nameGroupsUpdated(updates); @@ -1190,12 +1505,7 @@ void SeasideCache::contactIdsAvailable() return; } - synchronizeList( - this, - m_contacts[m_fetchFilter], - m_cacheIndex, - m_contactIdRequest.ids(), - m_queryIndex); + synchronizeList(this, m_contacts[m_syncFilter], m_cacheIndex, m_contactIdRequest.ids(), m_queryIndex); } void SeasideCache::relationshipsAvailable() @@ -1303,11 +1613,11 @@ int SeasideCache::insertRange( return end - index + 1; } -void SeasideCache::appendContacts(const QList &contacts) +void SeasideCache::appendContacts(const QList &contacts, FilterType filterType, bool partialFetch) { if (!contacts.isEmpty()) { - QVector &cacheIds = m_contacts[m_fetchFilter]; - QList &models = m_models[m_fetchFilter]; + QVector &cacheIds = m_contacts[filterType]; + QList &models = m_models[filterType]; cacheIds.reserve(contacts.count()); @@ -1325,20 +1635,17 @@ void SeasideCache::appendContacts(const QList &contacts) cacheIds.append(apiId); CacheItem &cacheItem = m_people[iid]; - cacheItem.contact = contact; - cacheItem.statusFlags = contact.detail().flagsValue(); - cacheItem.contactState = ContactFetched; - if (m_fetchFilter == FilterAll) - addToContactNameGroup(iid, nameGroupForCacheItem(&cacheItem), 0); - - foreach (const QContactPhoneNumber &phoneNumber, contact.details()) { - QString normalizedNumber = Normalization::normalizePhoneNumber(phoneNumber.number()); - m_phoneNumberIds[normalizedNumber] = iid; + // If we have already requested this contact as a favorite, don't update with fewer details + if ((cacheItem.iid == 0) || + (cacheItem.contact.detail().isFavorite() == false)) { + cacheItem.iid = iid; + updateContactIndexing(QContact(), contact, iid, QSet()); + updateCache(&cacheItem, contact, partialFetch); } - foreach (const QContactEmailAddress &emailAddress, contact.details()) { - m_emailAddressIds[emailAddress.emailAddress().toLower()] = iid; + if (filterType == FilterAll) { + addToContactNameGroup(iid, nameGroupForCacheItem(&cacheItem), 0); } } @@ -1358,6 +1665,8 @@ void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) QContactAbstractRequest *request = static_cast(sender()); + bool activityCompleted = true; + if (request == &m_relationshipsFetchRequest) { if (!m_contactsToFetchConstituents.isEmpty() && m_constituentIds.isEmpty()) { // We didn't find any constituents - report the empty list @@ -1420,15 +1729,28 @@ void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) if (cacheItem->itemData) { cacheItem->itemData->mergeCandidatesFetched(candidateIds); } - } - else if (m_fetchFilter != FilterNone) { + } else if (m_syncFilter != FilterNone) { // We have completed fetching this filter set - completeSynchronizeList( - this, - m_contacts[m_fetchFilter], - m_cacheIndex, - m_contactIdRequest.ids(), - m_queryIndex); + completeSynchronizeList(this, m_contacts[m_syncFilter], m_cacheIndex, m_contactIdRequest.ids(), m_queryIndex); + finalizeUpdate(m_syncFilter); + + if (m_syncFilter == FilterFavorites) { + // Next, query for all contacts (including favorites) + m_syncFilter = FilterAll; + m_contactIdRequest.setFilter(allFilter()); + m_contactIdRequest.start(); + + activityCompleted = false; + } else if (m_syncFilter == FilterAll) { + // Next, query for online contacts + m_syncFilter = FilterOnline; + m_contactIdRequest.setFilter(onlineFilter()); + m_contactIdRequest.start(); + + activityCompleted = false; + } + } else { + qWarning() << "ID fetch completed with no filter?"; } } else if (request == &m_relationshipSaveRequest || request == &m_relationshipRemoveRequest) { QSet contactIds; @@ -1446,60 +1768,77 @@ void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) if (cacheItem && cacheItem->itemData) cacheItem->itemData->aggregationOperationCompleted(); } - } - - if (m_fetchFilter == FilterFavorites) { - // Next, query for all contacts - m_fetchFilter = FilterAll; + } else if (request == &m_fetchRequest) { + if (m_populateProgress == Unpopulated && m_keepPopulated) { + // Start a query to fully populate the cache, starting with favorites + m_fetchRequest.setFilter(favoriteFilter()); + m_fetchRequest.setFetchHint(favoriteFetchHint(m_fetchTypes)); + m_fetchRequest.start(); - if (!isPopulated(FilterFavorites)) { - qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; m_appendIndex = 0; - m_fetchRequest.setFilter(QContactFilter()); - m_fetchRequest.start(); + m_populateProgress = FetchFavorites; + activityCompleted = false; + } else if (m_populateProgress == FetchFavorites) { makePopulated(FilterFavorites); - } else { - finalizeUpdate(FilterFavorites); - m_contactIdRequest.setFilter(QContactFilter()); - m_contactIdRequest.start(); - } - } else if (m_fetchFilter == FilterAll) { - // Next, query for online contacts - m_fetchFilter = FilterOnline; + qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; - if (!isPopulated(FilterAll)) { - qDebug() << "All queried in" << m_timer.elapsed() << "ms"; - // Not correct, but better than nothing... - m_appendIndex = 0; - m_fetchRequest.setFilter(QContactGlobalPresence::match(QContactPresence::PresenceAvailable)); + // Next, query for all contacts (except favorites) + // Request the metadata of all contacts (only data from the primary table) + m_fetchRequest.setFilter(allFilter()); + m_fetchRequest.setFetchHint(metadataFetchHint(m_fetchTypes)); m_fetchRequest.start(); + + m_fetchTypesChanged = false; + m_appendIndex = 0; + m_populateProgress = FetchMetadata; + activityCompleted = false; + } else if (m_populateProgress == FetchMetadata) { makePopulated(FilterNone); makePopulated(FilterAll); - } else { - finalizeUpdate(FilterAll); - m_contactIdRequest.setFilter(QContactGlobalPresence::match(QContactPresence::PresenceAvailable)); - m_contactIdRequest.start(); - } - } else if (m_fetchFilter == FilterOnline) { - m_fetchFilter = FilterNone; + qDebug() << "All queried in" << m_timer.elapsed() << "ms"; - if (m_updatesPending) { - QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); - } + // Now query for online contacts + m_fetchRequest.setFilter(onlineFilter()); + m_fetchRequest.setFetchHint(onlineFetchHint(m_fetchTypes)); + m_fetchRequest.start(); - if (!isPopulated(FilterOnline)) { - qDebug() << "Online queried in" << m_timer.elapsed() << "ms"; - m_fetchRequest.setFetchHint(QContactFetchHint()); + m_appendIndex = 0; + m_populateProgress = FetchOnline; + activityCompleted = false; + } else if (m_populateProgress == FetchOnline) { makePopulated(FilterOnline); + qDebug() << "Online queried in" << m_timer.elapsed() << "ms"; + + m_populateProgress = Populated; + } else if (m_populateProgress == RefetchFavorites) { + // Re-fetch the non-favorites + m_fetchRequest.setFilter(nonfavoriteFilter()); + m_fetchRequest.setFetchHint(onlineFetchHint(m_fetchTypes)); + m_fetchRequest.start(); + + m_populateProgress = RefetchOthers; + } else if (m_populateProgress == RefetchOthers) { + // We're up to date again + m_populateProgress = Populated; } else { - finalizeUpdate(FilterOnline); - } - } else if (m_fetchFilter == FilterNone) { - // Result of a specific query - if (m_updatesPending) { - QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); + // Result of a specific query + if (m_activeResolve) { + CacheItem *item = 0; + if (!m_fetchRequest.contacts().isEmpty()) { + item = itemById(apiId(m_fetchRequest.contacts().first()), false); + } + m_activeResolve->listener->addressResolved(item); + + m_activeResolve = 0; + m_resolveAddresses.takeFirst(); + } } } + + if (activityCompleted) { + // See if there are any more requests to dispatch + QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); + } } void SeasideCache::makePopulated(FilterType filter) @@ -1608,7 +1947,7 @@ QString SeasideCache::exportContacts() for (iterator it = instancePtr->m_people.begin(); it != instancePtr->m_people.end(); ++it) { if (it.key() == selfId) { continue; - } else if (it->contactState == ContactFetched) { + } else if (it->contactState == ContactComplete) { contacts.append(it->contact); } else { contactsToFetch.append(apiId(it.key())); @@ -1655,15 +1994,17 @@ QString SeasideCache::exportContacts() return vcard.fileName(); } -void SeasideCache::keepPopulated() +void SeasideCache::keepPopulated(quint32 fetchTypes) { + if ((m_fetchTypes & fetchTypes) != fetchTypes) { + m_fetchTypes |= fetchTypes; + m_fetchTypesChanged = true; + requestUpdate(); + } + if (!m_keepPopulated) { m_keepPopulated = true; - - // Start a query to fully populate the cache, starting with favorites - m_fetchFilter = FilterFavorites; - m_fetchRequest.setFilter(QContactFavorite::match()); - m_fetchRequest.start(); + requestUpdate(); } } @@ -1764,6 +2105,18 @@ void SeasideCache::completeContactAggregation(const ContactIdType &contact1Id, c 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; + + m_resolveAddresses.append(data); + requestUpdate(); +} + QContactRelationship SeasideCache::makeRelationship(const QString &type, const QContact &contact1, const QContact &contact2) { QContactRelationship relationship; diff --git a/src/seasidecache.h b/src/seasidecache.h index 3254417..7583da3 100644 --- a/src/seasidecache.h +++ b/src/seasidecache.h @@ -67,6 +67,12 @@ USE_CONTACTS_NAMESPACE +#ifdef USING_QTPIM +typedef QContactDetail::DetailType DetailTypeId; +#else +typedef QLatin1String DetailTypeId; +#endif + class CONTACTCACHE_EXPORT SeasideNameGroupChangeListener { public: @@ -90,6 +96,13 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject FilterTypesCount }; + enum FetchDataType { + FetchNone = 0, + FetchAccountUri = (1 << 0), + FetchPhoneNumber = (1 << 1), + FetchEmailAddress = (1 << 2) + }; + enum DisplayLabelOrder { FirstNameFirst, LastNameFirst @@ -97,8 +110,9 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject enum ContactState { ContactAbsent, + ContactPartial, ContactRequested, - ContactFetched + ContactComplete }; struct ItemData @@ -108,7 +122,7 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject virtual QString getDisplayLabel() const = 0; virtual void displayLabelOrderChanged(DisplayLabelOrder order) = 0; - virtual void updateContact(const QContact &newContact, QContact *oldContact) = 0; + virtual void updateContact(const QContact &newContact, QContact *oldContact, ContactState state) = 0; virtual void constituentsFetched(const QList &ids) = 0; virtual void mergeCandidatesFetched(const QList &ids) = 0; @@ -121,7 +135,7 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject { virtual ~ModelData() {} - virtual void contactChanged(const QContact &newContact) = 0; + virtual void contactChanged(const QContact &newContact, ContactState state) = 0; }; struct CacheItem @@ -168,6 +182,13 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject virtual void updateDisplayLabelOrder() = 0; }; + struct ResolveListener + { + virtual ~ResolveListener() {} + + virtual void addressResolved(CacheItem *item) = 0; + }; + static SeasideCache *instance(); static ContactIdType apiId(const QContact &contact); @@ -184,7 +205,7 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject static quint32 internalId(QContactLocalId id); #endif - static void registerModel(ListModel *model, FilterType type); + static void registerModel(ListModel *model, FilterType type, FetchDataType extraData = FetchNone); static void unregisterModel(ListModel *model); static void registerUser(QObject *user); @@ -201,18 +222,27 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject #ifdef USING_QTPIM static CacheItem *existingItem(quint32 iid); #endif - static CacheItem *itemById(const ContactIdType &id); + static CacheItem *itemById(const ContactIdType &id, bool requireComplete = true); #ifdef USING_QTPIM - static CacheItem *itemById(int id); + static CacheItem *itemById(int id, bool requireComplete = true); #endif static ContactIdType selfContactId(); static QContact contactById(const ContactIdType &id); + + static void ensureCompletion(CacheItem *cacheItem); + static QChar nameGroupForCacheItem(CacheItem *cacheItem); static QList allNameGroups(); static QHash > nameGroupMembers(); - static CacheItem *itemByPhoneNumber(const QString &msisdn); - static CacheItem *itemByEmailAddress(const QString &email); + static CacheItem *itemByPhoneNumber(const QString &number, bool requireComplete = true); + static CacheItem *itemByEmailAddress(const QString &address, bool requireComplete = true); + static CacheItem *itemByOnlineAccount(const QString &localUid, const QString &remoteUid, bool requireComplete = true); + + static CacheItem *resolvePhoneNumber(ResolveListener *listener, const QString &number, bool requireComplete = true); + static CacheItem *resolveEmailAddress(ResolveListener *listener, const QString &address, bool requireComplete = true); + static CacheItem *resolveOnlineAccount(ResolveListener *listener, const QString &localUid, const QString &remoteUid, bool requireComplete = true); + static bool saveContact(const QContact &contact); static bool removeContact(const QContact &contact); @@ -235,8 +265,8 @@ class CONTACTCACHE_EXPORT SeasideCache : public QObject // For synchronizeLists() int insertRange(int index, int count, const QList &source, int sourceIndex) { - return insertRange(m_fetchFilter, index, count, source, sourceIndex); } - int removeRange(int index, int count) { removeRange(m_fetchFilter, index, count); return 0; } + return insertRange(m_syncFilter, index, count, source, sourceIndex); } + int removeRange(int index, int count) { removeRange(m_syncFilter, index, count); return 0; } protected: void timerEvent(QTimerEvent *event); @@ -260,18 +290,31 @@ private slots: void displayLabelOrderChanged(); private: + enum PopulateProgress { + Unpopulated, + FetchFavorites, + FetchMetadata, + FetchOnline, + Populated, + RefetchFavorites, + RefetchOthers + }; + SeasideCache(); ~SeasideCache(); static void checkForExpiry(); - void keepPopulated(); + void keepPopulated(quint32 fetchTypes); void requestUpdate(); - void appendContacts(const QList &contacts); + void appendContacts(const QList &contacts, FilterType filterType, bool partialFetch); void fetchContacts(); void updateContacts(const QList &contactIds); + bool updateContactIndexing(const QContact &oldContact, const QContact &contact, quint32 iid, const QSet &queryDetailTypes); + void updateCache(CacheItem *item, const QContact &contact, bool partialFetch); + void finalizeUpdate(FilterType filter); void removeRange(FilterType filter, int index, int count); int insertRange( @@ -281,7 +324,8 @@ private slots: const QList &queryIds, int queryIndex); - void updateContactData(const ContactIdType &contactId, FilterType filter); + void contactDataChanged(const ContactIdType &contactId); + void contactDataChanged(const ContactIdType &contactId, FilterType filter); void removeContactData(const ContactIdType &contactId, FilterType filter); void makePopulated(FilterType filter); @@ -292,6 +336,8 @@ private slots: void updateConstituentAggregations(const ContactIdType &contactId); void completeContactAggregation(const ContactIdType &contact1Id, const ContactIdType &contact2Id); + void resolveAddress(ResolveListener *listener, const QString &first, const QString &second, bool requireComplete); + static QContactRelationship makeRelationship(const QString &type, const QContact &contact1, const QContact &contact2); QBasicTimer m_expiryTimer; @@ -299,6 +345,7 @@ private slots: QHash m_people; QHash m_phoneNumberIds; QHash m_emailAddressIds; + QHash, quint32> m_onlineAccountIds; QHash m_contactsToSave; QHash > m_contactNameGroups; QList m_contactsToCreate; @@ -335,16 +382,27 @@ private slots: int m_cacheIndex; int m_queryIndex; int m_appendIndex; - FilterType m_fetchFilter; + FilterType m_syncFilter; DisplayLabelOrder m_displayLabelOrder; bool m_keepPopulated; + PopulateProgress m_populateProgress; + quint32 m_fetchTypes; + bool m_fetchTypesChanged; bool m_updatesPending; - bool m_fetchActive; bool m_refreshRequired; bool m_contactsUpdated; QList m_constituentIds; QList m_candidateIds; + struct ResolveData { + QString first; + QString second; + bool requireComplete; + ResolveListener *listener; + }; + QList m_resolveAddresses; + const ResolveData *m_activeResolve; + QElapsedTimer m_timer; QElapsedTimer m_fetchPostponed;