/* * 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 "seasideimport.h" #include "seasidecache.h" #include "seasidepropertyhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USING_QTPIM #include #include #else #include #endif #include #include #include #include #include #include namespace { QContactFetchHint basicFetchHint() { QContactFetchHint fetchHint; fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships | QContactFetchHint::NoActionPreferences | QContactFetchHint::NoBinaryBlobs); return fetchHint; } QContactFilter localContactFilter() { // Contacts that are local to the device have sync target 'local' or 'was_local' QContactDetailFilter filterLocal, filterWasLocal; #ifdef USING_QTPIM filterLocal.setDetailType(QContactSyncTarget::Type, QContactSyncTarget::FieldSyncTarget); filterWasLocal.setDetailType(QContactSyncTarget::Type, QContactSyncTarget::FieldSyncTarget); #else filterLocal.setDetailDefinitionName(QContactSyncTarget::DefinitionName, QContactSyncTarget::FieldSyncTarget); filterWasLocal.setDetailDefinitionName(QContactSyncTarget::DefinitionName, QContactSyncTarget::FieldSyncTarget); #endif filterLocal.setValue(QString::fromLatin1("local")); filterWasLocal.setValue(QString::fromLatin1("was_local")); return filterLocal | filterWasLocal; } QString contactNameString(const QContact &contact) { QStringList details; QContactName name(contact.detail()); details.append(name.prefix()); details.append(name.firstName()); details.append(name.middleName()); details.append(name.lastName()); details.append(name.suffix()); return details.join(QChar::fromLatin1('|')); } template QVariant detailValue(const T &detail, F field) { #ifdef USING_QTPIM return detail.value(field); #else return detail.variantValue(field); #endif } #ifdef USING_QTPIM typedef QMap DetailMap; #else typedef QVariantMap DetailMap; #endif DetailMap detailValues(const QContactDetail &detail) { #ifdef USING_QTPIM DetailMap rv(detail.values()); #else DetailMap rv(detail.variantValues()); #endif return rv; } static bool variantEqual(const QVariant &lhs, const QVariant &rhs) { #ifdef USING_QTPIM // Work around incorrect result from QVariant::operator== when variants contain QList static const int QListIntType = QMetaType::type("QList"); const int lhsType = lhs.userType(); if (lhsType != rhs.userType()) { return false; } if (lhsType == QListIntType) { return (lhs.value >() == rhs.value >()); } #endif return (lhs == rhs); } static bool detailValuesSuperset(const QContactDetail &lhs, const QContactDetail &rhs) { // True if all values in rhs are present in lhs const DetailMap lhsValues(detailValues(lhs)); const DetailMap rhsValues(detailValues(rhs)); if (lhsValues.count() < rhsValues.count()) { return false; } foreach (const DetailMap::key_type &key, rhsValues.keys()) { if (!variantEqual(lhsValues[key], rhsValues[key])) { return false; } } return true; } static void fixupDetail(QContactDetail &) { } #ifdef USING_QTPIM // Fixup QContactUrl because importer produces incorrectly typed URL field static void fixupDetail(QContactUrl &url) { QVariant urlField = url.value(QContactUrl::FieldUrl); if (!urlField.isNull()) { QString urlString = urlField.toString(); if (!urlString.isEmpty()) { url.setValue(QContactUrl::FieldUrl, QUrl(urlString)); } else { url.setValue(QContactUrl::FieldUrl, QVariant()); } } } // Fixup QContactOrganization because importer produces invalid department static void fixupDetail(QContactOrganization &org) { QVariant deptField = org.value(QContactOrganization::FieldDepartment); if (!deptField.isNull()) { QStringList deptList = deptField.toStringList(); // Remove any empty elements from the list QStringList::iterator it = deptList.begin(); while (it != deptList.end()) { if ((*it).isEmpty()) { it = deptList.erase(it); } else { ++it; } } if (!deptList.isEmpty()) { org.setValue(QContactOrganization::FieldDepartment, deptList); } else { org.setValue(QContactOrganization::FieldDepartment, QVariant()); } } } #endif template bool updateExistingDetails(QContact *updateContact, const QContact &importedContact, bool singular = false) { bool rv = false; QList existingDetails(updateContact->details()); if (singular && !existingDetails.isEmpty()) return rv; foreach (T detail, importedContact.details()) { // Make any corrections to the input fixupDetail(detail); // See if the contact already has a detail which is a superset of this one bool found = false; foreach (const T &existing, existingDetails) { if (detailValuesSuperset(existing, detail)) { found = true; break; } } if (!found) { updateContact->saveDetail(&detail); rv = true; } } return rv; } bool mergeIntoExistingContact(QContact *updateContact, const QContact &importedContact) { bool rv = false; // Update the existing contact with any details in the new import rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact, true); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); rv |= updateExistingDetails(updateContact, importedContact); #ifdef USING_QTPIM rv |= updateExistingDetails(updateContact, importedContact); #endif return rv; } bool updateExistingContact(QContact *updateContact, const QContact &contact) { // Replace the imported contact with the existing version QContact importedContact(*updateContact); *updateContact = contact; return mergeIntoExistingContact(updateContact, importedContact); } void setNickname(QContact &contact, const QString &text) { foreach (const QContactNickname &nick, contact.details()) { if (nick.nickname() == text) { return; } } QContactNickname nick; nick.setNickname(text); contact.saveDetail(&nick); } } QList SeasideImport::buildImportContacts(const QList &details, int *newCount, int *updatedCount) { if (newCount) *newCount = 0; if (updatedCount) *updatedCount = 0; // Read the contacts from the import details SeasidePropertyHandler propertyHandler; QVersitContactImporter importer; importer.setPropertyHandler(&propertyHandler); importer.importDocuments(details); QList importedContacts(importer.contacts()); QHash importGuids; QHash importNames; QHash importLabels; // Merge any duplicates in the import list QList::iterator it = importedContacts.begin(); while (it != importedContacts.end()) { QContact &contact(*it); const QString guid = contact.detail().guid(); const bool emptyName = contact.detail().isEmpty(); const QString name = contactNameString(contact); const QString label = contact.detail().label(); int previousIndex = -1; QHash::const_iterator git = importGuids.find(guid); if (git != importGuids.end()) { previousIndex = git.value(); } else { QHash::const_iterator nit = importNames.find(name); if (nit != importNames.end()) { previousIndex = nit.value(); } else { // Only if name is empty, use displayLabel - probably SIM import if (emptyName) { QHash::const_iterator lit = importLabels.find(label); if (lit != importLabels.end()) { previousIndex = lit.value(); } } } } if (previousIndex != -1) { // Combine these duplicate contacts QContact &previous(importedContacts[previousIndex]); mergeIntoExistingContact(&previous, contact); it = importedContacts.erase(it); } else { const int index = it - importedContacts.begin(); if (!guid.isEmpty()) { importGuids.insert(guid, index); } if (!emptyName) { importNames.insert(name, index); } else if (!label.isEmpty()) { importLabels.insert(label, index); // Modify this contact to have the label as a nickname setNickname(contact, label); } ++it; } } // Find all names and GUIDs for local contacts that might match these contacts QContactFetchHint fetchHint(basicFetchHint()); #ifdef USING_QTPIM fetchHint.setDetailTypesHint(QList() << QContactName::Type << QContactNickname::Type << QContactGuid::Type); #else fetchHint.setDetailDefinitionsHint(QStringList() << QContactName::DefinitionName << QContactNickname::DefinitionName << QContactGuid::DefinitionName); #endif QHash existingGuids; QHash existingNames; QHash existingNicknames; QContactManager *mgr(SeasideCache::manager()); foreach (const QContact &contact, mgr->contacts(localContactFilter(), QList(), fetchHint)) { const QString guid = contact.detail().guid(); const QString name = contactNameString(contact); if (!guid.isEmpty()) { existingGuids.insert(guid, contact.id()); } if (!name.isEmpty()) { existingNames.insert(name, contact.id()); } foreach (const QContactNickname &nick, contact.details()) { existingNicknames.insert(nick.nickname(), contact.id()); } } // Find any imported contacts that match contacts we already have QMap existingIds; it = importedContacts.begin(); while (it != importedContacts.end()) { const QString guid = (*it).detail().guid(); QContactId existingId; bool existing = true; QHash::const_iterator git = existingGuids.find(guid); if (git != existingGuids.end()) { existingId = *git; } else { const bool emptyName = (*it).detail().isEmpty(); if (!emptyName) { const QString name = contactNameString(*it); QHash::const_iterator nit = existingNames.find(name); if (nit != existingNames.end()) { existingId = *nit; } else { existing = false; } } else { const QString label = (*it).detail().label(); if (!label.isEmpty()) { QHash::const_iterator nit = existingNicknames.find(label); if (nit != existingNicknames.end()) { existingId = *nit; } else { existing = false; } } else { existing = false; } } } if (existing) { QMap::iterator eit = existingIds.find(existingId); if (eit == existingIds.end()) { existingIds.insert(existingId, (it - importedContacts.begin())); ++it; } else { // Combine these contacts with matching names QContact &previous(importedContacts[*eit]); mergeIntoExistingContact(&previous, *it); it = importedContacts.erase(it); } } else { ++it; } } int existingCount(existingIds.count()); if (existingCount > 0) { // Retrieve all the contacts that we have matches for #ifdef USING_QTPIM QContactIdFilter idFilter; idFilter.setIds(existingIds.keys()); #else QContactLocalIdFilter idFilter; QList localIds; foreach (const QContactId &id, existingIds.keys()) { localids.append(id.toLocal()); } #endif QSet modifiedContacts; QSet unmodifiedContacts; foreach (const QContact &contact, mgr->contacts(idFilter & localContactFilter(), QList(), basicFetchHint())) { QMap::const_iterator it = existingIds.find(contact.id()); if (it != existingIds.end()) { // Update the existing version of the contact with any new details QContact &importContact(importedContacts[*it]); bool modified = updateExistingContact(&importContact, contact); if (modified) { modifiedContacts.insert(importContact.id()); } else { unmodifiedContacts.insert(importContact.id()); } } else { qWarning() << "unable to update existing contact:" << contact.id(); } } if (!unmodifiedContacts.isEmpty()) { QList::iterator it = importedContacts.begin(); while (it != importedContacts.end()) { const QContact &importContact(*it); const QContactId contactId(importContact.id()); if (unmodifiedContacts.contains(contactId) && !modifiedContacts.contains(contactId)) { // This contact was not modified by import - don't update it it = importedContacts.erase(it); --existingCount; } else { ++it; } } } } if (updatedCount) *updatedCount = existingCount; if (newCount) *newCount = importedContacts.count() - existingCount; return importedContacts; }