pax_global_header00006660000000000000000000000064143637310760014524gustar00rootroot0000000000000052 comment=c771978ab639dc4c0086afd93de95ba5cd0d36c6 qtcontacts-sqlite-0.3.19/000077500000000000000000000000001436373107600153005ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/.building000066400000000000000000000000001436373107600170640ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/.gitignore000066400000000000000000000000401436373107600172620ustar00rootroot00000000000000*.moc *.o .*.swp Makefile moc_* qtcontacts-sqlite-0.3.19/LICENSE.BSD000066400000000000000000000026531436373107600167220ustar00rootroot00000000000000Redistribution 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 the copyright holder 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. qtcontacts-sqlite-0.3.19/README000066400000000000000000000001651436373107600161620ustar00rootroot00000000000000This repository contains a backend for the QtContacts API. It stores contact information to a local SQLite database. qtcontacts-sqlite-0.3.19/config.pri000066400000000000000000000005371436373107600172660ustar00rootroot00000000000000# Try to optimise for code size a bit QMAKE_CXXFLAGS += -ffunction-sections -fdata-sections -Wl,--gc-sections -flto CONFIG += \ c++11 \ link_pkgconfig PKGCONFIG += Qt5Contacts packagesExist(mlite5) { DEFINES += HAS_MLITE PKGCONFIG += mlite5 } DEFINES += CONTACTS_DATABASE_PATH=\"\\\"$$[QT_INSTALL_LIBS]/qtcontacts-sqlite-qt5/\\\"\" qtcontacts-sqlite-0.3.19/qtcontacts-sqlite.pro000066400000000000000000000001761436373107600215100ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ src \ tests OTHER_FILES += rpm/qtcontacts-sqlite-qt5.spec tests.depends = src qtcontacts-sqlite-0.3.19/rpm/000077500000000000000000000000001436373107600160765ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/rpm/qtcontacts-sqlite-qt5.spec000066400000000000000000000026561436373107600231540ustar00rootroot00000000000000Name: qtcontacts-sqlite-qt5 Version: 0.3.9 Release: 0 Summary: SQLite-based plugin for QtPIM Contacts License: BSD URL: https://github.com/sailfishos/qtcontacts-sqlite Source0: %{name}-%{version}.tar.gz BuildRequires: pkgconfig(Qt5Core) BuildRequires: pkgconfig(Qt5Sql) BuildRequires: pkgconfig(Qt5DBus) BuildRequires: pkgconfig(Qt5Contacts) >= 5.2.0 BuildRequires: pkgconfig(mlite5) Requires: qt5-plugin-sqldriver-sqlite %description %{summary}. %files %defattr(-,root,root,-) %license LICENSE.BSD %{_libdir}/qt5/plugins/contacts/*.so* %package tests Summary: Unit tests for qtcontacts-sqlite-qt5 BuildRequires: pkgconfig(Qt5Test) Requires: blts-tools Requires: %{name} = %{version}-%{release} %description tests This package contains unit tests for the qtcontacts-sqlite-qt5 library. %files tests %defattr(-,root,root,-) /opt/tests/qtcontacts-sqlite-qt5/* %{_libdir}/qtcontacts-sqlite-qt5/libtestdlgg.so %package extensions Summary: QtContacts extension headers for qtcontacts-sqlite-qt5 Requires: %{name} = %{version}-%{release} %description extensions This package contains extension headers for the qtcontacts-sqlite-qt5 library. %files extensions %defattr(-,root,root,-) %{_libdir}/pkgconfig/qtcontacts-sqlite-qt5-extensions.pc %{_includedir}/qtcontacts-sqlite-qt5-extensions/* %prep %setup -q -n %{name}-%{version} %build %qmake5 "VERSION=%{version}" "PKGCONFIG_LIB=%{_lib}" make %{?_smp_mflags} %install %qmake5_install qtcontacts-sqlite-0.3.19/src/000077500000000000000000000000001436373107600160675ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/src/engine/000077500000000000000000000000001436373107600173345ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/src/engine/.gitignore000066400000000000000000000000121436373107600213150ustar00rootroot00000000000000contacts/ qtcontacts-sqlite-0.3.19/src/engine/contactid.cpp000066400000000000000000000113261436373107600220130ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2014 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * 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 "contactid_p.h" #include #include #include namespace { QString dbIdToString(quint32 dbId, bool isCollection = false) { return isCollection ? QStringLiteral("col-%1").arg(dbId) : QStringLiteral("sql-%1").arg(dbId); } quint32 dbIdFromString(const QString &s, bool isCollection = false) { if ((isCollection && s.startsWith(QStringLiteral("col-"))) || (!isCollection && s.startsWith(QStringLiteral("sql-")))) { return s.mid(4).toUInt(); } return 0; } QByteArray dbIdToByteArray(quint32 dbId, bool isCollection = false) { return isCollection ? (QByteArrayLiteral("col-") + QByteArray::number(dbId)) : (QByteArrayLiteral("sql-") + QByteArray::number(dbId)); } quint32 dbIdFromByteArray(const QByteArray &b, bool isCollection = false) { if ((isCollection && b.startsWith(QByteArrayLiteral("col-"))) || (!isCollection && b.startsWith(QByteArrayLiteral("sql-")))) { return b.mid(4).toUInt(); } return 0; } } namespace ContactId { QContactId apiId(const QContact &contact) { return contact.id(); } QContactId apiId(quint32 dbId, const QString &manager_uri) { return QContactId(manager_uri, dbIdToByteArray(dbId)); } quint32 databaseId(const QContact &contact) { return databaseId(contact.id()); } quint32 databaseId(const QContactId &apiId) { return dbIdFromByteArray(apiId.localId()); } QString toString(const QContactId &apiId) { return dbIdToString(databaseId(apiId)); } QString toString(const QContact &c) { return toString(c.id()); } QContactId fromString(const QString &s, const QString &manager_uri) { return apiId(dbIdFromString(s), manager_uri); } bool isValid(const QContact &contact) { return isValid(databaseId(contact)); } bool isValid(const QContactId &contactId) { return isValid(databaseId(contactId)); } bool isValid(quint32 dbId) { return (dbId != 0); } } // namespace ContactId namespace ContactCollectionId { QContactCollectionId apiId(const QContactCollection &collection) { return collection.id(); } QContactCollectionId apiId(quint32 dbId, const QString &manager_uri) { return QContactCollectionId(manager_uri, dbIdToByteArray(dbId, true)); } quint32 databaseId(const QContactCollection &collection) { return databaseId(collection.id()); } quint32 databaseId(const QContactCollectionId &apiId) { return dbIdFromByteArray(apiId.localId(), true); } QString toString(const QContactCollectionId &apiId) { return dbIdToString(databaseId(apiId), true); } QString toString(const QContactCollection &c) { return toString(c.id()); } QContactCollectionId fromString(const QString &s, const QString &manager_uri) { return apiId(dbIdFromString(s, true), manager_uri); } bool isValid(const QContactCollection &collection) { return isValid(databaseId(collection)); } bool isValid(const QContactCollectionId &collectionId) { return isValid(databaseId(collectionId)); } bool isValid(quint32 dbId) { return (dbId != 0); } } // namespace ContactCollectionId qtcontacts-sqlite-0.3.19/src/engine/contactid_p.h000066400000000000000000000060561436373107600220030ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2014 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTSSQLITE_CONTACTID_P_H #define QTCONTACTSSQLITE_CONTACTID_P_H #include #include #include #include #include QTCONTACTS_USE_NAMESPACE namespace ContactId { QContactId apiId(const QContact &contact); QContactId apiId(quint32 databaseId, const QString &manager_uri); quint32 databaseId(const QContact &contact); quint32 databaseId(const QContactId &apiId); bool isValid(const QContact &contact); bool isValid(const QContactId &apiId); bool isValid(quint32 dbId); QString toString(const QContactId &apiId); QString toString(const QContact &c); QContactId fromString(const QString &id); } // namespace ContactId namespace ContactCollectionId { QContactCollectionId apiId(const QContactCollection &collection); QContactCollectionId apiId(quint32 databaseId, const QString &manager_uri); quint32 databaseId(const QContactCollection &collection); quint32 databaseId(const QContactCollectionId &apiId); bool isValid(const QContactCollection &collection); bool isValid(const QContactCollectionId &apiId); bool isValid(quint32 dbId); QString toString(const QContactCollectionId &apiId); QString toString(const QContactCollection &c); QContactCollectionId fromString(const QString &id); } // namespace ContactId #endif // QTCONTACTSSQLITE_CONTACTIDIMPL qtcontacts-sqlite-0.3.19/src/engine/contactnotifier.cpp000066400000000000000000000224451436373107600232420ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * 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 "contactnotifier.h" #include "trace_p.h" #include #include #include #include #include #include #include #define NOTIFIER_NAME "org.nemomobile.contacts.sqlite.uuid_%1" #define NOTIFIER_PATH "/org/nemomobile/contacts/sqlite" #define NOTIFIER_INTERFACE "org.nemomobile.contacts.sqlite" Q_DECLARE_METATYPE(QVector) namespace { bool initialized = false; void initialize() { if (!initialized) { initialized = true; qDBusRegisterMetaType >(); } } QString pathName() { return QString::fromLatin1(NOTIFIER_PATH); } QString interfaceName(bool nonprivileged) { return QString::fromLatin1(NOTIFIER_INTERFACE) + QString::fromLatin1(nonprivileged ? ".np" : ""); } QDBusMessage createSignal(const char *name, bool nonprivileged) { return QDBusMessage::createSignal(pathName(), interfaceName(nonprivileged), QString::fromLatin1(name)); } QVector idVector(const QList &contactIds) { QVector ids; ids.reserve(contactIds.size()); foreach (const QContactId &id, contactIds) { ids.append(ContactId::databaseId(id)); } return ids; } QVector idVector(const QList &collectionIds) { QVector ids; ids.reserve(collectionIds.size()); foreach (const QContactCollectionId &id, collectionIds) { ids.append(ContactCollectionId::databaseId(id)); } return ids; } } ContactNotifier::ContactNotifier(bool nonprivileged) : m_nonprivileged(nonprivileged) { initialize(); } ContactNotifier::~ContactNotifier() { if (QDBusConnection::sessionBus().isConnected() && !m_serviceName.isEmpty()) { QDBusConnection::sessionBus().unregisterService(m_serviceName); } } void ContactNotifier::collectionsAdded(const QList &collectionIds) { if (!collectionIds.isEmpty()) { QDBusMessage message = createSignal("collectionsAdded", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(collectionIds))); sendMessage(message); } } void ContactNotifier::collectionsChanged(const QList &collectionIds) { if (!collectionIds.isEmpty()) { QDBusMessage message = createSignal("collectionsChanged", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(collectionIds))); sendMessage(message); } } void ContactNotifier::collectionsRemoved(const QList &collectionIds) { if (!collectionIds.isEmpty()) { QDBusMessage message = createSignal("collectionsRemoved", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(collectionIds))); sendMessage(message); } } void ContactNotifier::contactsAdded(const QList &contactIds) { if (!contactIds.isEmpty()) { QDBusMessage message = createSignal("contactsAdded", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(contactIds))); sendMessage(message); } } void ContactNotifier::contactsChanged(const QList &contactIds) { if (!contactIds.isEmpty()) { QDBusMessage message = createSignal("contactsChanged", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(contactIds))); sendMessage(message); } } void ContactNotifier::contactsPresenceChanged(const QList &contactIds) { if (!contactIds.isEmpty()) { QDBusMessage message = createSignal("contactsPresenceChanged", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(contactIds))); sendMessage(message); } } // notify that synced contacts have changed in the given collections void ContactNotifier::collectionContactsChanged(const QList &collectionIds) { if (!collectionIds.isEmpty()) { QDBusMessage message = createSignal("collectionContactsChanged", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(collectionIds))); sendMessage(message); } } void ContactNotifier::contactsRemoved(const QList &contactIds) { if (!contactIds.isEmpty()) { QDBusMessage message = createSignal("contactsRemoved", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(contactIds))); sendMessage(message); } } void ContactNotifier::selfContactIdChanged(QContactId oldId, QContactId newId) { if (oldId != newId) { QDBusMessage message = createSignal("selfContactIdChanged", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(ContactId::databaseId(oldId)) << QVariant::fromValue(ContactId::databaseId(newId))); sendMessage(message); } } void ContactNotifier::relationshipsAdded(const QSet &contactIds) { if (!contactIds.isEmpty()) { QDBusMessage message = createSignal("relationshipsAdded", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(contactIds.toList()))); sendMessage(message); } } void ContactNotifier::relationshipsRemoved(const QSet &contactIds) { if (!contactIds.isEmpty()) { QDBusMessage message = createSignal("relationshipsRemoved", m_nonprivileged); message.setArguments(QVariantList() << QVariant::fromValue(idVector(contactIds.toList()))); sendMessage(message); } } void ContactNotifier::displayLabelGroupsChanged() { QDBusMessage message = createSignal("displayLabelGroupsChanged", m_nonprivileged); sendMessage(message); } bool ContactNotifier::connect(const char *name, const char *signature, QObject *receiver, const char *slot) { static QDBusConnection connection(QDBusConnection::sessionBus()); if (!connection.isConnected()) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Session Bus is not connected")); return false; } if (!connection.connect(QString(), pathName(), interfaceName(m_nonprivileged), QLatin1String(name), QLatin1String(signature), receiver, slot)) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Unable to connect DBUS signal: %1").arg(name)); return false; } return true; } void ContactNotifier::sendMessage(const QDBusMessage &message) { static QDBusConnection connection(QDBusConnection::sessionBus()); if (!connection.isConnected()) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Session Bus is not connected")); return; } if (m_serviceName.isEmpty()) { // Register a unique name for this signal source on the session bus. // Remove surrounding braces and hyphens from the generated uuid. const QString uuid = QUuid::createUuid().toString(); const QString serviceName = QString(NOTIFIER_NAME) .arg(uuid.mid(1, uuid.length() - 2).replace('-', QString())); if (connection.registerService(serviceName)) { m_serviceName = serviceName; } else { QTCONTACTS_SQLITE_DEBUG( QString::fromLatin1("Failed to register D-Bus service name %1 for contact change notifications: %2 %3") .arg(serviceName) .arg(connection.lastError().name()) .arg(connection.lastError().message())); return; } } connection.send(message); } qtcontacts-sqlite-0.3.19/src/engine/contactnotifier.h000066400000000000000000000057431436373107600227110ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTSSQLITE_CONTACTNOTIFIER_H #define QTCONTACTSSQLITE_CONTACTNOTIFIER_H #include "contactid_p.h" #include #include #include class QDBusMessage; QTCONTACTS_USE_NAMESPACE class ContactNotifier { bool m_nonprivileged; public: ContactNotifier(bool nonprivileged); ~ContactNotifier(); void collectionsAdded(const QList &collectionIds); void collectionsChanged(const QList &collectionIds); void collectionsRemoved(const QList &collectionIds); void collectionContactsChanged(const QList &collectionIds); void contactsAdded(const QList &contactIds); void contactsChanged(const QList &contactIds); void contactsPresenceChanged(const QList &contactIds); void contactsRemoved(const QList &contactIds); void selfContactIdChanged(QContactId oldId, QContactId newId); void relationshipsAdded(const QSet &contactIds); void relationshipsRemoved(const QSet &contactIds); void displayLabelGroupsChanged(); bool connect(const char *name, const char *signature, QObject *receiver, const char *slot); private: void sendMessage(const QDBusMessage &message); QString m_serviceName; }; #endif qtcontacts-sqlite-0.3.19/src/engine/contactreader.cpp000066400000000000000000004414051436373107600226660ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2019 Jolla Ltd. * Copyright (c) 2019 - 2020 Open Mobile Platform LLC. * * 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 "contactreader.h" #include "contactsengine.h" #include "trace_p.h" #include "../extensions/qtcontacts-extensions.h" #include "../extensions/qcontactdeactivated.h" #include "../extensions/qcontactoriginmetadata.h" #include "../extensions/qcontactstatusflags.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 #include #include #include #include #include #include #include #include #include static const int ReportBatchSize = 50; static const QString aggregateSyncTarget(QStringLiteral("aggregate")); static const QString localSyncTarget(QStringLiteral("local")); static const QString wasLocalSyncTarget(QStringLiteral("was_local")); enum FieldType { StringField = 0, StringListField, LocalizedField, LocalizedListField, IntegerField, DateField, BooleanField, RealField, OtherField }; static const int invalidField = -1; struct FieldInfo { int field; const char *column; FieldType fieldType; }; static void setValue(QContactDetail *detail, int key, const QVariant &value) { if (value.type() != QVariant::String || !value.toString().isEmpty()) detail->setValue(key, value); } static void setDetailImmutableIfAggregate(bool isAggregate, QContactDetail *detail) { // all details of an aggregate contact are immutable. if (isAggregate) { setValue(detail, QContactDetail__FieldModifiable, false); QContactManagerEngine::setDetailAccessConstraints(detail, QContactDetail::ReadOnly | QContactDetail::Irremovable); } } static QVariant stringListValue(const QVariant &columnValue) { if (columnValue.isNull()) return columnValue; QString listString(columnValue.toString()); return listString.split(QLatin1Char(';'), QString::SkipEmptyParts); } static QVariant urlValue(const QVariant &columnValue) { if (columnValue.isNull()) return columnValue; QString urlString(columnValue.toString()); return QUrl(urlString); } static QVariant dateValue(const QVariant &columnValue) { if (columnValue.isNull()) return columnValue; QString dtString(columnValue.toString()); return QDate::fromString(dtString, Qt::ISODate); } static const FieldInfo timestampFields[] = { { QContactTimestamp::FieldCreationTimestamp, "created", DateField }, { QContactTimestamp::FieldModificationTimestamp, "modified", DateField } }; static const FieldInfo statusFlagsFields[] = { // No specific field; tests hasPhoneNumber/hasEmailAddress/hasOnlineAccount/isOnline/isDeactivated/isDeleted { QContactStatusFlags::FieldFlags, "", OtherField } }; static const FieldInfo typeFields[] = { { QContactType::FieldType, "type", IntegerField } }; static const FieldInfo addressFields[] = { { QContactAddress::FieldStreet, "street", LocalizedField }, { QContactAddress::FieldPostOfficeBox, "postOfficeBox", LocalizedField }, { QContactAddress::FieldRegion, "region", LocalizedField }, { QContactAddress::FieldLocality, "locality", LocalizedField }, { QContactAddress::FieldPostcode, "postCode", LocalizedField }, { QContactAddress::FieldCountry, "country", LocalizedField }, { QContactAddress::FieldSubTypes, "subTypes", StringListField }, { QContactDetail::FieldContext, "context", StringField } }; static QList subTypeList(const QStringList &subTypeValues) { QList rv; foreach (const QString &value, subTypeValues) { rv.append(value.toInt()); } return rv; } static void setValues(QContactAddress *detail, QSqlQuery *query, const int offset) { typedef QContactAddress T; setValue(detail, T::FieldStreet , query->value(offset + 0)); setValue(detail, T::FieldPostOfficeBox, query->value(offset + 1)); setValue(detail, T::FieldRegion , query->value(offset + 2)); setValue(detail, T::FieldLocality , query->value(offset + 3)); setValue(detail, T::FieldPostcode , query->value(offset + 4)); setValue(detail, T::FieldCountry , query->value(offset + 5)); const QStringList subTypeValues(query->value(offset + 6).toString().split(QLatin1Char(';'), QString::SkipEmptyParts)); setValue(detail, T::FieldSubTypes , QVariant::fromValue >(subTypeList(subTypeValues))); } static const FieldInfo anniversaryFields[] = { { QContactAnniversary::FieldOriginalDate, "originalDateTime", DateField }, { QContactAnniversary::FieldCalendarId, "calendarId", StringField }, { QContactAnniversary::FieldSubType, "subType", StringField }, { QContactAnniversary::FieldEvent, "event", StringField } }; static void setValues(QContactAnniversary *detail, QSqlQuery *query, const int offset) { typedef QContactAnniversary T; setValue(detail, T::FieldOriginalDate, dateValue(query->value(offset + 0))); setValue(detail, T::FieldCalendarId , query->value(offset + 1)); setValue(detail, T::FieldSubType , QVariant::fromValue(query->value(offset + 2).toString())); setValue(detail, T::FieldEvent , query->value(offset + 3)); } static const FieldInfo avatarFields[] = { { QContactAvatar::FieldImageUrl, "imageUrl", StringField }, { QContactAvatar::FieldVideoUrl, "videoUrl", StringField }, { QContactAvatar::FieldMetaData, "avatarMetadata", StringField } }; static void setValues(QContactAvatar *detail, QSqlQuery *query, const int offset) { typedef QContactAvatar T; setValue(detail, T::FieldImageUrl, urlValue(query->value(offset + 0))); setValue(detail, T::FieldVideoUrl, urlValue(query->value(offset + 1))); setValue(detail, QContactAvatar::FieldMetaData, query->value(offset + 2)); } static const FieldInfo birthdayFields[] = { { QContactBirthday::FieldBirthday, "birthday", DateField }, { QContactBirthday::FieldCalendarId, "calendarId", StringField } }; static void setValues(QContactBirthday *detail, QSqlQuery *query, const int offset) { typedef QContactBirthday T; setValue(detail, T::FieldBirthday , dateValue(query->value(offset + 0))); setValue(detail, T::FieldCalendarId, query->value(offset + 1)); } static const FieldInfo displayLabelFields[] = { { QContactDisplayLabel::FieldLabel, "displayLabel", LocalizedField }, { QContactDisplayLabel__FieldLabelGroup, "displayLabelGroup", LocalizedField }, { QContactDisplayLabel__FieldLabelGroupSortOrder, "displayLabelGroupSortOrder", IntegerField } }; static void setValues(QContactDisplayLabel *detail, QSqlQuery *query, const int offset) { typedef QContactDisplayLabel T; const QString label = query->value(offset + 0).toString(); const QString group = query->value(offset + 1).toString(); const int sortOrder = query->value(offset + 2).toInt(); if (!label.trimmed().isEmpty()) setValue(detail, T::FieldLabel, label); if (!group.trimmed().isEmpty()) setValue(detail, QContactDisplayLabel__FieldLabelGroup, group); if (!label.trimmed().isEmpty() || !group.trimmed().isEmpty()) setValue(detail, QContactDisplayLabel__FieldLabelGroupSortOrder, sortOrder); } static const FieldInfo emailAddressFields[] = { { QContactEmailAddress::FieldEmailAddress, "emailAddress", StringField }, { invalidField, "lowerEmailAddress", StringField }, { QContactDetail::FieldContext, "context", StringField } }; static void setValues(QContactEmailAddress *detail, QSqlQuery *query, const int offset) { typedef QContactEmailAddress T; setValue(detail, T::FieldEmailAddress, query->value(offset + 0)); // ignore lowerEmailAddress } static const FieldInfo familyFields[] = { { QContactFamily::FieldSpouse, "spouse", LocalizedField }, { QContactFamily::FieldChildren, "children", LocalizedListField } }; static void setValues(QContactFamily *detail, QSqlQuery *query, const int offset) { typedef QContactFamily T; setValue(detail, T::FieldSpouse , query->value(offset + 0)); setValue(detail, T::FieldChildren, query->value(offset + 1).toString().split(QLatin1Char(';'), QString::SkipEmptyParts)); } static const FieldInfo favoriteFields[] = { { QContactFavorite::FieldFavorite, "isFavorite", BooleanField }, }; static void setValues(QContactFavorite *detail, QSqlQuery *query, const int offset) { typedef QContactFavorite T; setValue(detail, T::FieldFavorite , query->value(offset + 0).toBool()); } static const FieldInfo genderFields[] = { { QContactGender::FieldGender, "gender", StringField }, }; static void setValues(QContactGender *detail, QSqlQuery *query, const int offset) { typedef QContactGender T; setValue(detail, T::FieldGender, static_cast(query->value(offset + 0).toString().toInt())); } static const FieldInfo geoLocationFields[] = { { QContactGeoLocation::FieldLabel, "label", LocalizedField }, { QContactGeoLocation::FieldLatitude, "latitude", RealField }, { QContactGeoLocation::FieldLongitude, "longitude", RealField }, { QContactGeoLocation::FieldAccuracy, "accuracy", RealField }, { QContactGeoLocation::FieldAltitude, "altitude", RealField }, { QContactGeoLocation::FieldAltitudeAccuracy, "altitudeAccuracy", RealField }, { QContactGeoLocation::FieldHeading, "heading", RealField }, { QContactGeoLocation::FieldSpeed, "speed", RealField }, { QContactGeoLocation::FieldTimestamp, "timestamp", DateField } }; static void setValues(QContactGeoLocation *detail, QSqlQuery *query, const int offset) { typedef QContactGeoLocation T; setValue(detail, T::FieldLabel , query->value(offset + 0)); setValue(detail, T::FieldLatitude , query->value(offset + 1).toDouble()); setValue(detail, T::FieldLongitude , query->value(offset + 2).toDouble()); setValue(detail, T::FieldAccuracy , query->value(offset + 3).toDouble()); setValue(detail, T::FieldAltitude , query->value(offset + 4).toDouble()); setValue(detail, T::FieldAltitudeAccuracy, query->value(offset + 5).toDouble()); setValue(detail, T::FieldHeading , query->value(offset + 6).toDouble()); setValue(detail, T::FieldSpeed , query->value(offset + 7).toDouble()); setValue(detail, T::FieldTimestamp , ContactsDatabase::fromDateTimeString(query->value(offset + 8).toString())); } static const FieldInfo guidFields[] = { { QContactGuid::FieldGuid, "guid", StringField } }; static void setValues(QContactGuid *detail, QSqlQuery *query, const int offset) { typedef QContactGuid T; setValue(detail, T::FieldGuid, query->value(offset + 0)); } static const FieldInfo hobbyFields[] = { { QContactHobby::FieldHobby, "hobby", LocalizedField } }; static void setValues(QContactHobby *detail, QSqlQuery *query, const int offset) { typedef QContactHobby T; setValue(detail, T::FieldHobby, query->value(offset + 0)); } static const FieldInfo nameFields[] = { { QContactName::FieldFirstName, "firstName", LocalizedField }, { invalidField, "lowerFirstName", LocalizedField }, { QContactName::FieldLastName, "lastName", LocalizedField }, { invalidField, "lowerLastName", LocalizedField }, { QContactName::FieldMiddleName, "middleName", LocalizedField }, { QContactName::FieldPrefix, "prefix", LocalizedField }, { QContactName::FieldSuffix, "suffix", LocalizedField }, { QContactName::FieldCustomLabel, "customLabel", LocalizedField } }; static void setValues(QContactName *detail, QSqlQuery *query, const int offset) { typedef QContactName T; setValue(detail, T::FieldFirstName, query->value(offset + 0)); // ignore lowerFirstName setValue(detail, T::FieldLastName, query->value(offset + 2)); // ignore lowerLastName setValue(detail, T::FieldMiddleName, query->value(offset + 4)); setValue(detail, T::FieldPrefix, query->value(offset + 5)); setValue(detail, T::FieldSuffix, query->value(offset + 6)); setValue(detail, T::FieldCustomLabel, query->value(offset + 7)); } static const FieldInfo nicknameFields[] = { { QContactNickname::FieldNickname, "nickname", LocalizedField }, { invalidField, "lowerNickname", LocalizedField } }; static void setValues(QContactNickname *detail, QSqlQuery *query, const int offset) { typedef QContactNickname T; setValue(detail, T::FieldNickname, query->value(offset + 0)); // ignore lowerNickname } static const FieldInfo noteFields[] = { { QContactNote::FieldNote, "note", LocalizedField } }; static void setValues(QContactNote *detail, QSqlQuery *query, const int offset) { typedef QContactNote T; setValue(detail, T::FieldNote, query->value(offset + 0)); } static const FieldInfo onlineAccountFields[] = { { QContactOnlineAccount::FieldAccountUri, "accountUri", StringField }, { invalidField, "lowerAccountUri", StringField }, { QContactOnlineAccount::FieldProtocol, "protocol", StringField }, { QContactOnlineAccount::FieldServiceProvider, "serviceProvider", LocalizedField }, { QContactOnlineAccount::FieldCapabilities, "capabilities", StringListField }, { QContactOnlineAccount::FieldSubTypes, "subTypes", StringListField }, { QContactOnlineAccount__FieldAccountPath, "accountPath", StringField }, { QContactOnlineAccount__FieldAccountIconPath, "accountIconPath", StringField }, { QContactOnlineAccount__FieldEnabled, "enabled", BooleanField }, { QContactOnlineAccount__FieldAccountDisplayName, "accountDisplayName", LocalizedField }, { QContactOnlineAccount__FieldServiceProviderDisplayName, "serviceProviderDisplayName", LocalizedField } }; static void setValues(QContactOnlineAccount *detail, QSqlQuery *query, const int offset) { typedef QContactOnlineAccount T; setValue(detail, T::FieldAccountUri , query->value(offset + 0)); // ignore lowerAccountUri setValue(detail, T::FieldProtocol , QVariant::fromValue(query->value(offset + 2).toString().toInt())); setValue(detail, T::FieldServiceProvider, query->value(offset + 3)); setValue(detail, T::FieldCapabilities , stringListValue(query->value(offset + 4))); const QStringList subTypeValues(query->value(offset + 5).toString().split(QLatin1Char(';'), QString::SkipEmptyParts)); setValue(detail, T::FieldSubTypes, QVariant::fromValue >(subTypeList(subTypeValues))); setValue(detail, QContactOnlineAccount__FieldAccountPath, query->value(offset + 6)); setValue(detail, QContactOnlineAccount__FieldAccountIconPath, query->value(offset + 7)); setValue(detail, QContactOnlineAccount__FieldEnabled, query->value(offset + 8)); setValue(detail, QContactOnlineAccount__FieldAccountDisplayName, query->value(offset + 9)); setValue(detail, QContactOnlineAccount__FieldServiceProviderDisplayName, query->value(offset + 10)); } static const FieldInfo organizationFields[] = { { QContactOrganization::FieldName, "name", LocalizedField }, { QContactOrganization::FieldRole, "role", LocalizedField }, { QContactOrganization::FieldTitle, "title", LocalizedField }, { QContactOrganization::FieldLocation, "location", LocalizedField }, { QContactOrganization::FieldDepartment, "department", LocalizedField }, { QContactOrganization::FieldLogoUrl, "logoUrl", StringField }, { QContactOrganization::FieldAssistantName, "assistantName", StringField } }; static void setValues(QContactOrganization *detail, QSqlQuery *query, const int offset) { typedef QContactOrganization T; setValue(detail, T::FieldName , query->value(offset + 0)); setValue(detail, T::FieldRole , query->value(offset + 1)); setValue(detail, T::FieldTitle , query->value(offset + 2)); setValue(detail, T::FieldLocation , query->value(offset + 3)); setValue(detail, T::FieldDepartment, stringListValue(query->value(offset + 4))); setValue(detail, T::FieldLogoUrl , urlValue(query->value(offset + 5))); setValue(detail, T::FieldAssistantName, query->value(offset + 6)); } static const FieldInfo phoneNumberFields[] = { { QContactPhoneNumber::FieldNumber, "phoneNumber", LocalizedField }, { QContactPhoneNumber::FieldNormalizedNumber, "normalizedNumber", StringField }, { QContactPhoneNumber::FieldSubTypes, "subTypes", StringListField } }; static void setValues(QContactPhoneNumber *detail, QSqlQuery *query, const int offset) { typedef QContactPhoneNumber T; setValue(detail, T::FieldNumber , query->value(offset + 0)); const QStringList subTypeValues(query->value(offset + 1).toString().split(QLatin1Char(';'), QString::SkipEmptyParts)); setValue(detail, T::FieldSubTypes, QVariant::fromValue >(subTypeList(subTypeValues))); setValue(detail, QContactPhoneNumber::FieldNormalizedNumber, query->value(offset + 2)); } static const FieldInfo presenceFields[] = { { QContactPresence::FieldPresenceState, "presenceState", IntegerField }, { QContactPresence::FieldTimestamp, "timestamp", DateField }, { QContactPresence::FieldNickname, "nickname", LocalizedField }, { QContactPresence::FieldCustomMessage, "customMessage", LocalizedField }, { QContactPresence::FieldPresenceStateText, "presenceStateText", StringField }, { QContactPresence::FieldPresenceStateImageUrl, "presenceStateImageUrl", StringField } }; static void setValues(QContactPresence *detail, QSqlQuery *query, const int offset) { typedef QContactPresence T; setValue(detail, T::FieldPresenceState, query->value(offset + 0).toInt()); setValue(detail, T::FieldTimestamp , ContactsDatabase::fromDateTimeString(query->value(offset + 1).toString())); setValue(detail, T::FieldNickname , query->value(offset + 2)); setValue(detail, T::FieldCustomMessage, query->value(offset + 3)); setValue(detail, T::FieldPresenceStateText, query->value(offset + 4)); setValue(detail, T::FieldPresenceStateImageUrl, urlValue(query->value(offset + 5))); } static void setValues(QContactGlobalPresence *detail, QSqlQuery *query, const int offset) { typedef QContactPresence T; setValue(detail, T::FieldPresenceState, query->value(offset + 0).toInt()); setValue(detail, T::FieldTimestamp , ContactsDatabase::fromDateTimeString(query->value(offset + 1).toString())); setValue(detail, T::FieldNickname , query->value(offset + 2)); setValue(detail, T::FieldCustomMessage, query->value(offset + 3)); setValue(detail, T::FieldPresenceStateText, query->value(offset + 4)); setValue(detail, T::FieldPresenceStateImageUrl, urlValue(query->value(offset + 5))); } static const FieldInfo ringtoneFields[] = { { QContactRingtone::FieldAudioRingtoneUrl, "audioRingtone", StringField }, { QContactRingtone::FieldVideoRingtoneUrl, "videoRingtone", StringField }, { QContactRingtone::FieldVibrationRingtoneUrl, "vibrationRingtone", StringField } }; static void setValues(QContactRingtone *detail, QSqlQuery *query, const int offset) { typedef QContactRingtone T; setValue(detail, T::FieldAudioRingtoneUrl, urlValue(query->value(offset + 0))); setValue(detail, T::FieldVideoRingtoneUrl, urlValue(query->value(offset + 1))); setValue(detail, T::FieldVibrationRingtoneUrl, urlValue(query->value(offset + 2))); } static const FieldInfo syncTargetFields[] = { { QContactSyncTarget::FieldSyncTarget, "syncTarget", StringField } }; static void setValues(QContactSyncTarget *detail, QSqlQuery *query, const int offset) { typedef QContactSyncTarget T; setValue(detail, T::FieldSyncTarget, query->value(offset + 0)); } static const FieldInfo tagFields[] = { { QContactTag::FieldTag, "tag", LocalizedField } }; static void setValues(QContactTag *detail, QSqlQuery *query, const int offset) { typedef QContactTag T; setValue(detail, T::FieldTag, query->value(offset + 0)); } static const FieldInfo urlFields[] = { { QContactUrl::FieldUrl, "url", StringField }, { QContactUrl::FieldSubType, "subTypes", StringField } }; static void setValues(QContactUrl *detail, QSqlQuery *query, const int offset) { typedef QContactUrl T; setValue(detail, T::FieldUrl , urlValue(query->value(offset + 0))); setValue(detail, T::FieldSubType, QVariant::fromValue(query->value(offset + 1).toString())); } static const FieldInfo originMetadataFields[] = { { QContactOriginMetadata::FieldId, "id", StringField }, { QContactOriginMetadata::FieldGroupId, "groupId", StringField }, { QContactOriginMetadata::FieldEnabled, "enabled", BooleanField } }; static void setValues(QContactOriginMetadata *detail, QSqlQuery *query, const int offset) { setValue(detail, QContactOriginMetadata::FieldId , query->value(offset + 0)); setValue(detail, QContactOriginMetadata::FieldGroupId, query->value(offset + 1)); setValue(detail, QContactOriginMetadata::FieldEnabled, query->value(offset + 2)); } static const FieldInfo extendedDetailFields[] = { { QContactExtendedDetail::FieldName, "name", StringField }, { QContactExtendedDetail::FieldData, "data", OtherField } }; static void setValues(QContactExtendedDetail *detail, QSqlQuery *query, const int offset) { setValue(detail, QContactExtendedDetail::FieldName, query->value(offset + 0)); QVariant rawValue = query->value(offset + 1); if (rawValue.type() == QVariant::Type(QMetaType::QByteArray)) { QByteArray bytes = rawValue.toByteArray(); QBuffer buffer(&bytes); buffer.open(QIODevice::ReadOnly); QDataStream in(&buffer); in.setVersion(QDataStream::Qt_5_6); QVariant deserialized; in >> deserialized; if (deserialized.isValid()) { setValue(detail, QContactExtendedDetail::FieldData, deserialized); return; } } setValue(detail, QContactExtendedDetail::FieldData, rawValue); } static QMap contextTypes() { QMap rv; rv.insert(QStringLiteral("Home"), QContactDetail::ContextHome); rv.insert(QStringLiteral("Work"), QContactDetail::ContextWork); rv.insert(QStringLiteral("Other"), QContactDetail::ContextOther); return rv; } static int contextType(const QString &type) { static const QMap types(contextTypes()); QMap::const_iterator it = types.find(type); if (it != types.end()) { return *it; } return -1; } template static void readDetail(QContact *contact, QSqlQuery &query, quint32 contactId, quint32 detailId, bool syncable, const QContactCollectionId &apiCollectionId, bool relaxConstraints, bool keepChangeFlags, int offset) { const quint32 collectionId = ContactCollectionId::databaseId(apiCollectionId); const bool aggregateContact(collectionId == ContactsDatabase::AggregateAddressbookCollectionId); T detail; int col = 0; const quint32 dbId = query.value(col++).toUInt(); Q_ASSERT(dbId == detailId); /*const quint32 contactId = query.value(1).toUInt();*/ col++; /*const QString detailName = query.value(2).toString();*/ col++; const QString detailUriValue = query.value(col++).toString(); const QString linkedDetailUrisValue = query.value(col++).toString(); const QString contextValue = query.value(col++).toString(); const int accessConstraints = query.value(col++).toInt(); QString provenance = query.value(col++).toString(); const QVariant modifiableVariant = query.value(col++); const bool nonexportable = query.value(col++).toBool(); const int changeFlags = query.value(col++).toInt(); const QDateTime created = query.value(col++).toDateTime(); const QDateTime modified = query.value(col++).toDateTime(); // only save the detail to the contact if it hasn't been deleted, // or if we are part of a sync fetch (i.e. keepChangeFlags is true) if (!keepChangeFlags && changeFlags >= 4) { // ChangeFlags::IsDeleted return; } setValue(&detail, QContactDetail__FieldDatabaseId, dbId); if (!detailUriValue.isEmpty()) { setValue(&detail, QContactDetail::FieldDetailUri, detailUriValue); } if (!linkedDetailUrisValue.isEmpty()) { setValue(&detail, QContactDetail::FieldLinkedDetailUris, linkedDetailUrisValue.split(QLatin1Char(';'), QString::SkipEmptyParts)); } if (!contextValue.isEmpty()) { QList contexts; foreach (const QString &context, contextValue.split(QLatin1Char(';'), QString::SkipEmptyParts)) { const int type = contextType(context); if (type != -1) { contexts.append(type); } } if (!contexts.isEmpty()) { detail.setContexts(contexts); } } // If the detail is not aggregated from another, then its provenance should match its ID. setValue(&detail, QContactDetail::FieldProvenance, aggregateContact ? provenance : QStringLiteral("%1:%2:%3").arg(collectionId).arg(contactId).arg(dbId)); // Only report modifiable state for non-local contacts. // local contacts are always (implicitly) modifiable. if (syncable && !modifiableVariant.isNull() && modifiableVariant.isValid()) { setValue(&detail, QContactDetail__FieldModifiable, modifiableVariant.toBool()); } // Only include non-exportable if it is set if (nonexportable) { setValue(&detail, QContactDetail__FieldNonexportable, nonexportable); } if (keepChangeFlags) { setValue(&detail, QContactDetail__FieldChangeFlags, changeFlags); } setValue(&detail, QContactDetail__FieldCreated, created); setValue(&detail, QContactDetail__FieldModified, modified); // Constraints should be applied unless generating a partial aggregate; the partial aggregate // is intended for modification, so adding constraints prevents it from being used correctly. // Normal aggregate contact details are always immutable. if (!relaxConstraints) { QContactManagerEngine::setDetailAccessConstraints(&detail, static_cast(accessConstraints)); } setValues(&detail, &query, offset); setDetailImmutableIfAggregate(aggregateContact, &detail); contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); } template static void appendUniqueDetail(QList *details, QSqlQuery &query) { T detail; setValues(&detail, &query, 0); details->append(detail); } static QContactRelationship makeRelationship(const QString &type, quint32 firstId, quint32 secondId, const QString &manager_uri) { QContactRelationship relationship; relationship.setRelationshipType(type); relationship.setFirst(ContactId::apiId(firstId, manager_uri)); relationship.setSecond(ContactId::apiId(secondId, manager_uri)); return relationship; } typedef void (*ReadDetail)(QContact *contact, QSqlQuery &query, quint32 contactId, quint32 detailId, bool syncable, const QContactCollectionId &collectionId, bool relaxConstraints, bool keepChangeFlags, int offset); typedef void (*AppendUniqueDetail)(QList *details, QSqlQuery &query); struct DetailInfo { QContactDetail::DetailType detailType; const char *detailName; const char *table; const FieldInfo *fields; const int fieldCount; const bool includesContext; const bool joinToSort; const ReadDetail read; const AppendUniqueDetail appendUnique; QString where(bool queryContacts) const { return table && queryContacts ? QStringLiteral("Contacts.contactId IN (SELECT contactId FROM %1 WHERE %2)").arg(QLatin1String(table)) : QStringLiteral("%2"); } QString whereExists(bool queryContacts) const { if (!queryContacts) { return QString(); } else if (table) { return QStringLiteral("EXISTS (SELECT contactId FROM %1 where contactId = Contacts.contactId)").arg(QLatin1String(table)); } else { return QStringLiteral("Contacts.contactId != 0"); } } QString orderByExistence(bool asc) const { return table ? QStringLiteral("CASE EXISTS (SELECT contactId FROM %1 where contactId = Contacts.contactId) WHEN 1 THEN %2 ELSE %3 END") .arg(QLatin1String(table)).arg(asc ? 0 : 1).arg(asc ? 1 : 0) : QString(); } }; template static int lengthOf(const T(&)[N]) { return N; } template QContactDetail::DetailType detailIdentifier() { return T::Type; } #define PREFIX_LENGTH 8 #define DEFINE_DETAIL(Detail, Table, fields, includesContext, joinToSort) \ { detailIdentifier(), #Detail + PREFIX_LENGTH, #Table, fields, lengthOf(fields), includesContext, joinToSort, readDetail, appendUniqueDetail } #define DEFINE_DETAIL_PRIMARY_TABLE(Detail, fields) \ { detailIdentifier(), #Detail + PREFIX_LENGTH, 0, fields, lengthOf(fields), false, false, nullptr, nullptr } // Note: joinToSort should be true only if there can be only a single row for each contact in that table static const DetailInfo detailInfo[] = { DEFINE_DETAIL_PRIMARY_TABLE(QContactTimestamp, timestampFields), DEFINE_DETAIL_PRIMARY_TABLE(QContactStatusFlags, statusFlagsFields), DEFINE_DETAIL_PRIMARY_TABLE(QContactType, typeFields), DEFINE_DETAIL(QContactAddress , Addresses , addressFields , true , false), DEFINE_DETAIL(QContactAnniversary , Anniversaries , anniversaryFields , false, false), DEFINE_DETAIL(QContactAvatar , Avatars , avatarFields , false, false), DEFINE_DETAIL(QContactBirthday , Birthdays , birthdayFields , false, true), DEFINE_DETAIL(QContactDisplayLabel , DisplayLabels , displayLabelFields , false, true), DEFINE_DETAIL(QContactEmailAddress , EmailAddresses , emailAddressFields , true , false), DEFINE_DETAIL(QContactFamily , Families , familyFields , false, false), DEFINE_DETAIL(QContactFavorite , Favorites , favoriteFields , false, false), DEFINE_DETAIL(QContactGender , Genders , genderFields , false, false), DEFINE_DETAIL(QContactGeoLocation , GeoLocations , geoLocationFields , false, false), DEFINE_DETAIL(QContactGuid , Guids , guidFields , false, true), DEFINE_DETAIL(QContactHobby , Hobbies , hobbyFields , false, false), DEFINE_DETAIL(QContactName , Names , nameFields , false, true), DEFINE_DETAIL(QContactNickname , Nicknames , nicknameFields , false, false), DEFINE_DETAIL(QContactNote , Notes , noteFields , false, false), DEFINE_DETAIL(QContactOnlineAccount , OnlineAccounts , onlineAccountFields , false, false), DEFINE_DETAIL(QContactOrganization , Organizations , organizationFields , false, false), DEFINE_DETAIL(QContactPhoneNumber , PhoneNumbers , phoneNumberFields , false, false), DEFINE_DETAIL(QContactPresence , Presences , presenceFields , false, false), DEFINE_DETAIL(QContactRingtone , Ringtones , ringtoneFields , false, false), DEFINE_DETAIL(QContactSyncTarget , SyncTargets , syncTargetFields , false, false), DEFINE_DETAIL(QContactTag , Tags , tagFields , false, false), DEFINE_DETAIL(QContactUrl , Urls , urlFields , false, false), DEFINE_DETAIL(QContactOriginMetadata, OriginMetadata , originMetadataFields, false, true), DEFINE_DETAIL(QContactGlobalPresence, GlobalPresences, presenceFields , false, true), DEFINE_DETAIL(QContactExtendedDetail, ExtendedDetails, extendedDetailFields, false, false), }; #undef DEFINE_DETAIL_PRIMARY_TABLE #undef DEFINE_DETAIL #undef PREFIX_LENGTH const DetailInfo &detailInformation(QContactDetail::DetailType type) { for (int i = 0; i < lengthOf(detailInfo); ++i) { const DetailInfo &detail = detailInfo[i]; if (type == detail.detailType) { return detail; } } static const DetailInfo nullDetail = { QContactDetail::TypeUndefined, "Undefined", "", 0, 0, false, false, nullptr, nullptr }; return nullDetail; } const FieldInfo &fieldInformation(const DetailInfo &detail, int field) { for (int i = 0; i < detail.fieldCount; ++i) { const FieldInfo &fieldInfo = detail.fields[i]; if (field == fieldInfo.field) { return fieldInfo; } } static const FieldInfo nullField = { invalidField, "", OtherField }; return nullField; } QContactDetail::DetailType detailIdentifier(const QString &name) { for (int i = 0; i < lengthOf(detailInfo); ++i) { const DetailInfo &detail = detailInfo[i]; if (name == QLatin1String(detail.detailName)) { return detail.detailType; } } return QContactDetail::TypeUndefined; } static QString fieldName(const char *table, const char *field) { return QString::fromLatin1(table ? table : "Contacts").append(QChar('.')).append(QString::fromLatin1(field)); } static QHash getCaseInsensitiveColumnNames() { QHash names; names.insert(fieldName("Names", "firstName"), QStringLiteral("lowerFirstName")); names.insert(fieldName("Names", "lastName"), QStringLiteral("lowerLastName")); names.insert(fieldName("EmailAddresses", "emailAddress"), QStringLiteral("lowerEmailAddress")); names.insert(fieldName("OnlineAccounts", "accountUri"), QStringLiteral("lowerAccountUri")); names.insert(fieldName("Nicknames", "nickname"), QStringLiteral("lowerNickname")); return names; } static QString caseInsensitiveColumnName(const char *table, const char *column) { static QHash columnNames(getCaseInsensitiveColumnNames()); return columnNames.value(fieldName(table, column)); } static QString dateString(const DetailInfo &detail, const QDateTime &qdt) { if (detail.detailType == QContactBirthday::Type || detail.detailType == QContactAnniversary::Type) { // just interested in the date, not the whole date time (local time) return ContactsDatabase::dateString(qdt); } return ContactsDatabase::dateTimeString(qdt.toUTC()); } template static bool matchOnType(const T1 &filter, T2 type) { return filter.detailType() == type; } template static bool filterOnField(const QContactDetailFilter &filter, F field) { return (filter.detailType() == T::Type && filter.detailField() == field); } template static bool validFilterField(F filter) { return (filter.detailField() != invalidField); } static QString convertFilterValueToString(const QContactDetailFilter &filter, const QString &defaultValue) { // Some enum values are stored in textual columns if (filter.detailType() == QContactOnlineAccount::Type) { if (filter.detailField() == QContactOnlineAccount::FieldProtocol) { return QString::number(filter.value().toInt()); } else if (filter.detailField() == QContactOnlineAccount::FieldSubTypes) { // TODO: what if the value is a list? return QString::number(filter.value().toInt()); } } else if (filter.detailType() == QContactPhoneNumber::Type) { if (filter.detailField() == QContactPhoneNumber::FieldSubTypes) { // TODO: what if the value is a list? return QString::number(filter.value().toInt()); } } else if (filter.detailType() == QContactAnniversary::Type) { if (filter.detailField() == QContactAnniversary::FieldSubType) { return QString::number(filter.value().toInt()); } } else if (filter.detailType() == QContactUrl::Type) { if (filter.detailField() == QContactUrl::FieldSubType) { return QString::number(filter.value().toInt()); } } else if (filter.detailType() == QContactGender::Type) { if (filter.detailField() == QContactGender::FieldGender) { return QString::number(filter.value().toInt()); } } return defaultValue; } static QString buildWhere(const QContactCollectionFilter &filter, QVariantList *bindings, bool *failed) { const QSet &filterIds(filter.collectionIds()); if (filterIds.isEmpty()) { // "retrieve all contacts, regardless of collection". return QStringLiteral("Contacts.collectionId IS NOT NULL"); } else if (filterIds.count() < 800) { QList dbIds; dbIds.reserve(filterIds.count()); bindings->reserve(filterIds.count()); foreach (const QContactCollectionId &id, filterIds) { dbIds.append(ContactCollectionId::databaseId(id)); } QString statement = QStringLiteral("Contacts.collectionId IN (?"); bindings->append(dbIds.first()); for (int i = 1; i < dbIds.count(); ++i) { statement += QStringLiteral(",?"); bindings->append(dbIds.at(i)); } return statement + QStringLiteral(")"); } else { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with too large collection ID list")); return QStringLiteral("FALSE"); } } static QString buildWhere( const QContactDetailFilter &filter, bool queryContacts, QVariantList *bindings, bool *failed, bool *transientModifiedRequired, bool *globalPresenceRequired) { if (filter.matchFlags() & QContactFilter::MatchKeypadCollation) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with filter requiring keypad collation")); return QStringLiteral("FAILED"); } const DetailInfo &detail(detailInformation(filter.detailType())); if (detail.detailType == QContactDetail::TypeUndefined) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with unknown detail type: %1").arg(filter.detailType())); return QStringLiteral("FAILED"); } if (filter.detailField() == invalidField) { // If there is no field, we're simply testing for the existence of matching details return detail.whereExists(queryContacts); } const FieldInfo &field(fieldInformation(detail, filter.detailField())); if (field.field == invalidField) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with unknown detail field: %1").arg(filter.detailField())); return QStringLiteral("FAILED"); } if (!filter.value().isValid() // "match if detail and field exists, don't care about value" filter || (filterOnField(filter, QContactSyncTarget::FieldSyncTarget) && filter.value().toString().isEmpty())) { // match all sync targets if empty sync target filter const QString comparison(QStringLiteral("%1 IS NOT NULL")); return detail.where(queryContacts).arg(comparison.arg(field.column)); } do { // Our match query depends on the value parameter if (field.fieldType == OtherField) { if (filterOnField(filter, QContactStatusFlags::FieldFlags)) { static const quint64 flags[] = { QContactStatusFlags::HasPhoneNumber, QContactStatusFlags::HasEmailAddress, QContactStatusFlags::HasOnlineAccount, QContactStatusFlags::IsOnline, QContactStatusFlags::IsDeactivated, QContactStatusFlags::IsAdded, QContactStatusFlags::IsModified, QContactStatusFlags::IsDeleted }; static const char *flagColumns[] = { "hasPhoneNumber", "hasEmailAddress", "hasOnlineAccount", "isOnline", "isDeactivated", "changeFlags", "changeFlags", "changeFlags" }; quint64 flagsValue = filter.value().value(); QStringList clauses; if (filter.matchFlags() == QContactFilter::MatchExactly) { *globalPresenceRequired = true; for (int i = 0; i < lengthOf(flags); ++i) { QString comparison; if (flags[i] == QContactStatusFlags::IsOnline) { // Use special case test to include transient presence state comparison = QStringLiteral("COALESCE(temp.GlobalPresenceStates.isOnline, Contacts.isOnline) = %1"); } else if (flags[i] == QContactStatusFlags::IsAdded) { // Use special case test to check changeFlags for added status comparison = QStringLiteral("(%1 & 1) = %2").arg(flagColumns[i]); // ChangeFlags::IsAdded } else if (flags[i] == QContactStatusFlags::IsModified) { // Use special case test to check changeFlags for modified status comparison = QStringLiteral("((%1 & 2)/2) = %2").arg(flagColumns[i]); // ChangeFlags::IsModified } else if (flags[i] == QContactStatusFlags::IsDeleted) { // Use special case test to check changeFlags for deleted status comparison = QStringLiteral("((%1 & 4)/4) = %2").arg(flagColumns[i]); // ChangeFlags::IsDeleted } else { comparison = QStringLiteral("%1 = %2").arg(flagColumns[i]); } clauses.append(comparison.arg((flagsValue & flags[i]) ? 1 : 0)); } } else if (filter.matchFlags() == QContactFilter::MatchContains) { for (int i = 0; i < lengthOf(flags); ++i) { if (flagsValue & flags[i]) { if (flags[i] == QContactStatusFlags::IsOnline) { *globalPresenceRequired = true; clauses.append(QStringLiteral("COALESCE(temp.GlobalPresenceStates.isOnline, Contacts.isOnline) = 1")); } else if (flags[i] == QContactStatusFlags::IsAdded) { // Use special case test to check changeFlags for added status clauses.append(QStringLiteral("(%1 & 1) = 1").arg(flagColumns[i])); // ChangeFlags::IsAdded } else if (flags[i] == QContactStatusFlags::IsModified) { // Use special case test to check changeFlags for modified status clauses.append(QStringLiteral("(%1 & 2) = 2").arg(flagColumns[i])); // ChangeFlags::IsModified } else if (flags[i] == QContactStatusFlags::IsDeleted) { // Use special case test to check changeFlags for deleted status clauses.append(QStringLiteral("%1 >= 4").arg(flagColumns[i])); // ChangeFlags::IsDeleted } else { clauses.append(QStringLiteral("%1 = 1").arg(flagColumns[i])); } } } } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unsupported flags matching contact status flags")); break; } if (!clauses.isEmpty()) { return detail.where(queryContacts).arg(clauses.join(QStringLiteral(" AND "))); } break; } } bool dateField = field.fieldType == DateField; bool stringField = field.fieldType == StringField || field.fieldType == StringListField || field.fieldType == LocalizedField || field.fieldType == LocalizedListField; bool phoneNumberMatch = filter.matchFlags() & QContactFilter::MatchPhoneNumber; bool fixedString = filter.matchFlags() & QContactFilter::MatchFixedString; bool useNormalizedNumber = false; int globValue = filter.matchFlags() & 7; if (field.fieldType == StringListField || field.fieldType == LocalizedListField) { // With a string list, the only string match type we can do is 'contains' globValue = QContactFilter::MatchContains; } // We need to perform case-insensitive matching if MatchFixedString is specified (unless // CaseSensitive is also specified) bool caseInsensitive = stringField && fixedString && ((filter.matchFlags() & QContactFilter::MatchCaseSensitive) == 0); QString clause(detail.where(queryContacts)); QString comparison = QStringLiteral("%1"); QString bindValue; QString column; if (caseInsensitive) { column = caseInsensitiveColumnName(detail.table, field.column); if (!column.isEmpty()) { // We don't need to use lower() on the values in this column } else { comparison = QStringLiteral("lower(%1)"); } } QString stringValue = filter.value().toString(); if (phoneNumberMatch) { // If the phone number match is on the number field of a phoneNumber detail, then // match on the normalized number rather than the unconstrained number (for simple matches) useNormalizedNumber = (filterOnField(filter, QContactPhoneNumber::FieldNumber) && globValue != QContactFilter::MatchStartsWith && globValue != QContactFilter::MatchContains && globValue != QContactFilter::MatchEndsWith); if (useNormalizedNumber) { // Normalize the input for comparison bindValue = ContactsEngine::normalizedPhoneNumber(stringValue); if (bindValue.isEmpty()) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed with invalid phone number: %1").arg(stringValue)); return QStringLiteral("FAILED"); } if (caseInsensitive) { bindValue = bindValue.toLower(); } column = QStringLiteral("normalizedNumber"); } else { // remove any non-digit characters from the column value when we do our comparison: +,-, ,#,(,) are removed. comparison = QStringLiteral("replace(replace(replace(replace(replace(replace(%1, '+', ''), '-', ''), '#', ''), '(', ''), ')', ''), ' ', '')"); QString tempValue = caseInsensitive ? stringValue.toLower() : stringValue; for (int i = 0; i < tempValue.size(); ++i) { QChar current = tempValue.at(i).toLower(); if (current.isDigit()) { bindValue.append(current); } } } } else { const QVariant &v(filter.value()); if (dateField) { bindValue = dateString(detail, v.toDateTime()); if (filterOnField(filter, QContactTimestamp::FieldModificationTimestamp)) { // Special case: we need to include the transient data timestamp in our comparison column = QStringLiteral("COALESCE(temp.Timestamps.modified, Contacts.modified)"); *transientModifiedRequired = true; } } else if (!stringField && (v.type() == QVariant::Bool)) { // Convert to "1"/"0" rather than "true"/"false" bindValue = QString::number(v.toBool() ? 1 : 0); } else { stringValue = convertFilterValueToString(filter, stringValue); bindValue = caseInsensitive ? stringValue.toLower() : stringValue; if (filterOnField(filter, QContactGlobalPresence::FieldPresenceState)) { // Special case: we need to include the transient data state in our comparison clause = QStringLiteral("Contacts.contactId IN (" "SELECT GlobalPresences.contactId FROM GlobalPresences " "LEFT JOIN temp.GlobalPresenceStates ON temp.GlobalPresenceStates.contactId = GlobalPresences.contactId " "WHERE %1)"); column = QStringLiteral("COALESCE(temp.GlobalPresenceStates.presenceState, GlobalPresences.presenceState)"); *globalPresenceRequired = true; } } } if (stringField || fixedString) { if (globValue == QContactFilter::MatchStartsWith) { bindValue = bindValue + QStringLiteral("*"); comparison += QStringLiteral(" GLOB ?"); bindings->append(bindValue); } else if (globValue == QContactFilter::MatchContains) { bindValue = QStringLiteral("*") + bindValue + QStringLiteral("*"); comparison += QStringLiteral(" GLOB ?"); bindings->append(bindValue); } else if (globValue == QContactFilter::MatchEndsWith) { bindValue = QStringLiteral("*") + bindValue; comparison += QStringLiteral(" GLOB ?"); bindings->append(bindValue); } else { if (bindValue.isEmpty()) { // An empty string test should match a NULL column also (no way to specify isNull from qtcontacts) comparison = QStringLiteral("COALESCE(%1,'') = ''").arg(comparison); } else { comparison += QStringLiteral(" = ?"); bindings->append(bindValue); } } } else { if (phoneNumberMatch && !useNormalizedNumber) { bindValue = QStringLiteral("*") + bindValue; comparison += QStringLiteral(" GLOB ?"); bindings->append(bindValue); } else { comparison += QStringLiteral(" = ?"); bindings->append(bindValue); } } return clause.arg(comparison.arg(column.isEmpty() ? field.column : column)); } while (false); *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to buildWhere with DetailFilter detail: %1 field: %2").arg(filter.detailType()).arg(filter.detailField())); return QStringLiteral("FALSE"); } static QString buildWhere(const QContactDetailRangeFilter &filter, bool queryContacts, QVariantList *bindings, bool *failed) { const DetailInfo &detail(detailInformation(filter.detailType())); if (detail.detailType == QContactDetail::TypeUndefined) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with unknown detail type: %1").arg(filter.detailType())); return QStringLiteral("FAILED"); } if (filter.detailField() == invalidField) { // If there is no field, we're simply testing for the existence of matching details return detail.whereExists(queryContacts); } const FieldInfo &field(fieldInformation(detail, filter.detailField())); if (field.field == invalidField) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with unknown detail field: %1").arg(filter.detailField())); return QStringLiteral("FAILED"); } if (!validFilterField(filter) || (!filter.minValue().isValid() && !filter.maxValue().isValid())) { // "match if detail exists, don't care about field or value" filter return detail.where(queryContacts).arg(QStringLiteral("%1 IS NOT NULL").arg(QLatin1String(field.column))); } // Our match query depends on the minValue/maxValue parameters QString comparison; bool dateField = field.fieldType == DateField; bool stringField = field.fieldType == StringField || field.fieldType == LocalizedField; bool caseInsensitive = stringField && filter.matchFlags() & QContactFilter::MatchFixedString && (filter.matchFlags() & QContactFilter::MatchCaseSensitive) == 0; bool needsAnd = false; if (filter.minValue().isValid()) { if (dateField) { bindings->append(dateString(detail, filter.minValue().toDateTime())); } else { bindings->append(filter.minValue()); } if (caseInsensitive) { comparison = (filter.rangeFlags() & QContactDetailRangeFilter::ExcludeLower) ? QStringLiteral("%1 > lower(?)") : QStringLiteral("%1 >= lower(?)"); } else { comparison = (filter.rangeFlags() & QContactDetailRangeFilter::ExcludeLower) ? QStringLiteral("%1 > ?") : QStringLiteral("%1 >= ?"); } needsAnd = true; } if (filter.maxValue().isValid()) { if (needsAnd) comparison += QStringLiteral(" AND "); if (dateField) { bindings->append(dateString(detail, filter.maxValue().toDateTime())); } else { bindings->append(filter.maxValue()); } if (caseInsensitive) { comparison += (filter.rangeFlags() & QContactDetailRangeFilter::IncludeUpper) ? QStringLiteral("%1 <= lower(?)") : QStringLiteral("%1 < lower(?)"); } else { comparison += (filter.rangeFlags() & QContactDetailRangeFilter::IncludeUpper) ? QStringLiteral("%1 <= ?") : QStringLiteral("%1 < ?"); } } QString comparisonArg = field.column; if (caseInsensitive) { comparisonArg = caseInsensitiveColumnName(detail.table, field.column); if (!comparisonArg.isEmpty()) { // We don't need to use lower() on the values in this column } else { comparisonArg = QStringLiteral("lower(%1)").arg(QLatin1String(field.column)); } } return detail.where(queryContacts).arg(comparison.arg(comparisonArg)); } static QString buildWhere(const QContactIdFilter &filter, ContactsDatabase &db, const QString &table, QVariantList *bindings, bool *failed) { const QList &filterIds(filter.ids()); if (filterIds.isEmpty()) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with empty contact ID list")); return QStringLiteral("FALSE"); } QList dbIds; dbIds.reserve(filterIds.count()); bindings->reserve(filterIds.count()); foreach (const QContactId &id, filterIds) { dbIds.append(ContactId::databaseId(id)); } // We don't want to exceed the maximum bound variables limit; if there are too // many IDs in the list, create a temporary table to look them up from const int maxInlineIdsCount = 800; if (filterIds.count() > maxInlineIdsCount) { QVariantList varIds; foreach (const QContactId &id, filterIds) { varIds.append(QVariant(ContactId::databaseId(id))); } QString transientTable; if (!db.createTransientContactIdsTable(table, varIds, &transientTable)) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere due to transient table failure")); return QStringLiteral("FALSE"); } return QStringLiteral("Contacts.contactId IN (SELECT contactId FROM %1)").arg(transientTable); } QString statement = QStringLiteral("Contacts.contactId IN (?"); bindings->append(dbIds.first()); for (int i = 1; i < dbIds.count(); ++i) { statement += QStringLiteral(",?"); bindings->append(dbIds.at(i)); } return statement + QStringLiteral(")"); } static QString buildWhere(const QContactRelationshipFilter &filter, QVariantList *bindings, bool *failed) { QContactId rci = filter.relatedContactId(); QContactRelationship::Role rcr = filter.relatedContactRole(); QString rt = filter.relationshipType(); quint32 dbId = ContactId::databaseId(rci); if (!rci.managerUri().isEmpty() && !rci.managerUri().startsWith(QStringLiteral("qtcontacts:org.nemomobile.contacts.sqlite"))) { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with invalid manager URI: %1").arg(rci.managerUri())); return QStringLiteral("FALSE"); } bool needsId = dbId != 0; bool needsType = !rt.isEmpty(); QString statement = QStringLiteral("Contacts.contactId IN (\n"); if (!needsId && !needsType) { // return the id of every contact who is in a relationship if (rcr == QContactRelationship::First) { // where the other contact is the First statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships"); statement += QStringLiteral(" WHERE firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); } else if (rcr == QContactRelationship::Second) { // where the other contact is the Second statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships"); statement += QStringLiteral(" WHERE firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); } else { // where the other contact is either First or Second statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships"); statement += QStringLiteral(" WHERE firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" UNION "); statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships"); statement += QStringLiteral(" WHERE firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); } } else if (!needsId && needsType) { // return the id of every contact who is in a relationship of the specified type if (rcr == QContactRelationship::First) { // where the other contact is the First statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships WHERE type = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(rt); } else if (rcr == QContactRelationship::Second) { // where the other contact is the Second statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships WHERE type = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(rt); } else { // where the other contact is either First or Second statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships WHERE type = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" UNION "); statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships WHERE type = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(rt); bindings->append(rt); } } else if (needsId && !needsType) { // return the id of every contact who is in a relationship with the specified contact if (rcr == QContactRelationship::First) { // where the specified contact is the First statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships WHERE firstId = ?"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(dbId); } else if (rcr == QContactRelationship::Second) { // where the specified contact is the Second statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships WHERE secondId = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(dbId); } else { // where the specified contact is either First or Second statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships WHERE firstId = ?"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" UNION "); statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships WHERE secondId = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(dbId); bindings->append(dbId); } } else if (needsId && needsType) { // return the id of every contact who is in a relationship of the specified type with the specified contact if (rcr == QContactRelationship::First) { // where the specified contact is the First statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships WHERE firstId = ? AND type = ?"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(dbId); bindings->append(rt); } else if (rcr == QContactRelationship::Second) { // where the specified contact is the Second statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships WHERE secondId = ? AND type = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(dbId); bindings->append(rt); } else { // where the specified contact is either First or Second statement += QStringLiteral(" SELECT DISTINCT secondId FROM Relationships WHERE firstId = ? AND type = ?"); statement += QStringLiteral(" AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(" UNION "); statement += QStringLiteral(" SELECT DISTINCT firstId FROM Relationships WHERE secondId = ? AND type = ?"); statement += QStringLiteral(" AND firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); statement += QStringLiteral(")"); bindings->append(dbId); bindings->append(rt); bindings->append(dbId); bindings->append(rt); } } return statement; } static QString buildWhere(const QContactChangeLogFilter &filter, QVariantList *bindings, bool *failed, bool *transientModifiedRequired) { static const QString statement(QStringLiteral("%1 >= ?")); bindings->append(ContactsDatabase::dateTimeString(filter.since().toUTC())); switch (filter.eventType()) { case QContactChangeLogFilter::EventAdded: return statement.arg(QStringLiteral("Contacts.created")); case QContactChangeLogFilter::EventChanged: *transientModifiedRequired = true; return statement.arg(QStringLiteral("COALESCE(temp.Timestamps.modified, Contacts.modified)")); default: break; } *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with changelog filter on removed timestamps")); return QStringLiteral("FALSE"); } typedef QString (*BuildFilterPart)( const QContactFilter &filter, ContactsDatabase &db, const QString &table, QContactDetail::DetailType detailType, QVariantList *bindings, bool *failed, bool *transientModifiedRequired, bool *globalPresenceRequired); static QString buildWhere( BuildFilterPart buildWhere, const QContactUnionFilter &filter, ContactsDatabase &db, const QString &table, QContactDetail::DetailType detailType, QVariantList *bindings, bool *failed, bool *transientModifiedRequired, bool *globalPresenceRequired) { const QList filters = filter.filters(); if (filters.isEmpty()) return QString(); QStringList fragments; foreach (const QContactFilter &filter, filters) { const QString fragment = buildWhere(filter, db, table, detailType, bindings, failed, transientModifiedRequired, globalPresenceRequired); if (!*failed && !fragment.isEmpty()) { fragments.append(fragment); } } return QStringLiteral("( %1 )").arg(fragments.join(QStringLiteral(" OR "))); } static QString buildWhere( BuildFilterPart buildWhere, const QContactIntersectionFilter &filter, ContactsDatabase &db, const QString &table, QContactDetail::DetailType detailType, QVariantList *bindings, bool *failed, bool *transientModifiedRequired, bool *globalPresenceRequired) { const QList filters = filter.filters(); if (filters.isEmpty()) return QString(); QStringList fragments; foreach (const QContactFilter &filter, filters) { const QString fragment = buildWhere(filter, db, table, detailType, bindings, failed, transientModifiedRequired, globalPresenceRequired); if (filter.type() != QContactFilter::DefaultFilter && !*failed) { // default filter gets special (permissive) treatment by the intersection filter. fragments.append(fragment.isEmpty() ? QStringLiteral("NULL") : fragment); } } return fragments.join(QStringLiteral(" AND ")); } static QString buildContactWhere(const QContactFilter &filter, ContactsDatabase &db, const QString &table, QContactDetail::DetailType detailType, QVariantList *bindings, bool *failed, bool *transientModifiedRequired, bool *globalPresenceRequired) { Q_ASSERT(failed); Q_ASSERT(globalPresenceRequired); Q_ASSERT(transientModifiedRequired); switch (filter.type()) { case QContactFilter::DefaultFilter: return QString(); case QContactFilter::ContactDetailFilter: return buildWhere(static_cast(filter), true, bindings, failed, transientModifiedRequired, globalPresenceRequired); case QContactFilter::ContactDetailRangeFilter: return buildWhere(static_cast(filter), true, bindings, failed); case QContactFilter::ChangeLogFilter: return buildWhere(static_cast(filter), bindings, failed, transientModifiedRequired); case QContactFilter::RelationshipFilter: return buildWhere(static_cast(filter), bindings, failed); case QContactFilter::IntersectionFilter: return buildWhere(buildContactWhere, static_cast(filter), db, table, detailType, bindings, failed, transientModifiedRequired, globalPresenceRequired); case QContactFilter::UnionFilter: return buildWhere(buildContactWhere, static_cast(filter), db, table, detailType, bindings, failed, transientModifiedRequired, globalPresenceRequired); case QContactFilter::IdFilter: return buildWhere(static_cast(filter), db, table, bindings, failed); case QContactFilter::CollectionFilter: return buildWhere(static_cast(filter), bindings, failed); default: *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with unknown filter type: %1").arg(filter.type())); return QStringLiteral("FALSE"); } } static QString buildDetailWhere( const QContactFilter &filter, ContactsDatabase &db, const QString &table, QContactDetail::DetailType detailType, QVariantList *bindings, bool *failed, bool *transientModifiedRequired, bool *globalPresenceRequired) { Q_ASSERT(failed); Q_ASSERT(globalPresenceRequired); Q_ASSERT(transientModifiedRequired); switch (filter.type()) { case QContactFilter::DefaultFilter: return QString(); case QContactFilter::ContactDetailFilter: { const QContactDetailFilter &detailFilter = static_cast(filter); if (detailFilter.detailType() == detailType) { return buildWhere( detailFilter, false, bindings, failed, transientModifiedRequired, globalPresenceRequired); } else { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot build detail query with mismatched details type: %1 %2").arg(detailType).arg(detailFilter.detailType())); return QStringLiteral("FALSE"); } } case QContactFilter::ContactDetailRangeFilter: { const QContactDetailRangeFilter &detailFilter = static_cast(filter); if (detailFilter.detailType() == detailType) { return buildWhere(detailFilter, false, bindings, failed); } else { *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot build detail query with mismatched details type: %1 != %2").arg(detailType).arg(detailFilter.detailType())); return QStringLiteral("FALSE"); } } case QContactFilter::IntersectionFilter: return buildWhere( buildDetailWhere, static_cast(filter), db, table, detailType, bindings, failed, transientModifiedRequired, globalPresenceRequired); case QContactFilter::UnionFilter: return buildWhere( buildDetailWhere, static_cast(filter), db, table, detailType, bindings, failed, transientModifiedRequired, globalPresenceRequired); case QContactFilter::ChangeLogFilter: case QContactFilter::RelationshipFilter: case QContactFilter::IdFilter: *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot build a detail query with a non-detail filter type: %1").arg(filter.type())); return QStringLiteral("FALSE"); default: *failed = true; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildWhere with unknown filter type: %1").arg(filter.type())); return QStringLiteral("FALSE"); } } static QString buildOrderBy( const QContactSortOrder &order, QContactDetail::DetailType detailType, QStringList *joins, bool *transientModifiedRequired, bool *globalPresenceRequired, bool useLocale ) { Q_ASSERT(joins); Q_ASSERT(transientModifiedRequired); Q_ASSERT(globalPresenceRequired); const DetailInfo &detail(detailInformation(order.detailType())); if (detail.detailType == QContactDetail::TypeUndefined) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildOrderBy with unknown detail type: %1").arg(order.detailType())); return QString(); } else if (detailType != QContactDetail::TypeUndefined && detail.detailType != detailType) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildOrderBy with unknown detail mismatched detail types: %1 != %2").arg(detailType).arg(order.detailType())); return QString(); } if (order.detailField() == invalidField) { // If there is no field, we're simply sorting by the existence or otherwise of the detail return detail.orderByExistence(order.direction() == Qt::AscendingOrder); } const bool joinToSort = detail.joinToSort && detailType == QContactDetail::TypeUndefined; const FieldInfo &field(fieldInformation(detail, order.detailField())); if (field.field == invalidField) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot buildOrderBy with unknown detail field: %1").arg(order.detailField())); return QString(); } const bool isDisplayLabelGroup = detail.detailType == QContactDisplayLabel::Type && field.field == QContactDisplayLabel__FieldLabelGroup; QString sortExpression(joinToSort ? QStringLiteral("%1.%2") .arg(detail.table) .arg(isDisplayLabelGroup ? QStringLiteral("DisplayLabelGroupSortOrder") : field.column) : QStringLiteral("%1") .arg(isDisplayLabelGroup ? QStringLiteral("DisplayLabelGroupSortOrder") : field.column)); bool sortBlanks = true; bool collate = true; bool localized = field.fieldType == LocalizedField; // Special case for accessing transient data if (detail.detailType == detailIdentifier() && field.field == QContactGlobalPresence::FieldPresenceState) { // We need to coalesce the transient values with the table values *globalPresenceRequired = true; // Look at the temporary state value if present, otherwise use the normal value sortExpression = QStringLiteral("COALESCE(temp.GlobalPresenceStates.presenceState, GlobalPresences.presenceState)"); sortBlanks = false; collate = false; #ifdef SORT_PRESENCE_BY_AVAILABILITY // The order we want is Available(1),Away(4),ExtendedAway(5),Busy(3),Hidden(2),Offline(6),Unknown(0) sortExpression = QStringLiteral("CASE %1 WHEN 1 THEN 0 " "WHEN 4 THEN 1 " "WHEN 5 THEN 2 " "WHEN 3 THEN 3 " "WHEN 2 THEN 4 " "WHEN 6 THEN 5 " "ELSE 6 END").arg(sortExpression); #endif } else if (detail.detailType == detailIdentifier() && field.field == QContactTimestamp::FieldModificationTimestamp) { *transientModifiedRequired = true; // Look at the temporary modified timestamp if present, otherwise use the normal value sortExpression = QStringLiteral("COALESCE(temp.Timestamps.modified, modified)"); sortBlanks = false; collate = false; } QString result; if (sortBlanks) { QString blanksLocation = (order.blankPolicy() == QContactSortOrder::BlanksLast) ? QStringLiteral("CASE WHEN COALESCE(%1, '') = '' THEN 1 ELSE 0 END, ") : QStringLiteral("CASE WHEN COALESCE(%1, '') = '' THEN 0 ELSE 1 END, "); result = blanksLocation.arg(sortExpression); } result.append(sortExpression); if (!isDisplayLabelGroup && collate) { if (localized && useLocale) { result.append(QStringLiteral(" COLLATE localeCollation")); } else { result.append((order.caseSensitivity() == Qt::CaseSensitive) ? QStringLiteral(" COLLATE RTRIM") : QStringLiteral(" COLLATE NOCASE")); } } result.append((order.direction() == Qt::AscendingOrder) ? QStringLiteral(" ASC") : QStringLiteral(" DESC")); if (joinToSort ) { QString join = QStringLiteral( "LEFT JOIN %1 ON Contacts.contactId = %1.contactId") .arg(QLatin1String(detail.table)); if (!joins->contains(join)) joins->append(join); return result; } else if (!detail.table || detailType != QContactDetail::TypeUndefined) { return result; } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("UNSUPPORTED SORTING: no join and not primary table for ORDER BY in query with: %1, %2") .arg(order.detailType()).arg(order.detailField())); } return QString(); } static QString buildOrderBy( const QList &order, QString *join, bool *transientModifiedRequired, bool *globalPresenceRequired, bool useLocale, QContactDetail::DetailType detailType = QContactDetail::TypeUndefined, const QString &finalOrder = QStringLiteral("Contacts.contactId")) { Q_ASSERT(join); Q_ASSERT(transientModifiedRequired); Q_ASSERT(globalPresenceRequired); if (order.isEmpty()) return QString(); QStringList joins; QStringList fragments; foreach (const QContactSortOrder &sort, order) { const QString fragment = buildOrderBy( sort, detailType, &joins, transientModifiedRequired, globalPresenceRequired, useLocale); if (!fragment.isEmpty()) { fragments.append(fragment); } } *join = joins.join(QStringLiteral(" ")); if (!finalOrder.isEmpty()) fragments.append(finalOrder); return fragments.join(QStringLiteral(", ")); } static void debugFilterExpansion(const QString &description, const QString &query, const QVariantList &bindings) { static const bool debugFilters = !qgetenv("QTCONTACTS_SQLITE_DEBUG_FILTERS").isEmpty(); if (debugFilters) { qDebug() << description << ContactsDatabase::expandQuery(query, bindings); } } ContactReader::ContactReader(ContactsDatabase &database, const QString &managerUri) : m_database(database), m_managerUri(managerUri) { } ContactReader::~ContactReader() { } struct Table { QSqlQuery *query; QContactDetail::DetailType detailType; ReadDetail read; quint32 currentId; }; namespace { // The selfId is fixed - DB ID 1 is the 'self' local contact, and DB ID 2 is the aggregate const quint32 selfId(2); bool includesSelfId(const QContactFilter &filter); // Returns true if this filter includes the self contact by ID bool includesSelfId(const QList &filters) { foreach (const QContactFilter &filter, filters) { if (includesSelfId(filter)) { return true; } } return false; } bool includesSelfId(const QContactIntersectionFilter &filter) { return includesSelfId(filter.filters()); } bool includesSelfId(const QContactUnionFilter &filter) { return includesSelfId(filter.filters()); } bool includesSelfId(const QContactIdFilter &filter) { foreach (const QContactId &id, filter.ids()) { if (ContactId::databaseId(id) == selfId) return true; } return false; } bool includesSelfId(const QContactFilter &filter) { switch (filter.type()) { case QContactFilter::DefaultFilter: case QContactFilter::ContactDetailFilter: case QContactFilter::ContactDetailRangeFilter: case QContactFilter::ChangeLogFilter: case QContactFilter::RelationshipFilter: case QContactFilter::CollectionFilter: return false; case QContactFilter::IntersectionFilter: return includesSelfId(static_cast(filter)); case QContactFilter::UnionFilter: return includesSelfId(static_cast(filter)); case QContactFilter::IdFilter: return includesSelfId(static_cast(filter)); default: QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot includesSelfId with unknown filter type %1").arg(filter.type())); return false; } } bool includesCollectionFilter(const QContactFilter &filter); // Returns true if this filter includes a filter for specific collection bool includesCollectionFilter(const QList &filters) { foreach (const QContactFilter &filter, filters) { if (includesCollectionFilter(filter)) { return true; } } return false; } bool includesCollectionFilter(const QContactIntersectionFilter &filter) { return includesCollectionFilter(filter.filters()); } bool includesCollectionFilter(const QContactUnionFilter &filter) { return includesCollectionFilter(filter.filters()); } bool includesCollectionFilter(const QContactFilter &filter) { switch (filter.type()) { case QContactFilter::CollectionFilter: return true; case QContactFilter::DefaultFilter: case QContactFilter::ContactDetailFilter: case QContactFilter::ContactDetailRangeFilter: case QContactFilter::ChangeLogFilter: case QContactFilter::RelationshipFilter: case QContactFilter::IdFilter: return false; case QContactFilter::IntersectionFilter: return includesCollectionFilter(static_cast(filter)); case QContactFilter::UnionFilter: return includesCollectionFilter(static_cast(filter)); default: QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot includesCollectionFilter with unknown filter type %1").arg(filter.type())); return false; } } bool includesDeleted(const QContactFilter &filter); // Returns true if this filter includes deleted contacts bool includesDeleted(const QList &filters) { foreach (const QContactFilter &filter, filters) { if (includesDeleted(filter)) { return true; } } return false; } bool includesDeleted(const QContactIntersectionFilter &filter) { return includesDeleted(filter.filters()); } bool includesDeleted(const QContactUnionFilter &filter) { return includesDeleted(filter.filters()); } bool includesDeleted(const QContactDetailFilter &filter) { if (filterOnField(filter, QContactStatusFlags::FieldFlags)) { quint64 flagsValue = filter.value().value(); if (flagsValue & QContactStatusFlags::IsDeleted) { return true; } } return false; } bool includesDeleted(const QContactFilter &filter) { switch (filter.type()) { case QContactFilter::IdFilter: case QContactFilter::DefaultFilter: case QContactFilter::ContactDetailRangeFilter: case QContactFilter::ChangeLogFilter: case QContactFilter::RelationshipFilter: case QContactFilter::CollectionFilter: return false; case QContactFilter::IntersectionFilter: return includesDeleted(static_cast(filter)); case QContactFilter::UnionFilter: return includesDeleted(static_cast(filter)); case QContactFilter::ContactDetailFilter: return includesDeleted(static_cast(filter)); default: QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot includesDeleted with unknown filter type %1").arg(filter.type())); return false; } } bool includesDeactivated(const QContactFilter &filter); // Returns true if this filter includes deactivated contacts bool includesDeactivated(const QList &filters) { foreach (const QContactFilter &filter, filters) { if (includesDeactivated(filter)) { return true; } } return false; } bool includesDeactivated(const QContactIntersectionFilter &filter) { return includesDeactivated(filter.filters()); } bool includesDeactivated(const QContactUnionFilter &filter) { return includesDeactivated(filter.filters()); } bool includesDeactivated(const QContactDetailFilter &filter) { if (filterOnField(filter, QContactStatusFlags::FieldFlags)) { quint64 flagsValue = filter.value().value(); if (flagsValue & QContactStatusFlags::IsDeactivated) { return true; } } return false; } bool includesDeactivated(const QContactFilter &filter) { switch (filter.type()) { case QContactFilter::IdFilter: case QContactFilter::DefaultFilter: case QContactFilter::ContactDetailRangeFilter: case QContactFilter::ChangeLogFilter: case QContactFilter::RelationshipFilter: case QContactFilter::CollectionFilter: return false; case QContactFilter::IntersectionFilter: return includesDeactivated(static_cast(filter)); case QContactFilter::UnionFilter: return includesDeactivated(static_cast(filter)); case QContactFilter::ContactDetailFilter: return includesDeactivated(static_cast(filter)); default: QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot includesDeactivated with unknown filter type %1").arg(filter.type())); return false; } } bool includesIdFilter(const QContactFilter &filter); // Returns true if this filter includes a filter for specific IDs bool includesIdFilter(const QList &filters) { foreach (const QContactFilter &filter, filters) { if (includesIdFilter(filter)) { return true; } } return false; } bool includesIdFilter(const QContactIntersectionFilter &filter) { return includesIdFilter(filter.filters()); } bool includesIdFilter(const QContactUnionFilter &filter) { return includesIdFilter(filter.filters()); } bool includesIdFilter(const QContactFilter &filter) { switch (filter.type()) { case QContactFilter::DefaultFilter: case QContactFilter::ContactDetailFilter: case QContactFilter::ContactDetailRangeFilter: case QContactFilter::ChangeLogFilter: case QContactFilter::RelationshipFilter: case QContactFilter::CollectionFilter: return false; case QContactFilter::IntersectionFilter: return includesIdFilter(static_cast(filter)); case QContactFilter::UnionFilter: return includesIdFilter(static_cast(filter)); case QContactFilter::IdFilter: return true; default: QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot includesIdFilter with unknown filter type %1").arg(filter.type())); return false; } } static bool deletedContactFilter(const QContactFilter &filter) { const QContactFilter::FilterType filterType(filter.type()); // The only queries we suport regarding deleted contacts are for the IDs, possibly // intersected with a syncTarget detail filter or a collection filter if (filterType == QContactFilter::ChangeLogFilter) { const QContactChangeLogFilter &changeLogFilter(static_cast(filter)); return changeLogFilter.eventType() == QContactChangeLogFilter::EventRemoved; } else if (filterType == QContactFilter::IntersectionFilter) { const QContactIntersectionFilter &intersectionFilter(static_cast(filter)); const QList filters(intersectionFilter.filters()); if (filters.count() <= 2) { foreach (const QContactFilter &partialFilter, filters) { if (partialFilter.type() == QContactFilter::ChangeLogFilter) { const QContactChangeLogFilter &changeLogFilter(static_cast(partialFilter)); if (changeLogFilter.eventType() == QContactChangeLogFilter::EventRemoved) { return true; } } } } } return false; } QString expandWhere(const QString &where, const QContactFilter &filter, const bool aggregating) { QStringList constraints; // remove the self contact, unless specifically included if (!includesSelfId(filter)) { constraints.append("Contacts.contactId > 2 "); } // if the filter does not specify contacts by ID if (!includesIdFilter(filter)) { if (aggregating) { // exclude non-aggregates, unless the filter specifies collections if (!includesCollectionFilter(filter)) { constraints.append("Contacts.collectionId = 1 "); // AggregateAddressbookCollectionId } } // exclude deactivated unless they're explicitly included if (!includesDeactivated(filter)) { constraints.append("Contacts.isDeactivated = 0 "); } // exclude deleted unless they're explicitly included if (!includesDeleted(filter)) { constraints.append("Contacts.changeFlags < 4 "); } } // some (union) filters can add spurious braces around empty expressions bool emptyFilter = false; { QString strippedWhere = where; strippedWhere.remove(QChar('(')); strippedWhere.remove(QChar(')')); strippedWhere.remove(QChar(' ')); emptyFilter = strippedWhere.isEmpty(); } if (emptyFilter && constraints.isEmpty()) return QString(); QString whereClause(QStringLiteral("WHERE ")); if (!constraints.isEmpty()) { whereClause += constraints.join(QStringLiteral("AND ")); if (!emptyFilter) { whereClause += QStringLiteral("AND "); } } if (!emptyFilter) { whereClause += where; } return whereClause; } } QContactManager::Error ContactReader::fetchContacts(const QContactCollectionId &collectionId, QList *addedContacts, QList *modifiedContacts, QList *deletedContacts, QList *unmodifiedContacts) { QContactCollectionFilter collectionFilter; collectionFilter.setCollectionId(collectionId); const QContactFilter addedContactsFilter = collectionFilter & QContactStatusFlags::matchFlag(QContactStatusFlags::IsAdded, QContactFilter::MatchContains); const QContactFilter modifiedContactsFilter = collectionFilter & QContactStatusFlags::matchFlag(QContactStatusFlags::IsModified, QContactFilter::MatchContains); const QContactFilter deletedContactsFilter = collectionFilter & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains); // optimisation: if the caller doesn't care about unmodified contacts, // we can save some memory by only fetching added/modified/deleted contacts. const QContactFilter filter = unmodifiedContacts ? (collectionFilter |deletedContactsFilter) : (addedContactsFilter |modifiedContactsFilter |deletedContactsFilter); const bool keepChangeFlags = true; QList allContacts; const QContactManager::Error error = readContacts( QStringLiteral("FetchContacts"), &allContacts, filter, QList(), QContactFetchHint(), keepChangeFlags); for (QList::const_iterator it = allContacts.constBegin(); it != allContacts.constEnd(); it++) { const QContactStatusFlags flags = it->detail(); if (flags.testFlag(QContactStatusFlags::IsDeleted)) { if (deletedContacts) { deletedContacts->append(*it); } } else if (flags.testFlag(QContactStatusFlags::IsAdded)) { if (addedContacts) { addedContacts->append(*it); } } else if (flags.testFlag(QContactStatusFlags::IsModified)) { if (modifiedContacts) { modifiedContacts->append(*it); } } else { Q_ASSERT(unmodifiedContacts); if (unmodifiedContacts) { unmodifiedContacts->append(*it); } } } return error; } QContactManager::Error ContactReader::readContacts( const QString &table, QList *contacts, const QContactFilter &filter, const QList &order, const QContactFetchHint &fetchHint, bool keepChangeFlags) { QMutexLocker locker(m_database.accessMutex()); m_database.clearTemporaryContactIdsTable(table); QString join; bool transientModifiedRequired = false; bool globalPresenceRequired = false; const QString orderBy = buildOrderBy(order, &join, &transientModifiedRequired, &globalPresenceRequired, m_database.localized()); bool whereFailed = false; QVariantList bindings; QString where = buildContactWhere(filter, m_database, table, QContactDetail::TypeUndefined, &bindings, &whereFailed, &transientModifiedRequired, &globalPresenceRequired); if (whereFailed) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to create WHERE expression: invalid filter specification")); return QContactManager::UnspecifiedError; } where = expandWhere(where, filter, m_database.aggregating()); if (transientModifiedRequired || globalPresenceRequired) { // Provide the temporary transient state information to filter/sort on if (!m_database.populateTemporaryTransientState(transientModifiedRequired, globalPresenceRequired)) { return QContactManager::UnspecifiedError; } if (transientModifiedRequired) { join.append(QStringLiteral(" LEFT JOIN temp.Timestamps ON Contacts.contactId = temp.Timestamps.contactId")); } if (globalPresenceRequired) { join.append(QStringLiteral(" LEFT JOIN temp.GlobalPresenceStates ON Contacts.contactId = temp.GlobalPresenceStates.contactId")); } } const int maximumCount = fetchHint.maxCountHint(); QContactManager::Error error = QContactManager::NoError; if (!m_database.createTemporaryContactIdsTable(table, join, where, orderBy, bindings, maximumCount)) { error = QContactManager::UnspecifiedError; } else { error = queryContacts(table, contacts, fetchHint, false /* relax constraints */, false /* ignore deleted - however they will be omitted unless filter specifically requires */, keepChangeFlags); } return error; } QContactManager::Error ContactReader::readContacts( const QString &table, QList *contacts, const QList &contactIds, const QContactFetchHint &fetchHint) { QList databaseIds; databaseIds.reserve(contactIds.size()); foreach (const QContactId &id, contactIds) { databaseIds.append(ContactId::databaseId(id)); } return readContacts(table, contacts, databaseIds, fetchHint); } QContactManager::Error ContactReader::readContacts( const QString &table, QList *contacts, const QList &databaseIds, const QContactFetchHint &fetchHint, bool relaxConstraints) { QMutexLocker locker(m_database.accessMutex()); QVariantList boundIds; boundIds.reserve(databaseIds.size()); foreach (quint32 id, databaseIds) { boundIds.append(id); } contacts->reserve(databaseIds.size()); m_database.clearTemporaryContactIdsTable(table); const int maximumCount = fetchHint.maxCountHint(); QContactManager::Error error = QContactManager::NoError; if (!m_database.createTemporaryContactIdsTable(table, boundIds, maximumCount)) { error = QContactManager::UnspecifiedError; } else { error = queryContacts(table, contacts, fetchHint, relaxConstraints, true /* ignore deleted */); } // the ordering of the queried contacts is identical to // the ordering of the input contact ids list. int contactIdsSize = databaseIds.size(); int contactsSize = contacts->size(); if (contactIdsSize != contactsSize) { for (int i = 0; i < contactIdsSize; ++i) { if (i >= contactsSize || ContactId::databaseId((*contacts)[i].id()) != databaseIds[i]) { // the id list contained a contact id which doesn't exist contacts->insert(i, QContact()); contactsSize++; error = QContactManager::DoesNotExistError; } } } return error; } QContactManager::Error ContactReader::queryContacts( const QString &tableName, QList *contacts, const QContactFetchHint &fetchHint, bool relaxConstraints, bool ignoreDeleted, bool keepChangeFlags) { QContactManager::Error err = QContactManager::NoError; const QString dataQueryStatement(QStringLiteral( "SELECT " // order and content can change due to schema upgrades, so list manually. "Contacts.contactId, " "Contacts.collectionId, " "Contacts.created, " "Contacts.modified, " "Contacts.deleted, " "Contacts.hasPhoneNumber, " "Contacts.hasEmailAddress, " "Contacts.hasOnlineAccount, " "Contacts.isOnline, " "Contacts.isDeactivated, " "Contacts.changeFlags " "FROM temp.%1 " "CROSS JOIN Contacts ON temp.%1.contactId = Contacts.contactId " // Cross join ensures we scan the temp table first "%2 " "ORDER BY temp.%1.rowId ASC").arg(tableName) .arg(ignoreDeleted ? QStringLiteral("WHERE Contacts.changeFlags < 4") // ChangeFlags::IsDeleted : QString())); const QString relationshipQueryStatement(QStringLiteral( "SELECT " "temp.%1.contactId AS contactId," "R1.type AS secondType," "R1.firstId AS firstId," "R2.type AS firstType," "R2.secondId AS secondId " "FROM temp.%1 " // Must join in this order to get correct query plan. // We also filter based on ChangeFlags::IsDeleted here. // TODO: if this performs poorly, instead do a separate SELECT query to get deleted contacts, // and manually filter out the results in-memory when adding the relationships to the contact, // in the queryContacts(..., relationshipQuery, ...) method. "LEFT JOIN Relationships AS R1 ON R1.secondId = temp.%1.contactId AND R1.firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4) " "LEFT JOIN Relationships AS R2 ON R2.firstId = temp.%1.contactId AND R2.secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4) " "ORDER BY contactId ASC").arg(tableName)); QSqlQuery contactQuery(m_database); QSqlQuery relationshipQuery(m_database); // Prepare the query for the contact properties if (!contactQuery.prepare(dataQueryStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare query for contact data:\n%1\nQuery:\n%2") .arg(contactQuery.lastError().text()) .arg(dataQueryStatement)); err = QContactManager::UnspecifiedError; } else { contactQuery.setForwardOnly(true); if (!ContactsDatabase::execute(contactQuery)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to execute query for contact data:\n%1\nQuery:\n%2") .arg(contactQuery.lastError().text()) .arg(dataQueryStatement)); err = QContactManager::UnspecifiedError; } else { QContactFetchHint::OptimizationHints optimizationHints(fetchHint.optimizationHints()); const bool fetchRelationships((optimizationHints & QContactFetchHint::NoRelationships) == 0); if (fetchRelationships) { // Prepare the query for the contact relationships if (!relationshipQuery.prepare(relationshipQueryStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare query for relationships:\n%1\nQuery:\n%2") .arg(relationshipQuery.lastError().text()) .arg(relationshipQueryStatement)); err = QContactManager::UnspecifiedError; } else { relationshipQuery.setForwardOnly(true); if (!ContactsDatabase::execute(relationshipQuery)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare query for relationships:\n%1\nQuery:\n%2") .arg(relationshipQuery.lastError().text()) .arg(relationshipQueryStatement)); err = QContactManager::UnspecifiedError; } else { // Move to the first row relationshipQuery.next(); } } } if (err == QContactManager::NoError) { err = queryContacts(tableName, contacts, fetchHint, relaxConstraints, keepChangeFlags, contactQuery, relationshipQuery); } contactQuery.finish(); if (fetchRelationships) { relationshipQuery.finish(); } } } return err; } QContactManager::Error ContactReader::queryContacts( const QString &tableName, QList *contacts, const QContactFetchHint &fetchHint, bool relaxConstraints, bool keepChangeFlags, QSqlQuery &contactQuery, QSqlQuery &relationshipQuery) { // Formulate the query to fetch the contact details const QString detailQueryTemplate(QStringLiteral( "SELECT " "Details.detailId," "Details.contactId," "Details.detail," "Details.detailUri," "Details.linkedDetailUris," "Details.contexts," "Details.accessConstraints," "Details.provenance," "Details.modifiable," "COALESCE(Details.nonexportable, 0)," "Details.changeFlags, " "Details.created, " "Details.modified, " "%1 " "FROM temp.%2 " "CROSS JOIN Details ON Details.contactId = temp.%2.contactId " // Cross join ensures we scan the temp table first "%3 " "%4 " "ORDER BY temp.%2.rowId ASC")); const QString selectTemplate(QStringLiteral( "%1.*")); const QString joinTemplate(QStringLiteral( "LEFT JOIN %1 ON %1.detailId = Details.detailId")); const QString detailNameTemplate(QStringLiteral( "WHERE Details.detail IN ('%1')")); QStringList selectSpec; QStringList joinSpec; QStringList detailNameSpec; QHash > readProperties; // Skip the Details table fields, and the indexing fields of the first join table int offset = 13 + 2; const ContactWriter::DetailList &definitionMask = fetchHint.detailTypesHint(); for (int i = 0; i < lengthOf(detailInfo); ++i) { const DetailInfo &detail = detailInfo[i]; if (!detail.read) continue; if (definitionMask.isEmpty() || definitionMask.contains(detail.detailType)) { // we need to join this particular detail table const QString detailTable(QString::fromLatin1(detail.table)); const QString detailName(QString::fromLatin1(detail.detailName)); selectSpec.append(selectTemplate.arg(detailTable)); joinSpec.append(joinTemplate.arg(detailTable)); detailNameSpec.append(detailName); readProperties.insert(detailName, qMakePair(detail.read, offset)); offset += detail.fieldCount + (detail.includesContext ? 1 : 2); } } // Formulate the query string we need QString detailQueryStatement(detailQueryTemplate.arg(selectSpec.join(QChar::fromLatin1(',')))); detailQueryStatement = detailQueryStatement.arg(tableName); detailQueryStatement = detailQueryStatement.arg(joinSpec.join(QChar::fromLatin1(' '))); if (definitionMask.isEmpty()) detailQueryStatement = detailQueryStatement.arg(QString()); else detailQueryStatement = detailQueryStatement.arg(detailNameTemplate.arg(detailNameSpec.join(QStringLiteral("','")))); // If selectSpec is empty, all required details are in the Contacts table ContactsDatabase::Query detailQuery(m_database.prepare(detailQueryStatement)); if (!selectSpec.isEmpty()) { // Read the details for these contacts detailQuery.setForwardOnly(true); if (!ContactsDatabase::execute(detailQuery)) { detailQuery.reportError(QStringLiteral("Failed to prepare query for joined details")); return QContactManager::UnspecifiedError; } else { // Move to the first row detailQuery.next(); } } const bool includeRelationships(relationshipQuery.isValid()); const bool includeDetails(detailQuery.isValid()); // We need to report our retrievals periodically int unreportedCount = 0; const int maximumCount = fetchHint.maxCountHint(); const int batchSize = (maximumCount > 0) ? 0 : ReportBatchSize; // If count is constrained, don't report periodically while (contactQuery.next()) { int col = 0; const quint32 dbId = contactQuery.value(col++).toUInt(); const quint32 collectionId = contactQuery.value(col++).toUInt(); const QContactCollectionId apiCollectionId = ContactCollectionId::apiId(collectionId, m_managerUri); const bool aggregateContact = collectionId == ContactsDatabase::AggregateAddressbookCollectionId; QContact contact; contact.setId(ContactId::apiId(dbId, m_managerUri)); contact.setCollectionId(apiCollectionId); QContactTimestamp timestamp; setValue(×tamp, QContactTimestamp::FieldCreationTimestamp , ContactsDatabase::fromDateTimeString(contactQuery.value(col++).toString())); setValue(×tamp, QContactTimestamp::FieldModificationTimestamp, ContactsDatabase::fromDateTimeString(contactQuery.value(col++).toString())); col++; // ignore Deleted timestamp. QContactStatusFlags flags; flags.setFlag(QContactStatusFlags::HasPhoneNumber, contactQuery.value(col++).toBool()); flags.setFlag(QContactStatusFlags::HasEmailAddress, contactQuery.value(col++).toBool()); flags.setFlag(QContactStatusFlags::HasOnlineAccount, contactQuery.value(col++).toBool()); flags.setFlag(QContactStatusFlags::IsOnline, contactQuery.value(col++).toBool()); flags.setFlag(QContactStatusFlags::IsDeactivated, contactQuery.value(col++).toBool()); const int changeFlags = contactQuery.value(col++).toInt(); flags.setFlag(QContactStatusFlags::IsAdded, changeFlags & ContactsDatabase::IsAdded); flags.setFlag(QContactStatusFlags::IsModified, changeFlags & ContactsDatabase::IsModified); flags.setFlag(QContactStatusFlags::IsDeleted, changeFlags >= ContactsDatabase::IsDeleted); if (flags.testFlag(QContactStatusFlags::IsDeactivated)) { QContactDeactivated deactivated; setDetailImmutableIfAggregate(aggregateContact, &deactivated); contact.saveDetail(&deactivated); } // ignore created and modified timestamps, will be saved by readDetail() col++; col++; int contactType = contactQuery.value(col++).toInt(); QContactType typeDetail = contact.detail(); typeDetail.setType(static_cast(contactType)); setDetailImmutableIfAggregate(aggregateContact, &typeDetail); contact.saveDetail(&typeDetail); bool syncable = collectionId != ContactsDatabase::AggregateAddressbookCollectionId && collectionId != ContactsDatabase::LocalAddressbookCollectionId; QSet transientTypes; // Find any transient details for this contact if (m_database.hasTransientDetails(dbId)) { const QPair > transientDetails(m_database.transientDetails(dbId)); if (!transientDetails.first.isNull()) { // Update the contact timestamp to that of the transient details setValue(×tamp, QContactTimestamp::FieldModificationTimestamp, transientDetails.first); QList::const_iterator it = transientDetails.second.constBegin(), end = transientDetails.second.constEnd(); for ( ; it != end; ++it) { // Copy the transient detail into the contact const QContactDetail &transient(*it); const QContactDetail::DetailType transientType(transient.type()); if (transientType == QContactGlobalPresence::Type) { // If global presence is in the transient details, the IsOnline status flag is out of date const int presenceState = transient.value(QContactGlobalPresence::FieldPresenceState); const bool isOnline(presenceState >= QContactPresence::PresenceAvailable && presenceState <= QContactPresence::PresenceExtendedAway); flags.setFlag(QContactStatusFlags::IsOnline, isOnline); } // Ignore details that aren't in the requested types if (!definitionMask.isEmpty() && !definitionMask.contains(transientType)) { continue; } QContactDetail detail(transient.type()); if (!relaxConstraints) { QContactManagerEngine::setDetailAccessConstraints(&detail, transient.accessConstraints()); } const QMap values(transient.values()); QMap::const_iterator vit = values.constBegin(), vend = values.constEnd(); for ( ; vit != vend; ++vit) { bool append(true); if (vit.key() == QContactDetail__FieldModifiable) { append = syncable; } if (append) { detail.setValue(vit.key(), vit.value()); } } setDetailImmutableIfAggregate(aggregateContact, &detail); contact.saveDetail(&detail); transientTypes.insert(transientType); } } } // Add the updated status flags QContactManagerEngine::setDetailAccessConstraints(&flags, QContactDetail::ReadOnly | QContactDetail::Irremovable); setDetailImmutableIfAggregate(aggregateContact, &flags); contact.saveDetail(&flags); // Add the timestamp info if (!timestamp.isEmpty()) { setDetailImmutableIfAggregate(aggregateContact, ×tamp); contact.saveDetail(×tamp); } // Add the details of this contact from the detail tables if (includeDetails) { if (detailQuery.isValid()) { quint32 firstContactDetailId = 0; do { const quint32 contactId = detailQuery.value(1).toUInt(); if (contactId != dbId) { break; } const quint32 detailId = detailQuery.value(0).toUInt(); if (firstContactDetailId == 0) { firstContactDetailId = detailId; } else if (firstContactDetailId == detailId) { // the client must have requested the same contact twice in a row, by id. // we have already processed all of this contact's details, so break. break; } const QString detailName = detailQuery.value(2).toString(); // Are we reporting this detail type? const QPair properties(readProperties[detailName]); if (properties.first && properties.second) { // Are there transient details of this type for this contact? const QContactDetail::DetailType detailType(detailIdentifier(detailName)); if (transientTypes.contains(detailType)) { // This contact has transient details of this type; skip the extraction continue; } // Extract the values from the result row (readDetail()). properties.first(&contact, detailQuery, contactId, detailId, syncable, apiCollectionId, relaxConstraints, keepChangeFlags, properties.second); } } while (detailQuery.next()); } } if (includeRelationships) { // Find any relationships for this contact if (relationshipQuery.isValid()) { // Find the relationships for the contacts in this batch QList relationships; do { const quint32 contactId = relationshipQuery.value(0).toUInt(); if (contactId != dbId) { break; } const QString secondType = relationshipQuery.value(1).toString(); const quint32 firstId = relationshipQuery.value(2).toUInt(); const QString firstType = relationshipQuery.value(3).toString(); const quint32 secondId = relationshipQuery.value(4).toUInt(); if (!firstType.isEmpty()) { QContactRelationship rel(makeRelationship(firstType, contactId, secondId, m_managerUri)); relationships.append(rel); } else if (!secondType.isEmpty()) { QContactRelationship rel(makeRelationship(secondType, firstId, contactId, m_managerUri)); relationships.append(rel); } } while (relationshipQuery.next()); QContactManagerEngine::setContactRelationships(&contact, relationships); } } // Append this contact to the output set contacts->append(contact); // Periodically report our retrievals if (++unreportedCount == batchSize) { unreportedCount = 0; contactsAvailable(*contacts); } } detailQuery.finish(); // If any retrievals are not yet reported, do so now if (unreportedCount > 0) { contactsAvailable(*contacts); } return QContactManager::NoError; } QContactManager::Error ContactReader::readDeletedContactIds( QList *contactIds, const QContactFilter &filter) { QDateTime since; QString syncTarget; QList collectionIds; // The only queries we support regarding deleted contacts are for the IDs, possibly // intersected with a syncTarget detail filter or a collection filter if (filter.type() == QContactFilter::ChangeLogFilter) { const QContactChangeLogFilter &changeLogFilter(static_cast(filter)); since = changeLogFilter.since(); } else if (filter.type() == QContactFilter::IntersectionFilter) { const QContactIntersectionFilter &intersectionFilter(static_cast(filter)); foreach (const QContactFilter &partialFilter, intersectionFilter.filters()) { const QContactFilter::FilterType filterType(partialFilter.type()); if (filterType == QContactFilter::ChangeLogFilter) { const QContactChangeLogFilter &changeLogFilter(static_cast(partialFilter)); since = changeLogFilter.since(); } else if (filterType == QContactFilter::ContactDetailFilter) { const QContactDetailFilter &detailFilter(static_cast(partialFilter)); if (filterOnField(detailFilter, QContactSyncTarget::FieldSyncTarget)) { syncTarget = detailFilter.value().toString(); } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot readDeletedContactIds with unsupported detail filter type: %1").arg(detailFilter.detailType())); return QContactManager::UnspecifiedError; } } else if (filterType == QContactFilter::CollectionFilter) { const QContactCollectionFilter &collectionFilter(static_cast(partialFilter)); collectionIds = collectionFilter.collectionIds().toList(); if (collectionIds.size() > 1) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot readDeletedContactIds with more than one collection specified: %1").arg(collectionIds.size())); return QContactManager::UnspecifiedError; } } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot readDeletedContactIds with invalid filter type: %1").arg(filterType)); return QContactManager::UnspecifiedError; } } } QStringList restrictions; QVariantList bindings; restrictions.append(QStringLiteral("changeFlags >= 4")); if (!since.isNull()) { restrictions.append(QStringLiteral("deleted >= ?")); bindings.append(ContactsDatabase::dateTimeString(since.toUTC())); } if (!syncTarget.isNull()) { restrictions.append(QStringLiteral("syncTarget = ?")); bindings.append(syncTarget); } if (!collectionIds.isEmpty()) { restrictions.append(QStringLiteral("collectionId = ?")); bindings.append(ContactCollectionId::databaseId(collectionIds.first())); } QString queryStatement(QStringLiteral("SELECT contactId FROM Contacts")); if (!restrictions.isEmpty()) { queryStatement.append(QStringLiteral(" WHERE ")); queryStatement.append(restrictions.takeFirst()); while (!restrictions.isEmpty()) { queryStatement.append(QStringLiteral(" AND ")); queryStatement.append(restrictions.takeFirst()); } } QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(queryStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare deleted contacts ids:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(queryStatement)); return QContactManager::UnspecifiedError; } for (int i = 0; i < bindings.count(); ++i) query.bindValue(i, bindings.at(i)); if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query deleted contacts ids\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(queryStatement)); return QContactManager::UnspecifiedError; } do { for (int i = 0; i < ReportBatchSize && query.next(); ++i) { contactIds->append(ContactId::apiId(query.value(0).toUInt(), m_managerUri)); } contactIdsAvailable(*contactIds); } while (query.isValid()); return QContactManager::NoError; } QContactManager::Error ContactReader::readContactIds( QList *contactIds, const QContactFilter &filter, const QList &order) { QMutexLocker locker(m_database.accessMutex()); // Is this a query on deleted contacts? if (deletedContactFilter(filter)) { return readDeletedContactIds(contactIds, filter); } // Use a dummy table name to identify any temporary tables we create const QString tableName(QStringLiteral("readContactIds")); m_database.clearTransientContactIdsTable(tableName); QString join; bool transientModifiedRequired = false; bool globalPresenceRequired = false; const QString orderBy = buildOrderBy(order, &join, &transientModifiedRequired, &globalPresenceRequired, m_database.localized()); bool failed = false; QVariantList bindings; QString where = buildContactWhere(filter, m_database, tableName, QContactDetail::TypeUndefined, &bindings, &failed, &transientModifiedRequired, &globalPresenceRequired); if (failed) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to create WHERE expression: invalid filter specification")); return QContactManager::UnspecifiedError; } where = expandWhere(where, filter, m_database.aggregating()); if (transientModifiedRequired || globalPresenceRequired) { // Provide the temporary transient state information to filter/sort on if (!m_database.populateTemporaryTransientState(transientModifiedRequired, globalPresenceRequired)) { return QContactManager::UnspecifiedError; } if (transientModifiedRequired) { join.append(QStringLiteral(" LEFT JOIN temp.Timestamps ON Contacts.contactId = temp.Timestamps.contactId")); } if (globalPresenceRequired) { join.append(QStringLiteral(" LEFT JOIN temp.GlobalPresenceStates ON Contacts.contactId = temp.GlobalPresenceStates.contactId")); } } QString queryString = QStringLiteral( "\n SELECT DISTINCT Contacts.contactId" "\n FROM Contacts %1" "\n %2").arg(join).arg(where); if (!orderBy.isEmpty()) { queryString.append(QStringLiteral(" ORDER BY ") + orderBy); } QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(queryString)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare contacts ids:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(queryString)); return QContactManager::UnspecifiedError; } for (int i = 0; i < bindings.count(); ++i) query.bindValue(i, bindings.at(i)); if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query contacts ids\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(queryString)); return QContactManager::UnspecifiedError; } else { debugFilterExpansion("Contact IDs selection:", queryString, bindings); } do { for (int i = 0; i < ReportBatchSize && query.next(); ++i) { contactIds->append(ContactId::apiId(query.value(0).toUInt(), m_managerUri)); } contactIdsAvailable(*contactIds); } while (query.isValid()); return QContactManager::NoError; } QContactManager::Error ContactReader::getIdentity( ContactsDatabase::Identity identity, QContactId *contactId) { QMutexLocker locker(m_database.accessMutex()); if (identity == ContactsDatabase::InvalidContactId) { return QContactManager::BadArgumentError; } else if (identity == ContactsDatabase::SelfContactId) { // we don't allow setting the self contact id, it's always static *contactId = ContactId::apiId(selfId, m_managerUri); } else { const QString identityId(QStringLiteral( " SELECT contactId" " FROM Identities" " WHERE identity = :identity" )); ContactsDatabase::Query query(m_database.prepare(identityId)); query.bindValue(":identity", identity); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch contact identity"); return QContactManager::UnspecifiedError; } if (!query.next()) { *contactId = QContactId(); return QContactManager::UnspecifiedError; } else { *contactId = ContactId::apiId(query.value(0), m_managerUri); } } return QContactManager::NoError; } QContactManager::Error ContactReader::readRelationships( QList *relationships, const QString &type, const QContactId &first, const QContactId &second) { QMutexLocker locker(m_database.accessMutex()); QStringList whereStatements; QVariantList bindings; if (!type.isEmpty()) { whereStatements.append(QStringLiteral("type = ?")); bindings.append(type); } quint32 firstId = ContactId::databaseId(first); if (firstId != 0) { whereStatements.append(QStringLiteral("firstId = ?")); bindings.append(firstId); } quint32 secondId = ContactId::databaseId(second); if (secondId != 0) { whereStatements.append(QStringLiteral("secondId = ?")); bindings.append(secondId); } const QString whereParticipantNotDeleted = QStringLiteral( "\n WHERE firstId NOT IN (" "\n SELECT contactId FROM Contacts WHERE changeFlags >= 4)" // ChangeFlags::IsDeleted "\n AND secondId NOT IN (" "\n SELECT contactId FROM Contacts WHERE changeFlags >= 4)"); // ChangeFlags::IsDeleted const QString where = whereParticipantNotDeleted + (!whereStatements.isEmpty() ? (QStringLiteral(" AND ") + whereStatements.join(QStringLiteral(" AND "))) : QString()); QString statement = QStringLiteral( "\n SELECT type, firstId, secondId" "\n FROM Relationships") + where + QStringLiteral(";"); QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare relationships query:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(statement)); return QContactManager::UnspecifiedError; } for (int i = 0; i < bindings.count(); ++i) query.bindValue(i, bindings.at(i)); if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query relationships: %1") .arg(query.lastError().text())); return QContactManager::UnspecifiedError; } while (query.next()) { QString type = query.value(0).toString(); quint32 firstId = query.value(1).toUInt(); quint32 secondId = query.value(2).toUInt(); relationships->append(makeRelationship(type, firstId, secondId, m_managerUri)); } query.finish(); return QContactManager::NoError; } QContactManager::Error ContactReader::readDetails( QList *details, QContactDetail::DetailType type, QList fields, const QContactFilter &filter, const QList &order, const QContactFetchHint &fetchHint) { const DetailInfo &info = detailInformation(type); if (info.detailType == QContactDetail::TypeUndefined) { return QContactManager::UnspecifiedError; } else if (!info.appendUnique) { return QContactManager::UnspecifiedError; } QMutexLocker locker(m_database.accessMutex()); QString join; bool transientModifiedRequired = false; bool globalPresenceRequired = false; const QString orderBy = buildOrderBy( order, &join, &transientModifiedRequired, &globalPresenceRequired, m_database.localized(), type, QString()); bool whereFailed = false; QVariantList bindings; QString where = buildDetailWhere( filter, m_database, QLatin1String(info.table), type, &bindings, &whereFailed, &transientModifiedRequired, &globalPresenceRequired); if (whereFailed) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to create WHERE expression: invalid filter specification")); return QContactManager::UnspecifiedError; } const int maximumCount = fetchHint.maxCountHint(); QStringList fieldNames; for (int i = 0; i < info.fieldCount; ++i) { if (fields.isEmpty() || fields.contains(info.fields[i].field)) { fieldNames.append(QLatin1String(info.fields[i].column)); } else { // Instead of making every column read for a detail optional for the columns we're not // interested in we'll insert a null value. fieldNames.append(QStringLiteral("NULL")); } } const QString statement = QStringLiteral( "SELECT %1, MAX(detailId) AS maxId" " FROM %2" "%3" // WHERE " GROUP BY %1" "%4" // ORDER BY "%5").arg( // LIMIT fieldNames.join(QStringLiteral(", ")), QLatin1String(info.table), !where.isEmpty() ? QStringLiteral(" WHERE ") + where : QString(), !orderBy.isEmpty() ? QStringLiteral(" ORDER BY ") + orderBy : QStringLiteral(" ORDER BY maxId DESC"), // If there's no sort order prioritize the most recent entries. maximumCount > 0 ? QStringLiteral(" LIMIT %1").arg(maximumCount): QString()); QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare a unique details query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return QContactManager::UnspecifiedError; } for (int i = 0; i < bindings.count(); ++i) { query.bindValue(i, bindings.at(i)); } if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query unique details\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(statement)); return QContactManager::UnspecifiedError; } while (query.next()) { info.appendUnique(details, query); } return QContactManager::NoError; } QContactManager::Error ContactReader::getCollectionIdentity( ContactsDatabase::CollectionIdentity identity, QContactCollectionId *collectionId) { switch (identity) { case ContactsDatabase::AggregateAddressbookCollectionId: // fall through case ContactsDatabase::LocalAddressbookCollectionId: *collectionId = ContactCollectionId::apiId(static_cast(identity), m_managerUri); break; default: return QContactManager::BadArgumentError; } return QContactManager::NoError; } QContactManager::Error ContactReader::readCollections( const QString &table, QList *collections) { Q_UNUSED(table); QList cols; QContactManager::Error err = fetchCollections(0, QString(), &cols, &cols, nullptr, &cols); if (err == QContactManager::NoError) { *collections = cols; collectionsAvailable(cols); } return err; } QContactManager::Error ContactReader::fetchCollections( int accountId, const QString &applicationName, QList *addedCollections, QList *modifiedCollections, QList *deletedCollections, QList *unmodifiedCollections) { const QString where = accountId > 0 ? (!applicationName.isEmpty() ? QStringLiteral("WHERE accountId = :accountId AND applicationName = :applicationName") : QStringLiteral("WHERE accountId = :accountId")) : (!applicationName.isEmpty() ? QStringLiteral("WHERE applicationName = :applicationName") : QString()); const QString collectionsQueryStatement = QStringLiteral( "SELECT " // order and content can change due to schema upgrades, so list manually. "collectionId, " "aggregable, " "name, " "description, " "color, " "secondaryColor, " "image, " "applicationName, " "accountId, " "remotePath, " "changeFlags " "FROM Collections " "%1 " "ORDER BY collectionId ASC").arg(where); QSqlQuery collectionsQuery(m_database); if (!collectionsQuery.prepare(collectionsQueryStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare query for collection details:\n%1\nQuery:\n%2") .arg(collectionsQuery.lastError().text()) .arg(collectionsQueryStatement)); return QContactManager::UnspecifiedError; } if (accountId > 0) { collectionsQuery.bindValue(":accountId", accountId); } if (!applicationName.isEmpty()) { collectionsQuery.bindValue(":applicationName", applicationName); } collectionsQuery.setForwardOnly(true); if (!ContactsDatabase::execute(collectionsQuery)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to execute query for collection details:\n%1\nQuery:\n%2") .arg(collectionsQuery.lastError().text()) .arg(collectionsQueryStatement)); return QContactManager::UnspecifiedError; } while (collectionsQuery.next()) { int col = 0; const quint32 dbId = collectionsQuery.value(col++).toUInt(); QContactCollection collection; collection.setId(ContactCollectionId::apiId(dbId, m_managerUri)); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, collectionsQuery.value(col++).toBool()); collection.setMetaData(QContactCollection::KeyName, collectionsQuery.value(col++).toString()); collection.setMetaData(QContactCollection::KeyDescription, collectionsQuery.value(col++).toString()); collection.setMetaData(QContactCollection::KeyColor, collectionsQuery.value(col++).toString()); collection.setMetaData(QContactCollection::KeySecondaryColor, collectionsQuery.value(col++).toString()); collection.setMetaData(QContactCollection::KeyImage, collectionsQuery.value(col++).toString()); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, collectionsQuery.value(col++).toString()); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, collectionsQuery.value(col++).toInt()); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, collectionsQuery.value(col++).toString()); const int changeFlags = collectionsQuery.value(col++).toInt(); const QString metadataStatement(QStringLiteral( "SELECT " // order and content can change due to schema upgrades, so list manually. "collectionId, " "key, " "value " "FROM CollectionsMetadata " "WHERE collectionId = :collectionId " "ORDER BY collectionId ASC")); QSqlQuery metadataQuery(m_database); if (!metadataQuery.prepare(metadataStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare query for collection metadata details:\n%1\nQuery:\n%2") .arg(metadataQuery.lastError().text()) .arg(metadataStatement)); return QContactManager::UnspecifiedError; } metadataQuery.bindValue(":collectionId", dbId); metadataQuery.setForwardOnly(true); if (!ContactsDatabase::execute(metadataQuery)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to execute query for collection metadata details:\n%1\nQuery:\n%2") .arg(metadataQuery.lastError().text()) .arg(metadataStatement)); return QContactManager::UnspecifiedError; } while (metadataQuery.next()) { int col = 0; const quint32 dbId = metadataQuery.value(col++).toUInt(); Q_ASSERT(ContactCollectionId::databaseId(collection.id()) == dbId); const QString key = metadataQuery.value(col++).toString(); const QVariant value = metadataQuery.value(col++); collection.setExtendedMetaData(key, value); } if (changeFlags & ContactsDatabase::IsDeleted) { if (deletedCollections) { deletedCollections->append(collection); } } else if (changeFlags & ContactsDatabase::IsAdded) { if (addedCollections) { addedCollections->append(collection); } } else if (changeFlags & ContactsDatabase::IsModified) { if (modifiedCollections) { modifiedCollections->append(collection); } } else { // unmodified. if (unmodifiedCollections) { unmodifiedCollections->append(collection); } } } return QContactManager::NoError; } QContactManager::Error ContactReader::recordUnhandledChangeFlags( const QContactCollectionId &collectionId, bool *record) { const QString unhandledChangeFlagsStatement = QStringLiteral( "SELECT recordUnhandledChangeFlags " "FROM Collections " "WHERE collectionId = :collectionId"); QSqlQuery query(m_database); if (!query.prepare(unhandledChangeFlagsStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare query for record unhandled change flags:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(unhandledChangeFlagsStatement)); return QContactManager::UnspecifiedError; } query.bindValue(":collectionId", ContactCollectionId::databaseId(collectionId)); query.setForwardOnly(true); if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to execute query for record unhandled change flags:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(unhandledChangeFlagsStatement)); return QContactManager::UnspecifiedError; } if (query.next()) { *record = query.value(0).toBool(); return QContactManager::NoError; } return QContactManager::DoesNotExistError; } bool ContactReader::fetchOOB(const QString &scope, const QStringList &keys, QMap *values) { QVariantList keyNames; QString statement(QStringLiteral("SELECT name, value, compressed FROM OOB WHERE name ")); if (keys.isEmpty()) { statement.append(QStringLiteral("LIKE '%1:%%'").arg(scope)); } else { const QChar colon(QChar::fromLatin1(':')); QString keyList; foreach (const QString &key, keys) { keyNames.append(scope + colon + key); keyList.append(keyList.isEmpty() ? QStringLiteral("?") : QStringLiteral(",?")); } statement.append(QStringLiteral("IN (%1)").arg(keyList)); } QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare OOB query:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const QVariant &name, keyNames) { query.addBindValue(name); } if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query OOB: %1") .arg(query.lastError().text())); return false; } while (query.next()) { const QString name(query.value(0).toString()); const QVariant value(query.value(1)); const quint32 compressed(query.value(2).toUInt()); const QString key(name.mid(scope.length() + 1)); if (compressed > 0) { QByteArray compressedData(value.value()); if (compressed == 1) { // QByteArray data values->insert(key, QVariant(qUncompress(compressedData))); } else if (compressed == 2) { // QString data values->insert(key, QVariant(QString::fromUtf8(qUncompress(compressedData)))); } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Invalid compression type for OOB data:%1, key:%2") .arg(compressed).arg(key)); } } else { values->insert(key, value); } } query.finish(); return true; } bool ContactReader::fetchOOBKeys(const QString &scope, QStringList *keys) { QString statement(QStringLiteral("SELECT name FROM OOB WHERE name LIKE '%1:%%'").arg(scope)); QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare OOB query:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query OOB: %1") .arg(query.lastError().text())); return false; } while (query.next()) { const QString name(query.value(0).toString()); keys->append(name.mid(scope.length() + 1)); } query.finish(); return true; } void ContactReader::contactsAvailable(const QList &) { } void ContactReader::contactIdsAvailable(const QList &) { } void ContactReader::collectionsAvailable(const QList &) { } qtcontacts-sqlite-0.3.19/src/engine/contactreader.h000066400000000000000000000134611436373107600223300ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2019 Jolla Ltd. * Copyright (c) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef CONTACTREADER_H #define CONTACTREADER_H #include "contactid_p.h" #include "contactsdatabase.h" #include #include #include #include QTCONTACTS_USE_NAMESPACE class ContactReader { public: ContactReader(ContactsDatabase &database, const QString &managerUri); virtual ~ContactReader(); QContactManager::Error readContacts( const QString &table, QList *contacts, const QContactFilter &filter, const QList &order, const QContactFetchHint &fetchHint, bool keepChangeFlags = false); // for sync fetch only QContactManager::Error readContacts( const QString &table, QList *contacts, const QList &contactIds, const QContactFetchHint &fetchHint); QContactManager::Error readContacts( const QString &table, QList *contacts, const QList &databaseIds, const QContactFetchHint &fetchHint, bool relaxConstraints = false); QContactManager::Error readContactIds( QList *contactIds, const QContactFilter &filter, const QList &order); QContactManager::Error getIdentity( ContactsDatabase::Identity identity, QContactId *contactId); QContactManager::Error readRelationships( QList *relationships, const QString &type, const QContactId &first, const QContactId &second); QContactManager::Error readDetails( QList *details, QContactDetail::DetailType type, QList fields, const QContactFilter &filter, const QList &order, const QContactFetchHint &hint); QContactManager::Error getCollectionIdentity( ContactsDatabase::CollectionIdentity identity, QContactCollectionId *collectionId); QContactManager::Error readCollections( const QString &table, QList *collections); QContactManager::Error fetchCollections( int accountId, const QString &applicationName, QList *addedCollections, QList *modifiedCollections, QList *deletedCollections, QList *unmodifiedCollections); QContactManager::Error fetchContacts( const QContactCollectionId &collectionId, QList *addedContacts, QList *modifiedContacts, QList *deletedContacts, QList *unmodifiedContacts); QContactManager::Error recordUnhandledChangeFlags( const QContactCollectionId &collectionId, bool *record); bool fetchOOB(const QString &scope, const QStringList &keys, QMap *values); bool fetchOOBKeys(const QString &scope, QStringList *keys); protected: QContactManager::Error readDeletedContactIds( QList *contactIds, const QContactFilter &filter); QContactManager::Error queryContacts( const QString &table, QList *contacts, const QContactFetchHint &fetchHint, bool relaxConstraints = false, bool ignoreDeleted = false, bool keepChangeFlags = false); QContactManager::Error queryContacts( const QString &table, QList *contacts, const QContactFetchHint &fetchHint, bool relaxConstraints, bool keepChangeFlags, QSqlQuery &query, QSqlQuery &relationshipQuery); virtual void contactsAvailable(const QList &contacts); virtual void contactIdsAvailable(const QList &contactIds); virtual void collectionsAvailable(const QList &collections); private: ContactsDatabase &m_database; QString m_managerUri; }; #endif qtcontacts-sqlite-0.3.19/src/engine/contactsdatabase.cpp000066400000000000000000005052041436373107600233510ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2019 Jolla Ltd. * Copyright (c) 2019 - 2020 Open Mobile Platform LLC. * * 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 "contactsdatabase.h" #include "contactsengine.h" #include "defaultdlggenerator.h" #include "conversion_p.h" #include "trace_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef QTCONTACTS_SQLITE_LOAD_ICU #include #endif static const char *setupEncoding = "\n PRAGMA encoding = \"UTF-16\";"; static const char *setupTempStore = "\n PRAGMA temp_store = MEMORY;"; static const char *setupJournal = "\n PRAGMA journal_mode = WAL;"; static const char *setupSynchronous = "\n PRAGMA synchronous = FULL;"; static const char *createCollectionsTable = "\n CREATE TABLE Collections (" "\n collectionId INTEGER PRIMARY KEY ASC AUTOINCREMENT," "\n aggregable BOOL DEFAULT 1," "\n name TEXT," "\n description TEXT," "\n color TEXT," "\n secondaryColor TEXT," "\n image TEXT," "\n applicationName TEXT," "\n accountId INTEGER," "\n remotePath TEXT," "\n changeFlags INTEGER DEFAULT 0," "\n recordUnhandledChangeFlags BOOL DEFAULT 0)"; static const char *createCollectionsMetadataTable = "\n CREATE TABLE CollectionsMetadata (" "\n collectionId INTEGER REFERENCES Collections (collectionId)," "\n key TEXT," "\n value BLOB," "\n PRIMARY KEY (collectionId, key))"; static const char *createContactsTable = "\n CREATE TABLE Contacts (" "\n contactId INTEGER PRIMARY KEY ASC AUTOINCREMENT," "\n collectionId INTEGER REFERENCES Collections (collectionId)," "\n created DATETIME," "\n modified DATETIME," "\n deleted DATETIME," "\n hasPhoneNumber BOOL DEFAULT 0," "\n hasEmailAddress BOOL DEFAULT 0," "\n hasOnlineAccount BOOL DEFAULT 0," "\n isOnline BOOL DEFAULT 0," "\n isDeactivated BOOL DEFAULT 0," "\n changeFlags INTEGER DEFAULT 0," "\n unhandledChangeFlags INTEGER DEFAULT 0," "\n type INTEGER DEFAULT 0);"; // QContactType::TypeContact static const char *createAddressesTable = "\n CREATE TABLE Addresses (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY ASC," "\n street TEXT," "\n postOfficeBox TEXT," "\n region TEXT," "\n locality TEXT," "\n postCode TEXT," "\n country TEXT," "\n subTypes TEXT);"; // Contains INTEGER values represented as TEXT, separated by ';' static const char *createAnniversariesTable = "\n CREATE TABLE Anniversaries (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n originalDateTime DATETIME," "\n calendarId TEXT," "\n subType TEXT," // Contains an INTEGER represented as TEXT "\n event TEXT);"; static const char *createAvatarsTable = "\n CREATE TABLE Avatars (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n imageUrl TEXT," "\n videoUrl TEXT," "\n avatarMetadata TEXT);"; // arbitrary metadata static const char *createBirthdaysTable = "\n CREATE TABLE Birthdays (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n birthday DATETIME," "\n calendarId TEXT);"; static const char *createDisplayLabelsTable = "\n CREATE TABLE DisplayLabels (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY UNIQUE," // only one display label detail per contact "\n displayLabel TEXT," "\n displayLabelGroup TEXT," "\n displayLabelGroupSortOrder INTEGER)"; static const char *createEmailAddressesTable = "\n CREATE TABLE EmailAddresses (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n emailAddress TEXT," "\n lowerEmailAddress TEXT);"; static const char *createFamiliesTable = "\n CREATE TABLE Families (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n spouse TEXT," "\n children TEXT);"; static const char *createFavoritesTable = "\n CREATE TABLE Favorites (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY UNIQUE," // only one favorite detail per contact "\n isFavorite BOOL)"; static const char *createGendersTable = "\n CREATE TABLE Genders (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY UNIQUE," // only one gender detail per contact "\n gender TEXT)"; // Contains an INTEGER represented as TEXT static const char *createGeoLocationsTable = "\n CREATE TABLE GeoLocations (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n label TEXT," "\n latitude REAL," "\n longitude REAL," "\n accuracy REAL," "\n altitude REAL," "\n altitudeAccuracy REAL," "\n heading REAL," "\n speed REAL," "\n timestamp DATETIME);"; static const char *createGlobalPresencesTable = "\n CREATE TABLE GlobalPresences (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n presenceState INTEGER," "\n timestamp DATETIME," "\n nickname TEXT," "\n customMessage TEXT," "\n presenceStateText TEXT," "\n presenceStateImageUrl TEXT);"; static const char *createGuidsTable = "\n CREATE TABLE Guids (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n guid TEXT);"; static const char *createHobbiesTable = "\n CREATE TABLE Hobbies (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n hobby TEXT);"; static const char *createNamesTable = "\n CREATE TABLE Names (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY UNIQUE," // only one name detail per contact "\n firstName TEXT," "\n lowerFirstName TEXT," "\n lastName TEXT," "\n lowerLastName TEXT," "\n middleName TEXT," "\n prefix TEXT," "\n suffix TEXT," "\n customLabel TEXT)"; static const char *createNicknamesTable = "\n CREATE TABLE Nicknames (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n nickname TEXT," "\n lowerNickname TEXT);"; static const char *createNotesTable = "\n CREATE TABLE Notes (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n note TEXT);"; static const char *createOnlineAccountsTable = "\n CREATE TABLE OnlineAccounts (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n accountUri TEXT," "\n lowerAccountUri TEXT," "\n protocol TEXT," // Contains an INTEGER represented as TEXT "\n serviceProvider TEXT," "\n capabilities TEXT," "\n subTypes TEXT," // Contains INTEGER values represented as TEXT, separated by ';' "\n accountPath TEXT," "\n accountIconPath TEXT," "\n enabled BOOL," "\n accountDisplayName TEXT," "\n serviceProviderDisplayName TEXT);"; static const char *createOrganizationsTable = "\n CREATE TABLE Organizations (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n name TEXT," "\n role TEXT," "\n title TEXT," "\n location TEXT," "\n department TEXT," "\n logoUrl TEXT," "\n assistantName TEXT);"; static const char *createPhoneNumbersTable = "\n CREATE TABLE PhoneNumbers (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n phoneNumber TEXT," "\n subTypes TEXT," // Contains INTEGER values represented as TEXT, separated by ';' "\n normalizedNumber TEXT);"; static const char *createPresencesTable = "\n CREATE TABLE Presences (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n presenceState INTEGER," "\n timestamp DATETIME," "\n nickname TEXT," "\n customMessage TEXT," "\n presenceStateText TEXT," "\n presenceStateImageUrl TEXT);"; static const char *createRingtonesTable = "\n CREATE TABLE Ringtones (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n audioRingtone TEXT," "\n videoRingtone TEXT," "\n vibrationRingtone TEXT);"; static const char *createSyncTargetsTable = "\n CREATE TABLE SyncTargets (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY UNIQUE," // only one sync target detail per contact "\n syncTarget TEXT)"; static const char *createTagsTable = "\n CREATE TABLE Tags (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n tag TEXT);"; static const char *createUrlsTable = "\n CREATE TABLE Urls (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n url TEXT," "\n subTypes TEXT);"; // Contains a (singular) INTEGER represented as TEXT (and should be named 'subType') static const char *createOriginMetadataTable = "\n CREATE TABLE OriginMetadata (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n id TEXT," "\n groupId TEXT," "\n enabled BOOL);"; static const char *createExtendedDetailsTable = "\n CREATE TABLE ExtendedDetails (" "\n detailId INTEGER PRIMARY KEY ASC REFERENCES Details (detailId)," "\n contactId INTEGER KEY," "\n name TEXT," "\n data BLOB);"; static const char *createDetailsTable = "\n CREATE TABLE Details (" "\n detailId INTEGER PRIMARY KEY ASC AUTOINCREMENT," "\n contactId INTEGER REFERENCES Contacts (contactId)," "\n detail TEXT," "\n detailUri TEXT," "\n linkedDetailUris TEXT," "\n contexts TEXT," "\n accessConstraints INTEGER," "\n provenance TEXT," "\n modifiable BOOL," "\n nonexportable BOOL," "\n changeFlags INTEGER DEFAULT 0," "\n unhandledChangeFlags INTEGER DEFAULT 0," "\n created DATETIME," "\n modified DATETIME);"; static const char *createDetailsRemoveIndex = "\n CREATE INDEX DetailsRemoveIndex ON Details(contactId, detail);"; static const char *createDetailsChangeFlagsIndex = "\n CREATE INDEX DetailsChangeFlagsIndex ON Details(changeFlags);"; static const char *createDetailsContactIdIndex = "\n CREATE INDEX DetailsContactIdIndex ON Details(contactId);"; static const char *createIdentitiesTable = "\n CREATE Table Identities (" "\n identity INTEGER PRIMARY KEY," "\n contactId INTEGER KEY);"; static const char *createRelationshipsTable = "\n CREATE Table Relationships (" "\n firstId INTEGER NOT NULL," "\n secondId INTEGER NOT NULL," "\n type TEXT," "\n PRIMARY KEY (firstId, secondId, type));"; static const char *createDeletedContactsTable = "\n CREATE TABLE DeletedContacts (" "\n contactId INTEGER PRIMARY KEY," "\n collectionId INTEGER NOT NULL," "\n deleted DATETIME);"; static const char *createOOBTable = "\n CREATE TABLE OOB (" "\n name TEXT PRIMARY KEY," "\n value BLOB," "\n compressed INTEGER DEFAULT 0);"; static const char *createDbSettingsTable = "\n CREATE TABLE DbSettings (" "\n name TEXT PRIMARY KEY," "\n value TEXT );"; // as at b8084fa7 static const char *createRemoveTrigger_0 = "\n CREATE TRIGGER RemoveContactDetails" "\n BEFORE DELETE" "\n ON Contacts" "\n BEGIN" "\n DELETE FROM Addresses WHERE contactId = old.contactId;" "\n DELETE FROM Anniversaries WHERE contactId = old.contactId;" "\n DELETE FROM Avatars WHERE contactId = old.contactId;" "\n DELETE FROM Birthdays WHERE contactId = old.contactId;" "\n DELETE FROM EmailAddresses WHERE contactId = old.contactId;" "\n DELETE FROM GlobalPresences WHERE contactId = old.contactId;" "\n DELETE FROM Guids WHERE contactId = old.contactId;" "\n DELETE FROM Hobbies WHERE contactId = old.contactId;" "\n DELETE FROM Nicknames WHERE contactId = old.contactId;" "\n DELETE FROM Notes WHERE contactId = old.contactId;" "\n DELETE FROM OnlineAccounts WHERE contactId = old.contactId;" "\n DELETE FROM Organizations WHERE contactId = old.contactId;" "\n DELETE FROM PhoneNumbers WHERE contactId = old.contactId;" "\n DELETE FROM Presences WHERE contactId = old.contactId;" "\n DELETE FROM Ringtones WHERE contactId = old.contactId;" "\n DELETE FROM SyncTargets WHERE contactId = old.contactId;" "\n DELETE FROM Tags WHERE contactId = old.contactId;" "\n DELETE FROM Urls WHERE contactId = old.contactId;" "\n DELETE FROM TpMetadata WHERE contactId = old.contactId;" "\n DELETE FROM ExtendedDetails WHERE contactId = old.contactId;" "\n DELETE FROM Details WHERE contactId = old.contactId;" "\n DELETE FROM Identities WHERE contactId = old.contactId;" "\n DELETE FROM Relationships WHERE firstId = old.contactId OR secondId = old.contactId;" "\n END;"; // as at 2c818a05 static const char *createRemoveTrigger_1 = createRemoveTrigger_0; // as at a18a1884 static const char *createRemoveTrigger_2 = "\n CREATE TRIGGER RemoveContactDetails" "\n BEFORE DELETE" "\n ON Contacts" "\n BEGIN" "\n INSERT INTO DeletedContacts (contactId, syncTarget, deleted) VALUES (old.contactId, old.syncTarget, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));" "\n DELETE FROM Addresses WHERE contactId = old.contactId;" "\n DELETE FROM Anniversaries WHERE contactId = old.contactId;" "\n DELETE FROM Avatars WHERE contactId = old.contactId;" "\n DELETE FROM Birthdays WHERE contactId = old.contactId;" "\n DELETE FROM EmailAddresses WHERE contactId = old.contactId;" "\n DELETE FROM GlobalPresences WHERE contactId = old.contactId;" "\n DELETE FROM Guids WHERE contactId = old.contactId;" "\n DELETE FROM Hobbies WHERE contactId = old.contactId;" "\n DELETE FROM Nicknames WHERE contactId = old.contactId;" "\n DELETE FROM Notes WHERE contactId = old.contactId;" "\n DELETE FROM OnlineAccounts WHERE contactId = old.contactId;" "\n DELETE FROM Organizations WHERE contactId = old.contactId;" "\n DELETE FROM PhoneNumbers WHERE contactId = old.contactId;" "\n DELETE FROM Presences WHERE contactId = old.contactId;" "\n DELETE FROM Ringtones WHERE contactId = old.contactId;" "\n DELETE FROM Tags WHERE contactId = old.contactId;" "\n DELETE FROM Urls WHERE contactId = old.contactId;" "\n DELETE FROM TpMetadata WHERE contactId = old.contactId;" "\n DELETE FROM ExtendedDetails WHERE contactId = old.contactId;" "\n DELETE FROM Details WHERE contactId = old.contactId;" "\n DELETE FROM Identities WHERE contactId = old.contactId;" "\n DELETE FROM Relationships WHERE firstId = old.contactId OR secondId = old.contactId;" "\n END;"; // as at 78256437 static const char *createRemoveTrigger_11 = "\n CREATE TRIGGER RemoveContactDetails" "\n BEFORE DELETE" "\n ON Contacts" "\n BEGIN" "\n INSERT INTO DeletedContacts (contactId, syncTarget, deleted) VALUES (old.contactId, old.syncTarget, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));" "\n DELETE FROM Addresses WHERE contactId = old.contactId;" "\n DELETE FROM Anniversaries WHERE contactId = old.contactId;" "\n DELETE FROM Avatars WHERE contactId = old.contactId;" "\n DELETE FROM Birthdays WHERE contactId = old.contactId;" "\n DELETE FROM EmailAddresses WHERE contactId = old.contactId;" "\n DELETE FROM GlobalPresences WHERE contactId = old.contactId;" "\n DELETE FROM Guids WHERE contactId = old.contactId;" "\n DELETE FROM Hobbies WHERE contactId = old.contactId;" "\n DELETE FROM Nicknames WHERE contactId = old.contactId;" "\n DELETE FROM Notes WHERE contactId = old.contactId;" "\n DELETE FROM OnlineAccounts WHERE contactId = old.contactId;" "\n DELETE FROM Organizations WHERE contactId = old.contactId;" "\n DELETE FROM PhoneNumbers WHERE contactId = old.contactId;" "\n DELETE FROM Presences WHERE contactId = old.contactId;" "\n DELETE FROM Ringtones WHERE contactId = old.contactId;" "\n DELETE FROM Tags WHERE contactId = old.contactId;" "\n DELETE FROM Urls WHERE contactId = old.contactId;" "\n DELETE FROM OriginMetadata WHERE contactId = old.contactId;" "\n DELETE FROM ExtendedDetails WHERE contactId = old.contactId;" "\n DELETE FROM Details WHERE contactId = old.contactId;" "\n DELETE FROM Identities WHERE contactId = old.contactId;" "\n DELETE FROM Relationships WHERE firstId = old.contactId OR secondId = old.contactId;" "\n END;"; // as at 8e0fb5e5 static const char *createRemoveTrigger_12 = "\n CREATE TRIGGER RemoveContactDetails" "\n BEFORE DELETE" "\n ON Contacts" "\n BEGIN" "\n INSERT INTO DeletedContacts (contactId, syncTarget, deleted) VALUES (old.contactId, old.syncTarget, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));" "\n DELETE FROM Addresses WHERE contactId = old.contactId;" "\n DELETE FROM Anniversaries WHERE contactId = old.contactId;" "\n DELETE FROM Avatars WHERE contactId = old.contactId;" "\n DELETE FROM Birthdays WHERE contactId = old.contactId;" "\n DELETE FROM EmailAddresses WHERE contactId = old.contactId;" "\n DELETE FROM Families WHERE contactId = old.contactId;" "\n DELETE FROM GeoLocations WHERE contactId = old.contactId;" "\n DELETE FROM GlobalPresences WHERE contactId = old.contactId;" "\n DELETE FROM Guids WHERE contactId = old.contactId;" "\n DELETE FROM Hobbies WHERE contactId = old.contactId;" "\n DELETE FROM Nicknames WHERE contactId = old.contactId;" "\n DELETE FROM Notes WHERE contactId = old.contactId;" "\n DELETE FROM OnlineAccounts WHERE contactId = old.contactId;" "\n DELETE FROM Organizations WHERE contactId = old.contactId;" "\n DELETE FROM PhoneNumbers WHERE contactId = old.contactId;" "\n DELETE FROM Presences WHERE contactId = old.contactId;" "\n DELETE FROM Ringtones WHERE contactId = old.contactId;" "\n DELETE FROM Tags WHERE contactId = old.contactId;" "\n DELETE FROM Urls WHERE contactId = old.contactId;" "\n DELETE FROM OriginMetadata WHERE contactId = old.contactId;" "\n DELETE FROM ExtendedDetails WHERE contactId = old.contactId;" "\n DELETE FROM Details WHERE contactId = old.contactId;" "\n DELETE FROM Identities WHERE contactId = old.contactId;" "\n DELETE FROM Relationships WHERE firstId = old.contactId OR secondId = old.contactId;" "\n END;"; static const char *createRemoveTrigger_21 = "\n CREATE TRIGGER RemoveContactDetails" "\n BEFORE DELETE" "\n ON Contacts" "\n BEGIN" "\n DELETE FROM Addresses WHERE contactId = old.contactId;" "\n DELETE FROM Anniversaries WHERE contactId = old.contactId;" "\n DELETE FROM Avatars WHERE contactId = old.contactId;" "\n DELETE FROM Birthdays WHERE contactId = old.contactId;" "\n DELETE FROM DisplayLabels WHERE contactId = old.contactId;" "\n DELETE FROM EmailAddresses WHERE contactId = old.contactId;" "\n DELETE FROM Families WHERE contactId = old.contactId;" "\n DELETE FROM Favorites WHERE contactId = old.contactId;" "\n DELETE FROM Genders WHERE contactId = old.contactId;" "\n DELETE FROM GeoLocations WHERE contactId = old.contactId;" "\n DELETE FROM GlobalPresences WHERE contactId = old.contactId;" "\n DELETE FROM Guids WHERE contactId = old.contactId;" "\n DELETE FROM Hobbies WHERE contactId = old.contactId;" "\n DELETE FROM Names WHERE contactId = old.contactId;" "\n DELETE FROM Nicknames WHERE contactId = old.contactId;" "\n DELETE FROM Notes WHERE contactId = old.contactId;" "\n DELETE FROM OnlineAccounts WHERE contactId = old.contactId;" "\n DELETE FROM Organizations WHERE contactId = old.contactId;" "\n DELETE FROM PhoneNumbers WHERE contactId = old.contactId;" "\n DELETE FROM Presences WHERE contactId = old.contactId;" "\n DELETE FROM Ringtones WHERE contactId = old.contactId;" "\n DELETE FROM SyncTargets WHERE contactId = old.contactId;" "\n DELETE FROM Tags WHERE contactId = old.contactId;" "\n DELETE FROM Urls WHERE contactId = old.contactId;" "\n DELETE FROM OriginMetadata WHERE contactId = old.contactId;" "\n DELETE FROM ExtendedDetails WHERE contactId = old.contactId;" "\n DELETE FROM Details WHERE contactId = old.contactId;" "\n DELETE FROM Identities WHERE contactId = old.contactId;" "\n DELETE FROM Relationships WHERE firstId = old.contactId OR secondId = old.contactId;" "\n END;"; static const char *createRemoveTrigger = createRemoveTrigger_21; // better if we had used foreign key constraints with cascade delete... static const char *createRemoveDetailsTrigger_22 = "\n CREATE TRIGGER CascadeRemoveSpecificDetails" "\n BEFORE DELETE" "\n ON Details" "\n BEGIN" "\n DELETE FROM Addresses WHERE detailId = old.detailId;" "\n DELETE FROM Anniversaries WHERE detailId = old.detailId;" "\n DELETE FROM Avatars WHERE detailId = old.detailId;" "\n DELETE FROM Birthdays WHERE detailId = old.detailId;" "\n DELETE FROM DisplayLabels WHERE detailId = old.detailId;" "\n DELETE FROM EmailAddresses WHERE detailId = old.detailId;" "\n DELETE FROM Families WHERE detailId = old.detailId;" "\n DELETE FROM Favorites WHERE detailId = old.detailId;" "\n DELETE FROM Genders WHERE detailId = old.detailId;" "\n DELETE FROM GeoLocations WHERE detailId = old.detailId;" "\n DELETE FROM GlobalPresences WHERE detailId = old.detailId;" "\n DELETE FROM Guids WHERE detailId = old.detailId;" "\n DELETE FROM Hobbies WHERE detailId = old.detailId;" "\n DELETE FROM Names WHERE detailId = old.detailId;" "\n DELETE FROM Nicknames WHERE detailId = old.detailId;" "\n DELETE FROM Notes WHERE detailId = old.detailId;" "\n DELETE FROM OnlineAccounts WHERE detailId = old.detailId;" "\n DELETE FROM Organizations WHERE detailId = old.detailId;" "\n DELETE FROM PhoneNumbers WHERE detailId = old.detailId;" "\n DELETE FROM Presences WHERE detailId = old.detailId;" "\n DELETE FROM Ringtones WHERE detailId = old.detailId;" "\n DELETE FROM SyncTargets WHERE detailId = old.detailId;" "\n DELETE FROM Tags WHERE detailId = old.detailId;" "\n DELETE FROM Urls WHERE detailId = old.detailId;" "\n DELETE FROM OriginMetadata WHERE detailId = old.detailId;" "\n DELETE FROM ExtendedDetails WHERE detailId = old.detailId;" "\n END;"; static const char *createRemoveDetailsTrigger = createRemoveDetailsTrigger_22; static const char *createLocalSelfContact = "\n INSERT INTO Contacts (" "\n contactId," "\n collectionId)" "\n VALUES (" "\n 1," "\n 2);"; static const char *createAggregateSelfContact = "\n INSERT INTO Contacts (" "\n contactId," "\n collectionId)" "\n VALUES (" "\n 2," "\n 1);"; static const char *createSelfContactRelationship = "\n INSERT INTO Relationships (firstId, secondId, type) VALUES (2, 1, 'Aggregates');"; static const char *createSelfContact = "\n INSERT INTO Contacts (" "\n contactId," "\n collectionId)" "\n VALUES (" "\n 2," "\n 2);"; static const char *createAggregateAddressbookCollection = "\n INSERT INTO Collections(" "\n collectionId," "\n aggregable," "\n name," "\n description," "\n color," "\n secondaryColor," "\n image," "\n accountId," "\n remotePath)" "\n VALUES (" "\n 1," "\n 0," "\n 'aggregate'," "\n 'Aggregate contacts whose data is merged from constituent (facet) contacts'," "\n 'blue'," "\n 'lightsteelblue'," "\n ''," "\n 0," "\n '')"; static const char *createLocalAddressbookCollection = "\n INSERT INTO Collections(" "\n collectionId," "\n aggregable," "\n name," "\n description," "\n color," "\n secondaryColor," "\n image," "\n accountId," "\n remotePath)" "\n VALUES (" "\n 2," "\n 1," "\n 'local'," "\n 'Device-storage addressbook'," "\n 'red'," "\n 'pink'," "\n ''," "\n 0," "\n '')"; static const char *createContactsCollectionIdIndex = "\n CREATE INDEX ContactsCollectionIdIndex ON Contacts(collectionId);"; static const char *createCollectionsChangeFlagsIndex = "\n CREATE INDEX CollectionsChangeFlagsIndex ON Collections(changeFlags);"; static const char *createContactsChangeFlagsIndex = "\n CREATE INDEX ContactsChangeFlagsIndex ON Contacts(changeFlags);"; static const char *createFirstNameIndex = "\n CREATE INDEX FirstNameIndex ON Names(lowerFirstName);"; static const char *createLastNameIndex = "\n CREATE INDEX LastNameIndex ON Names(lowerLastName);"; static const char *createContactsModifiedIndex = "\n CREATE INDEX ContactsModifiedIndex ON Contacts(modified);"; static const char *createContactsTypeIndex = "\n CREATE INDEX ContactsTypeIndex ON Contacts(type);"; static const char *createRelationshipsFirstIdIndex = "\n CREATE INDEX RelationshipsFirstIdIndex ON Relationships(firstId);"; static const char *createRelationshipsSecondIdIndex = "\n CREATE INDEX RelationshipsSecondIdIndex ON Relationships(secondId);"; static const char *createPhoneNumbersIndex = "\n CREATE INDEX PhoneNumbersIndex ON PhoneNumbers(normalizedNumber);"; static const char *createEmailAddressesIndex = "\n CREATE INDEX EmailAddressesIndex ON EmailAddresses(lowerEmailAddress);"; static const char *createOnlineAccountsIndex = "\n CREATE INDEX OnlineAccountsIndex ON OnlineAccounts(lowerAccountUri);"; static const char *createNicknamesIndex = "\n CREATE INDEX NicknamesIndex ON Nicknames(lowerNickname);"; static const char *createOriginMetadataIdIndex = "\n CREATE INDEX OriginMetadataIdIndex ON OriginMetadata(id);"; static const char *createOriginMetadataGroupIdIndex = "\n CREATE INDEX OriginMetadataGroupIdIndex ON OriginMetadata(groupId);"; // Running ANALYZE on an empty database is not useful, // so seed it with ANALYZE results based on a developer device // that has a good mix of active accounts. // // Having the ANALYZE data available prevents some bad query plans // such as using ContactsIsDeactivatedIndex for most queries because // they have "WHERE isDeactivated = 0". // // NOTE: when adding an index to the schema, add a row for it to // this table. The format is table name, index name, data, and // the data is a string containing numbers, it starts with the // table size and then has one number for each column in the index, // where that number is the average number of rows selected by // an indexed value. // The best way to get these numbers is to run ANALYZE on a // real database and scale the results to the numbers here // (5000 contacts and 25000 details). static const char *createAnalyzeData1 = // ANALYZE creates the sqlite_stat1 table; constrain it to sqlite_master // just to make sure it doesn't do needless work. "\n ANALYZE sqlite_master;"; static const char *createAnalyzeData2 = "\n DELETE FROM sqlite_stat1;"; static const char *createAnalyzeData3 = "\n INSERT INTO sqlite_stat1 VALUES" "\n ('DbSettings','sqlite_autoindex_DbSettings_1','2 1')," "\n ('Collections','','12')," "\n ('Relationships','RelationshipsSecondIdIndex','3000 2')," "\n ('Relationships','RelationshipsFirstIdIndex','3000 2')," "\n ('Relationships','sqlite_autoindex_Relationships_1','3000 2 2 1')," "\n ('Contacts','ContactsTypeIndex','5000 5000')," "\n ('Contacts','ContactsModifiedIndex','5000 30')," "\n ('Contacts','ContactsChangeFlagsIndex','5000 200')," "\n ('Contacts','ContactsCollectionIdIndex','5000 500')," "\n ('Details', 'DetailsRemoveIndex', '25000 6 2')," "\n ('Details', 'DetailsContactIdIndex', '25000 6 2')," "\n ('Favorites','sqlite_autoindex_Favorites_1','100 2')," "\n ('Names','LastNameIndex','3000 50')," "\n ('Names','FirstNameIndex','3000 80')," "\n ('Names','sqlite_autoindex_Names_1','3000 1')," "\n ('DisplayLabels','sqlite_autoindex_DisplayLabels_1','5000 1')," "\n ('OnlineAccounts','OnlineAccountsIndex','1000 3')," "\n ('Nicknames','NicknamesIndex','2000 4')," "\n ('OriginMetadata','OriginMetadataGroupIdIndex','2500 500')," "\n ('OriginMetadata','OriginMetadataIdIndex','2500 6')," "\n ('PhoneNumbers','PhoneNumbersIndex','4500 7')," "\n ('EmailAddresses','EmailAddressesIndex','4000 5')," "\n ('OOB','sqlite_autoindex_OOB_1','29 1');"; static const char *createStatements[] = { createCollectionsTable, createCollectionsMetadataTable, createContactsTable, createAddressesTable, createAnniversariesTable, createAvatarsTable, createBirthdaysTable, createDisplayLabelsTable, createEmailAddressesTable, createFamiliesTable, createFavoritesTable, createGendersTable, createGeoLocationsTable, createGlobalPresencesTable, createGuidsTable, createHobbiesTable, createNamesTable, createNicknamesTable, createNotesTable, createOnlineAccountsTable, createOrganizationsTable, createPhoneNumbersTable, createPresencesTable, createRingtonesTable, createSyncTargetsTable, createTagsTable, createUrlsTable, createOriginMetadataTable, createExtendedDetailsTable, createDetailsTable, createDetailsRemoveIndex, createDetailsChangeFlagsIndex, createDetailsContactIdIndex, createIdentitiesTable, createRelationshipsTable, createOOBTable, createDbSettingsTable, createRemoveTrigger, createRemoveDetailsTrigger, createContactsCollectionIdIndex, createContactsChangeFlagsIndex, createFirstNameIndex, createLastNameIndex, createRelationshipsFirstIdIndex, createRelationshipsSecondIdIndex, createPhoneNumbersIndex, createEmailAddressesIndex, createOnlineAccountsIndex, createNicknamesIndex, createOriginMetadataIdIndex, createOriginMetadataGroupIdIndex, createContactsModifiedIndex, createContactsTypeIndex, createAnalyzeData1, createAnalyzeData2, createAnalyzeData3, }; // Upgrade statement indexed by old version static const char *upgradeVersion0[] = { createContactsModifiedIndex, "PRAGMA user_version=1", 0 // NULL-terminated }; static const char *upgradeVersion1[] = { createDeletedContactsTable, "DROP TRIGGER RemoveContactDetails", createRemoveTrigger_2, "PRAGMA user_version=2", 0 // NULL-terminated }; static const char *upgradeVersion2[] = { "ALTER TABLE Contacts ADD COLUMN isDeactivated BOOL DEFAULT 0", "PRAGMA user_version=3", 0 // NULL-terminated }; static const char *upgradeVersion3[] = { "ALTER TABLE Contacts ADD COLUMN isIncidental BOOL DEFAULT 0", "PRAGMA user_version=4", 0 // NULL-terminated }; static const char *upgradeVersion4[] = { // We can't create this in final form anymore, since we're modifying it in version 8->9 //createOOBTable, "CREATE TABLE OOB (" "name TEXT PRIMARY KEY," "value BLOB)", "PRAGMA user_version=5", 0 // NULL-terminated }; static const char *upgradeVersion5[] = { "ALTER TABLE Contacts ADD COLUMN type INTEGER DEFAULT 0", createContactsTypeIndex, "PRAGMA user_version=6", 0 // NULL-terminated }; static const char *upgradeVersion6[] = { "ALTER TABLE Details ADD COLUMN nonexportable BOOL DEFAULT 0", "PRAGMA user_version=7", 0 // NULL-terminated }; static const char *upgradeVersion7[] = { "PRAGMA user_version=8", 0 // NULL-terminated }; static const char *upgradeVersion8[] = { // Alter the OOB table; this alteration requires that the earlier upgrade // creates the obsolete form of the table rather thna the current one "ALTER TABLE OOB ADD COLUMN compressed INTEGER DEFAULT 0", "PRAGMA user_version=9", 0 // NULL-terminated }; static const char *upgradeVersion9[] = { "DROP INDEX IF EXISTS DetailsJoinIndex", // Don't recreate the index since it doesn't exist in versions after 10: //createDetailsJoinIndex, "PRAGMA user_version=10", 0 // NULL-terminated }; static const char *upgradeVersion10[] = { // Drop the remove trigger "DROP TRIGGER RemoveContactDetails", // Preserve the existing state of the Details table "ALTER TABLE Details RENAME TO OldDetails", // Create an index to map new version of detail rows to the old ones "CREATE TEMP TABLE DetailsIndexing(" "detailId INTEGER PRIMARY KEY ASC AUTOINCREMENT," "oldDetailId INTEGER," "contactId INTEGER," "detail TEXT," "syncTarget TEXT," "provenance TEXT)", "INSERT INTO DetailsIndexing(oldDetailId, contactId, detail, syncTarget, provenance) " "SELECT OD.detailId, OD.contactId, OD.detail, Contacts.syncTarget, CASE WHEN Contacts.syncTarget = 'aggregate' THEN OD.provenance ELSE '' END " "FROM OldDetails AS OD " "JOIN Contacts ON Contacts.contactId = OD.contactId", // Index the indexing table by the detail ID and type name used to select from it "CREATE INDEX DetailsIndexingOldDetailIdIndex ON DetailsIndexing(oldDetailId)", "CREATE INDEX DetailsIndexingDetailIndex ON DetailsIndexing(detail)", // Find the new detail ID for existing provenance ID values "CREATE TEMP TABLE ProvenanceIndexing(" "detailId INTEGER PRIMARY KEY," "detail TEXT," "provenance TEXT," "provenanceContactId TEXT," "provenanceDetailId TEXT," "provenanceSyncTarget TEXT," "newProvenanceDetailId TEXT)", "INSERT INTO ProvenanceIndexing(detailId, detail, provenance) " "SELECT detailId, detail, provenance " "FROM DetailsIndexing " "WHERE provenance != ''", // Calculate the new equivalent form for the existing 'provenance' values "UPDATE ProvenanceIndexing SET " "provenanceContactId = substr(provenance, 0, instr(provenance, ':'))," "provenance = substr(provenance, instr(provenance, ':') + 1)", "UPDATE ProvenanceIndexing SET " "provenanceDetailId = substr(provenance, 0, instr(provenance, ':'))," "provenanceSyncTarget = substr(provenance, instr(provenance, ':') + 1)," "provenance = ''", "REPLACE INTO ProvenanceIndexing (detailId, provenance) " "SELECT PI.detailId, PI.provenanceContactId || ':' || DI.detailId || ':' || PI.provenanceSyncTarget " "FROM ProvenanceIndexing AS PI " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = PI.provenanceDetailId AND DI.detail = PI.detail", // Update the provenance values in the DetailsIndexing table with the updated values "REPLACE INTO DetailsIndexing (detailId, oldDetailId, contactId, detail, syncTarget, provenance) " "SELECT PI.detailId, DI.oldDetailId, DI.contactId, DI.detail, DI.syncTarget, PI.provenance " "FROM ProvenanceIndexing PI " "JOIN DetailsIndexing DI ON DI.detailId = PI.detailId", "DROP TABLE ProvenanceIndexing", // Re-create and populate the Details table from the old version createDetailsTable, "INSERT INTO Details(" "detailId," "contactId," "detail," "detailUri," "linkedDetailUris," "contexts," "accessConstraints," "provenance," "modifiable," "nonexportable) " "SELECT " "DI.detailId," "OD.contactId," "OD.detail," "OD.detailUri," "OD.linkedDetailUris," "OD.contexts," "OD.accessConstraints," "DI.provenance," "OD.modifiable," "OD.nonexportable " "FROM DetailsIndexing AS DI " "JOIN OldDetails AS OD ON OD.detailId = DI.oldDetailId AND OD.detail = DI.detail", "DROP INDEX IF EXISTS DetailsJoinIndex", "DROP INDEX IF EXISTS DetailsRemoveIndex", "DROP TABLE OldDetails", // Drop all indexes for tables we are rebuilding "DROP INDEX IF EXISTS AddressesDetailsContactIdIndex", "DROP INDEX IF EXISTS AnniversariesDetailsContactIdIndex", "DROP INDEX IF EXISTS AvatarsDetailsContactIdIndex", "DROP INDEX IF EXISTS BirthdaysDetailsContactIdIndex", "DROP INDEX IF EXISTS EmailAddressesDetailsContactIdIndex", "DROP INDEX IF EXISTS GlobalPresencesDetailsContactIdIndex", "DROP INDEX IF EXISTS GuidsDetailsContactIdIndex", "DROP INDEX IF EXISTS HobbiesDetailsContactIdIndex", "DROP INDEX IF EXISTS NicknamesDetailsContactIdIndex", "DROP INDEX IF EXISTS NotesDetailsContactIdIndex", "DROP INDEX IF EXISTS OnlineAccountsDetailsContactIdIndex", "DROP INDEX IF EXISTS OrganizationsDetailsContactIdIndex", "DROP INDEX IF EXISTS PhoneNumbersDetailsContactIdIndex", "DROP INDEX IF EXISTS PresencesDetailsContactIdIndex", "DROP INDEX IF EXISTS RingtonesDetailsContactIdIndex", "DROP INDEX IF EXISTS TagsDetailsContactIdIndex", "DROP INDEX IF EXISTS UrlsDetailsContactIdIndex", "DROP INDEX IF EXISTS TpMetadataDetailsContactIdIndex", "DROP INDEX IF EXISTS ExtendedDetailsContactIdIndex", "DROP INDEX IF EXISTS PhoneNumbersIndex", "DROP INDEX IF EXISTS EmailAddressesIndex", "DROP INDEX IF EXISTS OnlineAccountsIndex", "DROP INDEX IF EXISTS NicknamesIndex", "DROP INDEX IF EXISTS TpMetadataTelepathyIdIndex", "DROP INDEX IF EXISTS TpMetadataAccountIdIndex", // Migrate the Addresses table to the new form "ALTER TABLE Addresses RENAME TO OldAddresses", createAddressesTable, "INSERT INTO Addresses(" "detailId," "contactId," "street," "postOfficeBox," "region," "locality," "postCode," "country," "subTypes) " "SELECT " "DI.detailId," "OD.contactId," "OD.street," "OD.postOfficeBox," "OD.region," "OD.locality," "OD.postCode," "OD.country," "OD.subTypes " "FROM OldAddresses AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Address'", "DROP TABLE OldAddresses", // Migrate the Anniversaries table to the new form "ALTER TABLE Anniversaries RENAME TO OldAnniversaries", createAnniversariesTable, "INSERT INTO Anniversaries(" "detailId," "contactId," "originalDateTime," "calendarId," "subType) " "SELECT " "DI.detailId," "OD.contactId," "OD.originalDateTime," "OD.calendarId," "OD.subType " "FROM OldAnniversaries AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Anniversary'", "DROP TABLE OldAnniversaries", // Migrate the Avatars table to the new form "ALTER TABLE Avatars RENAME TO OldAvatars", createAvatarsTable, "INSERT INTO Avatars(" "detailId," "contactId," "imageUrl," "videoUrl," "avatarMetadata) " "SELECT " "DI.detailId," "OD.contactId," "OD.imageUrl," "OD.videoUrl," "OD.avatarMetadata " "FROM OldAvatars AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Avatar'", "DROP TABLE OldAvatars", // Migrate the Birthdays table to the new form "ALTER TABLE Birthdays RENAME TO OldBirthdays", createBirthdaysTable, "INSERT INTO Birthdays(" "detailId," "contactId," "birthday," "calendarId) " "SELECT " "DI.detailId," "OD.contactId," "OD.birthday," "OD.calendarId " "FROM OldBirthdays AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Birthday'", "DROP TABLE OldBirthdays", // Migrate the EmailAddresses table to the new form "ALTER TABLE EmailAddresses RENAME TO OldEmailAddresses", createEmailAddressesTable, "INSERT INTO EmailAddresses(" "detailId," "contactId," "emailAddress," "lowerEmailAddress) " "SELECT " "DI.detailId," "OD.contactId," "OD.emailAddress," "OD.lowerEmailAddress " "FROM OldEmailAddresses AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'EmailAddress'", "DROP TABLE OldEmailAddresses", // Migrate the GlobalPresences table to the new form "ALTER TABLE GlobalPresences RENAME TO OldGlobalPresences", createGlobalPresencesTable, "INSERT INTO GlobalPresences(" "detailId," "contactId," "presenceState," "timestamp," "nickname," "customMessage) " "SELECT " "DI.detailId," "OD.contactId," "OD.presenceState," "OD.timestamp," "OD.nickname," "OD.customMessage " "FROM OldGlobalPresences AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'GlobalPresence'", "DROP TABLE OldGlobalPresences", // Migrate the Guids table to the new form "ALTER TABLE Guids RENAME TO OldGuids", createGuidsTable, "INSERT INTO Guids(" "detailId," "contactId," "guid) " "SELECT " "DI.detailId," "OD.contactId," "OD.guid " "FROM OldGuids AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Guid'", "DROP TABLE OldGuids", // Migrate the Hobbies table to the new form "ALTER TABLE Hobbies RENAME TO OldHobbies", createHobbiesTable, "INSERT INTO Hobbies(" "detailId," "contactId," "hobby) " "SELECT " "DI.detailId," "OD.contactId," "OD.hobby " "FROM OldHobbies AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Hobby'", "DROP TABLE OldHobbies", // Migrate the Nicknames table to the new form "ALTER TABLE Nicknames RENAME TO OldNicknames", createNicknamesTable, "INSERT INTO Nicknames(" "detailId," "contactId," "nickname," "lowerNickname) " "SELECT " "DI.detailId," "OD.contactId," "OD.nickname," "OD.lowerNickname " "FROM OldNicknames AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Nickname'", "DROP TABLE OldNicknames", // Migrate the Notes table to the new form "ALTER TABLE Notes RENAME TO OldNotes", createNotesTable, "INSERT INTO Notes(" "detailId," "contactId," "note) " "SELECT " "DI.detailId," "OD.contactId," "OD.note " "FROM OldNotes AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Note'", "DROP TABLE OldNotes", // Migrate the OnlineAccounts table to the new form "ALTER TABLE OnlineAccounts RENAME TO OldOnlineAccounts", createOnlineAccountsTable, "INSERT INTO OnlineAccounts(" "detailId," "contactId," "accountUri," "lowerAccountUri," "protocol," "serviceProvider," "capabilities," "subTypes," "accountPath," "accountIconPath," "enabled," "accountDisplayName," "serviceProviderDisplayName) " "SELECT " "DI.detailId," "OD.contactId," "OD.accountUri," "OD.lowerAccountUri," "OD.protocol," "OD.serviceProvider," "OD.capabilities," "OD.subTypes," "OD.accountPath," "OD.accountIconPath," "OD.enabled," "OD.accountDisplayName," "OD.serviceProviderDisplayName " "FROM OldOnlineAccounts AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'OnlineAccount'", "DROP TABLE OldOnlineAccounts", // Migrate the Organizations table to the new form "ALTER TABLE Organizations RENAME TO OldOrganizations", createOrganizationsTable, "INSERT INTO Organizations(" "detailId," "contactId," "name," "role," "title," "location," "department," "logoUrl) " "SELECT " "DI.detailId," "OD.contactId," "OD.name," "OD.role," "OD.title," "OD.location," "OD.department," "OD.logoUrl " "FROM OldOrganizations AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Organization'", "DROP TABLE OldOrganizations", // Migrate the PhoneNumbers table to the new form "ALTER TABLE PhoneNumbers RENAME TO OldPhoneNumbers", createPhoneNumbersTable, "INSERT INTO PhoneNumbers(" "detailId," "contactId," "phoneNumber," "subTypes," "normalizedNumber) " "SELECT " "DI.detailId," "OD.contactId," "OD.phoneNumber," "OD.subTypes," "OD.normalizedNumber " "FROM OldPhoneNumbers AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'PhoneNumber'", "DROP TABLE OldPhoneNumbers", // Migrate the Presences table to the new form "ALTER TABLE Presences RENAME TO OldPresences", createPresencesTable, "INSERT INTO Presences(" "detailId," "contactId," "presenceState," "timestamp," "nickname," "customMessage) " "SELECT " "DI.detailId," "OD.contactId," "OD.presenceState," "OD.timestamp," "OD.nickname," "OD.customMessage " "FROM OldPresences AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Presence'", "DROP TABLE OldPresences", // Migrate the Ringtones table to the new form "ALTER TABLE Ringtones RENAME TO OldRingtones", createRingtonesTable, "INSERT INTO Ringtones(" "detailId," "contactId," "audioRingtone," "videoRingtone) " "SELECT " "DI.detailId," "OD.contactId," "OD.audioRingtone," "OD.videoRingtone " "FROM OldRingtones AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Ringtone'", "DROP TABLE OldRingtones", // Migrate the Tags table to the new form "ALTER TABLE Tags RENAME TO OldTags", createTagsTable, "INSERT INTO Tags(" "detailId," "contactId," "tag) " "SELECT " "DI.detailId," "OD.contactId," "OD.tag " "FROM OldTags AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Tag'", "DROP TABLE OldTags", // Migrate the Urls table to the new form "ALTER TABLE Urls RENAME TO OldUrls", createUrlsTable, "INSERT INTO Urls(" "detailId," "contactId," "url," "subTypes) " "SELECT " "DI.detailId," "OD.contactId," "OD.url," "OD.subTypes " "FROM OldUrls AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'Url'", "DROP TABLE OldUrls", // Migrate the TpMetadata table to the new form (and rename it to the correct name) createOriginMetadataTable, "INSERT INTO OriginMetadata(" "detailId," "contactId," "id," "groupId," "enabled) " "SELECT " "DI.detailId," "OD.contactId," "OD.telepathyId," "OD.accountId," "OD.accountEnabled " "FROM TpMetadata AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'OriginMetadata'", "DROP TABLE TpMetadata", // Migrate the ExtendedDetails table to the new form "ALTER TABLE ExtendedDetails RENAME TO OldExtendedDetails", createExtendedDetailsTable, "INSERT INTO ExtendedDetails(" "detailId," "contactId," "name," "data) " "SELECT " "DI.detailId," "OD.contactId," "OD.name," "OD.data " "FROM OldExtendedDetails AS OD " "JOIN DetailsIndexing AS DI ON DI.oldDetailId = OD.detailId AND DI.detail = 'ExtendedDetail'", "DROP TABLE OldExtendedDetails", // Drop the indexing table "DROP INDEX IF EXISTS DetailsIndexingOldDetailIdIndex", "DROP INDEX IF EXISTS DetailsIndexingDetailIndex", "DROP TABLE DetailsIndexing", // Rebuild the indexes we dropped createDetailsRemoveIndex, createPhoneNumbersIndex, createEmailAddressesIndex, createOnlineAccountsIndex, createNicknamesIndex, createOriginMetadataIdIndex, createOriginMetadataGroupIdIndex, // Recreate the remove trigger createRemoveTrigger_11, // Finished "PRAGMA user_version=11", 0 // NULL-terminated }; static const char *upgradeVersion11[] = { createFamiliesTable, createGeoLocationsTable, // Recreate the remove trigger to include these details "DROP TRIGGER RemoveContactDetails", createRemoveTrigger_12, "PRAGMA user_version=12", 0 // NULL-terminated }; static const char *upgradeVersion12[] = { // Preserve the existing state of the Details table "ALTER TABLE Details RENAME TO OldDetails", createDetailsTable, "INSERT INTO Details(" "detailId," "contactId," "detail," "detailUri," "linkedDetailUris," "contexts," "accessConstraints," "provenance," "modifiable," "nonexportable)" "SELECT " "detailId," "contactId," "detail," "detailUri," "linkedDetailUris," "contexts," "accessConstraints," "provenance," "modifiable," "nonexportable " "FROM OldDetails", "DROP TABLE OldDetails", "PRAGMA user_version=13", 0 // NULL-terminated }; static const char *upgradeVersion13[] = { // upgradeVersion12 forgot to recreate this index. // use IF NOT EXISTS for people who worked around by adding it manually "CREATE INDEX IF NOT EXISTS DetailsRemoveIndex ON Details(contactId, detail)", "PRAGMA user_version=14", 0 // NULL-terminated }; static const char *upgradeVersion14[] = { // Drop indexes that will never be used by the query planner once // the ANALYZE data is there. (Boolean indexes can't be selective // enough unless the planner knows which value is more common, // which it doesn't.) "DROP INDEX IF EXISTS ContactsIsDeactivatedIndex", "DROP INDEX IF EXISTS ContactsIsOnlineIndex", "DROP INDEX IF EXISTS ContactsHasOnlineAccountIndex", "DROP INDEX IF EXISTS ContactsHasEmailAddressIndex", "DROP INDEX IF EXISTS ContactsHasPhoneNumberIndex", "DROP INDEX IF EXISTS ContactsIsFavoriteIndex", createAnalyzeData1, createAnalyzeData2, createAnalyzeData3, "PRAGMA user_version=15", 0 // NULL-terminated }; static const char *upgradeVersion15[] = { "ALTER TABLE Anniversaries ADD COLUMN event TEXT", "ALTER TABLE GlobalPresences ADD COLUMN presenceStateText TEXT", "ALTER TABLE GlobalPresences ADD COLUMN presenceStateImageUrl TEXT", "ALTER TABLE Organizations ADD COLUMN assistantName TEXT", "ALTER TABLE Presences ADD COLUMN presenceStateText TEXT", "ALTER TABLE Presences ADD COLUMN presenceStateImageUrl TEXT", "ALTER TABLE Ringtones ADD COLUMN vibrationRingtone TEXT", "PRAGMA user_version=16", 0 // NULL-terminated }; static const char *upgradeVersion16[] = { "PRAGMA user_version=17", 0 // NULL-terminated }; static const char *upgradeVersion17[] = { createDbSettingsTable, "PRAGMA user_version=18", 0 // NULL-terminated }; static const char *upgradeVersion18[] = { "PRAGMA user_version=19", 0 // NULL-terminated }; static const char *upgradeVersion19[] = { "PRAGMA user_version=20", 0 // NULL-terminated }; static const char *upgradeVersion20[] = { // create the collections table and the built-in collections. createCollectionsTable, createCollectionsMetadataTable, createAggregateAddressbookCollection, createLocalAddressbookCollection, // we need to recreate the contacts table // but avoid deleting all detail data // so we drop the trigger and re-create it later. "DROP TRIGGER RemoveContactDetails", // also recreate the deleted contacts table with new schema // sync plugins need to re-sync anyway... "DROP TABLE DeletedContacts", // this table is no longer used. // drop a bunch of indexes which we will need to recreate "DROP INDEX IF EXISTS DetailsRemoveIndex", "DROP INDEX IF EXISTS AddressesDetailsContactIdIndex", "DROP INDEX IF EXISTS AnniversariesDetailsContactIdIndex", "DROP INDEX IF EXISTS AvatarsDetailsContactIdIndex", "DROP INDEX IF EXISTS BirthdaysDetailsContactIdIndex", "DROP INDEX IF EXISTS EmailAddressesDetailsContactIdIndex", "DROP INDEX IF EXISTS FamiliesDetailsContactIdIndex", "DROP INDEX IF EXISTS GeoLocationsDetailsContactIdIndex", "DROP INDEX IF EXISTS GlobalPresencesDetailsContactIdIndex", "DROP INDEX IF EXISTS GuidsDetailsContactIdIndex", "DROP INDEX IF EXISTS HobbiesDetailsContactIdIndex", "DROP INDEX IF EXISTS NicknamesDetailsContactIdIndex", "DROP INDEX IF EXISTS NotesDetailsContactIdIndex", "DROP INDEX IF EXISTS OnlineAccountsDetailsContactIdIndex", "DROP INDEX IF EXISTS OrganizationsDetailsContactIdIndex", "DROP INDEX IF EXISTS PhoneNumbersDetailsContactIdIndex", "DROP INDEX IF EXISTS PresencesDetailsContactIdIndex", "DROP INDEX IF EXISTS RingtonesDetailsContactIdIndex", "DROP INDEX IF EXISTS TagsDetailsContactIdIndex", "DROP INDEX IF EXISTS UrlsDetailsContactIdIndex", "DROP INDEX IF EXISTS OriginMetadataDetailsContactIdIndex", "DROP INDEX IF EXISTS ExtendedDetailsContactIdIndex", "DROP INDEX IF EXISTS PhoneNumbersIndex", "DROP INDEX IF EXISTS EmailAddressesIndex", "DROP INDEX IF EXISTS OnlineAccountsIndex", "DROP INDEX IF EXISTS NicknamesIndex", "DROP INDEX IF EXISTS OriginMetadataIdIndex", "DROP INDEX IF EXISTS OriginMetadataGroupIdIndex", // cannot alter a table to add a foreign key // instead, rename the existing table and recreate it with the foreign key. // we only keep "local" and "aggregate" contacts. "ALTER TABLE Contacts RENAME TO OldContacts", createContactsTable, "INSERT INTO Contacts (" "contactId, " "collectionId, " "created, " "modified, " "deleted, " "hasPhoneNumber, " "hasEmailAddress, " "hasOnlineAccount, " "isOnline, " "isDeactivated, " "changeFlags, " "unhandledChangeFlags, " "type " ") " "SELECT " "OC.contactId, " "CASE " "WHEN OC.syncTarget LIKE '%aggregate%' THEN 1 " // AggregateAddressbookCollectionId "ELSE 2 " // LocalAddressbookCollectionId "END, " "OC.created, " "OC.modified, " "NULL, " // not deleted if it exists currently in the old table. "OC.hasPhoneNumber, " "OC.hasEmailAddress, " "OC.hasOnlineAccount, " "OC.isOnline, " "OC.isDeactivated, " "0, " // no changes recorded currently. "0, " // no unhandled changes recorded currently. "OC.type " "FROM OldContacts AS OC " "WHERE OC.syncTarget IN ('aggregate', 'local', 'was_local')", // Now delete any details of contacts we didn't keep (i.e. not local or aggregate) "DELETE FROM Addresses WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Anniversaries WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Avatars WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Birthdays WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM EmailAddresses WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Families WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM GeoLocations WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM GlobalPresences WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Guids WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Hobbies WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Nicknames WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Notes WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM OnlineAccounts WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Organizations WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM PhoneNumbers WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Presences WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Ringtones WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Tags WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Urls WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM OriginMetadata WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM ExtendedDetails WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Details WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Identities WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Relationships WHERE firstId NOT IN (SELECT contactId FROM Contacts) OR secondId NOT IN (SELECT contactId FROM Contacts)", // add the changeFlags and unhandledChangeFlags columns to the Details table "ALTER TABLE Details ADD COLUMN changeFlags INTEGER DEFAULT 0", "ALTER TABLE Details ADD COLUMN unhandledChangeFlags INTEGER DEFAULT 0", // create the unique-detail tables we added createDisplayLabelsTable, createFavoritesTable, createGendersTable, createNamesTable, createSyncTargetsTable, // and fill them with data from the old contacts table // note: local contacts have no sync target field, so no need to set those. "INSERT INTO Details (contactId, detail) SELECT ContactId, 'DisplayLabel' FROM OldContacts", "INSERT INTO DisplayLabels (detailId, contactId, displayLabel, displayLabelGroup, displayLabelGroupSortOrder)" " SELECT Details.detailId, Details.contactId, displayLabel, displayLabelGroup, displayLabelGroupSortOrder" " FROM Details" " INNER JOIN OldContacts ON OldContacts.contactId = Details.contactId" " WHERE Details.detail = 'DisplayLabel'", "INSERT INTO Details (contactId, detail) SELECT ContactId, 'Favorite' FROM OldContacts WHERE OldContacts.isFavorite NOT NULL", "INSERT INTO Favorites (detailId, contactId, isFavorite)" " SELECT Details.detailId, Details.contactId, isFavorite" " FROM Details" " INNER JOIN OldContacts ON OldContacts.contactId = Details.contactId" " WHERE Details.detail = 'Favorite'", "INSERT INTO Details (contactId, detail) SELECT ContactId, 'Gender' FROM OldContacts WHERE OldContacts.gender NOT NULL", "INSERT INTO Genders (detailId, contactId, gender)" " SELECT Details.detailId, Details.contactId, gender" " FROM Details" " INNER JOIN OldContacts ON OldContacts.contactId = Details.contactId" " WHERE Details.detail = 'Gender'", "INSERT INTO Details (contactId, detail)" " SELECT ContactId, 'Name'" " FROM OldContacts" " WHERE firstName NOT NULL" " OR lastName NOT NULL" " OR middleName NOT NULL" " OR prefix NOT NULL" " OR suffix NOT NULL" " OR customLabel NOT NULL", "INSERT INTO Names (detailId, contactId, firstName, lowerFirstName, lastName, lowerLastName, middleName, prefix, suffix, customLabel)" " SELECT Details.detailId, Details.contactId, firstName, lowerFirstName, lastName, lowerLastName, middleName, prefix, suffix, customLabel" " FROM Details" " INNER JOIN OldContacts ON OldContacts.contactId = Details.contactId" " WHERE Details.detail = 'Name'", // delete the old contacts table "DROP TABLE OldContacts", // we need to regenerate aggregates, but cannot do it via a query. // instead, we do it manually from C++ after the schema upgrade is complete. // we also need to drop and recreate OOB as it will have stale // sync data in it. "DROP TABLE OOB", createOOBTable, // rebuild the indexes we dropped createDetailsRemoveIndex, createPhoneNumbersIndex, createEmailAddressesIndex, createOnlineAccountsIndex, createNicknamesIndex, createOriginMetadataIdIndex, createOriginMetadataGroupIdIndex, // create the new indexes createCollectionsChangeFlagsIndex, createContactsCollectionIdIndex, createContactsChangeFlagsIndex, createDetailsChangeFlagsIndex, createDetailsContactIdIndex, // recreate the remove trigger. createRemoveTrigger_21, "PRAGMA user_version=21", 0 // NULL-terminated }; static const char *upgradeVersion21[] = { // the previous version upgrade could result in aggregates left over which had no constituents // (as all non-local constituents would have been deleted). // delete all of the associated data now. // also delete all synced contacts, require user to resync again. "DELETE FROM Contacts WHERE collectionId NOT IN (1, 2)", // delete synced contacts "DELETE FROM Contacts WHERE contactId IN (SELECT contactId FROM Contacts WHERE collectionId = 1 AND contactId NOT IN (SELECT firstId FROM Relationships))", "DELETE FROM Addresses WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Anniversaries WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Avatars WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Birthdays WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM DisplayLabels WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM EmailAddresses WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Families WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Favorites WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Genders WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM GeoLocations WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM GlobalPresences WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Guids WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Hobbies WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Names WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Nicknames WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Notes WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM OnlineAccounts WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Organizations WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM PhoneNumbers WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Presences WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Ringtones WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Tags WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Urls WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM OriginMetadata WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM ExtendedDetails WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Details WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "DELETE FROM Identities WHERE contactId NOT IN (SELECT contactId FROM Contacts)", "PRAGMA user_version=22", 0 // NULL-terminated }; static const char *upgradeVersion22[] = { // the previous version didn't add the trigger to automatically remove details from the specific // tables when they were removed from the common details table, so remove those stale details now. "DELETE FROM Addresses WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Anniversaries WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Avatars WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Birthdays WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM DisplayLabels WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM EmailAddresses WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Families WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Favorites WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Genders WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM GeoLocations WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM GlobalPresences WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Guids WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Hobbies WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Names WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Nicknames WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Notes WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM OnlineAccounts WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Organizations WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM PhoneNumbers WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Presences WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Ringtones WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM SyncTargets WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Tags WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM Urls WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM OriginMetadata WHERE detailId NOT IN (SELECT detailId FROM Details)", "DELETE FROM ExtendedDetails WHERE detailId NOT IN (SELECT detailId FROM Details)", // recreate the remove details trigger. createRemoveDetailsTrigger_22, "PRAGMA user_version=23", 0 // NULL-terminated }; static const char *upgradeVersion23[] = { "\n ALTER TABLE Details ADD COLUMN created DATETIME", "\n ALTER TABLE Details ADD COLUMN modified DATETIME", "PRAGMA user_version=24", 0 // NULL-terminated }; typedef bool (*UpgradeFunction)(QSqlDatabase &database); struct UpdatePhoneNormalization { quint32 detailId; QString normalizedNumber; }; static bool updateNormalizedNumbers(QSqlDatabase &database) { QList updates; QString statement(QStringLiteral("SELECT detailId, phoneNumber, normalizedNumber FROM PhoneNumbers")); QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } while (query.next()) { const quint32 detailId(query.value(0).value()); const QString number(query.value(1).value()); const QString normalized(query.value(2).value()); const QString currentNormalization(ContactsEngine::normalizedPhoneNumber(number)); if (currentNormalization != normalized) { UpdatePhoneNormalization data = { detailId, currentNormalization }; updates.append(data); } } query.finish(); if (!updates.isEmpty()) { query = QSqlQuery(database); statement = QStringLiteral("UPDATE PhoneNumbers SET normalizedNumber = :normalizedNumber WHERE detailId = :detailId"); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare data upgrade query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const UpdatePhoneNormalization &update, updates) { query.bindValue(":normalizedNumber", update.normalizedNumber); query.bindValue(":detailId", update.detailId); if (!query.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade data: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } query.finish(); } } return true; } struct UpdateAddressStorage { quint32 detailId; QString subTypes; }; struct UpdateAnniversaryStorage { quint32 detailId; int subType; }; struct UpdateGenderStorage { quint32 contactId; int gender; }; struct UpdateOnlineAccountStorage { quint32 detailId; int protocol; QString subTypes; }; struct UpdatePhoneNumberStorage { quint32 detailId; QString subTypes; }; struct UpdateUrlStorage { quint32 detailId; int subType; }; static bool updateStorageTypes(QSqlDatabase &database) { using namespace Conversion; // Where data is stored in the type that corresponds to the representation // used in QtMobility.Contacts, update to match the type used in qtpim { // QContactAddress::subTypes: string list -> int list QList updates; QString statement(QStringLiteral("SELECT detailId, subTypes FROM Addresses WHERE subTypes IS NOT NULL")); QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } while (query.next()) { const quint32 detailId(query.value(0).value()); const QString originalSubTypes(query.value(1).value()); QStringList subTypeNames(originalSubTypes.split(QLatin1Char(';'), QString::SkipEmptyParts)); QStringList subTypeValues; foreach (int subTypeValue, Address::subTypeList(subTypeNames)) { subTypeValues.append(QString::number(subTypeValue)); } UpdateAddressStorage data = { detailId, subTypeValues.join(QLatin1Char(';')) }; updates.append(data); } query.finish(); if (!updates.isEmpty()) { query = QSqlQuery(database); statement = QStringLiteral("UPDATE Addresses SET subTypes = :subTypes WHERE detailId = :detailId"); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare data upgrade query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const UpdateAddressStorage &update, updates) { query.bindValue(":subTypes", update.subTypes); query.bindValue(":detailId", update.detailId); if (!query.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade data: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } query.finish(); } } } { // QContactAnniversary::subType: string -> int QList updates; QString statement(QStringLiteral("SELECT detailId, subType FROM Anniversaries WHERE subType IS NOT NULL")); QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } while (query.next()) { const quint32 detailId(query.value(0).value()); const QString originalSubType(query.value(1).value()); UpdateAnniversaryStorage data = { detailId, Anniversary::subType(originalSubType) }; updates.append(data); } query.finish(); if (!updates.isEmpty()) { query = QSqlQuery(database); statement = QStringLiteral("UPDATE Anniversaries SET subType = :subType WHERE detailId = :detailId"); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare data upgrade query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const UpdateAnniversaryStorage &update, updates) { query.bindValue(":subType", QString::number(update.subType)); query.bindValue(":detailId", update.detailId); if (!query.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade data: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } query.finish(); } } } { // QContactGender::gender: string -> int QList updates; QString statement(QStringLiteral("SELECT contactId, gender FROM Contacts WHERE gender IS NOT NULL")); QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } while (query.next()) { const quint32 contactId(query.value(0).value()); const QString originalGender(query.value(1).value()); // Logic from contactreader: int gender = QContactGender::GenderUnspecified; if (originalGender.startsWith(QChar::fromLatin1('f'), Qt::CaseInsensitive)) { gender = QContactGender::GenderFemale; } else if (originalGender.startsWith(QChar::fromLatin1('m'), Qt::CaseInsensitive)) { gender = QContactGender::GenderMale; } UpdateGenderStorage data = { contactId, gender }; updates.append(data); } query.finish(); if (!updates.isEmpty()) { query = QSqlQuery(database); statement = QStringLiteral("UPDATE Contacts SET gender = :gender WHERE contactId = :contactId"); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare data upgrade query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const UpdateGenderStorage &update, updates) { query.bindValue(":gender", QString::number(update.gender)); query.bindValue(":contactId", update.contactId); if (!query.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade data: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } query.finish(); } } } { // QContactOnlineAccount::protocol: string -> int // QContactOnlineAccount::subTypes: string list -> int list QList updates; QString statement(QStringLiteral("SELECT detailId, protocol, subTypes FROM OnlineAccounts WHERE (protocol IS NOT NULL OR subTypes IS NOT NULL)")); QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } while (query.next()) { const quint32 detailId(query.value(0).value()); const QString originalProtocol(query.value(1).value()); const QString originalSubTypes(query.value(2).value()); QStringList subTypeNames(originalSubTypes.split(QLatin1Char(';'), QString::SkipEmptyParts)); QStringList subTypeValues; foreach (int subTypeValue, OnlineAccount::subTypeList(subTypeNames)) { subTypeValues.append(QString::number(subTypeValue)); } UpdateOnlineAccountStorage data = { detailId, OnlineAccount::protocol(originalProtocol), subTypeValues.join(QLatin1Char(';')) }; updates.append(data); } query.finish(); if (!updates.isEmpty()) { query = QSqlQuery(database); statement = QStringLiteral("UPDATE OnlineAccounts SET protocol = :protocol, subTypes = :subTypes WHERE detailId = :detailId"); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare data upgrade query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const UpdateOnlineAccountStorage &update, updates) { query.bindValue(":protocol", QString::number(update.protocol)); query.bindValue(":subTypes", update.subTypes); query.bindValue(":detailId", update.detailId); if (!query.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade data: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } query.finish(); } } } { // QContactPhoneNumber::subTypes: string list -> int list QList updates; QString statement(QStringLiteral("SELECT detailId, subTypes FROM PhoneNumbers WHERE subTypes IS NOT NULL")); QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } while (query.next()) { const quint32 detailId(query.value(0).value()); const QString originalSubTypes(query.value(1).value()); QStringList subTypeNames(originalSubTypes.split(QLatin1Char(';'), QString::SkipEmptyParts)); QStringList subTypeValues; foreach (int subTypeValue, PhoneNumber::subTypeList(subTypeNames)) { subTypeValues.append(QString::number(subTypeValue)); } UpdatePhoneNumberStorage data = { detailId, subTypeValues.join(QLatin1Char(';')) }; updates.append(data); } query.finish(); if (!updates.isEmpty()) { query = QSqlQuery(database); statement = QStringLiteral("UPDATE PhoneNumbers SET subTypes = :subTypes WHERE detailId = :detailId"); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare data upgrade query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const UpdatePhoneNumberStorage &update, updates) { query.bindValue(":subTypes", update.subTypes); query.bindValue(":detailId", update.detailId); if (!query.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade data: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } query.finish(); } } } { // QContactUrl::subType: string -> int QList updates; QString statement(QStringLiteral("SELECT detailId, subTypes FROM Urls WHERE subTypes IS NOT NULL")); QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } while (query.next()) { const quint32 detailId(query.value(0).value()); const QString originalSubType(query.value(1).value()); UpdateUrlStorage data = { detailId, Url::subType(originalSubType) }; updates.append(data); } query.finish(); if (!updates.isEmpty()) { query = QSqlQuery(database); statement = QStringLiteral("UPDATE Urls SET subTypes = :subTypes WHERE detailId = :detailId"); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare data upgrade query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } foreach (const UpdateUrlStorage &update, updates) { query.bindValue(":subTypes", QString::number(update.subType)); query.bindValue(":detailId", update.detailId); if (!query.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade data: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } query.finish(); } } } return true; } static bool addDisplayLabelGroup(QSqlDatabase &database) { // add the display label group (e.g. ribbon group / name bucket) column { QSqlQuery alterQuery(database); const QString statement = QStringLiteral("ALTER TABLE Contacts ADD COLUMN displayLabelGroup TEXT"); if (!alterQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare add display label group column query: %1\n%2") .arg(alterQuery.lastError().text()) .arg(statement)); return false; } if (!alterQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to add display label group column: %1\n%2") .arg(alterQuery.lastError().text()) .arg(statement)); return false; } alterQuery.finish(); } // add the display label group sort order column (precalculated sort index) { QSqlQuery alterQuery(database); const QString statement = QStringLiteral("ALTER TABLE Contacts ADD COLUMN displayLabelGroupSortOrder INTEGER"); if (!alterQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare add display label group sort order column query: %1\n%2") .arg(alterQuery.lastError().text()) .arg(statement)); return false; } if (!alterQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to add display label group sort order column: %1\n%2") .arg(alterQuery.lastError().text()) .arg(statement)); return false; } alterQuery.finish(); } return true; } static bool forceRegenDisplayLabelGroups(QSqlDatabase &database) { bool settingExists = false; const QString localeName = QLocale().name(); QString targetLocaleName(localeName); { QSqlQuery selectQuery(database); selectQuery.setForwardOnly(true); const QString statement = QStringLiteral("SELECT Value FROM DbSettings WHERE Name = 'LocaleName'"); if (!selectQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare locale setting (regen) selection query: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } if (!selectQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to select locale setting (regen) value: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } if (selectQuery.next()) { settingExists = true; if (selectQuery.value(0).toString() == localeName) { // the locale setting in the database matches the device's locale. // to force regenerating the display label groups, we want to // modify the database setting, to trigger the regeneration codepath. targetLocaleName = localeName == QStringLiteral("en_GB") ? QStringLiteral("fi_FI") : QStringLiteral("en_GB"); } } } if (settingExists) { QSqlQuery setLocaleQuery(database); const QString statement = settingExists ? QStringLiteral("UPDATE DbSettings SET Value = ? WHERE Name = 'LocaleName'") : QStringLiteral("INSERT INTO DbSettings (Name, Value) VALUES ('LocaleName', ?)"); if (!setLocaleQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare locale setting update (regen) query: %1\n%2") .arg(setLocaleQuery.lastError().text()) .arg(statement)); return false; } setLocaleQuery.addBindValue(QVariant(targetLocaleName)); if (!setLocaleQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to update locale setting (regen) value: %1\n%2") .arg(setLocaleQuery.lastError().text()) .arg(statement)); return false; } } return true; } struct UpgradeOperation { UpgradeFunction fn; const char **statements; }; static UpgradeOperation upgradeVersions[] = { { 0, upgradeVersion0 }, { 0, upgradeVersion1 }, { 0, upgradeVersion2 }, { 0, upgradeVersion3 }, { 0, upgradeVersion4 }, { 0, upgradeVersion5 }, { 0, upgradeVersion6 }, { updateNormalizedNumbers, upgradeVersion7 }, { 0, upgradeVersion8 }, { 0, upgradeVersion9 }, { 0, upgradeVersion10 }, { 0, upgradeVersion11 }, { 0, upgradeVersion12 }, { 0, upgradeVersion13 }, { 0, upgradeVersion14 }, { 0, upgradeVersion15 }, { updateStorageTypes, upgradeVersion16 }, { addDisplayLabelGroup, upgradeVersion17 }, { forceRegenDisplayLabelGroups, upgradeVersion18 }, { forceRegenDisplayLabelGroups, upgradeVersion19 }, { 0, upgradeVersion20 }, { 0, upgradeVersion21 }, { 0, upgradeVersion22 }, { 0, upgradeVersion23 }, }; static const int currentSchemaVersion = 24; static bool execute(QSqlDatabase &database, const QString &statement) { QSqlQuery query(database); if (!query.exec(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Query failed: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return false; } else { return true; } } static bool beginTransaction(QSqlDatabase &database) { // Use immediate lock acquisition; we should already have an IPC lock, so // there will be no lock contention with other writing processes return execute(database, QStringLiteral("BEGIN IMMEDIATE TRANSACTION")); } static bool commitTransaction(QSqlDatabase &database) { return execute(database, QStringLiteral("COMMIT TRANSACTION")); } static bool rollbackTransaction(QSqlDatabase &database) { return execute(database, QStringLiteral("ROLLBACK TRANSACTION")); } static bool finalizeTransaction(QSqlDatabase &database, bool success) { if (success) { return commitTransaction(database); } rollbackTransaction(database); return false; } template static int lengthOf(T) { return 0; } template static int lengthOf(const T(&)[N]) { return N; } static bool executeDisplayLabelGroupLocalizationStatements(QSqlDatabase &database, ContactsDatabase *cdb, bool *changed = Q_NULLPTR) { // determine if the current system locale is equal to that used for the display label groups. // if not, update them all. bool sameLocale = false; bool settingExists = false; const QString localeName = QLocale().name(); { QSqlQuery selectQuery(database); selectQuery.setForwardOnly(true); const QString statement = QStringLiteral("SELECT Value FROM DbSettings WHERE Name = 'LocaleName'"); if (!selectQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare locale setting selection query: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } if (!selectQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to select locale setting value: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } if (selectQuery.next()) { settingExists = true; if (selectQuery.value(0).toString() == localeName) { sameLocale = true; // no need to update the display label groups due to locale. } } } // update the database settings with the current locale name if needed. if (!sameLocale) { QSqlQuery setLocaleQuery(database); const QString statement = settingExists ? QStringLiteral("UPDATE DbSettings SET Value = ? WHERE Name = 'LocaleName'") : QStringLiteral("INSERT INTO DbSettings (Name, Value) VALUES ('LocaleName', ?)"); if (!setLocaleQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare locale setting update query: %1\n%2") .arg(setLocaleQuery.lastError().text()) .arg(statement)); return false; } setLocaleQuery.addBindValue(QVariant(localeName)); if (!setLocaleQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to update locale setting value: %1\n%2") .arg(setLocaleQuery.lastError().text()) .arg(statement)); return false; } } #ifndef HAS_MLITE bool sameGroupProperty = true; #else // also determine if the current system setting for deriving the group from the first vs last // name is the same since the display label groups were generated. // if not, update them all. bool sameGroupProperty = false; const QString groupProperty = cdb->displayLabelGroupPreferredProperty(); { QSqlQuery selectQuery(database); selectQuery.setForwardOnly(true); const QString statement = QStringLiteral("SELECT Value FROM DbSettings WHERE Name = 'GroupProperty'"); if (!selectQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare group property setting selection query: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } if (!selectQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to select group property setting value: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } if (selectQuery.next()) { settingExists = true; if (selectQuery.value(0).toString() == groupProperty) { sameGroupProperty = true; // no need to update the display label groups due to group property. } } } // update the database settings with the current group property name if needed. if (!sameGroupProperty) { QSqlQuery setGroupPropertyQuery(database); const QString statement = settingExists ? QStringLiteral("UPDATE DbSettings SET Value = ? WHERE Name = 'GroupProperty'") : QStringLiteral("INSERT INTO DbSettings (Name, Value) VALUES ('GroupProperty', ?)"); if (!setGroupPropertyQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare group property setting update query: %1\n%2") .arg(setGroupPropertyQuery.lastError().text()) .arg(statement)); return false; } setGroupPropertyQuery.addBindValue(QVariant(groupProperty)); if (!setGroupPropertyQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to update group property setting value: %1\n%2") .arg(setGroupPropertyQuery.lastError().text()) .arg(statement)); return false; } } #endif // HAS_MLITE if (sameLocale && sameGroupProperty) { // no need to update the previously generated display label groups. if (changed) *changed = false; return true; } else { if (changed) *changed = true; } // for every single contact in our database, read the data required to generate the display label group data. bool emitDisplayLabelGroupChange = false; QVariantList contactIds; QVariantList displayLabelGroups; QVariantList displayLabelGroupSortOrders; { QSqlQuery selectQuery(database); selectQuery.setForwardOnly(true); const QString statement = QStringLiteral( " SELECT c.contactId, n.firstName, n.lastName, d.displayLabel" " FROM Contacts c" " LEFT JOIN Names n ON c.contactId = n.contactId" " LEFT JOIN DisplayLabels d ON c.contactId = d.contactId"); if (!selectQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare display label groups data selection query: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } if (!selectQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to select display label groups data: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return false; } while (selectQuery.next()) { const quint32 dbId = selectQuery.value(0).toUInt(); const QString firstName = selectQuery.value(1).toString(); const QString lastName = selectQuery.value(2).toString(); const QString displayLabel = selectQuery.value(3).toString(); contactIds.append(dbId); QContactName n; n.setFirstName(firstName); n.setLastName(lastName); QContactDisplayLabel dl; dl.setLabel(displayLabel); QContact c; c.saveDetail(&n); c.saveDetail(&dl); const QString dlg = cdb->determineDisplayLabelGroup(c, &emitDisplayLabelGroupChange); displayLabelGroups.append(dlg); displayLabelGroupSortOrders.append(cdb->displayLabelGroupSortValue(dlg)); } selectQuery.finish(); } // now write the generated data back to the database. // do it in batches, otherwise it can fail if any single batch is too big. { for (int i = 0; i < displayLabelGroups.size(); i += 167) { const QVariantList groups = displayLabelGroups.mid(i, qMin(displayLabelGroups.size() - i, 167)); const QVariantList sortorders = displayLabelGroupSortOrders.mid(i, qMin(displayLabelGroups.size() - i, 167)); const QVariantList ids = contactIds.mid(i, qMin(displayLabelGroups.size() - i, 167)); QSqlQuery updateQuery(database); const QString statement = QStringLiteral("UPDATE DisplayLabels SET displayLabelGroup = ?, displayLabelGroupSortOrder = ? WHERE contactId = ?"); if (!updateQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare update display label groups query: %1\n%2") .arg(updateQuery.lastError().text()) .arg(statement)); return false; } updateQuery.addBindValue(groups); updateQuery.addBindValue(sortorders); updateQuery.addBindValue(ids); if (!updateQuery.execBatch()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to update display label groups: %1\n%2") .arg(updateQuery.lastError().text()) .arg(statement)); return false; } updateQuery.finish(); } } return true; } static bool executeUpgradeStatements(QSqlDatabase &database) { // Check that the defined schema matches the array of upgrade scripts if (currentSchemaVersion != lengthOf(upgradeVersions)) { qWarning() << "Invalid schema version:" << currentSchemaVersion; return false; } QSqlQuery versionQuery(database); versionQuery.prepare("PRAGMA user_version"); if (!versionQuery.exec() || !versionQuery.next()) { qWarning() << "User version query failed:" << versionQuery.lastError(); return false; } int schemaVersion = versionQuery.value(0).toInt(); versionQuery.finish(); while (schemaVersion < currentSchemaVersion) { qWarning() << "Upgrading contacts database from schema version" << schemaVersion; if (upgradeVersions[schemaVersion].fn) { if (!(*upgradeVersions[schemaVersion].fn)(database)) { qWarning() << "Unable to update data for schema version" << schemaVersion; return false; } } if (upgradeVersions[schemaVersion].statements) { for (unsigned i = 0; upgradeVersions[schemaVersion].statements[i]; i++) { if (!execute(database, QLatin1String(upgradeVersions[schemaVersion].statements[i]))) return false; } } if (!versionQuery.exec() || !versionQuery.next()) { qWarning() << "User version query failed:" << versionQuery.lastError(); return false; } int version = versionQuery.value(0).toInt(); versionQuery.finish(); if (version <= schemaVersion) { qWarning() << "Contacts database schema upgrade cycle detected - aborting"; return false; } else { schemaVersion = version; if (schemaVersion == currentSchemaVersion) { qWarning() << "Contacts database upgraded to version" << schemaVersion; } } } if (schemaVersion > currentSchemaVersion) { qWarning() << "Contacts database schema is newer than expected - this may result in failures or corruption"; } return true; } static bool checkDatabase(QSqlDatabase &database) { QSqlQuery query(database); if (query.exec(QStringLiteral("PRAGMA quick_check"))) { while (query.next()) { const QString result(query.value(0).toString()); if (result == QStringLiteral("ok")) { return true; } qWarning() << "Integrity problem:" << result; } } return false; } static bool upgradeDatabase(QSqlDatabase &database, ContactsDatabase *cdb) { if (!beginTransaction(database)) return false; bool success = executeUpgradeStatements(database); if (success) { success = executeDisplayLabelGroupLocalizationStatements(database, cdb); } return finalizeTransaction(database, success); } static bool configureDatabase(QSqlDatabase &database, QString &localeName) { #ifdef QTCONTACTS_SQLITE_LOAD_ICU // Load the ICU extension QVariant v = database.driver()->handle(); if (v.isValid()) { // v.data() returns a pointer to the handle sqlite3 *handle = *static_cast(v.data()); if (handle) { sqlite3_enable_load_extension(handle, 1); char *err = nullptr; int rc = sqlite3_load_extension(handle, "libSqliteIcu", "sqlite3_icu_init", &err); if (rc != SQLITE_OK) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to load ICU extension: %1") .arg(err)); sqlite3_free(err); } } } #endif if (!execute(database, QLatin1String(setupEncoding)) || !execute(database, QLatin1String(setupTempStore)) || !execute(database, QLatin1String(setupJournal)) || !execute(database, QLatin1String(setupSynchronous))) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to configure contacts database: %1") .arg(database.lastError().text())); return false; } else { const QString cLocaleName(QStringLiteral("C")); if (localeName != cLocaleName) { // Create a collation for sorting by the current locale const QString statement(QStringLiteral("SELECT icu_load_collation('%1', 'localeCollation')")); if (!execute(database, statement.arg(localeName))) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to configure collation for locale %1: %2") .arg(localeName).arg(database.lastError().text())); // Revert to using C locale for sorting localeName = cLocaleName; } } } return true; } static bool executeCreationStatements(QSqlDatabase &database) { for (int i = 0; i < lengthOf(createStatements); ++i) { QSqlQuery query(database); if (!query.exec(QLatin1String(createStatements[i]))) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Database creation failed: %1\n%2") .arg(query.lastError().text()) .arg(createStatements[i])); return false; } } if (!execute(database, QStringLiteral("PRAGMA user_version=%1").arg(currentSchemaVersion))) { return false; } return true; } static bool executeBuiltInCollectionsStatements(QSqlDatabase &database, const bool aggregating) { const char *createStatements[] = { createLocalAddressbookCollection, 0 }; const char *aggregatingCreateStatements[] = { createAggregateAddressbookCollection, createLocalAddressbookCollection, 0 }; const char **statement = (aggregating ? aggregatingCreateStatements : createStatements); for ( ; *statement != 0; ++statement) { QSqlQuery query(database); if (!query.exec(QString::fromLatin1(*statement))) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Create built-in collection query failed: %1\n%2") .arg(query.lastError().text()) .arg(*statement)); return false; } } return true; } static bool executeSelfContactStatements(QSqlDatabase &database, const bool aggregating) { const char *createStatements[] = { createSelfContact, 0 }; const char *aggregatingCreateStatements[] = { createLocalSelfContact, createAggregateSelfContact, createSelfContactRelationship, 0 }; const char **statement = (aggregating ? aggregatingCreateStatements : createStatements); for ( ; *statement != 0; ++statement) { QSqlQuery query(database); if (!query.exec(QString::fromLatin1(*statement))) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Create self contact query failed: %1\n%2") .arg(query.lastError().text()) .arg(*statement)); return false; } } return true; } static bool prepareDatabase(QSqlDatabase &database, ContactsDatabase *cdb, const bool aggregating, QString &localeName) { if (!configureDatabase(database, localeName)) return false; if (!beginTransaction(database)) return false; bool success = executeCreationStatements(database); if (success) { success = executeBuiltInCollectionsStatements(database, aggregating); } if (success) { success = executeSelfContactStatements(database, aggregating); } if (success) { success = executeDisplayLabelGroupLocalizationStatements(database, cdb); } return finalizeTransaction(database, success); } template static void debugFilterExpansion(const QString &description, const QString &query, const ValueContainer &bindings) { static const bool debugFilters = !qgetenv("QTCONTACTS_SQLITE_DEBUG_FILTERS").isEmpty(); if (debugFilters) { qDebug() << description << ContactsDatabase::expandQuery(query, bindings); } } static void bindValues(QSqlQuery &query, const QVariantList &values) { for (int i = 0; i < values.count(); ++i) { query.bindValue(i, values.at(i)); } } static void bindValues(ContactsDatabase::Query &query, const QMap &values) { QMap::const_iterator it = values.constBegin(), end = values.constEnd(); for ( ; it != end; ++it) { query.bindValue(it.key(), it.value()); } } static bool countTransientTables(ContactsDatabase &, QSqlDatabase &db, const QString &table, int *count) { static const QString sql(QStringLiteral("SELECT COUNT(*) FROM sqlite_temp_master WHERE type = 'table' and name LIKE '%1_transient%'")); *count = 0; QSqlQuery query(db); if (!query.prepare(sql.arg(table)) || !ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to count transient tables for table: %1").arg(table)); return false; } else while (query.next()) { *count = query.value(0).toInt(); } return true; } static bool findTransientTables(ContactsDatabase &, QSqlDatabase &db, const QString &table, QStringList *tableNames) { static const QString sql(QStringLiteral("SELECT name FROM sqlite_temp_master WHERE type = 'table' and name LIKE '%1_transient%'")); QSqlQuery query(db); if (!query.prepare(sql.arg(table)) || !ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to query transient tables for table: %1").arg(table)); return false; } else while (query.next()) { tableNames->append(query.value(0).toString()); } return true; } static bool dropTransientTables(ContactsDatabase &cdb, QSqlDatabase &db, const QString &table) { static const QString dropTableStatement = QStringLiteral("DROP TABLE temp.%1"); QStringList tableNames; if (!findTransientTables(cdb, db, table, &tableNames)) return false; foreach (const QString tableName, tableNames) { QSqlQuery dropTableQuery(db); const QString dropStatement(dropTableStatement.arg(tableName)); if (!dropTableQuery.prepare(dropStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare drop transient table query: %1\n%2") .arg(dropTableQuery.lastError().text()) .arg(dropStatement)); return false; } if (!ContactsDatabase::execute(dropTableQuery)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to drop transient temporary table: %1\n%2") .arg(dropTableQuery.lastError().text()) .arg(dropStatement)); return false; } } return true; } template bool createTemporaryContactIdsTable(ContactsDatabase &cdb, QSqlDatabase &, const QString &table, bool filter, const QVariantList &boundIds, const QString &join, const QString &where, const QString &orderBy, const ValueContainer &boundValues, int limit) { static const QString createStatement(QStringLiteral("CREATE TABLE IF NOT EXISTS temp.%1 (contactId INTEGER)")); static const QString insertFilterStatement(QStringLiteral("INSERT INTO temp.%1 (contactId) SELECT Contacts.contactId FROM Contacts %2 %3")); // Create the temporary table (if we haven't already). { ContactsDatabase::Query tableQuery(cdb.prepare(createStatement.arg(table))); if (!ContactsDatabase::execute(tableQuery)) { tableQuery.reportError(QString::fromLatin1("Failed to create temporary contact ids table %1").arg(table)); return false; } } // insert into the temporary table, all of the ids // which will be specified either by id list, or by filter. if (filter) { // specified by filter QString insertStatement = insertFilterStatement.arg(table).arg(join).arg(where); if (!orderBy.isEmpty()) { insertStatement.append(QStringLiteral(" ORDER BY ") + orderBy); } if (limit > 0) { insertStatement.append(QStringLiteral(" LIMIT %1").arg(limit)); } ContactsDatabase::Query insertQuery(cdb.prepare(insertStatement)); bindValues(insertQuery, boundValues); if (!ContactsDatabase::execute(insertQuery)) { insertQuery.reportError(QString::fromLatin1("Failed to insert temporary contact ids into table %1").arg(table)); return false; } else { debugFilterExpansion("Contacts selection:", insertStatement, boundValues); } } else { // specified by id list // NOTE: we must preserve the order of the bound ids being // inserted (to match the order of the input list), so that // the result of queryContacts() is ordered according to the // order of input ids. if (!boundIds.isEmpty()) { QVariantList::const_iterator it = boundIds.constBegin(), end = boundIds.constEnd(); if ((limit > 0) && (limit < boundIds.count())) { end = it + limit; } while (it != end) { // SQLite allows up to 500 rows per insert quint32 remainder = (end - it); QVariantList::const_iterator batchEnd = it + std::min(remainder, 500); const QString insertStatement = QStringLiteral("INSERT INTO temp.%1 (contactId) VALUES (:contactId)").arg(table); ContactsDatabase::Query insertQuery(cdb.prepare(insertStatement)); QVariantList cids; while (true) { const QVariant &v(*it); const quint32 dbId(v.value()); cids.append(dbId); if (++it == batchEnd) { break; } } insertQuery.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(insertQuery)) { insertQuery.reportError(QString::fromLatin1("Failed to insert temporary contact ids list into table %1").arg(table)); return false; } } } } return true; } void dropOrDeleteTable(ContactsDatabase &cdb, QSqlDatabase &db, const QString &table) { const QString dropTableStatement = QStringLiteral("DROP TABLE IF EXISTS temp.%1").arg(table); ContactsDatabase::Query dropTableQuery(cdb.prepare(dropTableStatement)); if (!ContactsDatabase::execute(dropTableQuery)) { // couldn't drop the table, just delete all entries instead. QSqlQuery deleteRecordsQuery(db); const QString deleteRecordsStatement = QStringLiteral("DELETE FROM temp.%1").arg(table); if (!deleteRecordsQuery.prepare(deleteRecordsStatement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare delete records query - the next query may return spurious results: %1\n%2") .arg(deleteRecordsQuery.lastError().text()) .arg(deleteRecordsStatement)); } if (!ContactsDatabase::execute(deleteRecordsQuery)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to delete temporary records - the next query may return spurious results: %1\n%2") .arg(deleteRecordsQuery.lastError().text()) .arg(deleteRecordsStatement)); } } } void clearTemporaryContactIdsTable(ContactsDatabase &cdb, QSqlDatabase &db, const QString &table) { // Drop any transient tables associated with this table dropTransientTables(cdb, db, table); dropOrDeleteTable(cdb, db, table); } bool createTemporaryContactTimestampTable(ContactsDatabase &cdb, QSqlDatabase &, const QString &table, const QList > &values) { static const QString createStatement(QStringLiteral("CREATE TABLE IF NOT EXISTS temp.%1 (" "contactId INTEGER PRIMARY KEY ASC," "modified DATETIME" ")")); // Create the temporary table (if we haven't already). { ContactsDatabase::Query tableQuery(cdb.prepare(createStatement.arg(table))); if (!ContactsDatabase::execute(tableQuery)) { tableQuery.reportError(QString::fromLatin1("Failed to create temporary contact timestamp table %1").arg(table)); return false; } } // insert into the temporary table, all of the values if (!values.isEmpty()) { QList >::const_iterator it = values.constBegin(), end = values.constEnd(); while (it != end) { // SQLite/QtSql limits the amount of data we can insert per individual query quint32 first = (it - values.constBegin()); quint32 remainder = (end - it); quint32 count = std::min(remainder, 250); QList >::const_iterator batchEnd = it + count; QString insertStatement = QStringLiteral("INSERT INTO temp.%1 (contactId, modified) VALUES ").arg(table); while (true) { insertStatement.append(QStringLiteral("(?,?)")); if (++it == batchEnd) { break; } else { insertStatement.append(QStringLiteral(",")); } } ContactsDatabase::Query insertQuery(cdb.prepare(insertStatement)); QList >::const_iterator vit = values.constBegin() + first, vend = vit + count; while (vit != vend) { const QPair &pair(*vit); ++vit; insertQuery.addBindValue(QVariant(pair.first)); insertQuery.addBindValue(QVariant(pair.second)); } if (!ContactsDatabase::execute(insertQuery)) { insertQuery.reportError(QString::fromLatin1("Failed to insert temporary contact timestamp values into table %1").arg(table)); return false; } } } return true; } void clearTemporaryContactTimestampTable(ContactsDatabase &cdb, QSqlDatabase &db, const QString &table) { dropOrDeleteTable(cdb, db, table); } bool createTemporaryContactPresenceTable(ContactsDatabase &cdb, QSqlDatabase &, const QString &table, const QList > &values) { static const QString createStatement(QStringLiteral("CREATE TABLE IF NOT EXISTS temp.%1 (" "contactId INTEGER PRIMARY KEY ASC," "presenceState INTEGER," "isOnline BOOL" ")")); // Create the temporary table (if we haven't already). { ContactsDatabase::Query tableQuery(cdb.prepare(createStatement.arg(table))); if (!ContactsDatabase::execute(tableQuery)) { tableQuery.reportError(QString::fromLatin1("Failed to create temporary contact presence table %1").arg(table)); return false; } } // insert into the temporary table, all of the values if (!values.isEmpty()) { QList >::const_iterator it = values.constBegin(), end = values.constEnd(); while (it != end) { // SQLite/QtSql limits the amount of data we can insert per individual query quint32 first = (it - values.constBegin()); quint32 remainder = (end - it); quint32 count = std::min(remainder, 167); QList >::const_iterator batchEnd = it + count; QString insertStatement = QStringLiteral("INSERT INTO temp.%1 (contactId, presenceState, isOnline) VALUES ").arg(table); while (true) { insertStatement.append(QStringLiteral("(?,?,?)")); if (++it == batchEnd) { break; } else { insertStatement.append(QStringLiteral(",")); } } ContactsDatabase::Query insertQuery(cdb.prepare(insertStatement)); QList >::const_iterator vit = values.constBegin() + first, vend = vit + count; while (vit != vend) { const QPair &pair(*vit); ++vit; insertQuery.addBindValue(QVariant(pair.first)); const int state(pair.second); insertQuery.addBindValue(QVariant(state)); insertQuery.addBindValue(QVariant(state >= QContactPresence::PresenceAvailable && state <= QContactPresence::PresenceExtendedAway)); } if (!ContactsDatabase::execute(insertQuery)) { insertQuery.reportError(QString::fromLatin1("Failed to insert temporary contact presence values into table %1").arg(table)); return false; } } } return true; } void clearTemporaryContactPresenceTable(ContactsDatabase &cdb, QSqlDatabase &db, const QString &table) { dropOrDeleteTable(cdb, db, table); } bool createTemporaryValuesTable(ContactsDatabase &cdb, QSqlDatabase &, const QString &table, const QVariantList &values) { static const QString createStatement(QStringLiteral("CREATE TABLE IF NOT EXISTS temp.%1 (value BLOB)")); // Create the temporary table (if we haven't already). { ContactsDatabase::Query tableQuery(cdb.prepare(createStatement.arg(table))); if (!ContactsDatabase::execute(tableQuery)) { tableQuery.reportError(QString::fromLatin1("Failed to create temporary table %1").arg(table)); return false; } } // insert into the temporary table, all of the values if (!values.isEmpty()) { QVariantList::const_iterator it = values.constBegin(), end = values.constEnd(); while (it != end) { // SQLite/QtSql limits the amount of data we can insert per individual query quint32 first = (it - values.constBegin()); quint32 remainder = (end - it); quint32 count = std::min(remainder, 500); QVariantList::const_iterator batchEnd = it + count; QString insertStatement = QStringLiteral("INSERT INTO temp.%1 (value) VALUES ").arg(table); while (true) { insertStatement.append(QStringLiteral("(?)")); if (++it == batchEnd) { break; } else { insertStatement.append(QStringLiteral(",")); } } ContactsDatabase::Query insertQuery(cdb.prepare(insertStatement)); foreach (const QVariant &v, values.mid(first, count)) { insertQuery.addBindValue(v); } if (!ContactsDatabase::execute(insertQuery)) { insertQuery.reportError(QString::fromLatin1("Failed to insert temporary values into table %1").arg(table)); return false; } } } return true; } void clearTemporaryValuesTable(ContactsDatabase &cdb, QSqlDatabase &db, const QString &table) { dropOrDeleteTable(cdb, db, table); } static bool createTransientContactIdsTable(ContactsDatabase &cdb, QSqlDatabase &db, const QString &table, const QVariantList &ids, QString *transientTableName) { static const QString createTableStatement(QStringLiteral("CREATE TABLE %1 (contactId INTEGER)")); static const QString insertIdsStatement(QStringLiteral("INSERT INTO %1 (contactId) VALUES(:contactId)")); int existingTables = 0; if (!countTransientTables(cdb, db, table, &existingTables)) return false; const QString tableName(QStringLiteral("temp.%1_transient%2").arg(table).arg(existingTables)); // Create the transient table (if we haven't already). { ContactsDatabase::Query tableQuery(cdb.prepare(createTableStatement.arg(tableName))); if (!ContactsDatabase::execute(tableQuery)) { tableQuery.reportError(QString::fromLatin1("Failed to create transient table %1").arg(table)); return false; } } // insert into the transient table, all of the values QVariantList::const_iterator it = ids.constBegin(), end = ids.constEnd(); while (it != end) { // SQLite allows up to 500 rows per insert quint32 remainder = (end - it); QVariantList::const_iterator batchEnd = it + std::min(remainder, 500); ContactsDatabase::Query insertQuery(cdb.prepare(insertIdsStatement.arg(tableName))); QVariantList cids; while (true) { const QVariant &v(*it); const quint32 dbId(v.value()); cids.append(dbId); if (++it == batchEnd) { break; } } insertQuery.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(insertQuery)) { insertQuery.reportError(QString::fromLatin1("Failed to insert transient contact ids into table %1").arg(table)); return false; } } *transientTableName = tableName; return true; } static const int initialSemaphoreValues[] = { 1, 0, 1 }; static size_t databaseOwnershipIndex = 0; static size_t databaseConnectionsIndex = 1; static size_t writeAccessIndex = 2; static QVector initializeDisplayLabelGroupGenerators() { QVector generators; QByteArray pluginsPathEnv = qgetenv("QTCONTACTS_SQLITE_PLUGIN_PATH"); const QString pluginsPath = pluginsPathEnv.isEmpty() ? CONTACTS_DATABASE_PATH : QString::fromUtf8(pluginsPathEnv); QDir pluginDir(pluginsPath); const QStringList pluginNames = pluginDir.entryList(); for (const QString &plugin : pluginNames) { if (plugin.endsWith(QStringLiteral(".so"))) { QPluginLoader loader(pluginsPath + plugin); QtContactsSqliteExtensions::DisplayLabelGroupGenerator *generator = qobject_cast(loader.instance()); bool inserted = false; const int prio = generator->priority(); for (int i = 0; i < generators.size(); ++i) { if (generators.at(i)->priority() < prio) { generators.insert(i, generator); inserted = true; break; } } if (!inserted) { generators.append(generator); } } } return generators; } static QVector s_dlgGenerators = initializeDisplayLabelGroupGenerators(); static qint32 displayLabelGroupSortValue(const QString &group, const QMap &knownDisplayLabelGroups) { static const int maxUnicodeCodePointValue = 1114111; // 0x10FFFF static const int numberGroupSortValue = maxUnicodeCodePointValue + 1; static const int otherGroupSortValue = numberGroupSortValue + 1; qint32 retn = -1; if (!group.isEmpty()) { retn = group == QStringLiteral("#") ? numberGroupSortValue : (group == QStringLiteral("?") ? otherGroupSortValue : knownDisplayLabelGroups.value(group, -1)); if (retn < 0) { // the group is not a previously-known display label group. // convert the group to a utf32 code point value. const QChar first = group.at(0); if (first.isSurrogate()) { if (group.size() >= 2) { const QChar second = group.at(1); retn = ((first.isHighSurrogate() ? first.unicode() : second.unicode() - 0xD800) * 0x400) + (second.isLowSurrogate() ? second.unicode() : first.unicode() - 0xDC00) + 0x10000; } else { // cannot calculate the true codepoint without the second character in the surrogate pair. // assume that it's the very last possible codepoint. retn = maxUnicodeCodePointValue; } } else { // use the unicode code point value as the sort value. retn = first.unicode(); // resolve overlap issue by compressing overlapping groups // into a single subsequent group. // e.g. in Chinese locale, there may be more than // 65 default display label groups, and thus the // letter 'A' (whose unicode value is 65) would overlap. int lastContiguousSortValue = -1; for (const int sortValue : knownDisplayLabelGroups) { if (sortValue != (lastContiguousSortValue + 1)) { break; } lastContiguousSortValue = sortValue; } // instead of placing into LCSV+1, we place into LCSV+2 // to ensure that ALL overlapping groups are compressed // into the same group, in order to avoid "first seen // will sort first" issues (e.g. B < A). const int compressedSortValue = lastContiguousSortValue + 2; if (retn < compressedSortValue) { retn = compressedSortValue; } } } } return retn; } // Adapted from the inter-process mutex in QMF // The first user creates the semaphore that all subsequent instances // attach to. We rely on undo semantics to release locked semaphores // on process failure. ContactsDatabase::ProcessMutex::ProcessMutex(const QString &path) : m_semaphore(path.toLatin1(), 3, initialSemaphoreValues) , m_initialProcess(false) { if (!m_semaphore.isValid()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to create semaphore array!")); } else { if (!m_semaphore.decrement(databaseOwnershipIndex)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to determine database ownership!")); } else { // Only the first process to connect to the semaphore is the owner m_initialProcess = (m_semaphore.value(databaseConnectionsIndex) == 0); if (!m_semaphore.increment(databaseConnectionsIndex)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to increment database connections!")); } m_semaphore.increment(databaseOwnershipIndex); } } } bool ContactsDatabase::ProcessMutex::lock() { return m_semaphore.decrement(writeAccessIndex); } bool ContactsDatabase::ProcessMutex::unlock() { return m_semaphore.increment(writeAccessIndex); } bool ContactsDatabase::ProcessMutex::isLocked() const { return (m_semaphore.value(writeAccessIndex) == 0); } bool ContactsDatabase::ProcessMutex::isInitialProcess() const { return m_initialProcess; } ContactsDatabase::Query::Query(const QSqlQuery &query) : m_query(query) { } void ContactsDatabase::Query::reportError(const QString &text) const { QString output(text + QStringLiteral("\n%1").arg(m_query.lastError().text())); QTCONTACTS_SQLITE_WARNING(output); } void ContactsDatabase::Query::reportError(const char *text) const { reportError(QString::fromLatin1(text)); } ContactsDatabase::ContactsDatabase(ContactsEngine *engine) : m_engine(engine) , m_mutex(QMutex::Recursive) , m_nonprivileged(false) , m_autoTest(false) , m_localeName(QLocale().name()) , m_defaultGenerator(new DefaultDlgGenerator) #ifdef HAS_MLITE , m_groupPropertyConf(QStringLiteral("/org/nemomobile/contacts/group_property")) #endif // HAS_MLITE { #ifdef HAS_MLITE QObject::connect(&m_groupPropertyConf, &MGConfItem::valueChanged, [this, engine] { this->regenerateDisplayLabelGroups(); // expensive, but if we don't do it, in multi-process case some clients may not get updated... // if contacts backend were daemonised, this problem would go away... // Emit some engine signals asynchronously. QMetaObject::invokeMethod(engine, "_q_displayLabelGroupsChanged", Qt::QueuedConnection); QMetaObject::invokeMethod(engine, "dataChanged", Qt::QueuedConnection); }); #endif // HAS_MLITE } ContactsDatabase::~ContactsDatabase() { if (m_database.isOpen()) { QSqlQuery optimizeQuery(m_database); const QString statement = QStringLiteral("PRAGMA optimize"); if (!optimizeQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to prepare OPTIMIZE query")); } else if (!optimizeQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to execute OPTIMIZE query")); } else { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Successfully executed OPTIMIZE query")); } } m_database.close(); } QMutex *ContactsDatabase::accessMutex() const { return const_cast(&m_mutex); } ContactsDatabase::ProcessMutex *ContactsDatabase::processMutex() const { if (!m_processMutex) { Q_ASSERT(m_database.isOpen()); m_processMutex.reset(new ProcessMutex(m_database.databaseName())); } return m_processMutex.data(); } // QDir::isReadable() doesn't support group permissions, only user permissions. bool directoryIsRW(const QString &dirPath) { QFileInfo databaseDirInfo(dirPath); return (databaseDirInfo.permission(QFile::ReadGroup | QFile::WriteGroup) || databaseDirInfo.permission(QFile::ReadUser | QFile::WriteUser)); } bool ContactsDatabase::open(const QString &connectionName, bool nonprivileged, bool autoTest, bool secondaryConnection) { QMutexLocker locker(accessMutex()); m_autoTest = autoTest; if (m_dlgGenerators.isEmpty()) { for (auto generator : s_dlgGenerators) { if (generator && (generator->name().contains(QStringLiteral("test")) == m_autoTest)) { m_dlgGenerators.append(generator); } } m_dlgGenerators.append(m_defaultGenerator.data()); // and build a "superlist" of known display label groups. const QLocale locale; QStringList knownDisplayLabelGroups; for (auto generator : m_dlgGenerators) { if (generator->validForLocale(locale)) { const QStringList groups = generator->displayLabelGroups(); for (const QString &group : groups) { if (!knownDisplayLabelGroups.contains(group)) { knownDisplayLabelGroups.append(group); } } } } knownDisplayLabelGroups.removeAll(QStringLiteral("#")); knownDisplayLabelGroups.append(QStringLiteral("#")); knownDisplayLabelGroups.removeAll(QStringLiteral("?")); knownDisplayLabelGroups.append(QStringLiteral("?")); // from that list, build a mapping from group to sort priority value, // based upon the position of each group in the list, // which defines a total sort ordering for known display label groups. for (int i = 0; i < knownDisplayLabelGroups.size(); ++i) { const QString &group(knownDisplayLabelGroups.at(i)); m_knownDisplayLabelGroupsSortValues.insert( group, (group == QStringLiteral("#") || group == QStringLiteral("?")) ? ::displayLabelGroupSortValue(group, m_knownDisplayLabelGroupsSortValues) : i); } // XXX TODO: do we need to add groups which currently exist in the database, // but which aren't currently included in the m_knownDisplayLabelGroupsSortValues? // I don't think we do, since it only matters on write, and we will update // the m_knownDisplayLabelGroupsSortValues in determineDisplayLabelGroup() during write... } if (m_database.isOpen()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to open database when already open: %1").arg(connectionName)); return false; } const QString systemDataDirPath(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/system/"); const QString privilegedDataDirPath(systemDataDirPath + QTCONTACTS_SQLITE_PRIVILEGED_DIR + "/"); QString databaseSubdir(QStringLiteral(QTCONTACTS_SQLITE_DATABASE_DIR)); if (m_autoTest) { databaseSubdir.append(QStringLiteral("-test")); } QDir databaseDir; if (!nonprivileged && databaseDir.mkpath(privilegedDataDirPath + databaseSubdir)) { // privileged. databaseDir = privilegedDataDirPath + databaseSubdir; } else { // not privileged. if (!databaseDir.mkpath(systemDataDirPath + databaseSubdir)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to create contacts database directory: %1").arg(systemDataDirPath + databaseSubdir)); return false; } databaseDir = systemDataDirPath + databaseSubdir; if (!nonprivileged) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Could not access privileged data directory; using nonprivileged")); } m_nonprivileged = true; } const QString databaseFile = databaseDir.absoluteFilePath(QStringLiteral(QTCONTACTS_SQLITE_DATABASE_NAME)); const bool databasePreexisting = QFile::exists(databaseFile); if (!databasePreexisting && secondaryConnection) { // The database must already be created/checked/opened by a primary connection return false; } m_database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connectionName); m_database.setDatabaseName(databaseFile); if (!m_database.open()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to open contacts database: %1") .arg(m_database.lastError().text())); return false; } if (!databasePreexisting && !prepareDatabase(m_database, this, aggregating(), m_localeName)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare contacts database - removing: %1") .arg(m_database.lastError().text())); m_database.close(); QFile::remove(databaseFile); return false; } else if (databasePreexisting && !configureDatabase(m_database, m_localeName)) { m_database.close(); return false; } // Get the process mutex for this database ProcessMutex *mutex(processMutex()); // Only the first connection in the first process to concurrently open the DB is the owner const bool databaseOwner(!secondaryConnection && mutex->isInitialProcess()); if (databasePreexisting && databaseOwner) { // Try to upgrade, if necessary if (mutex->lock()) { // Perform an integrity check if (!checkDatabase(m_database)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to check integrity of contacts database: %1") .arg(m_database.lastError().text())); m_database.close(); mutex->unlock(); return false; } if (!upgradeDatabase(m_database, this)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to upgrade contacts database: %1") .arg(m_database.lastError().text())); m_database.close(); mutex->unlock(); return false; } mutex->unlock(); } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to lock mutex for contacts database: %1") .arg(databaseFile)); m_database.close(); return false; } } else if (databasePreexisting && !databaseOwner) { // check that the version is correct. If not, it is probably because another process // with an open database connection is preventing upgrade of the database schema. QSqlQuery versionQuery(m_database); versionQuery.prepare("PRAGMA user_version"); if (!versionQuery.exec() || !versionQuery.next()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query existing database schema version: %1").arg(versionQuery.lastError().text())); m_database.close(); return false; } int schemaVersion = versionQuery.value(0).toInt(); if (schemaVersion != currentSchemaVersion) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Existing database schema version is unexpected: %1 != %2. " "Is a process preventing schema upgrade?") .arg(schemaVersion).arg(currentSchemaVersion)); m_database.close(); return false; } } // Attach to the transient store - any process can create it, but only the primary connection of each if (!m_transientStore.open(nonprivileged, !secondaryConnection, !databasePreexisting)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to open contacts transient store")); m_database.close(); return false; } QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Opened contacts database: %1 Locale: %2").arg(databaseFile).arg(m_localeName)); return true; } ContactsDatabase::operator QSqlDatabase &() { return m_database; } ContactsDatabase::operator QSqlDatabase const &() const { return m_database; } QSqlError ContactsDatabase::lastError() const { return m_database.lastError(); } bool ContactsDatabase::isOpen() const { return m_database.isOpen(); } bool ContactsDatabase::nonprivileged() const { return m_nonprivileged; } bool ContactsDatabase::localized() const { return (m_localeName != QStringLiteral("C")); } bool ContactsDatabase::aggregating() const { // Currently true only in the privileged database return !m_nonprivileged; } bool ContactsDatabase::beginTransaction() { ProcessMutex *mutex(processMutex()); // We use a cross-process mutex to ensure only one process can write // to the DB at once. Without external locking, SQLite will back off // on write contention, and the backed-off process may never get access // if other processes are performing regular writes. if (mutex->lock()) { if (::beginTransaction(m_database)) return true; mutex->unlock(); } return false; } bool ContactsDatabase::commitTransaction() { ProcessMutex *mutex(processMutex()); if (::commitTransaction(m_database)) { if (mutex->isLocked()) { mutex->unlock(); } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Lock error: no lock held on commit")); } return true; } return false; } bool ContactsDatabase::rollbackTransaction() { ProcessMutex *mutex(processMutex()); const bool rv = ::rollbackTransaction(m_database); if (mutex->isLocked()) { mutex->unlock(); } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Lock error: no lock held on rollback")); } return rv; } ContactsDatabase::Query ContactsDatabase::prepare(const char *statement) { return prepare(QString::fromLatin1(statement)); } ContactsDatabase::Query ContactsDatabase::prepare(const QString &statement) { QMutexLocker locker(accessMutex()); QHash::const_iterator it = m_preparedQueries.constFind(statement); if (it == m_preparedQueries.constEnd()) { QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare query: %1\n%2") .arg(query.lastError().text()) .arg(statement)); return Query(QSqlQuery()); } it = m_preparedQueries.insert(statement, query); } return Query(*it); } bool ContactsDatabase::hasTransientDetails(quint32 contactId) { return m_transientStore.contains(contactId); } QPair > ContactsDatabase::transientDetails(quint32 contactId) const { return m_transientStore.contactDetails(contactId); } bool ContactsDatabase::setTransientDetails(quint32 contactId, const QDateTime ×tamp, const QList &details) { return m_transientStore.setContactDetails(contactId, timestamp, details); } bool ContactsDatabase::removeTransientDetails(quint32 contactId) { return m_transientStore.remove(contactId); } bool ContactsDatabase::removeTransientDetails(const QList &contactIds) { return m_transientStore.remove(contactIds); } bool ContactsDatabase::execute(QSqlQuery &query) { static const bool debugSql = !qgetenv("QTCONTACTS_SQLITE_DEBUG_SQL").isEmpty(); QElapsedTimer t; t.start(); const bool rv = query.exec(); if (debugSql && rv) { const qint64 elapsed = t.elapsed(); const int n = query.isSelect() ? query.size() : query.numRowsAffected(); const QString s(expandQuery(query)); qDebug().nospace() << "Query in " << elapsed << "ms, affecting " << n << " rows: " << qPrintable(s); } return rv; } bool ContactsDatabase::executeBatch(QSqlQuery &query, QSqlQuery::BatchExecutionMode mode) { static const bool debugSql = !qgetenv("QTCONTACTS_SQLITE_DEBUG_SQL").isEmpty(); QElapsedTimer t; t.start(); const bool rv = query.execBatch(mode); if (debugSql && rv) { const qint64 elapsed = t.elapsed(); const int n = query.isSelect() ? query.size() : query.numRowsAffected(); const QString s(expandQuery(query)); qDebug().nospace() << "Batch query in " << elapsed << "ms, affecting " << n << " rows: " << qPrintable(s); } return rv; } QString ContactsDatabase::expandQuery(const QString &queryString, const QVariantList &bindings) { QString query(queryString); int index = 0; for (int i = 0; i < bindings.count(); ++i) { static const QChar marker = QChar::fromLatin1('?'); QString value = bindings.at(i).toString(); index = query.indexOf(marker, index); if (index == -1) break; query.replace(index, 1, value); index += value.length(); } return query; } QString ContactsDatabase::expandQuery(const QString &queryString, const QMap &bindings) { QString query(queryString); int index = 0; while (true) { static const QChar marker = QChar::fromLatin1(':'); index = query.indexOf(marker, index); if (index == -1) break; int remaining = query.length() - index; int len = 1; for ( ; (len < remaining) && query.at(index + len).isLetter(); ) { ++len; } const QString key(query.mid(index, len)); QVariant value = bindings.value(key); QString valueText; if (value.type() == QVariant::String) { valueText = QStringLiteral("'%1'").arg(value.toString()); } else { valueText = value.toString(); } query.replace(index, len, valueText); index += valueText.length(); } return query; } QString ContactsDatabase::expandQuery(const QSqlQuery &query) { return expandQuery(query.lastQuery(), query.boundValues()); } bool ContactsDatabase::createTemporaryContactIdsTable(const QString &table, const QVariantList &boundIds, int limit) { QMutexLocker locker(accessMutex()); return ::createTemporaryContactIdsTable(*this, m_database, table, false, boundIds, QString(), QString(), QString(), QVariantList(), limit); } bool ContactsDatabase::createTemporaryContactIdsTable(const QString &table, const QString &join, const QString &where, const QString &orderBy, const QVariantList &boundValues, int limit) { QMutexLocker locker(accessMutex()); return ::createTemporaryContactIdsTable(*this, m_database, table, true, QVariantList(), join, where, orderBy, boundValues, limit); } bool ContactsDatabase::createTemporaryContactIdsTable(const QString &table, const QString &join, const QString &where, const QString &orderBy, const QMap &boundValues, int limit) { QMutexLocker locker(accessMutex()); return ::createTemporaryContactIdsTable(*this, m_database, table, true, QVariantList(), join, where, orderBy, boundValues, limit); } void ContactsDatabase::clearTemporaryContactIdsTable(const QString &table) { QMutexLocker locker(accessMutex()); ::clearTemporaryContactIdsTable(*this, m_database, table); } bool ContactsDatabase::createTemporaryValuesTable(const QString &table, const QVariantList &values) { QMutexLocker locker(accessMutex()); return ::createTemporaryValuesTable(*this, m_database, table, values); } void ContactsDatabase::clearTemporaryValuesTable(const QString &table) { QMutexLocker locker(accessMutex()); ::clearTemporaryValuesTable(*this, m_database, table); } bool ContactsDatabase::createTransientContactIdsTable(const QString &table, const QVariantList &ids, QString *transientTableName) { QMutexLocker locker(accessMutex()); return ::createTransientContactIdsTable(*this, m_database, table, ids, transientTableName); } void ContactsDatabase::clearTransientContactIdsTable(const QString &table) { QMutexLocker locker(accessMutex()); ::dropTransientTables(*this, m_database, table); } bool ContactsDatabase::populateTemporaryTransientState(bool timestamps, bool globalPresence) { const QString timestampTable(QStringLiteral("Timestamps")); const QString presenceTable(QStringLiteral("GlobalPresenceStates")); QMutexLocker locker(accessMutex()); if (timestamps) { ::clearTemporaryContactTimestampTable(*this, m_database, timestampTable); } if (globalPresence) { ::clearTemporaryContactPresenceTable(*this, m_database, presenceTable); } // Find the current temporary states from transient storage QList > presenceValues; QList > timestampValues; { ContactsTransientStore::DataLock lock(m_transientStore.dataLock()); ContactsTransientStore::const_iterator it = m_transientStore.constBegin(lock), end = m_transientStore.constEnd(lock); for ( ; it != end; ++it) { QPair > details(it.value()); if (details.first.isNull()) continue; if (timestamps) { timestampValues.append(qMakePair(it.key(), dateTimeString(details.first))); } if (globalPresence) { foreach (const QContactDetail &detail, details.second) { if (detail.type() == QContactGlobalPresence::Type) { presenceValues.append(qMakePair(it.key(), detail.value(QContactGlobalPresence::FieldPresenceState))); break; } } } } } bool rv = true; if (timestamps && !::createTemporaryContactTimestampTable(*this, m_database, timestampTable, timestampValues)) { rv = false; } else if (globalPresence && !::createTemporaryContactPresenceTable(*this, m_database, presenceTable, presenceValues)) { rv = false; } return rv; } QString ContactsDatabase::dateTimeString(const QDateTime &qdt) { // Input must be UTC return QLocale::c().toString(qdt, QStringLiteral("yyyy-MM-ddThh:mm:ss.zzz")); } QString ContactsDatabase::dateString(const QDateTime &qdt) { // Input must be UTC return QLocale::c().toString(qdt, QStringLiteral("yyyy-MM-dd")); } QDateTime ContactsDatabase::fromDateTimeString(const QString &s) { // Sorry for the handparsing, but QDateTime::fromString was really slow. // Replacing that call with this loop made contacts loading 30% faster. // (benchmarking this function in isolation showed a 60x speedup) static const int p_len = strlen("yyyy-MM-ddThh:mm:ss.zzz"); static const char pattern[] = "0000-00-00T00:00:00.000"; int values[7] = { 0, }; int v = 0; int s_len = s.length(); // allow length with or without microseconds if (Q_UNLIKELY(s_len != p_len && s_len != p_len - 4)) return QDateTime(); for (int i = 0; i < s_len; i++) { ushort c = s[i].unicode(); if (pattern[i] == '0') { if (Q_UNLIKELY(c < '0' || c > '9')) return QDateTime(); values[v] = values[v] * 10 + (c - '0'); } else { v++; if (Q_UNLIKELY(c != pattern[i])) return QDateTime(); } } // year, month, day QDate datepart(values[0], values[1], values[2]); // hour, minute, second, msec QTime timepart(values[3], values[4], values[5], values[6]) ; if (Q_UNLIKELY(!datepart.isValid() || !timepart.isValid())) return QDateTime(); return QDateTime(datepart, timepart, Qt::UTC); } void ContactsDatabase::regenerateDisplayLabelGroups() { if (!beginTransaction()) { qWarning() << "Unable to begin transaction to regenerate display label groups"; } else { bool changed = false; bool success = executeDisplayLabelGroupLocalizationStatements(m_database, this, &changed); if (success) { if (!commitTransaction()) { qWarning() << "Failed to commit regenerated display label groups"; rollbackTransaction(); } else if (changed) { // TODO: when daemonised, emit here instead of in the lambda! // Emit some engine signals asynchronously. //QMetaObject::invokeMethod(m_engine, "_q_displayLabelGroupsChanged", Qt::QueuedConnection); //QMetaObject::invokeMethod(m_engine, "dataChanged", Qt::QueuedConnection); } } else { qWarning() << "Failed to regenerate display label groups"; rollbackTransaction(); } } } QString ContactsDatabase::displayLabelGroupPreferredProperty() const { QString retn(QStringLiteral("QContactName::FieldFirstName")); #ifdef HAS_MLITE const QVariant groupPropertyConf = m_groupPropertyConf.value(); if (groupPropertyConf.isValid()) { const QString gpcString = groupPropertyConf.toString(); if (gpcString.compare(QStringLiteral("FirstName"), Qt::CaseInsensitive) == 0) { retn = QStringLiteral("QContactName::FieldFirstName"); } else if (gpcString.compare(QStringLiteral("LastName"), Qt::CaseInsensitive) == 0) { retn = QStringLiteral("QContactName::FieldLastName"); } else if (gpcString.compare(QStringLiteral("DisplayLabel"), Qt::CaseInsensitive) == 0) { retn = QStringLiteral("QContactDisplayLabel::FieldLabel"); } } #endif return m_autoTest ? QStringLiteral("QContactName::FieldLastName") : retn; } QString ContactsDatabase::determineDisplayLabelGroup(const QContact &c, bool *emitDisplayLabelGroupChange) { // Read system setting to determine whether display label group // should be generated from last name, first name, or display label. const QString prefDlgProp = displayLabelGroupPreferredProperty(); const int preferredDetail = prefDlgProp.startsWith("QContactName") ? QContactName::Type : QContactDisplayLabel::Type; const int preferredField = prefDlgProp.endsWith("FieldLastName") ? QContactName::FieldLastName : QContactName::FieldFirstName; QString data; if (preferredDetail == QContactName::Type) { // try to use the preferred field data. if (preferredField == QContactName::FieldLastName) { data = c.detail().lastName(); } else if (preferredField == QContactName::FieldFirstName) { data = c.detail().firstName(); } // preferred field is empty? try to use the other. if (data.isEmpty()) { if (preferredField == QContactName::FieldLastName) { data = c.detail().firstName(); } else { data = c.detail().lastName(); } } // fall back to using display label data if (data.isEmpty()) { data = c.detail().label(); } } if (preferredDetail == QContactDisplayLabel::Type) { // try to use the preferred field data. data = c.detail().label(); // if display label is empty, fall back to name data. if (data.isEmpty()) { data = c.detail().firstName(); } if (data.isEmpty()) { data = c.detail().lastName(); } } QLocale locale; QString group; for (int i = 0; i < m_dlgGenerators.size(); ++i) { if (m_dlgGenerators.at(i)->validForLocale(locale)) { group = m_dlgGenerators.at(i)->displayLabelGroup(data); if (!group.isNull()) { break; } } } if (emitDisplayLabelGroupChange && !group.isEmpty() && !m_knownDisplayLabelGroupsSortValues.contains(group)) { // We are about to write a contact to the database which has a // display label group which previously was not known / observed. // Calculate the sort value for the display label group, // and add it to our map of displayLabelGroup->sortValue. // Note: this should be thread-safe since we only call this method within writes. *emitDisplayLabelGroupChange = true; m_knownDisplayLabelGroupsSortValues.insert( group, ::displayLabelGroupSortValue( group, m_knownDisplayLabelGroupsSortValues)); } return group; } QStringList ContactsDatabase::displayLabelGroups() const { QStringList groups; const QLocale locale; for (int i = 0; i < m_dlgGenerators.size(); ++i) { if (m_dlgGenerators.at(i)->preferredForLocale(locale)) { groups = m_dlgGenerators.at(i)->displayLabelGroups(); if (!groups.isEmpty()) { break; } } } if (groups.isEmpty()) { for (int i = 0; i < m_dlgGenerators.size(); ++i) { if (m_dlgGenerators.at(i)->validForLocale(locale)) { groups = m_dlgGenerators.at(i)->displayLabelGroups(); if (!groups.isEmpty()) { break; } } } } if (groups.contains(QStringLiteral("#"))) { groups.removeAll(QStringLiteral("#")); } if (groups.contains(QStringLiteral("?"))) { groups.removeAll(QStringLiteral("?")); } { QMutexLocker locker(accessMutex()); QSqlQuery selectQuery(m_database); selectQuery.setForwardOnly(true); const QString statement = QStringLiteral(" SELECT DISTINCT DisplayLabelGroup" " FROM DisplayLabels" " ORDER BY DisplayLabelGroupSortOrder ASC"); if (!selectQuery.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare distinct display label group selection query: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return QStringList(); } if (!selectQuery.exec()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to select distinct display label groups: %1\n%2") .arg(selectQuery.lastError().text()) .arg(statement)); return QStringList(); } while (selectQuery.next()) { // naive, but the number of groups should be small. const QString seenGroup = selectQuery.value(0).toString(); if (seenGroup != QStringLiteral("#") && seenGroup != QStringLiteral("?") && !groups.contains(seenGroup)) { groups.append(seenGroup); } } } groups.append("#"); groups.append("?"); return groups; } int ContactsDatabase::displayLabelGroupSortValue(const QString &group) const { static const int maxUnicodeCodePointValue = 1114111; // 0x10FFFF static const int nullGroupSortValue = maxUnicodeCodePointValue + 1; return m_knownDisplayLabelGroupsSortValues.value(group, nullGroupSortValue); } #include "../extensions/qcontactdeactivated_impl.h" #include "../extensions/qcontactundelete_impl.h" #include "../extensions/qcontactoriginmetadata_impl.h" #include "../extensions/qcontactstatusflags_impl.h" qtcontacts-sqlite-0.3.19/src/engine/contactsdatabase.h000066400000000000000000000166611436373107600230220ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2019 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTSSQLITE_CONTACTSDATABASE #define QTCONTACTSSQLITE_CONTACTSDATABASE #include "semaphore_p.h" #include "contactstransientstore.h" #include "../extensions/displaylabelgroupgenerator.h" #ifdef HAS_MLITE #include #endif #include #include #include #include #include #include #include #include #include class ContactsEngine; class ContactsDatabase { public: enum Identity { InvalidContactId = 0, SelfContactId }; enum CollectionIdentity { InvalidAddressbookCollectionId = 0, AggregateAddressbookCollectionId, LocalAddressbookCollectionId }; enum ChangeFlags { NoChange = 0, IsAdded = 1, IsModified = 2, IsDeleted = 4 }; class ProcessMutex { Semaphore m_semaphore; bool m_initialProcess; public: ProcessMutex(const QString &path); bool lock(); bool unlock(); bool isLocked() const; bool isInitialProcess() const; }; // This class is required to finish() each query at destruction class Query { friend class ContactsDatabase; QSqlQuery m_query; Query(const QSqlQuery &query); public: ~Query() { finish(); } void bindValue(const QString &id, const QVariant &value) { m_query.bindValue(id, value); } void bindValue(int pos, const QVariant &value) { m_query.bindValue(pos, value); } void addBindValue(const QVariant &value) { m_query.addBindValue(value); } bool next() { return m_query.next(); } bool isValid() { return m_query.isValid(); } void finish() { return m_query.finish(); } void setForwardOnly(bool forwardOnly) { m_query.setForwardOnly(forwardOnly); } QVariant lastInsertId() const { return m_query.lastInsertId(); } QVariant value(int index) { return m_query.value(index); } template T value(int index) { return m_query.value(index).value(); } operator QSqlQuery &() { return m_query; } operator QSqlQuery const &() const { return m_query; } void reportError(const QString &text) const; void reportError(const char *text) const; }; ContactsDatabase(ContactsEngine *engine); ~ContactsDatabase(); QMutex *accessMutex() const; ProcessMutex *processMutex() const; bool open(const QString &databaseName, bool nonprivileged, bool autoTest, bool secondaryConnection = false); operator QSqlDatabase &(); operator QSqlDatabase const &() const; QSqlError lastError() const; bool isOpen() const; bool nonprivileged() const; bool aggregating() const; bool localized() const; bool beginTransaction(); bool commitTransaction(); bool rollbackTransaction(); bool createTemporaryContactIdsTable(const QString &table, const QVariantList &boundIds, int limit = 0); bool createTemporaryContactIdsTable(const QString &table, const QString &join, const QString &where, const QString &orderBy, const QVariantList &boundValues, int limit = 0); bool createTemporaryContactIdsTable(const QString &table, const QString &join, const QString &where, const QString &orderBy, const QMap &boundValues, int limit = 0); void clearTemporaryContactIdsTable(const QString &table); bool createTemporaryValuesTable(const QString &table, const QVariantList &values); void clearTemporaryValuesTable(const QString &table); bool createTransientContactIdsTable(const QString &table, const QVariantList &ids, QString *transientTableName); void clearTransientContactIdsTable(const QString &table); bool populateTemporaryTransientState(bool timestamps, bool globalPresence); Query prepare(const char *statement); Query prepare(const QString &statement); bool hasTransientDetails(quint32 contactId); QPair > transientDetails(quint32 contactId) const; bool setTransientDetails(quint32 contactId, const QDateTime ×tamp, const QList &details); bool removeTransientDetails(quint32 contactId); bool removeTransientDetails(const QList &contactIds); void regenerateDisplayLabelGroups(); QString displayLabelGroupPreferredProperty() const; QString determineDisplayLabelGroup(const QContact &c, bool *emitDisplayLabelGroupChange = Q_NULLPTR); QStringList displayLabelGroups() const; int displayLabelGroupSortValue(const QString &group) const; static bool execute(QSqlQuery &query); static bool executeBatch(QSqlQuery &query, QSqlQuery::BatchExecutionMode mode = QSqlQuery::ValuesAsRows); static QString expandQuery(const QString &queryString, const QVariantList &bindings); static QString expandQuery(const QString &queryString, const QMap &bindings); static QString expandQuery(const QSqlQuery &query); // Input must be UTC static QString dateTimeString(const QDateTime &qdt); static QString dateString(const QDateTime &qdt); // Output is UTC static QDateTime fromDateTimeString(const QString &s); private: ContactsEngine *m_engine; QSqlDatabase m_database; ContactsTransientStore m_transientStore; QMutex m_mutex; mutable QScopedPointer m_processMutex; bool m_nonprivileged; bool m_autoTest; QString m_localeName; QHash m_preparedQueries; QVector m_dlgGenerators; QScopedPointer m_defaultGenerator; QMap m_knownDisplayLabelGroupsSortValues; #ifdef HAS_MLITE MGConfItem m_groupPropertyConf; #endif // HAS_MLITE }; #endif qtcontacts-sqlite-0.3.19/src/engine/contactsengine.cpp000066400000000000000000002170711436373107600230540ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2019 Jolla Ltd. * Copyright (c) 2019 - 2020 Open Mobile Platform LLC. * * 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 "contactsengine.h" #include "contactsdatabase.h" #include "contactnotifier.h" #include "contactreader.h" #include "contactwriter.h" #include "trace_p.h" #include "qtcontacts-extensions.h" #include "qtcontacts-extensions_impl.h" #include "qcontactdetailfetchrequest_p.h" #include "qcontactcollectionchangesfetchrequest_p.h" #include "qcontactchangesfetchrequest_p.h" #include "qcontactchangessaverequest_p.h" #include "qcontactclearchangeflagsrequest_p.h" #include "displaylabelgroupgenerator.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // ---- for schema modification ------ #include #include #include #include #include #include #include #include #include #include #include // ----------------------------------- #include class Job { public: struct WriterProxy { ContactsEngine &engine; ContactsDatabase &database; ContactNotifier ¬ifier; ContactReader &reader; mutable ContactWriter *writer; WriterProxy(ContactsEngine &e, ContactsDatabase &db, ContactNotifier &n, ContactReader &r) : engine(e), database(db), notifier(n), reader(r), writer(0) { } ContactWriter *operator->() const { if (!writer) { writer = new ContactWriter(engine, database, ¬ifier, &reader); } return writer; } }; Job() { } virtual ~Job() { } virtual QObject *request() = 0; virtual void clear() = 0; virtual void execute(ContactReader *reader, WriterProxy &writer) = 0; virtual void update(QMutex *) {} virtual void updateState(QContactAbstractRequest::State state) = 0; virtual void setError(QContactManager::Error) {} virtual void contactsAvailable(const QList &) {} virtual void contactIdsAvailable(const QList &) {} virtual void collectionsAvailable(const QList &) {} virtual QString description() const = 0; virtual QContactManager::Error error() const = 0; }; template class TemplateJob : public Job { public: TemplateJob(T *request) : m_request(request) , m_error(QContactManager::NoError) { } QObject *request() { return m_request; } void clear() override { m_request = 0; } QContactManager::Error error() const { return m_error; } void setError(QContactManager::Error error) override { m_error = error; } protected: T *m_request; QContactManager::Error m_error; }; class ContactSaveJob : public TemplateJob { public: ContactSaveJob(QContactSaveRequest *request) : TemplateJob(request) , m_contacts(request->contacts()) , m_definitionMask(request->typeMask()) { } void execute(ContactReader *, WriterProxy &writer) override { m_error = writer->save(&m_contacts, m_definitionMask, 0, &m_errorMap, false, false, false); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateContactSaveRequest( m_request, m_contacts, m_error, m_errorMap, state); } QString description() const override { QString s(QLatin1String("Save")); foreach (const QContact &c, m_contacts) { s.append(' ').append(ContactId::toString(c)); } return s; } private: QList m_contacts; ContactWriter::DetailList m_definitionMask; QMap m_errorMap; }; class ContactRemoveJob : public TemplateJob { public: ContactRemoveJob(QContactRemoveRequest *request) : TemplateJob(request) , m_contactIds(request->contactIds()) { } void execute(ContactReader *, WriterProxy &writer) override { m_errorMap.clear(); m_error = writer->remove(m_contactIds, &m_errorMap, false, false); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateContactRemoveRequest( m_request, m_error, m_errorMap, state); } QString description() const override { QString s(QLatin1String("Remove")); foreach (const QContactId &id, m_contactIds) { s.append(' ').append(ContactId::toString(id)); } return s; } private: QList m_contactIds; QMap m_errorMap; }; class ContactFetchJob : public TemplateJob { public: ContactFetchJob(QContactFetchRequest *request) : TemplateJob(request) , m_filter(request->filter()) , m_fetchHint(request->fetchHint()) , m_sorting(request->sorting()) { } void execute(ContactReader *reader, WriterProxy &) override { QList contacts; m_error = reader->readContacts( QLatin1String("AsynchronousFilter"), &contacts, m_filter, m_sorting, m_fetchHint); } void update(QMutex *mutex) override { QList contacts; { QMutexLocker locker(mutex); contacts = m_contacts; } QContactManagerEngine::updateContactFetchRequest( m_request, contacts, QContactManager::NoError, QContactAbstractRequest::ActiveState); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateContactFetchRequest(m_request, m_contacts, m_error, state); } void contactsAvailable(const QList &contacts) override { m_contacts = contacts; } QString description() const override { QString s(QLatin1String("Fetch")); return s; } private: QContactFilter m_filter; QContactFetchHint m_fetchHint; QList m_sorting; QList m_contacts; }; class IdFetchJob : public TemplateJob { public: IdFetchJob(QContactIdFetchRequest *request) : TemplateJob(request) , m_filter(request->filter()) , m_sorting(request->sorting()) { } void execute(ContactReader *reader, WriterProxy &) override { QList contactIds; m_error = reader->readContactIds(&contactIds, m_filter, m_sorting); } void update(QMutex *mutex) override { QList contactIds; { QMutexLocker locker(mutex); contactIds = m_contactIds; } QContactManagerEngine::updateContactIdFetchRequest( m_request, contactIds, QContactManager::NoError, QContactAbstractRequest::ActiveState); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateContactIdFetchRequest( m_request, m_contactIds, m_error, state); } void contactIdsAvailable(const QList &contactIds) override { m_contactIds = contactIds; } QString description() const override { QString s(QLatin1String("Fetch IDs")); return s; } private: QContactFilter m_filter; QList m_sorting; QList m_contactIds; }; class ContactFetchByIdJob : public TemplateJob { public: ContactFetchByIdJob(QContactFetchByIdRequest *request) : TemplateJob(request) , m_contactIds(request->contactIds()) , m_fetchHint(request->fetchHint()) { } void execute(ContactReader *reader, WriterProxy &) override { QList contacts; m_error = reader->readContacts( QLatin1String("AsynchronousIds"), &contacts, m_contactIds, m_fetchHint); } void update(QMutex *mutex) override { QList contacts; { QMutexLocker locker(mutex); contacts = m_contacts; } QContactManagerEngine::updateContactFetchByIdRequest( m_request, contacts, QContactManager::NoError, QMap(), QContactAbstractRequest::ActiveState); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateContactFetchByIdRequest( m_request, m_contacts, m_error, QMap(), state); } void contactsAvailable(const QList &contacts) override { m_contacts = contacts; } QString description() const override { QString s(QLatin1String("FetchByID")); foreach (const QContactId &id, m_contactIds) { s.append(' ').append(ContactId::toString(id)); } return s; } private: QList m_contactIds; QContactFetchHint m_fetchHint; QList m_contacts; }; class CollectionSaveJob : public TemplateJob { public: CollectionSaveJob(QContactCollectionSaveRequest *request) : TemplateJob(request) , m_collections(request->collections()) { } void execute(ContactReader *, WriterProxy &writer) override { m_error = writer->save(&m_collections, 0, &m_errorMap, false); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateCollectionSaveRequest( m_request, m_collections, m_error, m_errorMap, state); } QString description() const override { QString s(QLatin1String("Save")); foreach (const QContactCollection &c, m_collections) { s.append(' ').append(ContactCollectionId::toString(c)); } return s; } private: QList m_collections; QMap m_errorMap; }; class CollectionRemoveJob : public TemplateJob { public: CollectionRemoveJob(QContactCollectionRemoveRequest *request) : TemplateJob(request) , m_collectionIds(request->collectionIds()) { } void execute(ContactReader *, WriterProxy &writer) override { m_errorMap.clear(); m_error = writer->remove(m_collectionIds, &m_errorMap, false, false); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateCollectionRemoveRequest( m_request, m_error, m_errorMap, state); } QString description() const override { QString s(QLatin1String("Remove")); foreach (const QContactCollectionId &id, m_collectionIds) { s.append(' ').append(ContactCollectionId::toString(id)); } return s; } private: QList m_collectionIds; QMap m_errorMap; }; class CollectionFetchJob : public TemplateJob { public: CollectionFetchJob(QContactCollectionFetchRequest *request) : TemplateJob(request) { } void execute(ContactReader *reader, WriterProxy &) override { QList collections; m_error = reader->readCollections( QLatin1String("AsynchronousFilter"), &collections); } void update(QMutex *mutex) override { QList collections; { QMutexLocker locker(mutex); collections = m_collections; } QContactManagerEngine::updateCollectionFetchRequest( m_request, collections, QContactManager::NoError, QContactAbstractRequest::ActiveState); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateCollectionFetchRequest(m_request, m_collections, m_error, state); } void collectionsAvailable(const QList &collections) override { m_collections = collections; } QString description() const override { QString s(QLatin1String("CollectionFetch")); return s; } private: QList m_collections; }; class RelationshipSaveJob : public TemplateJob { public: RelationshipSaveJob(QContactRelationshipSaveRequest *request) : TemplateJob(request) , m_relationships(request->relationships()) { } void execute(ContactReader *, WriterProxy &writer) override { m_error = writer->save(m_relationships, &m_errorMap, false, false); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateRelationshipSaveRequest( m_request, m_relationships, m_error, m_errorMap, state); } QString description() const override { QString s(QLatin1String("Relationship Save")); return s; } private: QList m_relationships; QMap m_errorMap; }; class RelationshipRemoveJob : public TemplateJob { public: RelationshipRemoveJob(QContactRelationshipRemoveRequest *request) : TemplateJob(request) , m_relationships(request->relationships()) { } void execute(ContactReader *, WriterProxy &writer) override { m_error = writer->remove(m_relationships, &m_errorMap, false); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateRelationshipRemoveRequest( m_request, m_error, m_errorMap, state); } QString description() const override { QString s(QLatin1String("Relationship Remove")); return s; } private: QList m_relationships; QMap m_errorMap; }; class RelationshipFetchJob : public TemplateJob { public: RelationshipFetchJob(QContactRelationshipFetchRequest *request) : TemplateJob(request) , m_type(request->relationshipType()) , m_first(request->first()) , m_second(request->second()) { } void execute(ContactReader *reader, WriterProxy &) override { m_error = reader->readRelationships( &m_relationships, m_type, m_first, m_second); } void updateState(QContactAbstractRequest::State state) override { QContactManagerEngine::updateRelationshipFetchRequest( m_request, m_relationships, m_error, state); } QString description() const override { QString s(QLatin1String("Relationship Fetch")); return s; } private: QString m_type; QContactId m_first; QContactId m_second; QList m_relationships; }; class DetailFetchJob : public TemplateJob { public: DetailFetchJob(QContactDetailFetchRequest *request, QContactDetailFetchRequestPrivate *d) : TemplateJob(request) , m_filter(d->filter) , m_fetchHint(d->hint) , m_sorting(d->sorting) , m_fields(d->fields) , m_type(d->type) { } void execute(ContactReader *reader, WriterProxy &) override { m_error = reader->readDetails( &m_details, m_type, m_fields, m_filter, m_sorting, m_fetchHint); } void updateState(QContactAbstractRequest::State state) override { if (m_request) { QContactDetailFetchRequestPrivate * const d = QContactDetailFetchRequestPrivate::get(m_request); d->details = m_details; d->error = m_error; d->state = state; if (state == QContactAbstractRequest::FinishedState) { emit (m_request->*(d->resultsAvailable))(); } emit (m_request->*(d->stateChanged))(state); } } QString description() const override { QString s(QLatin1String("Detail Fetch")); return s; } private: const QContactFilter m_filter; const QContactFetchHint m_fetchHint; const QList m_sorting; const QList m_fields; QList m_details; const QContactDetail::DetailType m_type; }; class CollectionChangesFetchJob : public TemplateJob { public: CollectionChangesFetchJob(QContactCollectionChangesFetchRequest *request, QContactCollectionChangesFetchRequestPrivate *d) : TemplateJob(request) , m_accountId(d->accountId) , m_applicationName(d->applicationName) , m_addedCollections(d->addedCollections) , m_modifiedCollections(d->modifiedCollections) , m_removedCollections(d->removedCollections) , m_unmodifiedCollections(d->unmodifiedCollections) { } void execute(ContactReader *, WriterProxy &writer) override { m_error = writer->fetchCollectionChanges( m_accountId, m_applicationName, &m_addedCollections, &m_modifiedCollections, &m_removedCollections, &m_unmodifiedCollections); } void updateState(QContactAbstractRequest::State state) override { if (m_request) { QContactCollectionChangesFetchRequestPrivate * const d = QContactCollectionChangesFetchRequestPrivate::get(m_request); d->error = m_error; d->state = state; if (state == QContactAbstractRequest::FinishedState) { d->addedCollections = m_addedCollections; d->modifiedCollections = m_modifiedCollections; d->removedCollections = m_removedCollections; d->unmodifiedCollections = m_unmodifiedCollections; emit (m_request->*(d->resultsAvailable))(); } emit (m_request->*(d->stateChanged))(state); } } QString description() const override { QString s(QLatin1String("Collection Changes Fetch")); return s; } private: const int m_accountId; const QString m_applicationName; QList m_addedCollections; QList m_modifiedCollections; QList m_removedCollections; QList m_unmodifiedCollections; }; class ContactChangesFetchJob : public TemplateJob { public: ContactChangesFetchJob(QContactChangesFetchRequest *request, QContactChangesFetchRequestPrivate *d) : TemplateJob(request) , m_collectionId(d->collectionId) , m_addedContacts(d->addedContacts) , m_modifiedContacts(d->modifiedContacts) , m_removedContacts(d->removedContacts) , m_unmodifiedContacts(d->unmodifiedContacts) { } void execute(ContactReader *, WriterProxy &writer) override { m_error = writer->fetchContactChanges( m_collectionId, &m_addedContacts, &m_modifiedContacts, &m_removedContacts, &m_unmodifiedContacts); } void updateState(QContactAbstractRequest::State state) override { if (m_request) { QContactChangesFetchRequestPrivate * const d = QContactChangesFetchRequestPrivate::get(m_request); d->error = m_error; d->state = state; if (state == QContactAbstractRequest::FinishedState) { d->addedContacts = m_addedContacts; d->modifiedContacts = m_modifiedContacts; d->removedContacts = m_removedContacts; d->unmodifiedContacts = m_unmodifiedContacts; emit (m_request->*(d->resultsAvailable))(); } emit (m_request->*(d->stateChanged))(state); } } QString description() const override { QString s(QLatin1String("Collection Changes Fetch")); return s; } private: const QContactCollectionId m_collectionId; QList m_addedContacts; QList m_modifiedContacts; QList m_removedContacts; QList m_unmodifiedContacts; }; class ContactChangesSaveJob : public TemplateJob { public: ContactChangesSaveJob(QContactChangesSaveRequest *request, QContactChangesSaveRequestPrivate *d) : TemplateJob(request) , m_addedCollections(d->addedCollections) , m_modifiedCollections(d->modifiedCollections) , m_removedCollections(d->removedCollections) , m_policy(d->policy == QContactChangesSaveRequest::PreserveLocalChanges ? QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges : QtContactsSqliteExtensions::ContactManagerEngine::PreserveRemoteChanges) , m_clearChangeFlags(d->clearChangeFlags) { } void execute(ContactReader *, WriterProxy &writer) override { QList collections; QList > contacts; QHash* > addedCollections_ptrs; QHash* > modifiedCollections_ptrs; // the storeSyncChanges method parameters are in+out parameters. // construct the appropriate data structures. QHash addedCollectionsIndexes; QHash modifiedCollectionsIndexes; QHash >::iterator ait, aend, mit, mend; ait = m_addedCollections.begin(), aend = m_addedCollections.end(); for ( ; ait != aend; ++ait) { addedCollectionsIndexes.insert(collections.size(), contacts.size()); collections.append(ait.key()); contacts.append(ait.value()); } mit = m_modifiedCollections.begin(), mend = m_modifiedCollections.end(); for ( ; mit != mend; ++mit) { modifiedCollectionsIndexes.insert(collections.size(), contacts.size()); collections.append(mit.key()); contacts.append(mit.value()); } // do this as a second phase to avoid non-const operations causing potential detach // and thus invalidating our references. QHash::const_iterator iit, iend; iit = addedCollectionsIndexes.constBegin(), iend = addedCollectionsIndexes.constEnd(); for ( ; iit != iend; ++iit) { addedCollections_ptrs.insert(&collections[iit.key()], &contacts[iit.value()]); } iit = modifiedCollectionsIndexes.constBegin(), iend = modifiedCollectionsIndexes.constEnd(); for ( ; iit != iend; ++iit) { modifiedCollections_ptrs.insert(&collections[iit.key()], &contacts[iit.value()]); } m_error = writer->storeChanges( &addedCollections_ptrs, &modifiedCollections_ptrs, m_removedCollections, m_policy, m_clearChangeFlags); if (m_error == QContactManager::NoError) { m_addedCollections.clear(); QHash* >::iterator ait, aend, mit, mend; ait = addedCollections_ptrs.begin(), aend = addedCollections_ptrs.end(); for ( ; ait != aend; ++ait) { m_addedCollections.insert(*ait.key(), *ait.value()); } m_modifiedCollections.clear(); mit = modifiedCollections_ptrs.begin(), mend = modifiedCollections_ptrs.end(); for ( ; mit != mend; ++mit) { m_modifiedCollections.insert(*mit.key(), *mit.value()); } } } void updateState(QContactAbstractRequest::State state) override { if (m_request) { QContactChangesSaveRequestPrivate * const d = QContactChangesSaveRequestPrivate::get(m_request); d->error = m_error; d->state = state; if (state == QContactAbstractRequest::FinishedState) { d->addedCollections = m_addedCollections; d->modifiedCollections = m_modifiedCollections; emit (m_request->*(d->resultsAvailable))(); } emit (m_request->*(d->stateChanged))(state); } } QString description() const override { QString s(QLatin1String("Changes Save")); return s; } private: QHash > m_addedCollections; QHash > m_modifiedCollections; QList m_removedCollections; const QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy m_policy; const bool m_clearChangeFlags; }; class ClearChangeFlagsJob : public TemplateJob { public: ClearChangeFlagsJob(QContactClearChangeFlagsRequest *request, QContactClearChangeFlagsRequestPrivate *d) : TemplateJob(request) , m_collectionId(d->collectionId) , m_contactIds(d->contactIds) { } void execute(ContactReader *, WriterProxy &writer) override { m_error = m_collectionId.isNull() ? writer->clearChangeFlags( m_contactIds, false) : writer->clearChangeFlags( m_collectionId, false); } void updateState(QContactAbstractRequest::State state) override { if (m_request) { QContactClearChangeFlagsRequestPrivate * const d = QContactClearChangeFlagsRequestPrivate::get(m_request); d->error = m_error; d->state = state; if (state == QContactAbstractRequest::FinishedState) { emit (m_request->*(d->resultsAvailable))(); } emit (m_request->*(d->stateChanged))(state); } } QString description() const override { QString s(QLatin1String("Clear Change Flags")); return s; } private: const QContactCollectionId m_collectionId; const QList m_contactIds; }; class JobThread : public QThread { struct MutexUnlocker { QMutexLocker &m_locker; explicit MutexUnlocker(QMutexLocker &locker) : m_locker(locker) { m_locker.unlock(); } ~MutexUnlocker() { m_locker.relock(); } }; public: JobThread(ContactsEngine *engine, const QString &databaseUuid, bool nonprivileged, bool autoTest) : m_currentJob(0) , m_engine(engine) , m_database(engine) , m_databaseUuid(databaseUuid) , m_updatePending(false) , m_running(false) , m_nonprivileged(nonprivileged) , m_autoTest(autoTest) { start(QThread::IdlePriority); // Don't return until the started thread has indicated it is running QMutexLocker locker(&m_mutex); if (!m_running) { m_wait.wait(&m_mutex); } } ~JobThread() { { QMutexLocker locker(&m_mutex); m_running = false; } m_wait.wakeOne(); wait(); } void run(); bool databaseOpen() const { return m_database.isOpen(); } bool nonprivileged() const { return m_nonprivileged; } void enqueue(Job *job) { QMutexLocker locker(&m_mutex); m_pendingJobs.append(job); m_wait.wakeOne(); } bool requestDestroyed(QObject *request) { QMutexLocker locker(&m_mutex); for (QList::iterator it = m_pendingJobs.begin(); it != m_pendingJobs.end(); it++) { if ((*it)->request() == request) { delete *it; m_pendingJobs.erase(it); return true; } } if (m_currentJob && m_currentJob->request() == request) { m_currentJob->clear(); return false; } for (QList::iterator it = m_finishedJobs.begin(); it != m_finishedJobs.end(); it++) { if ((*it)->request() == request) { delete *it; m_finishedJobs.erase(it); return false; } } for (QList::iterator it = m_cancelledJobs.begin(); it != m_cancelledJobs.end(); it++) { if ((*it)->request() == request) { delete *it; m_cancelledJobs.erase(it); return false; } } return false; } bool cancelRequest(QObject *request) { QMutexLocker locker(&m_mutex); for (QList::iterator it = m_pendingJobs.begin(); it != m_pendingJobs.end(); it++) { if ((*it)->request() == request) { m_cancelledJobs.append(*it); m_pendingJobs.erase(it); return true; } } return false; } bool waitForFinished(QObject *request, const int msecs) { long timeout = msecs <= 0 ? INT32_MAX : msecs; Job *finishedJob = 0; { QMutexLocker locker(&m_mutex); for (;;) { bool pendingJob = false; if (m_currentJob && m_currentJob->request() == request) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Wait for current job: %1 ms").arg(timeout)); // wait for the current job to updateState. if (!m_finishedWait.wait(&m_mutex, timeout)) return false; } else for (int i = 0; i < m_pendingJobs.size(); i++) { Job *job = m_pendingJobs[i]; if (job->request() == request) { // If the job is pending, move it to the front of the queue and wait for // the current job to end. QElapsedTimer timer; timer.start(); m_pendingJobs.move(i, 0); if (!m_finishedWait.wait(&m_mutex, timeout)) return false; timeout -= timer.elapsed(); if (timeout <= 0) return false; pendingJob = true; break; } } // Job is either finished, cancelled, or there is no job. if (!pendingJob) break; } for (QList::iterator it = m_finishedJobs.begin(); it != m_finishedJobs.end(); it++) { if ((*it)->request() == request) { finishedJob = *it; m_finishedJobs.erase(it); break; } } } if (finishedJob) { finishedJob->updateState(QContactAbstractRequest::FinishedState); delete finishedJob; return true; } else for (QList::iterator it = m_cancelledJobs.begin(); it != m_cancelledJobs.end(); it++) { if ((*it)->request() == request) { (*it)->updateState(QContactAbstractRequest::CanceledState); delete *it; m_cancelledJobs.erase(it); return true; } } return false; } void postUpdate() { if (!m_updatePending) { m_updatePending = true; QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); } } void contactsAvailable(const QList &contacts) { QMutexLocker locker(&m_mutex); m_currentJob->contactsAvailable(contacts); postUpdate(); } void contactIdsAvailable(const QList &contactIds) { QMutexLocker locker(&m_mutex); m_currentJob->contactIdsAvailable(contactIds); postUpdate(); } void collectionsAvailable(const QList &collections) { QMutexLocker locker(&m_mutex); m_currentJob->collectionsAvailable(collections); postUpdate(); } bool event(QEvent *event) { if (event->type() == QEvent::UpdateRequest) { QList finishedJobs; QList cancelledJobs; Job *currentJob; { QMutexLocker locker(&m_mutex); finishedJobs = m_finishedJobs; cancelledJobs = m_cancelledJobs; m_finishedJobs.clear(); m_cancelledJobs.clear(); currentJob = m_currentJob; m_updatePending = false; } while (!finishedJobs.isEmpty()) { Job *job = finishedJobs.takeFirst(); job->updateState(QContactAbstractRequest::FinishedState); delete job; } while (!cancelledJobs.isEmpty()) { Job *job = cancelledJobs.takeFirst(); job->updateState(QContactAbstractRequest::CanceledState); delete job; } if (currentJob) currentJob->update(&m_mutex); return true; } else { return QThread::event(event); } } private: QMutex m_mutex; QWaitCondition m_wait; QWaitCondition m_finishedWait; QList m_pendingJobs; QList m_finishedJobs; QList m_cancelledJobs; Job *m_currentJob; ContactsEngine *m_engine; ContactsDatabase m_database; QString m_databaseUuid; bool m_updatePending; bool m_running; bool m_nonprivileged; bool m_autoTest; }; class JobContactReader : public ContactReader { public: JobContactReader(ContactsDatabase &database, const QString &managerUri, JobThread *thread) : ContactReader(database, managerUri) , m_thread(thread) { } void contactsAvailable(const QList &contacts) override { m_thread->contactsAvailable(contacts); } void contactIdsAvailable(const QList &contactIds) override { m_thread->contactIdsAvailable(contactIds); } void collectionsAvailable(const QList &collections) override { m_thread->collectionsAvailable(collections); } private: JobThread *m_thread; }; void JobThread::run() { QString dbId(QStringLiteral("qtcontacts-sqlite%1-job-%2")); dbId = dbId.arg(m_autoTest ? QStringLiteral("-test") : QString()).arg(m_databaseUuid); QMutexLocker locker(&m_mutex); m_database.open(dbId, m_nonprivileged, m_autoTest); m_nonprivileged = m_database.nonprivileged(); m_running = true; { MutexUnlocker unlocker(locker); m_wait.wakeOne(); } if (!m_database.isOpen()) { while (m_running) { if (m_pendingJobs.isEmpty()) { m_wait.wait(&m_mutex); } else { m_currentJob = m_pendingJobs.takeFirst(); m_currentJob->setError(QContactManager::UnspecifiedError); m_finishedJobs.append(m_currentJob); m_currentJob = 0; postUpdate(); m_finishedWait.wakeOne(); } } } else { ContactNotifier notifier(m_nonprivileged); JobContactReader reader(m_database, m_engine->managerUri(), this); Job::WriterProxy writer(*m_engine, m_database, notifier, reader); while (m_running) { if (m_pendingJobs.isEmpty()) { m_wait.wait(&m_mutex); } else { m_currentJob = m_pendingJobs.takeFirst(); { MutexUnlocker unlocker(locker); QElapsedTimer timer; timer.start(); m_currentJob->execute(&reader, writer); QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Job executed in %1 ms : %2 : error = %3") .arg(timer.elapsed()).arg(m_currentJob->description()).arg(m_currentJob->error())); } m_finishedJobs.append(m_currentJob); m_currentJob = 0; postUpdate(); m_finishedWait.wakeOne(); } } } } ContactsEngine::ContactsEngine(const QString &name, const QMap ¶meters) : m_name(name) , m_parameters(parameters) { static bool registered = qRegisterMetaType >("QList") && qRegisterMetaType >("QList") && qRegisterMetaTypeStreamOperators >(); Q_UNUSED(registered) QString nonprivileged = m_parameters.value(QString::fromLatin1("nonprivileged")); if (nonprivileged.toLower() == QLatin1String("true") || nonprivileged.toInt() == 1) { setNonprivileged(true); } QString mergePresenceChanges = m_parameters.value(QString::fromLatin1("mergePresenceChanges")); if (mergePresenceChanges.isEmpty()) { qWarning("The 'mergePresenceChanges' option has not been configured - presence changes will only be reported via ContactManagerEngine::contactsPresenceChanged()"); } else if (mergePresenceChanges.toLower() == QLatin1String("true") || mergePresenceChanges.toInt() == 1) { setMergePresenceChanges(true); } QString autoTest = m_parameters.value(QString::fromLatin1("autoTest")); if (autoTest.toLower() == QLatin1String("true") || autoTest.toInt() == 1) { setAutoTest(true); } /* Store the engine into a property of QCoreApplication, so that it can be * retrieved by the extension code */ QCoreApplication *app = QCoreApplication::instance(); QList engines = app->property(CONTACT_MANAGER_ENGINE_PROP).toList(); engines.append(QVariant::fromValue(this)); app->setProperty(CONTACT_MANAGER_ENGINE_PROP, engines); m_managerUri = managerUri(); } ContactsEngine::~ContactsEngine() { QCoreApplication *app = QCoreApplication::instance(); QList engines = app->property(CONTACT_MANAGER_ENGINE_PROP).toList(); for (int i = 0; i < engines.size(); ++i) { QContactManagerEngine *engine = static_cast(engines[i].value()); if (engine == this) { engines.removeAt(i); break; } } app->setProperty(CONTACT_MANAGER_ENGINE_PROP, engines); } QString ContactsEngine::databaseUuid() { if (m_databaseUuid.isEmpty()) { m_databaseUuid = QUuid::createUuid().toString(); } return m_databaseUuid; } QContactManager::Error ContactsEngine::open() { // Start the async thread, and wait to see if it can open the database if (!m_jobThread) { m_jobThread.reset(new JobThread(this, databaseUuid(), m_nonprivileged, m_autoTest)); if (m_jobThread->databaseOpen()) { // We may not have got privileged access if we requested it setNonprivileged(m_jobThread->nonprivileged()); if (!m_notifier) { m_notifier.reset(new ContactNotifier(m_nonprivileged)); m_notifier->connect("collectionsAdded", "au", this, SLOT(_q_collectionsAdded(QVector))); m_notifier->connect("collectionsChanged", "au", this, SLOT(_q_collectionsChanged(QVector))); m_notifier->connect("collectionsRemoved", "au", this, SLOT(_q_collectionsRemoved(QVector))); m_notifier->connect("collectionContactsChanged", "au", this, SLOT(_q_collectionContactsChanged(QVector))); m_notifier->connect("contactsAdded", "au", this, SLOT(_q_contactsAdded(QVector))); m_notifier->connect("contactsChanged", "au", this, SLOT(_q_contactsChanged(QVector))); m_notifier->connect("contactsPresenceChanged", "au", this, SLOT(_q_contactsPresenceChanged(QVector))); m_notifier->connect("contactsRemoved", "au", this, SLOT(_q_contactsRemoved(QVector))); m_notifier->connect("selfContactIdChanged", "uu", this, SLOT(_q_selfContactIdChanged(quint32,quint32))); m_notifier->connect("relationshipsAdded", "au", this, SLOT(_q_relationshipsAdded(QVector))); m_notifier->connect("relationshipsRemoved", "au", this, SLOT(_q_relationshipsRemoved(QVector))); m_notifier->connect("displayLabelGroupsChanged", "", this, SLOT(_q_displayLabelGroupsChanged())); } } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to open asynchronous engine database connection")); } } return m_jobThread->databaseOpen() ? QContactManager::NoError : QContactManager::UnspecifiedError; } QString ContactsEngine::managerName() const { return m_name; } QMap ContactsEngine::managerParameters() const { return m_parameters; } QMap ContactsEngine::idInterpretationParameters() const { const bool nonprivileged = m_parameters.value(QString::fromLatin1("nonprivileged")).compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || m_parameters.value(QString::fromLatin1("nonprivileged")).compare(QStringLiteral("1"), Qt::CaseInsensitive) == 0; const bool autoTest = m_parameters.value(QString::fromLatin1("autoTest")).compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || m_parameters.value(QString::fromLatin1("autoTest")).compare(QStringLiteral("1"), Qt::CaseInsensitive) == 0; if (nonprivileged && autoTest) { return { { QString::fromLatin1("nonprivileged"), QString::fromLatin1("true") }, { QString::fromLatin1("autoTest"), QString::fromLatin1("true") } }; } else if (nonprivileged) { return { { QString::fromLatin1("nonprivileged"), QString::fromLatin1("true") } }; } else if (autoTest) { return { { QString::fromLatin1("autoTest"), QString::fromLatin1("true") } }; } else { return QMap(); } } int ContactsEngine::managerVersion() const { return 1; } QList ContactsEngine::contactIds( const QContactFilter &filter, const QList &sortOrders, QContactManager::Error* error) const { QList contactIds; QContactManager::Error err = reader()->readContactIds(&contactIds, filter, sortOrders); if (error) *error = err; return contactIds; } QList ContactsEngine::contacts( const QContactFilter &filter, const QList &sortOrders, const QContactFetchHint &fetchHint, QContactManager::Error* error) const { QList contacts; QContactManager::Error err = reader()->readContacts( QLatin1String("SynchronousFilter"), &contacts, filter, sortOrders, fetchHint); if (error) *error = err; return contacts; } QList ContactsEngine::contacts( const QContactFilter &filter, const QList &sortOrders, const QContactFetchHint &fetchHint, QMap *errorMap, QContactManager::Error *error) const { Q_UNUSED(errorMap); return contacts(filter, sortOrders, fetchHint, error); } QList ContactsEngine::contacts( const QList &localIds, const QContactFetchHint &fetchHint, QMap *errorMap, QContactManager::Error *error) const { Q_UNUSED(errorMap); QList contacts; QContactManager::Error err = reader()->readContacts( QLatin1String("SynchronousIds"), &contacts, localIds, fetchHint); if (error) *error = err; return contacts; } QContact ContactsEngine::contact( const QContactId &contactId, const QContactFetchHint &fetchHint, QContactManager::Error* error) const { QMap errorMap; QList contacts = ContactsEngine::contacts( QList() << contactId, fetchHint, &errorMap, error); return !contacts.isEmpty() ? contacts.first() : QContact(); } bool ContactsEngine::saveContacts( QList *contacts, QMap *errorMap, QContactManager::Error *error) { return saveContacts(contacts, ContactWriter::DetailList(), errorMap, error); } bool ContactsEngine::saveContacts( QList *contacts, const ContactWriter::DetailList &definitionMask, QMap *errorMap, QContactManager::Error *error) { QContactManager::Error err = writer()->save(contacts, definitionMask, 0, errorMap, false, false, false); if (error) *error = err; return err == QContactManager::NoError; } bool ContactsEngine::removeContact(const QContactId &contactId, QContactManager::Error* error) { QMap errorMap; return removeContacts(QList() << contactId, &errorMap, error); } bool ContactsEngine::removeContacts( const QList &contactIds, QMap *errorMap, QContactManager::Error* error) { QContactManager::Error err = writer()->remove(contactIds, errorMap, false, false); if (error) *error = err; return err == QContactManager::NoError; } QContactId ContactsEngine::selfContactId(QContactManager::Error* error) const { QContactId contactId; QContactManager::Error err = reader()->getIdentity( ContactsDatabase::SelfContactId, &contactId); if (error) *error = err; return contactId; } bool ContactsEngine::setSelfContactId( const QContactId&, QContactManager::Error* error) { *error = QContactManager::NotSupportedError; return false; } QList ContactsEngine::relationships( const QString &relationshipType, const QContactId &participantId, QContactRelationship::Role role, QContactManager::Error *error) const { QContactId first = participantId; QContactId second; if (role == QContactRelationship::Second) qSwap(first, second); QList relationships; QContactManager::Error err = reader()->readRelationships( &relationships, relationshipType, first, second); if (error) *error = err; return relationships; } bool ContactsEngine::saveRelationships( QList *relationships, QMap *errorMap, QContactManager::Error *error) { QContactManager::Error err = writer()->save(*relationships, errorMap, false, false); if (error) *error = err; if (err == QContactManager::NoError) { return true; } return false; } bool ContactsEngine::removeRelationships( const QList &relationships, QMap *errorMap, QContactManager::Error *error) { QContactManager::Error err = writer()->remove(relationships, errorMap, false); if (error) *error = err; return err == QContactManager::NoError; } QContactCollectionId ContactsEngine::defaultCollectionId() const { QContactCollectionId collectionId; QContactManager::Error err = reader()->getCollectionIdentity( ContactsDatabase::LocalAddressbookCollectionId, &collectionId); return err == QContactManager::NoError ? collectionId : QContactCollectionId(); } QContactCollection ContactsEngine::collection( const QContactCollectionId &collectionId, QContactManager::Error *error) const { const QList collections = ContactsEngine::collections(error); if (*error == QContactManager::NoError) { for (const QContactCollection &collection : collections) { if (collection.id() == collectionId) { return collection; } } *error = QContactManager::DoesNotExistError; } return QContactCollection(); } QList ContactsEngine::collections( QContactManager::Error *error) const { QList collections; QContactManager::Error err = reader()->readCollections( QLatin1String("SynchronousFilter"), &collections); if (error) *error = err; return collections; } bool ContactsEngine::saveCollections( QList *collections, QMap *errorMap, QContactManager::Error *error) { QContactManager::Error err = writer()->save(collections, errorMap, false, false); if (error) *error = err; return err == QContactManager::NoError; } bool ContactsEngine::saveCollection( QContactCollection *collection, QContactManager::Error *error) { bool ret = false; if (collection) { QList collections; collections.append(*collection); QMap errorMap; ret = saveCollections(&collections, &errorMap, error); if (errorMap.size()) { *error = errorMap.constBegin().value(); } *collection = collections.first(); } else { *error = QContactManager::BadArgumentError; } return ret; } bool ContactsEngine::removeCollections( const QList &collectionIds, QMap *errorMap, QContactManager::Error *error) { QContactManager::Error err = writer()->remove(collectionIds, errorMap, false, false); if (error) *error = err; return err == QContactManager::NoError; } bool ContactsEngine::removeCollection( const QContactCollectionId &collectionId, QContactManager::Error *error) { QMap errorMap; return removeCollections(QList() << collectionId, &errorMap, error); } void ContactsEngine::requestDestroyed(QContactAbstractRequest* req) { requestDestroyed(static_cast(req)); } void ContactsEngine::requestDestroyed(QObject* req) { if (m_jobThread) m_jobThread->requestDestroyed(req); } bool ContactsEngine::startRequest(QContactAbstractRequest* request) { Job *job = 0; switch (request->type()) { case QContactAbstractRequest::ContactSaveRequest: job = new ContactSaveJob(qobject_cast(request)); break; case QContactAbstractRequest::ContactRemoveRequest: job = new ContactRemoveJob(qobject_cast(request)); break; case QContactAbstractRequest::ContactFetchRequest: job = new ContactFetchJob(qobject_cast(request)); break; case QContactAbstractRequest::ContactIdFetchRequest: job = new IdFetchJob(qobject_cast(request)); break; case QContactAbstractRequest::ContactFetchByIdRequest: job = new ContactFetchByIdJob(qobject_cast(request)); break; case QContactAbstractRequest::RelationshipFetchRequest: job = new RelationshipFetchJob(qobject_cast(request)); break; case QContactAbstractRequest::RelationshipSaveRequest: job = new RelationshipSaveJob(qobject_cast(request)); break; case QContactAbstractRequest::RelationshipRemoveRequest: job = new RelationshipRemoveJob(qobject_cast(request)); break; case QContactAbstractRequest::CollectionFetchRequest: job = new CollectionFetchJob(qobject_cast(request)); break; case QContactAbstractRequest::CollectionSaveRequest: job = new CollectionSaveJob(qobject_cast(request)); break; case QContactAbstractRequest::CollectionRemoveRequest: job = new CollectionRemoveJob(qobject_cast(request)); break; default: return false; } job->updateState(QContactAbstractRequest::ActiveState); m_jobThread->enqueue(job); return true; } bool ContactsEngine::startRequest(QContactDetailFetchRequest* request) { Job *job = new DetailFetchJob(request, QContactDetailFetchRequestPrivate::get(request)); job->updateState(QContactAbstractRequest::ActiveState); m_jobThread->enqueue(job); return true; } bool ContactsEngine::startRequest(QContactCollectionChangesFetchRequest* request) { Job *job = new CollectionChangesFetchJob(request, QContactCollectionChangesFetchRequestPrivate::get(request)); job->updateState(QContactAbstractRequest::ActiveState); m_jobThread->enqueue(job); return true; } bool ContactsEngine::startRequest(QContactChangesFetchRequest* request) { Job *job = new ContactChangesFetchJob(request, QContactChangesFetchRequestPrivate::get(request)); job->updateState(QContactAbstractRequest::ActiveState); m_jobThread->enqueue(job); return true; } bool ContactsEngine::startRequest(QContactChangesSaveRequest* request) { Job *job = new ContactChangesSaveJob(request, QContactChangesSaveRequestPrivate::get(request)); job->updateState(QContactAbstractRequest::ActiveState); m_jobThread->enqueue(job); return true; } bool ContactsEngine::startRequest(QContactClearChangeFlagsRequest* request) { Job *job = new ClearChangeFlagsJob(request, QContactClearChangeFlagsRequestPrivate::get(request)); job->updateState(QContactAbstractRequest::ActiveState); m_jobThread->enqueue(job); return true; } bool ContactsEngine::cancelRequest(QContactAbstractRequest* req) { return cancelRequest(static_cast(req)); } bool ContactsEngine::cancelRequest(QObject* req) { if (m_jobThread) return m_jobThread->cancelRequest(req); return false; } bool ContactsEngine::waitForRequestFinished(QContactAbstractRequest* req, int msecs) { return waitForRequestFinished(static_cast(req), msecs); } bool ContactsEngine::waitForRequestFinished(QObject* req, int msecs) { if (m_jobThread) return m_jobThread->waitForFinished(req, msecs); return true; } bool ContactsEngine::isRelationshipTypeSupported(const QString &relationshipType, QContactType::TypeValues contactType) const { Q_UNUSED(relationshipType); return contactType == QContactType::TypeContact; } QList ContactsEngine::supportedContactTypes() const { return QList() << QContactType::TypeContact; } void ContactsEngine::regenerateDisplayLabel(QContact &contact, bool *emitDisplayLabelGroupChange) { QContactManager::Error displayLabelError = QContactManager::NoError; const QString label = synthesizedDisplayLabel(contact, &displayLabelError); if (displayLabelError != QContactManager::NoError) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Unable to regenerate displayLabel for contact: %1").arg(ContactId::toString(contact))); } QContact tempContact(contact); setContactDisplayLabel(&tempContact, label, QString(), -1); const QString group = m_database ? m_database->determineDisplayLabelGroup(tempContact, emitDisplayLabelGroupChange) : QString(); const int sortOrder = m_database ? m_database->displayLabelGroupSortValue(group) : -1; setContactDisplayLabel(&contact, label, group, sortOrder); } bool ContactsEngine::clearChangeFlags(const QList &contactIds, QContactManager::Error *error) { Q_ASSERT(error); *error = writer()->clearChangeFlags(contactIds, false); return (*error == QContactManager::NoError); } bool ContactsEngine::clearChangeFlags(const QContactCollectionId &collectionId, QContactManager::Error *error) { Q_ASSERT(error); *error = writer()->clearChangeFlags(collectionId, false); return (*error == QContactManager::NoError); } bool ContactsEngine::fetchCollectionChanges(int accountId, const QString &applicationName, QList *addedCollections, QList *modifiedCollections, QList *deletedCollections, QList *unmodifiedCollections, QContactManager::Error *error) { Q_ASSERT(error); *error = writer()->fetchCollectionChanges(accountId, applicationName, addedCollections, modifiedCollections, deletedCollections, unmodifiedCollections); return (*error == QContactManager::NoError); } bool ContactsEngine::fetchContactChanges(const QContactCollectionId &collectionId, QList *addedContacts, QList *modifiedContacts, QList *deletedContacts, QList *unmodifiedContacts, QContactManager::Error *error) { Q_ASSERT(error); *error = writer()->fetchContactChanges(collectionId, addedContacts, modifiedContacts, deletedContacts, unmodifiedContacts); return (*error == QContactManager::NoError); } bool ContactsEngine::storeChanges(QHash * /* added contacts */> *addedCollections, QHash * /* added/modified/deleted contacts */> *modifiedCollections, const QList &deletedCollections, ConflictResolutionPolicy conflictResolutionPolicy, bool clearChangeFlags, QContactManager::Error *error) { Q_ASSERT(error); *error = writer()->storeChanges(addedCollections, modifiedCollections, deletedCollections, conflictResolutionPolicy, clearChangeFlags); return (*error == QContactManager::NoError); } bool ContactsEngine::fetchOOB(const QString &scope, const QString &key, QVariant *value) { QMap values; if (reader()->fetchOOB(scope, QStringList() << key, &values)) { *value = values[key]; return true; } return false; } bool ContactsEngine::fetchOOB(const QString &scope, const QStringList &keys, QMap *values) { return reader()->fetchOOB(scope, keys, values); } bool ContactsEngine::fetchOOB(const QString &scope, QMap *values) { return reader()->fetchOOB(scope, QStringList(), values); } bool ContactsEngine::fetchOOBKeys(const QString &scope, QStringList *keys) { return reader()->fetchOOBKeys(scope, keys); } bool ContactsEngine::storeOOB(const QString &scope, const QString &key, const QVariant &value) { QMap values; values.insert(key, value); return writer()->storeOOB(scope, values); } bool ContactsEngine::storeOOB(const QString &scope, const QMap &values) { return writer()->storeOOB(scope, values); } bool ContactsEngine::removeOOB(const QString &scope, const QString &key) { return writer()->removeOOB(scope, QStringList() << key); } bool ContactsEngine::removeOOB(const QString &scope, const QStringList &keys) { return writer()->removeOOB(scope, keys); } bool ContactsEngine::removeOOB(const QString &scope) { return writer()->removeOOB(scope, QStringList()); } QStringList ContactsEngine::displayLabelGroups() { return database().displayLabelGroups(); } bool ContactsEngine::setContactDisplayLabel(QContact *contact, const QString &label, const QString &group, int sortOrder) { QContactDisplayLabel detail(contact->detail()); bool needSave = false; if (!label.trimmed().isEmpty()) { detail.setLabel(label); needSave = true; } if (!group.trimmed().isEmpty()) { detail.setValue(QContactDisplayLabel__FieldLabelGroup, group); needSave = true; } if (sortOrder >= 0) { detail.setValue(QContactDisplayLabel__FieldLabelGroupSortOrder, sortOrder); needSave = true; } if (needSave) { return contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); } return true; } QString ContactsEngine::normalizedPhoneNumber(const QString &input) { // TODO: Use a configuration variable to specify max characters: static const int maxCharacters = QtContactsSqliteExtensions::DefaultMaximumPhoneNumberCharacters; return QtContactsSqliteExtensions::minimizePhoneNumber(input, maxCharacters); } QString ContactsEngine::synthesizedDisplayLabel(const QContact &contact, QContactManager::Error *error) const { *error = QContactManager::NoError; QContactName name = contact.detail(); // If a custom label has been set, return that const QString customLabel = name.value(QContactName::FieldCustomLabel); if (!customLabel.isEmpty()) return customLabel; QString displayLabel; if (!name.firstName().isEmpty()) displayLabel.append(name.firstName()); if (!name.lastName().isEmpty()) { if (!displayLabel.isEmpty()) displayLabel.append(" "); displayLabel.append(name.lastName()); } if (!displayLabel.isEmpty()) { return displayLabel; } foreach (const QContactNickname& nickname, contact.details()) { if (!nickname.nickname().isEmpty()) { return nickname.nickname(); } } foreach (const QContactGlobalPresence& gp, contact.details()) { if (!gp.nickname().isEmpty()) { return gp.nickname(); } } foreach (const QContactOrganization& organization, contact.details()) { if (!organization.name().isEmpty()) { return organization.name(); } } 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(); } *error = QContactManager::UnspecifiedError; return QString(); } static QList idList(const QVector &contactIds, const QString &manager_uri) { QList ids; ids.reserve(contactIds.size()); foreach (quint32 dbId, contactIds) { ids.append(ContactId::apiId(dbId, manager_uri)); } return ids; } static QList collectionIdList(const QVector &collectionIds, const QString &manager_uri) { QList ids; ids.reserve(collectionIds.size()); foreach (quint32 dbId, collectionIds) { ids.append(ContactCollectionId::apiId(dbId, manager_uri)); } return ids; } void ContactsEngine::_q_collectionsAdded(const QVector &collectionIds) { emit collectionsAdded(collectionIdList(collectionIds, m_managerUri)); } void ContactsEngine::_q_collectionsChanged(const QVector &collectionIds) { emit collectionsChanged(collectionIdList(collectionIds, m_managerUri)); } void ContactsEngine::_q_collectionsRemoved(const QVector &collectionIds) { emit collectionsRemoved(collectionIdList(collectionIds, m_managerUri)); } void ContactsEngine::_q_contactsAdded(const QVector &contactIds) { emit contactsAdded(idList(contactIds, m_managerUri)); } void ContactsEngine::_q_contactsChanged(const QVector &contactIds) { // TODO: also emit the detail types.. emit contactsChanged(idList(contactIds, m_managerUri), QList()); } void ContactsEngine::_q_contactsPresenceChanged(const QVector &contactIds) { if (m_mergePresenceChanges) { // TODO: also emit the detail types.. emit contactsChanged(idList(contactIds, m_managerUri), QList()); } else { emit contactsPresenceChanged(idList(contactIds, m_managerUri)); } } void ContactsEngine::_q_collectionContactsChanged(const QVector &collectionIds) { emit collectionContactsChanged(collectionIdList(collectionIds, m_managerUri)); } void ContactsEngine::_q_displayLabelGroupsChanged() { emit displayLabelGroupsChanged(displayLabelGroups()); } void ContactsEngine::_q_contactsRemoved(const QVector &contactIds) { emit contactsRemoved(idList(contactIds, m_managerUri)); } void ContactsEngine::_q_selfContactIdChanged(quint32 oldId, quint32 newId) { emit selfContactIdChanged(ContactId::apiId(oldId, m_managerUri), ContactId::apiId(newId, m_managerUri)); } void ContactsEngine::_q_relationshipsAdded(const QVector &contactIds) { emit relationshipsAdded(idList(contactIds, m_managerUri)); } void ContactsEngine::_q_relationshipsRemoved(const QVector &contactIds) { emit relationshipsRemoved(idList(contactIds, m_managerUri)); } ContactsDatabase &ContactsEngine::database() { if (!m_database) { QString dbId(QStringLiteral("qtcontacts-sqlite%1-%2")); dbId = dbId.arg(m_autoTest ? QStringLiteral("-test") : QString()).arg(databaseUuid()); m_database.reset(new ContactsDatabase(this)); if (!m_database->open(dbId, m_nonprivileged, m_autoTest, true)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to open synchronous engine database connection")); } else if (!m_nonprivileged && !regenerateAggregatesIfNeeded()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to regenerate aggregates after schema upgrade")); } } return *m_database; } bool ContactsEngine::regenerateAggregatesIfNeeded() { QContactManager::Error err = QContactManager::NoError; QContactCollectionFilter aggregatesFilter, localsFilter; aggregatesFilter.setCollectionId(QContactCollectionId(m_managerUri, QByteArrayLiteral("col-1"))); localsFilter.setCollectionId(QContactCollectionId(m_managerUri, QByteArrayLiteral("col-2"))); const QList aggregateIds = contactIds(aggregatesFilter, QList(), &err); if (err != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read aggregate contact ids during attempt to regenerate aggregates")); return false; } if (!aggregateIds.isEmpty()) { // if we already have aggregates, then aggregates must // have been regenerated already. return true; } const QList localIds = contactIds(localsFilter, QList(), &err); if (err != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read local contact ids during attempt to regenerate aggregates")); return false; } if (localIds.isEmpty()) { // no local contacts in database to be aggregated. return true; } // We need to regenerate aggregates for our local contacts, due to // the database schema upgrade from version 20 to version 21. QList localContacts = contacts( localsFilter, QList(), QContactFetchHint(), &err); if (err != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read local contacts during attempt to regenerate aggregates")); return false; } // Simply save them all; this should regenerate aggregates as required. if (!saveContacts(&localContacts, nullptr, &err)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to save local contacts during attempt to regenerate aggregates")); return false; } return true; } ContactReader *ContactsEngine::reader() const { if (!m_synchronousReader) { m_synchronousReader.reset(new ContactReader(const_cast(this)->database(), const_cast(this)->managerUri())); } return m_synchronousReader.data(); } ContactWriter *ContactsEngine::writer() { if (!m_synchronousWriter) { m_synchronousWriter.reset(new ContactWriter(*this, database(), m_notifier.data(), reader())); } return m_synchronousWriter.data(); } qtcontacts-sqlite-0.3.19/src/engine/contactsengine.h000066400000000000000000000264331436373107600225210ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2019 Jolla Ltd. * Copyright (c) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTSSQLITE_CONTACTSENGINE #define QTCONTACTSSQLITE_CONTACTSENGINE #include "contactmanagerengine.h" #include #include #include #include #include #include #include "contactsdatabase.h" #include "contactnotifier.h" #include "contactreader.h" #include "contactwriter.h" // QList is widely used in qtpim Q_DECLARE_METATYPE(QList) QTCONTACTS_USE_NAMESPACE // Force an ambiguity with QContactDetail::operator== so that we can't call it // It does not compare correctly if the values contains QList inline void operator==(const QContactDetail &, const QContactDetail &) {} class JobThread; class ContactsEngine : public QtContactsSqliteExtensions::ContactManagerEngine { Q_OBJECT public: ContactsEngine(const QString &name, const QMap ¶meters); ~ContactsEngine(); QContactManager::Error open(); QString managerName() const override; QMap managerParameters() const override; QMap idInterpretationParameters() const override; int managerVersion() const override; QList contactIds( const QContactFilter &filter, const QList &sortOrders, QContactManager::Error* error) const override; QList contacts( const QList &localIds, const QContactFetchHint &fetchHint, QMap *errorMap, QContactManager::Error *error) const override; QContact contact( const QContactId &contactId, const QContactFetchHint &fetchHint, QContactManager::Error* error) const override; QList contacts( const QContactFilter &filter, const QList &sortOrders, const QContactFetchHint &fetchHint, QContactManager::Error* error) const override; QList contacts( const QContactFilter &filter, const QList &sortOrders, const QContactFetchHint &fetchHint, QMap *errorMap, QContactManager::Error *error) const; bool saveContacts( QList *contacts, QMap *errorMap, QContactManager::Error *error) override; bool saveContacts( QList *contacts, const ContactWriter::DetailList &definitionMask, QMap *errorMap, QContactManager::Error *error) override; bool removeContact(const QContactId& contactId, QContactManager::Error* error); bool removeContacts( const QList &contactIds, QMap *errorMap, QContactManager::Error* error) override; QContactId selfContactId(QContactManager::Error* error) const override; bool setSelfContactId(const QContactId& contactId, QContactManager::Error* error) override; QList relationships( const QString &relationshipType, const QContactId &participantId, QContactRelationship::Role role, QContactManager::Error *error) const override; bool saveRelationships( QList *relationships, QMap *errorMap, QContactManager::Error *error) override; bool removeRelationships( const QList &relationships, QMap *errorMap, QContactManager::Error *error) override; QContactCollectionId defaultCollectionId() const override; QContactCollection collection(const QContactCollectionId &collectionId, QContactManager::Error *error) const override; QList collections(QContactManager::Error *error) const override; bool saveCollection(QContactCollection *collection, QContactManager::Error *error) override; bool removeCollection(const QContactCollectionId &collectionId, QContactManager::Error *error) override; bool saveCollections(QList *collections, QMap *errorMap, QContactManager::Error *error); // non-override. bool removeCollections(const QList &collectionIds, QMap *errorMap, QContactManager::Error *error); // non-override. void requestDestroyed(QContactAbstractRequest* req) override; void requestDestroyed(QObject* request) override; bool startRequest(QContactAbstractRequest* req) override; bool startRequest(QContactDetailFetchRequest* request) override; bool startRequest(QContactCollectionChangesFetchRequest* request) override; bool startRequest(QContactChangesFetchRequest* request) override; bool startRequest(QContactChangesSaveRequest* request) override; bool startRequest(QContactClearChangeFlagsRequest* request) override; bool cancelRequest(QContactAbstractRequest* req) override; bool cancelRequest(QObject* request) override; bool waitForRequestFinished(QContactAbstractRequest* req, int msecs) override; bool waitForRequestFinished(QObject* req, int msecs) override; bool isRelationshipTypeSupported(const QString &relationshipType, QContactType::TypeValues contactType) const override; QList supportedContactTypes() const override; void regenerateDisplayLabel(QContact &contact, bool *emitDisplayLabelGroupChange); bool clearChangeFlags(const QList &contactIds, QContactManager::Error *error) override; bool clearChangeFlags(const QContactCollectionId &collectionId, QContactManager::Error *error) override; bool fetchCollectionChanges(int accountId, const QString &applicationName, QList *addedCollections, QList *modifiedCollections, QList *deletedCollections, QList *unmodifiedCollections, QContactManager::Error *error) override; bool fetchContactChanges(const QContactCollectionId &collectionId, QList *addedContacts, QList *modifiedContacts, QList *deletedContacts, QList *unmodifiedContacts, QContactManager::Error *error) override; bool storeChanges(QHash * /* added contacts */> *addedCollections, QHash * /* added/modified/deleted contacts */> *modifiedCollections, const QList &deletedCollections, ConflictResolutionPolicy conflictResolutionPolicy, bool clearChangeFlags, QContactManager::Error *error) override; bool fetchOOB(const QString &scope, const QString &key, QVariant *value) override; bool fetchOOB(const QString &scope, const QStringList &keys, QMap *values) override; bool fetchOOB(const QString &scope, QMap *values) override; bool fetchOOBKeys(const QString &scope, QStringList *keys) override; bool storeOOB(const QString &scope, const QString &key, const QVariant &value) override; bool storeOOB(const QString &scope, const QMap &values) override; bool removeOOB(const QString &scope, const QString &key) override; bool removeOOB(const QString &scope, const QStringList &keys) override; bool removeOOB(const QString &scope) override; QStringList displayLabelGroups() override; QString synthesizedDisplayLabel(const QContact &contact, QContactManager::Error *error) const; static bool setContactDisplayLabel(QContact *contact, const QString &label, const QString &group, int sortOrder); static QString normalizedPhoneNumber(const QString &input); private slots: void _q_collectionsAdded(const QVector &collectionIds); void _q_collectionsChanged(const QVector &collectionIds); void _q_collectionsRemoved(const QVector &collectionIds); void _q_collectionContactsChanged(const QVector &collectionIds); void _q_contactsChanged(const QVector &contactIds); void _q_contactsPresenceChanged(const QVector &contactIds); void _q_contactsAdded(const QVector &contactIds); void _q_contactsRemoved(const QVector &contactIds); void _q_selfContactIdChanged(quint32,quint32); void _q_relationshipsAdded(const QVector &contactIds); void _q_relationshipsRemoved(const QVector &contactIds); void _q_displayLabelGroupsChanged(); private: bool regenerateAggregatesIfNeeded(); QString databaseUuid(); ContactsDatabase &database(); ContactReader *reader() const; ContactWriter *writer(); QString m_databaseUuid; const QString m_name; QMap m_parameters; QString m_managerUri; QScopedPointer m_database; mutable QScopedPointer m_synchronousReader; QScopedPointer m_synchronousWriter; QScopedPointer m_notifier; QScopedPointer m_jobThread; Q_DISABLE_COPY(ContactsEngine); }; #endif qtcontacts-sqlite-0.3.19/src/engine/contactsplugin.cpp000066400000000000000000000053131436373107600230770ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * 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 "contactsengine.h" #include #include QTCONTACTS_USE_NAMESPACE class ContactsFactory : public QContactManagerEngineFactory { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QContactManagerEngineFactoryInterface" FILE "plugin.json") public: ContactsFactory(); QContactManagerEngine *engine( const QMap ¶meters, QContactManager::Error* error); QString managerName() const; }; ContactsFactory::ContactsFactory() { } QContactManagerEngine *ContactsFactory::engine( const QMap ¶meters, QContactManager::Error* error) { ContactsEngine *engine = new ContactsEngine(managerName(), parameters); QContactManager::Error err = engine->open(); if (error) *error = err; if (err != QContactManager::NoError) { delete engine; return 0; } else { return engine; } } QString ContactsFactory::managerName() const { return QString::fromLatin1("org.nemomobile.contacts.sqlite"); } #include "contactsplugin.moc" qtcontacts-sqlite-0.3.19/src/engine/contactstransientstore.cpp000066400000000000000000000666731436373107600247050ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Contact: Matt Vogt * * 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 "contactstransientstore.h" #include "memorytable_p.h" #include "semaphore_p.h" #include "trace_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class SharedMemoryManager { // Maintain a connection to a shared memory region containing table data, and a MemoryTable addressing that data struct SharedMemoryTable { explicit SharedMemoryTable(QSharedPointer region) : m_region(region) , m_table(m_region->data(), m_region->size(), false) { } ~SharedMemoryTable() { } QSharedPointer m_region; MemoryTable m_table; }; public: typedef std::tr1::function Function; SharedMemoryManager() : m_mutex(QMutex::Recursive) { } // This handle references a shared memory table - it keeps it alive while the client // holds a reference, and unlocks it on closure, if required. struct TableHandle { TableHandle() : m_table(0) { } explicit TableHandle(QSharedPointer table, Function release = Function()) : m_table(table) , m_release(release) { } ~TableHandle() { if (m_release) m_release(); } bool isNull() const { return m_table.isNull(); } operator bool() const { return !isNull(); } MemoryTable *operator->() { return &(m_table->m_table); } const MemoryTable *operator->() const { return &(m_table->m_table); } operator const MemoryTable *() const { return &(m_table->m_table); } private: QSharedPointer m_table; Function m_release; }; bool open(const QString &identifier, bool createIfNecessary, bool reinitialize); TableHandle table(const QString &identifier); TableHandle reallocateTable(const QString &identifier); private: // For each database (privileged/nonprivileged), we have a shared memory region that holds the data, // and another with a fixed key, that contains the identifier needed to access the data region. If the // data region is exhausted, a new region is allocated, the data is copied, and the table is updated // reference the new region. The key region is updated to refer to the new region. struct TableData { TableData(QSharedPointer keyRegion, QSharedPointer dataTable, quint32 generation) : m_keyRegion(keyRegion) , m_dataTable(dataTable) , m_generation(generation) { } ~TableData() { } QSharedPointer m_keyRegion; QSharedPointer m_dataTable; quint32 m_generation; }; struct SemaphoreLock { explicit SemaphoreLock(Function release) : m_release(release) { } ~SemaphoreLock() { if (m_release) m_release(); } operator bool() const { return m_release != nullptr; } private: Function m_release; }; static const quint32 keyDataFormatVersion = 1; static const quint32 initialGeneration = 1; static const int keyIndex = 0; static const int dataIndex = 1; QString getNativeIdentifier(const QString &identifier, bool createIfNecessary) const; quint32 getRegionGeneration(QSharedPointer keyRegion) const; void setRegionGeneration(QSharedPointer keyRegion, quint32 regionGeneration); QSharedPointer getDataRegion(const QString &identifier, quint32 generation, bool createIfNecessary, size_t dataSize = 0, bool reinitialize = false) const; enum { DefaultWaitMs = 5000 }; Function lockKeyRegion() const; Function lockDataRegion(int waitMs = DefaultWaitMs) const; Function acquire(int index, int waitMs = DefaultWaitMs) const; void release(int index) const; QMap m_tables; QScopedPointer m_semaphore; QMutex m_mutex; }; Q_GLOBAL_STATIC(SharedMemoryManager, sharedMemory); bool SharedMemoryManager::open(const QString &identifier, bool createIfNecessary, bool reinitialize) { QMutexLocker threadLock(&m_mutex); // Is this region already open? if (m_tables.contains(identifier)) return true; const QString semaphoreToken(getNativeIdentifier(identifier + QStringLiteral("-semaphore"), true)); if (semaphoreToken.isEmpty()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to create semaphore token for %1") .arg(identifier)); return false; } // Create semaphores to be able to lock the key and data region const int initialSemaphoreValues[] = { 1, 1 }; m_semaphore.reset(new Semaphore(semaphoreToken.toLatin1(), 2, initialSemaphoreValues)); if (!m_semaphore) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to create semaphore for %1") .arg(identifier)); return false; } const QString nativeKey(getNativeIdentifier(identifier, true)); if (nativeKey.isEmpty()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to create key token for %1") .arg(identifier)); return false; } // Attach to the memory region where the key to the data region is stored QSharedPointer keyRegion(new QSharedMemory()); keyRegion->setNativeKey(nativeKey); // Table reallocation requires the process holding the data lock to acquire the key lock, // while we need to acquire the locks in the other order. To avoid potential deadlock, // give up and try again if we can't acquire the data lock while holding the key lock int lockAttempts = 0; while (true) { // Lock the region, so that only one process can find the region nonexisting SemaphoreLock keyLock(lockKeyRegion()); if (!keyLock) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to lock key memory region for %1") .arg(identifier)); return false; } if (!keyRegion->isAttached() && !keyRegion->attach()) { if (keyRegion->error() != QSharedMemory::NotFound || !createIfNecessary) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to attach key memory region for %1: %2") .arg(identifier).arg(keyRegion->errorString())); return false; } // Allow far more space than we need in the key region, in case we want to use it for something else const int keyRegionSize = 512; if (!keyRegion->create(keyRegionSize)) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to create key memory region for %1: %2") .arg(identifier).arg(keyRegion->errorString())); return false; } else { // Write the key details to the key region setRegionGeneration(keyRegion, initialGeneration); } } // Find the details from the key region const QByteArray keyData(QByteArray::fromRawData(reinterpret_cast(keyRegion->data()), keyRegion->size())); QDataStream is(keyData); quint32 formatVersion; quint32 regionGeneration; is >> formatVersion; if (formatVersion == keyDataFormatVersion) { is >> regionGeneration; } else { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Invalid key data format in key region for %1: %2") .arg(identifier).arg(formatVersion)); return false; } // We need the data lock in order to validate the data region SemaphoreLock dataLock(lockDataRegion(100)); if (!dataLock) { if (++lockAttempts >= 50) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to lock data memory region during open for %1") .arg(identifier)); return false; } else if ((lockAttempts % 10) == 0) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to lock data memory region for %1 after %2 attempts") .arg(identifier).arg(lockAttempts)); } continue; } // Try to open the data region // What size should we use? Using an estimate of 512 bytes per contact, we could store about 2K contacts in a 1M region const int memoryRegionSize = 1024 * 1024; QSharedPointer dataRegion(getDataRegion(identifier, regionGeneration, true, memoryRegionSize, reinitialize)); if (!dataRegion || !dataRegion->isAttached()) return false; QSharedPointer dataTable(new SharedMemoryTable(dataRegion)); // Store our handle to this table TableData tableData(keyRegion, dataTable, regionGeneration); m_tables.insert(identifier, tableData); return true; } } SharedMemoryManager::TableHandle SharedMemoryManager::table(const QString &identifier) { QMutexLocker threadLock(&m_mutex); // Table reallocation requires the process holding the data lock to acquire the key lock, // while we need to acquire the locks in the other order. To avoid potential deadlock, // give up and try again if we can't acquire the data lock while holding the key lock int lockAttempts = 0; while (true) { SemaphoreLock keyLock(lockKeyRegion()); if (!keyLock) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to lock key memory region for %1") .arg(identifier)); return TableHandle(); } QMap::iterator it = m_tables.find(identifier); if (it == m_tables.end()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot open unknown shared memory table: %1") .arg(identifier)); return TableHandle(); } TableData &tableData(*it); // Find the current generation of the table quint32 regionGeneration = getRegionGeneration(tableData.m_keyRegion); // Lock the data region Function dataRelease(lockDataRegion(100)); if (!dataRelease) { if (++lockAttempts >= 50) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to lock data region for table access for %1") .arg(identifier)); return TableHandle(); } else if ((lockAttempts % 10) == 0) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to lock data region for table access for %1 after %2 attempts") .arg(identifier).arg(lockAttempts)); } continue; } // Release the data lock if we can't yield an initialized table handle struct Cleanup { Function release; bool active; Cleanup(Function f) : release(f), active(true) {} ~Cleanup() { if (active) release(); } } cleanup(dataRelease); if (regionGeneration != tableData.m_generation) { // We need to attach to the new version of the table QSharedPointer newRegion(getDataRegion(identifier, regionGeneration, false)); if (!newRegion || !newRegion->isAttached()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to attach to new data region for %1") .arg(identifier)); return TableHandle(); } QSharedPointer dataTable(new SharedMemoryTable(newRegion)); // Update the table with the new region tableData.m_generation = regionGeneration; tableData.m_dataTable = dataTable; } // The handle will release the lock on destruction cleanup.active = false; return TableHandle(tableData.m_dataTable, dataRelease); } } SharedMemoryManager::TableHandle SharedMemoryManager::reallocateTable(const QString &identifier) { QMutexLocker threadLock(&m_mutex); QMap::iterator it = m_tables.find(identifier); if (it == m_tables.end()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot reallocate unknown table: %1") .arg(identifier)); return TableHandle(); } TableData &tableData(*it); // We already hold the data lock, we need the key lock also SemaphoreLock keyLock(lockKeyRegion()); if (!keyLock) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to lock key memory region for %1") .arg(identifier)); return TableHandle(); } quint32 nextGeneration = tableData.m_generation + 1; int nextSize = tableData.m_dataTable->m_region->size() * 2; // We need to create a new, bigger region to migrate the table to QSharedPointer nextRegion(getDataRegion(identifier, nextGeneration, true, nextSize, false)); if (!nextRegion) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot allocate new shared memory region for table: %1 %2 %3") .arg(identifier).arg(nextGeneration).arg(nextSize)); return TableHandle(); } // Create a new table for the new memory region QSharedPointer nextDataTable(new SharedMemoryTable(nextRegion)); // Migrate the existing data to the new region MemoryTable::Error error = tableData.m_dataTable->m_table.migrateTo(nextDataTable->m_table); if (error != MemoryTable::NoError) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot migrate to new shared memory region for table: %1") .arg(identifier)); return TableHandle(); } // Update the key region to store the new generation value setRegionGeneration(tableData.m_keyRegion, nextGeneration); // Replace the old table with the new table tableData.m_dataTable = nextDataTable; tableData.m_generation = nextGeneration; // Return an unlocked handle to the next table (the handle to the predecessor table is still active) return TableHandle(tableData.m_dataTable); } QString SharedMemoryManager::getNativeIdentifier(const QString &identifier, bool createIfNecessary) const { // Despite the documentation, QSharedMemory on unix needs the identifier to be the path // of an existing file, in order to use ftok. Create a file to use, if necessary QString path(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + identifier); if (!QFile::exists(path)) { if (createIfNecessary) { // Try to create this file QFile pathFile; pathFile.setFileName(path); pathFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ReadGroup | QFileDevice::WriteGroup); if (!pathFile.open(QIODevice::WriteOnly)) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to create native lock file %1: %2") .arg(identifier).arg(path)); path = QString(); } else { pathFile.close(); } } else { path = QString(); } } return path; } quint32 SharedMemoryManager::getRegionGeneration(QSharedPointer keyRegion) const { // Extract the current generation from the key region const QByteArray keyData(QByteArray::fromRawData(reinterpret_cast(keyRegion->data()), keyRegion->size())); QDataStream is(keyData); quint32 regionGeneration = 0; quint32 formatVersion; is >> formatVersion; if (formatVersion == 1) { is >> regionGeneration; } return regionGeneration; } void SharedMemoryManager::setRegionGeneration(QSharedPointer keyRegion, quint32 regionGeneration) { QByteArray keyData; QDataStream os(&keyData, QIODevice::WriteOnly); os << keyDataFormatVersion << regionGeneration; std::memcpy(keyRegion->data(), keyData.constData(), keyData.size()); } QSharedPointer SharedMemoryManager::getDataRegion(const QString &identifier, quint32 generation, bool createIfNecessary, size_t dataSize, bool reinitialize) const { // We must hold the data lock before calling this function const QString dataIdentifier(QStringLiteral("%1-data-%2").arg(identifier).arg(generation)); const QString nativeKey(getNativeIdentifier(dataIdentifier, true)); if (nativeKey.isEmpty()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to open token file: %1").arg(dataIdentifier)); return QSharedPointer(); } QSharedPointer memoryRegion(new QSharedMemory()); memoryRegion->setNativeKey(nativeKey); bool attached = memoryRegion->attach(); if (!attached && (memoryRegion->error() == QSharedMemory::NotFound) && (generation > initialGeneration)) { // It's possible that the current generation has been destroyed, but the previous generation // is still active; try to connect to that. We could fall back all the way to the initial // generation, but the possible benefit rapidly decreases... const QString previousIdentifier(QStringLiteral("%1-data-%2").arg(identifier).arg(generation - 1)); const QString previousKey(getNativeIdentifier(previousIdentifier, false)); if (!previousKey.isEmpty()) { memoryRegion->setNativeKey(previousKey); attached = memoryRegion->attach(); } } if (!attached) { // Only the initial process can create the key region if (memoryRegion->error() != QSharedMemory::NotFound || !createIfNecessary) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to attach data memory region for %1: %2") .arg(dataIdentifier).arg(memoryRegion->errorString())); return memoryRegion; } memoryRegion->setNativeKey(nativeKey); if (!memoryRegion->create(dataSize)) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to create data memory region for %1 (%2): %3") .arg(dataIdentifier).arg(dataSize).arg(memoryRegion->errorString())); return memoryRegion; } // Initialize the data region as a MemoryTable MemoryTable mt(memoryRegion->data(), memoryRegion->size(), true); if (!mt.isValid()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to initialize table in data memory region for %1") .arg(dataIdentifier)); memoryRegion->detach(); } } else { // Verify that the region contains a valid memory table, or reinitialize if required MemoryTable mt(memoryRegion->data(), memoryRegion->size(), reinitialize); if (!mt.isValid()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Failed to initialize table in existing data memory region for %1") .arg(dataIdentifier)); memoryRegion->detach(); } } return memoryRegion; } SharedMemoryManager::Function SharedMemoryManager::lockKeyRegion() const { return acquire(keyIndex); } SharedMemoryManager::Function SharedMemoryManager::lockDataRegion(int waitMs) const { return acquire(dataIndex, waitMs); } SharedMemoryManager::Function SharedMemoryManager::acquire(int index, int waitMs) const { if (m_semaphore) { if (m_semaphore->decrement(index, true, waitMs)) { return std::tr1::bind(&SharedMemoryManager::release, this, index); } } return Function(); } void SharedMemoryManager::release(int index) const { if (m_semaphore) { if (index >= keyIndex && index <= dataIndex) { m_semaphore->increment(index); } else { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Invalid index to release: %1").arg(index)); } } } ContactsTransientStore::const_iterator::const_iterator(const MemoryTable *table, quint32 position) : MemoryTable::const_iterator(table, position) { } ContactsTransientStore::const_iterator::const_iterator(const const_iterator &other) : MemoryTable::const_iterator(other) { } ContactsTransientStore::const_iterator &ContactsTransientStore::const_iterator::operator=(const const_iterator &other) { MemoryTable::const_iterator::operator=(other); return *this; } quint32 ContactsTransientStore::const_iterator::key() { return MemoryTable::const_iterator::key(); } class DataLockPrivate { public: SharedMemoryManager::TableHandle m_table; DataLockPrivate(SharedMemoryManager::TableHandle table) : m_table(table) { } }; ContactsTransientStore::DataLock::DataLock(DataLockPrivate *p) : lock(p) { } ContactsTransientStore::DataLock::~DataLock() { } ContactsTransientStore::DataLock::DataLock(const DataLock &other) { *this = other; } ContactsTransientStore::DataLock &ContactsTransientStore::DataLock::operator=(const DataLock &other) { lock = other.lock; return *this; } ContactsTransientStore::DataLock::operator bool() const { return lock->m_table; } QPair > ContactsTransientStore::const_iterator::value() { const QByteArray data(MemoryTable::const_iterator::value()); if (!data.isEmpty()) { QDataStream is(data); QDateTime dt; QList details; is >> dt >> details; return qMakePair(dt, details); } return qMakePair(QDateTime(), QList()); } ContactsTransientStore::ContactsTransientStore() { } ContactsTransientStore::~ContactsTransientStore() { } bool ContactsTransientStore::open(bool nonprivileged, bool createIfNecessary, bool reinitialize) { const QString identifier(nonprivileged ? QStringLiteral("qtcontacts-sqlite-np") : QStringLiteral("qtcontacts-sqlite")); if (!m_identifier.isNull()) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot re-open active transient store: %1 (%2)") .arg(identifier).arg(m_identifier)); return false; } if (sharedMemory()->open(identifier, createIfNecessary, reinitialize)) { m_identifier = identifier; return true; } return false; } bool ContactsTransientStore::contains(quint32 contactId) const { const SharedMemoryManager::TableHandle table(sharedMemory()->table(m_identifier)); if (table) { return table->contains(contactId); } return false; } QPair > ContactsTransientStore::contactDetails(quint32 contactId) const { const SharedMemoryManager::TableHandle table(sharedMemory()->table(m_identifier)); if (table) { const QByteArray data(table->value(contactId)); if (!data.isEmpty()) { QDataStream is(data); QDateTime dt; QList details; is >> dt >> details; return qMakePair(dt, details); } } return qMakePair(QDateTime(), QList()); } bool ContactsTransientStore::setContactDetails(quint32 contactId, const QDateTime ×tamp, const QList &details) { SharedMemoryManager::TableHandle table(sharedMemory()->table(m_identifier)); if (table) { QByteArray data; QDataStream os(&data, QIODevice::WriteOnly); os << timestamp << details; MemoryTable::Error err = table->insert(contactId, data); if (err == MemoryTable::InsufficientSpace) { // Reallocate the table to provide more space SharedMemoryManager::TableHandle newTable(sharedMemory()->reallocateTable(m_identifier)); if (newTable) { // Perform the write to the new table err = newTable->insert(contactId, data); } else { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot reallocate exhausted transient store: %1") .arg(m_identifier)); return false; } } if (err == MemoryTable::NoError) return true; QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot store contact details to transient store: %1") .arg(m_identifier)); } return false; } bool ContactsTransientStore::remove(quint32 contactId) { SharedMemoryManager::TableHandle table(sharedMemory()->table(m_identifier)); if (table) { return table->remove(contactId); } return false; } bool ContactsTransientStore::remove(const QList &contactIds) { SharedMemoryManager::TableHandle table(sharedMemory()->table(m_identifier)); if (table) { bool removed(false); foreach (quint32 contactId, contactIds) { removed |= table->remove(contactId); } return removed; } return false; } ContactsTransientStore::DataLock ContactsTransientStore::dataLock() const { SharedMemoryManager::TableHandle table(sharedMemory()->table(m_identifier)); return DataLock(new DataLockPrivate(table)); } ContactsTransientStore::const_iterator ContactsTransientStore::constBegin(const DataLock &lock) const { if (!lock) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot iterate over unlocked data: %1") .arg(m_identifier)); return const_iterator(0, 0); } const MemoryTable *tablePtr(lock.lock->m_table); return const_iterator(tablePtr, 0); } ContactsTransientStore::const_iterator ContactsTransientStore::constEnd(const DataLock &lock) const { if (!lock) { QTCONTACTS_SQLITE_WARNING(QStringLiteral("Cannot iterate over unlocked data: %1") .arg(m_identifier)); return const_iterator(0, 0); } const MemoryTable *tablePtr(lock.lock->m_table); return const_iterator(tablePtr, tablePtr->count()); } qtcontacts-sqlite-0.3.19/src/engine/contactstransientstore.h000066400000000000000000000064701436373107600243370ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Contact: Matt Vogt * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef __CONTACTSTRANSIENTSTORE_H__ #define __CONTACTSTRANSIENTSTORE_H__ #include "memorytable_p.h" #include #include #include #include QTCONTACTS_USE_NAMESPACE class DataLockPrivate; class ContactsTransientStore { public: class const_iterator : public MemoryTable::const_iterator { friend class ContactsTransientStore; protected: const_iterator(const MemoryTable *table, quint32 position); public: const_iterator(const const_iterator &other); const_iterator &operator=(const const_iterator &other); quint32 key(); QPair > value(); }; class DataLock { public: ~DataLock(); DataLock(const DataLock &other); DataLock& operator=(const DataLock &other); operator bool() const; private: friend class ContactsTransientStore; DataLock(DataLockPrivate *); QSharedPointer lock; }; ContactsTransientStore(); ~ContactsTransientStore(); bool open(bool nonprivileged, bool createIfNecessary, bool reinitialize); bool contains(quint32 contactId) const; QPair > contactDetails(quint32 contactId) const; bool setContactDetails(quint32 contactId, const QDateTime ×tamp, const QList &details); bool remove(quint32 contactId); bool remove(const QList &contactId); DataLock dataLock() const; const_iterator constBegin(const DataLock &) const; const_iterator constEnd(const DataLock &) const; private: QString m_identifier; }; #endif qtcontacts-sqlite-0.3.19/src/engine/contactwriter.cpp000066400000000000000000006557211436373107600227500ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2019 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * 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 "contactwriter.h" #include "contactsengine.h" #include "contactreader.h" #include "trace_p.h" #include "../extensions/contactdelta_impl.h" #include "../extensions/qcontactundelete.h" #include "../extensions/qcontactdeactivated.h" #include "../extensions/qcontactstatusflags.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { void dumpContact(const QContact &c) { Q_FOREACH (const QContactDetail &det, c.details()) { dumpContactDetail(det); } } double log2(double n) { const double scale = 1.44269504088896340736; return std::log(n) * scale; } double entropy(QByteArray::const_iterator it, QByteArray::const_iterator end, size_t total) { // Shannon's entropy formula, yields [0..1] (low to high information density) double entropy = 0.0; int frequency[256] = { 0 }; for ( ; it != end; ++it) { frequency[static_cast(*it)] += 1; } for (int i = 0; i < 256; ++i) { if (frequency[i] != 0) { double p = static_cast(frequency[i]) / total; entropy -= p * log2(p); } } return entropy / 8; } } static const QString aggregateSyncTarget(QStringLiteral("aggregate")); static const QString localSyncTarget(QStringLiteral("local")); static const QString wasLocalSyncTarget(QStringLiteral("was_local")); static const QString exportSyncTarget(QStringLiteral("export")); static const QString aggregationIdsTable(QStringLiteral("aggregationIds")); static const QString modifiableContactsTable(QStringLiteral("modifiableContacts")); static const QString syncConstituentsTable(QStringLiteral("syncConstituents")); static const QString syncAggregatesTable(QStringLiteral("syncAggregates")); static const QString possibleAggregatesTable(QStringLiteral("possibleAggregates")); static const QString matchEmailAddressesTable(QStringLiteral("matchEmailAddresses")); static const QString matchPhoneNumbersTable(QStringLiteral("matchPhoneNumbers")); static const QString matchOnlineAccountsTable(QStringLiteral("matchOnlineAccounts")); ContactWriter::ContactWriter(ContactsEngine &engine, ContactsDatabase &database, ContactNotifier *notifier, ContactReader *reader) : m_engine(engine) , m_database(database) , m_notifier(notifier) , m_reader(reader) , m_managerUri(engine.managerUri()) , m_displayLabelGroupsChanged(false) { Q_ASSERT(notifier); Q_ASSERT(reader); } ContactWriter::~ContactWriter() { } bool ContactWriter::beginTransaction() { return m_database.beginTransaction(); } bool ContactWriter::commitTransaction() { if (!m_database.commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Commit error: %1").arg(m_database.lastError().text())); rollbackTransaction(); return false; } if (m_displayLabelGroupsChanged) { m_notifier->displayLabelGroupsChanged(); m_displayLabelGroupsChanged = false; } if (!m_addedCollectionIds.isEmpty()) { m_notifier->collectionsAdded(m_addedCollectionIds.toList()); m_addedCollectionIds.clear(); } if (!m_changedCollectionIds.isEmpty()) { m_notifier->collectionsChanged(m_changedCollectionIds.toList()); m_changedCollectionIds.clear(); } if (!m_addedIds.isEmpty()) { m_notifier->contactsAdded(m_addedIds.toList()); m_addedIds.clear(); } if (!m_changedIds.isEmpty()) { m_notifier->contactsChanged(m_changedIds.toList()); m_changedIds.clear(); } if (!m_presenceChangedIds.isEmpty()) { m_notifier->contactsPresenceChanged(m_presenceChangedIds.toList()); m_presenceChangedIds.clear(); } if (m_suppressedCollectionIds.size()) { QSet collectionContactsChanged = m_collectionContactsChanged; Q_FOREACH (const QContactCollectionId &suppressed, m_suppressedCollectionIds) { collectionContactsChanged.remove(suppressed); } m_collectionContactsChanged = collectionContactsChanged; } m_suppressedCollectionIds.clear(); if (!m_collectionContactsChanged.isEmpty()) { m_notifier->collectionContactsChanged(m_collectionContactsChanged.toList()); m_collectionContactsChanged.clear(); } if (!m_removedIds.isEmpty()) { // Remove any transient data for these obsolete contacts QList removedDbIds; foreach (const QContactId &id, m_removedIds) { removedDbIds.append(ContactId::databaseId(id)); } m_database.removeTransientDetails(removedDbIds); m_notifier->contactsRemoved(m_removedIds.toList()); m_removedIds.clear(); } if (!m_removedCollectionIds.isEmpty()) { m_notifier->collectionsRemoved(m_removedCollectionIds.toList()); m_removedCollectionIds.clear(); } return true; } void ContactWriter::rollbackTransaction() { m_database.rollbackTransaction(); m_addedCollectionIds.clear(); m_changedCollectionIds.clear(); m_removedCollectionIds.clear(); m_removedIds.clear(); m_suppressedCollectionIds.clear(); m_collectionContactsChanged.clear(); m_presenceChangedIds.clear(); m_changedIds.clear(); m_addedIds.clear(); m_displayLabelGroupsChanged = false; } QContactManager::Error ContactWriter::setIdentity(ContactsDatabase::Identity identity, QContactId contactId) { const QString insertIdentity(QStringLiteral("INSERT OR REPLACE INTO Identities (identity, contactId) VALUES (:identity, :contactId)")); const QString removeIdentity(QStringLiteral("DELETE FROM Identities WHERE identity = :identity")); QMutexLocker locker(m_database.accessMutex()); quint32 dbId = ContactId::databaseId(contactId); ContactsDatabase::Query query(m_database.prepare(dbId == 0 ? removeIdentity : insertIdentity)); query.bindValue(0, identity); if (dbId != 0) { query.bindValue(1, dbId); } if (ContactsDatabase::execute(query)) { // Notify.. return QContactManager::NoError; } else { query.reportError(QStringLiteral("Unable to update the identity ID: %1").arg(identity)); return QContactManager::UnspecifiedError; } } // This function is currently unused - but the way we currently build up the // relationships query is hideously inefficient, so in the future we should // rewrite this bindRelationships function and use execBatch(). /* static QContactManager::Error bindRelationships( QSqlQuery *query, const QList &relationships, QMap *errorMap, QSet *contactIds, QMultiMap > *bucketedRelationships, int *removedDuplicatesCount) { QVariantList firstIds; QVariantList secondIds; QVariantList types; *removedDuplicatesCount = 0; for (int i = 0; i < relationships.count(); ++i) { const QContactRelationship &relationship = relationships.at(i); const QContactLocalId firstId = relationship.first().localId(); const QContactLocalId secondId = relationship.second().localId(); const QString &type = relationship.relationshipType(); if (firstId == 0 || secondId == 0) { if (errorMap) errorMap->insert(i, QContactManager::UnspecifiedError); } else if (type.isEmpty()) { if (errorMap) errorMap->insert(i, QContactManager::UnspecifiedError); } else { if (bucketedRelationships->find(firstId, QPair(type, secondId)) != bucketedRelationships->end()) { // this relationship is already represented in our database. // according to the semantics defined in tst_qcontactmanager, // we allow saving duplicates by "overwriting" (with identical values) // which means that we simply "drop" this one from the list // of relationships to add to the database. *removedDuplicatesCount += 1; } else { // this relationships has not yet been represented in our database. firstIds.append(firstId - 1); secondIds.append(secondId - 1); types.append(type); contactIds->insert(firstId); contactIds->insert(secondId); bucketedRelationships->insert(firstId, QPair(type, secondId)); } } } if (firstIds.isEmpty() && *removedDuplicatesCount == 0) { // if we "successfully overwrote" some duplicates, it's not an error. return QContactManager::UnspecifiedError; } if (firstIds.size() == 1) { query->bindValue(0, firstIds.at(0).toUInt()); query->bindValue(1, secondIds.at(0).toUInt()); query->bindValue(2, types.at(0).toString()); } else if (firstIds.size() > 1) { query->bindValue(0, firstIds); query->bindValue(1, secondIds); query->bindValue(2, types); } return QContactManager::NoError; } */ QContactManager::Error ContactWriter::save( const QList &relationships, QMap *errorMap, bool withinTransaction, bool withinAggregateUpdate) { QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); if (relationships.isEmpty()) return QContactManager::NoError; if (!withinTransaction && !beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while saving relationships")); return QContactManager::UnspecifiedError; } QContactManager::Error error = saveRelationships(relationships, errorMap, withinAggregateUpdate); if (error != QContactManager::NoError) { if (!withinTransaction) { // only rollback if we created a transaction. rollbackTransaction(); return error; } } if (!withinTransaction && !commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after relationship save")); return QContactManager::UnspecifiedError; } return QContactManager::NoError; } template QString relationshipString(T type) { return type(); } QContactManager::Error ContactWriter::saveRelationships( const QList &relationships, QMap *errorMap, bool withinAggregateUpdate) { // in order to perform duplicate detection we build up the following datastructure. QMultiMap > bucketedRelationships; // first id to . { const QString existingRelationships(QStringLiteral( " SELECT firstId, secondId, type FROM Relationships" )); ContactsDatabase::Query query(m_database.prepare(existingRelationships)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch existing relationships for duplicate detection during insert"); return QContactManager::UnspecifiedError; } while (query.next()) { quint32 fid = query.value(0); quint32 sid = query.value(1); QString rt = query.value(2); bucketedRelationships.insert(fid, qMakePair(rt, sid)); } } // in order to perform validity detection we build up the following set. // XXX TODO: use foreign key constraint or similar in Relationships table? QSet validContactIds; { const QString existingContactIds(QStringLiteral( " SELECT contactId FROM Contacts WHERE changeFlags < 4" // ChangeFlags::IsDeleted )); ContactsDatabase::Query query(m_database.prepare(existingContactIds)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch existing contacts for validity detection during insert"); return QContactManager::UnspecifiedError; } while (query.next()) { validContactIds.insert(query.value(0)); } } QList firstIdsToBind; QList secondIdsToBind; QList typesToBind; QSet aggregatesAffected; QSqlQuery multiInsertQuery(m_database); QString queryString = QStringLiteral("INSERT INTO Relationships"); int realInsertions = 0; int invalidInsertions = 0; for (int i = 0; i < relationships.size(); ++i) { const QContactRelationship &relationship = relationships.at(i); QContactId first(relationship.first()); QContactId second(relationship.second()); const quint32 firstId = ContactId::databaseId(first); const quint32 secondId = ContactId::databaseId(second); const QString &type = relationship.relationshipType(); if ((firstId == secondId) || (!first.managerUri().isEmpty() && !first.managerUri().startsWith(m_managerUri) ) || (!second.managerUri().isEmpty() && !second.managerUri().startsWith(m_managerUri) ) || (!validContactIds.contains(firstId) || !validContactIds.contains(secondId))) { // invalid contact specified in relationship, don't insert. invalidInsertions += 1; if (errorMap) errorMap->insert(i, QContactManager::InvalidRelationshipError); continue; } if (bucketedRelationships.find(firstId, qMakePair(type, secondId)) != bucketedRelationships.end()) { // duplicate, don't insert. continue; } else { if (realInsertions == 0) { queryString += QStringLiteral("\n SELECT :firstId%1 as firstId, :secondId%1 as secondId, :type%1 as type") .arg(QString::number(realInsertions)); } else { queryString += QStringLiteral("\n UNION SELECT :firstId%1, :secondId%1, :type%1") .arg(QString::number(realInsertions)); } firstIdsToBind.append(firstId); secondIdsToBind.append(secondId); typesToBind.append(type); bucketedRelationships.insert(firstId, qMakePair(type, secondId)); realInsertions += 1; if (m_database.aggregating() && (type == relationshipString(QContactRelationship::Aggregates))) { // This aggregate needs to be regenerated aggregatesAffected.insert(firstId); } } } if (realInsertions > 0 && !multiInsertQuery.prepare(queryString)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare multiple insert relationships query:\n%1\nQuery:\n%2") .arg(multiInsertQuery.lastError().text()) .arg(queryString)); return QContactManager::UnspecifiedError; } for (int i = 0; i < realInsertions; ++i) { multiInsertQuery.bindValue(QStringLiteral(":firstId%1").arg(QString::number(i)), firstIdsToBind.at(i)); multiInsertQuery.bindValue(QStringLiteral(":secondId%1").arg(QString::number(i)), secondIdsToBind.at(i)); multiInsertQuery.bindValue(QStringLiteral(":type%1").arg(QString::number(i)), typesToBind.at(i)); } if (realInsertions > 0 && !ContactsDatabase::execute(multiInsertQuery)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to insert relationships:\n%1\nQuery:\n%2") .arg(multiInsertQuery.lastError().text()) .arg(queryString)); return QContactManager::UnspecifiedError; } if (invalidInsertions > 0) { return QContactManager::InvalidRelationshipError; } if (m_database.aggregating() && !aggregatesAffected.isEmpty() && !withinAggregateUpdate) { QContactManager::Error writeError = regenerateAggregates(aggregatesAffected.toList(), DetailList(), true); if (writeError != QContactManager::NoError) { return writeError; } } return QContactManager::NoError; } QContactManager::Error ContactWriter::remove( const QList &relationships, QMap *errorMap, bool withinTransaction) { QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); if (relationships.isEmpty()) return QContactManager::NoError; if (!withinTransaction && !beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while removing relationships")); return QContactManager::UnspecifiedError; } QContactManager::Error error = removeRelationships(relationships, errorMap); if (error != QContactManager::NoError) { if (!withinTransaction) { // only rollback if we created a transaction. rollbackTransaction(); return error; } } if (!withinTransaction && !commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after relationship removal")); return QContactManager::UnspecifiedError; } return QContactManager::NoError; } QContactManager::Error ContactWriter::removeRelationships( const QList &relationships, QMap *errorMap) { // in order to perform existence detection we build up the following datastructure. QMultiMap > bucketedRelationships; // first id to . { const QString existingRelationships(QStringLiteral( " SELECT firstId, secondId, type FROM Relationships" " WHERE firstId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)" " AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)" // ChangeFlags::IsDeleted )); ContactsDatabase::Query query(m_database.prepare(existingRelationships)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch existing relationships for duplicate detection during insert"); return QContactManager::UnspecifiedError; } while (query.next()) { quint32 fid = query.value(0); quint32 sid = query.value(1); QString rt = query.value(2); bucketedRelationships.insert(fid, qMakePair(rt, sid)); } } QContactManager::Error worstError = QContactManager::NoError; QSet alreadyRemoved; QSet aggregatesAffected; bool removeInvalid = false; for (int i = 0; i < relationships.size(); ++i) { QContactRelationship curr = relationships.at(i); if (alreadyRemoved.contains(curr)) { continue; } quint32 currFirst = ContactId::databaseId(curr.first()); quint32 currSecond = ContactId::databaseId(curr.second()); QString type(curr.relationshipType()); if (bucketedRelationships.find(currFirst, qMakePair(curr.relationshipType(), currSecond)) == bucketedRelationships.end()) { removeInvalid = true; if (errorMap) errorMap->insert(i, QContactManager::DoesNotExistError); continue; } if (m_database.aggregating() && (type == relationshipString(QContactRelationship::Aggregates))) { // This aggregate needs to be regenerated aggregatesAffected.insert(currFirst); } const QString removeRelationship(QStringLiteral( " DELETE FROM Relationships" " WHERE firstId = :firstId AND secondId = :secondId AND type = :type" )); ContactsDatabase::Query query(m_database.prepare(removeRelationship)); query.bindValue(":firstId", currFirst); query.bindValue(":secondId", currSecond); query.bindValue(":type", type); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to remove relationship"); worstError = QContactManager::UnspecifiedError; if (errorMap) errorMap->insert(i, worstError); continue; } alreadyRemoved.insert(curr); } if (removeInvalid) { return QContactManager::DoesNotExistError; } if (m_database.aggregating()) { // remove any aggregates that no longer aggregate any contacts. QList removedIds; QContactManager::Error removeError = removeChildlessAggregates(&removedIds); if (removeError != QContactManager::NoError) return removeError; foreach (const QContactId &id, removedIds) { m_removedIds.insert(id); aggregatesAffected.remove(ContactId::databaseId(id)); } if (!aggregatesAffected.isEmpty()) { QContactManager::Error writeError = regenerateAggregates(aggregatesAffected.toList(), DetailList(), true); if (writeError != QContactManager::NoError) return writeError; } // Some contacts may need to have new aggregates created QContactManager::Error aggregateError = aggregateOrphanedContacts(true, false); if (aggregateError != QContactManager::NoError) return aggregateError; } return QContactManager::NoError; } QContactManager::Error ContactWriter::saveCollection(QContactCollection *collection) { bool collectionExists = ContactCollectionId::isValid(collection->id()); ContactsDatabase::Query query(bindCollectionDetails(*collection)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to save collection"); return QContactManager::UnspecifiedError; } if (!collectionExists) { quint32 collectionId = query.lastInsertId().toUInt(); collection->setId(ContactCollectionId::apiId(collectionId, m_managerUri)); } int extendedMetadataCount = 0; ContactsDatabase::Query metadataQuery(bindCollectionMetadataDetails(*collection, &extendedMetadataCount)); if (extendedMetadataCount > 0 && !ContactsDatabase::executeBatch(metadataQuery)) { query.reportError("Failed to save collection metadata"); return QContactManager::UnspecifiedError; } return QContactManager::NoError; } QContactManager::Error ContactWriter::save( QList *collections, QMap *errorMap, bool withinTransaction, bool withinSyncUpdate) { Q_UNUSED(withinSyncUpdate) // TODO QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); if (!withinTransaction && !beginTransaction()) { // if we are not already within a transaction, create a transaction. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while saving collections")); return QContactManager::UnspecifiedError; } QContactManager::Error ret = QContactManager::NoError; QSet addedIds; QSet changedIds; for (int i = 0; i < collections->size(); ++i) { QContactCollection &collection = (*collections)[i]; // rely on reference stability... bool exists = ContactCollectionId::isValid(collection.id()); QContactManager::Error saveError = QContactManager::NoError; if (exists) { const QString queryCollectionExistence(QStringLiteral( " SELECT COUNT(*) FROM Collections WHERE collectionId = :collectionId" )); ContactsDatabase::Query query(m_database.prepare(queryCollectionExistence)); query.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collection.id())); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to query collection existence"); saveError = QContactManager::UnspecifiedError; } else if (query.next()) { exists = query.value(0) == 1; } } if (saveError == QContactManager::NoError) { saveError = saveCollection(&collection); if (saveError == QContactManager::NoError) { if (exists) { changedIds.insert(collection.id()); } else { addedIds.insert(collection.id()); } } } if (errorMap) { errorMap->insert(i, saveError); } if (saveError != QContactManager::NoError) { ret = saveError; } } if (ret != QContactManager::NoError) { if (!withinTransaction) { // only rollback if we created a transaction. rollbackTransaction(); } } else { foreach (const QContactCollectionId &cid, changedIds) { m_changedCollectionIds.insert(cid); } foreach (const QContactCollectionId &aid, addedIds) { m_addedCollectionIds.insert(aid); } if (!withinTransaction && !commitTransaction()) { // only commit if we created a transaction. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit collection save")); ret = QContactManager::UnspecifiedError; } } return ret; } QContactManager::Error ContactWriter::removeCollection(const QContactCollectionId &collectionId, bool onlyIfFlagged) { const QString removeCollectionMetadataStatement(QStringLiteral( " DELETE FROM CollectionsMetadata WHERE collectionId = :collectionId %1" ).arg(onlyIfFlagged ? QStringLiteral("AND collectionId IN (SELECT collectionId FROM Collections WHERE changeFlags >= 4)") : QString())); // ChangeFlags::IsDeleted ContactsDatabase::Query removeMetadata(m_database.prepare(removeCollectionMetadataStatement)); removeMetadata.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collectionId)); if (!ContactsDatabase::execute(removeMetadata)) { removeMetadata.reportError("Failed to remove collection"); return QContactManager::UnspecifiedError; } const QString removeCollectionStatement(QStringLiteral( " DELETE FROM Collections WHERE collectionId = :collectionId %1" ).arg(onlyIfFlagged ? QStringLiteral("AND changeFlags >= 4") : QString())); // ChangeFlags::IsDeleted ContactsDatabase::Query remove(m_database.prepare(removeCollectionStatement)); remove.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collectionId)); if (!ContactsDatabase::execute(remove)) { remove.reportError("Failed to remove collection"); return QContactManager::UnspecifiedError; } return QContactManager::NoError; } QContactManager::Error ContactWriter::deleteCollection(const QContactCollectionId &collectionId) { const QString deleteCollectionStatement(QStringLiteral( " UPDATE Collections SET" " changeFlags = changeFlags | 4" // ChangeFlags::IsDeleted " WHERE collectionId = :collectionId" )); ContactsDatabase::Query deleteCollection(m_database.prepare(deleteCollectionStatement)); deleteCollection.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collectionId)); if (!ContactsDatabase::execute(deleteCollection)) { deleteCollection.reportError("Failed to delete collection"); return QContactManager::UnspecifiedError; } const QString deleteCollectionContactsStatement(QStringLiteral( " UPDATE Contacts SET" " changeFlags = changeFlags | 4," // ChangeFlags::IsDeleted " deleted = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')" " WHERE collectionId = :collectionId" )); ContactsDatabase::Query deleteCollectionContacts(m_database.prepare(deleteCollectionContactsStatement)); deleteCollectionContacts.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collectionId)); if (!ContactsDatabase::execute(deleteCollectionContacts)) { deleteCollectionContacts.reportError("Failed to delete collection contacts"); return QContactManager::UnspecifiedError; } return QContactManager::NoError; } QContactManager::Error ContactWriter::remove( const QList &collectionIds, QMap *errorMap, bool withinTransaction, bool withinSyncUpdate) { QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); if (!withinTransaction && !beginTransaction()) { // if we are not already within a transaction, create a transaction. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while removing collections")); return QContactManager::UnspecifiedError; } QContactManager::Error ret = QContactManager::NoError; QSet removedContactIds; QSet removedCollectionIds; for (int i = 0; i < collectionIds.size(); ++i) { const QContactCollectionId &collectionId(collectionIds[i]); if (ContactCollectionId::databaseId(collectionId) <= ContactsDatabase::LocalAddressbookCollectionId) { // don't allow removing the built-in collections. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to remove built-in collections")); ret = QContactManager::BadArgumentError; } else { QContactManager::Error removeError = QContactManager::NoError; QList collectionContacts; const QString queryContactIds(QStringLiteral( " SELECT ContactId FROM Contacts WHERE collectionId = :collectionId AND changeFlags < 4" // ChangeFlags::IsDeleted )); ContactsDatabase::Query query(m_database.prepare(queryContactIds)); query.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collectionId)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to query collection contacts"); removeError = QContactManager::UnspecifiedError; } else while (query.next()) { collectionContacts.append(ContactId::apiId(query.value(0), m_managerUri)); } if (removeError == QContactManager::NoError) { removeError = remove(collectionContacts, nullptr, true, withinSyncUpdate); if (removeError != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to remove contacts while removing collection")); } else { foreach (const QContactId &rid, collectionContacts) { removedContactIds.insert(rid); } removeError = deleteCollection(collectionId); if (removeError == QContactManager::NoError) { removedCollectionIds.insert(collectionId); } } } if (errorMap) { errorMap->insert(i, removeError); } if (removeError != QContactManager::NoError) { ret = removeError; } } } if (ret != QContactManager::NoError) { if (!withinTransaction) { // only rollback if we created a transaction. rollbackTransaction(); } } else { foreach (const QContactId &rid, removedContactIds) { m_removedIds.insert(rid); } foreach (const QContactCollectionId &cid, removedCollectionIds) { m_removedCollectionIds.insert(cid); } if (!withinTransaction && !commitTransaction()) { // only commit if we created a transaction. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit collection removal")); return QContactManager::UnspecifiedError; } } return ret; } QContactManager::Error ContactWriter::removeContacts(const QVariantList &ids, bool onlyIfFlagged) { const QString removeContact(QStringLiteral( " DELETE FROM Contacts WHERE contactId = :contactId %1" ).arg(onlyIfFlagged ? QStringLiteral("AND changeFlags >= 4 AND unhandledChangeFlags < 4") // ChangeFlags::IsDeleted : QString())); // do it in batches, otherwise the query can fail due to too many bound values. for (int i = 0; i < ids.size(); i += 167) { const QVariantList cids = ids.mid(i, qMin(ids.size() - i, 167)); ContactsDatabase::Query query(m_database.prepare(removeContact)); query.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(query)) { query.reportError("Failed to remove contacts"); return QContactManager::UnspecifiedError; } } return QContactManager::NoError; } QContactManager::Error ContactWriter::removeDetails(const QVariantList &contactIds, bool onlyIfFlagged) { const QString removeDetail(QStringLiteral( " DELETE FROM Details WHERE contactId = :contactId %1" ).arg(onlyIfFlagged ? QStringLiteral("AND changeFlags >= 4 AND unhandledChangeFlags < 4") // ChangeFlags::IsDeleted : QString())); // do it in batches, otherwise the query can fail due to too many bound values. for (int i = 0; i < contactIds.size(); i += 167) { const QVariantList cids = contactIds.mid(i, qMin(contactIds.size() - i, 167)); ContactsDatabase::Query query(m_database.prepare(removeDetail)); query.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(query)) { query.reportError("Failed to remove details"); return QContactManager::UnspecifiedError; } } return QContactManager::NoError; } // NOTE: this should NEVER be used for synced contacts, only local contacts (for undo support). QContactManager::Error ContactWriter::undeleteContacts(const QVariantList &ids, bool recordUnhandledChangeFlags) { // TODO: CONSIDER THE POSSIBLE SYNC ISSUES RELATED TO THIS OPERATION... I SUSPECT THIS CAN NEVER WORK const QString undeleteContact(QStringLiteral( " UPDATE Contacts SET" " changeFlags = CASE WHEN changeFlags >= 4 THEN changeFlags - 4 ELSE changeFlags END," // ChangeFlags::IsDeleted " unhandledChangeFlags = %1," " deleted = NULL" " WHERE contactId = :contactId" ).arg(recordUnhandledChangeFlags ? QStringLiteral("CASE WHEN unhandledChangeFlags >= 4 THEN unhandledChangeFlags - 4 ELSE unhandledChangeFlags END") : QStringLiteral("unhandledChangeFlags"))); // do it in batches, otherwise the query can fail due to too many bound values. for (int i = 0; i < ids.size(); i += 167) { const QVariantList cids = ids.mid(i, qMin(ids.size() - i, 167)); ContactsDatabase::Query query(m_database.prepare(undeleteContact)); query.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(query)) { query.reportError("Failed to undelete contact"); return QContactManager::UnspecifiedError; } } return QContactManager::NoError; } QContactManager::Error ContactWriter::deleteContacts(const QVariantList &ids, bool recordUnhandledChangeFlags) { const QString deleteContact(QStringLiteral( " UPDATE Contacts SET" " changeFlags = changeFlags | 4," // ChangeFlags::IsDeleted " %1" " deleted = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')" " WHERE contactId = :contactId" ).arg(recordUnhandledChangeFlags ? QStringLiteral(" unhandledChangeFlags = unhandledChangeFlags | 4,") : QString())); // do it in batches, otherwise the query can fail due to too many bound values. for (int i = 0; i < ids.size(); i += 167) { const QVariantList cids = ids.mid(i, qMin(ids.size() - i, 167)); ContactsDatabase::Query query(m_database.prepare(deleteContact)); query.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(query)) { query.reportError("Failed to delete contacts"); return QContactManager::UnspecifiedError; } } return QContactManager::NoError; } QContactManager::Error ContactWriter::remove(const QList &contactIds, QMap *errorMap, bool withinTransaction, bool withinSyncUpdate) { QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); if (contactIds.isEmpty()) return QContactManager::NoError; // grab the self-contact id so we can avoid removing it. quint32 selfContactId = 0; { QContactId id; QContactManager::Error err; if ((err = m_reader->getIdentity(ContactsDatabase::SelfContactId, &id)) != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to determine self ID while deleting contacts")); return err; } selfContactId = ContactId::databaseId(id); // the aggregate self contact id, the local will be less than it. } // grab the existing contact ids so that we can perform removal detection // we also determine whether the contact is an aggregate (and prevent if so). QHash existingContactIds; // contactId to collectionId { const QString findExistingContactIds(QStringLiteral( " SELECT contactId, collectionId FROM Contacts WHERE changeFlags < 4" // ChangeFlags::IsDeleted )); ContactsDatabase::Query query(m_database.prepare(findExistingContactIds)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch existing contact ids during delete"); return QContactManager::UnspecifiedError; } while (query.next()) { const quint32 contactId = query.value(0); const quint32 collectionId = query.value(1); existingContactIds.insert(contactId, collectionId); } } // determine which contacts we actually need to remove QContactManager::Error error = QContactManager::NoError; QList realRemoveIds; QVariantList boundRealRemoveIds; QSet removeChangedCollectionIds; quint32 collectionId = 0; for (int i = 0; i < contactIds.size(); ++i) { QContactId currId = contactIds.at(i); quint32 dbId = ContactId::databaseId(currId); if (dbId == 0) { if (errorMap) errorMap->insert(i, QContactManager::DoesNotExistError); error = QContactManager::DoesNotExistError; } else if (selfContactId > 0 && dbId <= selfContactId) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Cannot delete special self contacts")); if (errorMap) errorMap->insert(i, QContactManager::BadArgumentError); error = QContactManager::BadArgumentError; } else if (existingContactIds.contains(dbId)) { const quint32 removeContactCollectionId = existingContactIds.value(dbId); if (removeContactCollectionId == ContactsDatabase::AggregateAddressbookCollectionId) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Cannot delete contacts from aggregate collection")); if (errorMap) errorMap->insert(i, QContactManager::BadArgumentError); error = QContactManager::BadArgumentError; } else { if (collectionId == 0) { collectionId = existingContactIds.value(dbId); } if (collectionId != existingContactIds.value(dbId)) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Cannot delete contacts from multiple collections in a single batch")); if (errorMap) errorMap->insert(i, QContactManager::BadArgumentError); error = QContactManager::BadArgumentError; } else { realRemoveIds.append(currId); boundRealRemoveIds.append(dbId); removeChangedCollectionIds.insert(ContactCollectionId::apiId(removeContactCollectionId, m_managerUri)); } } } else { if (errorMap) errorMap->insert(i, QContactManager::DoesNotExistError); error = QContactManager::DoesNotExistError; } } if (realRemoveIds.size() == 0 || error != QContactManager::NoError) { return error; } bool recordUnhandledChangeFlags = false; if (!withinSyncUpdate && m_reader->recordUnhandledChangeFlags(ContactCollectionId::apiId(collectionId, realRemoveIds.first().managerUri()), &recordUnhandledChangeFlags) != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to determine recordUnhandledChangeFlags value for collection: %1") .arg(collectionId)); return QContactManager::UnspecifiedError; } if (!m_database.aggregating()) { // If we don't perform aggregation, we simply need to remove every // (valid, non-self) contact specified in the list. if (!withinTransaction && !beginTransaction()) { // if we are not already within a transaction, create a transaction. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while deleting contacts")); return QContactManager::UnspecifiedError; } QContactManager::Error removeError = deleteContacts(boundRealRemoveIds, recordUnhandledChangeFlags); if (removeError != QContactManager::NoError) { if (!withinTransaction) { // only rollback if we created a transaction. rollbackTransaction(); } return removeError; } foreach (const QContactId &rrid, realRemoveIds) { m_removedIds.insert(rrid); } foreach (const QContactCollectionId &rccid, removeChangedCollectionIds) { m_collectionContactsChanged.insert(rccid); } if (!withinTransaction && !commitTransaction()) { // only commit if we created a transaction. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit deletion")); return QContactManager::UnspecifiedError; } return error; } // grab the ids of aggregate contacts which aggregate any of the contacts // which we're about to remove. We will regenerate them after successful // remove. QList aggregatesOfRemoved; m_database.clearTemporaryContactIdsTable(aggregationIdsTable); if (!m_database.createTemporaryContactIdsTable(aggregationIdsTable, boundRealRemoveIds)) { return QContactManager::UnspecifiedError; } else { const QString findAggregateForContactIds(QStringLiteral( " SELECT DISTINCT Relationships.firstId" " FROM Relationships" " JOIN temp.aggregationIds ON Relationships.secondId = temp.aggregationIds.contactId" " WHERE Relationships.type = 'Aggregates'" )); ContactsDatabase::Query query(m_database.prepare(findAggregateForContactIds)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch aggregator contact ids during delete"); return QContactManager::UnspecifiedError; } while (query.next()) { aggregatesOfRemoved.append(query.value(0)); } } if (!withinTransaction && !beginTransaction()) { // only create a transaction if we're not already within one QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while deleting contacts")); return QContactManager::UnspecifiedError; } // remove the non-aggregate contacts which were specified for removal. if (boundRealRemoveIds.size() > 0) { QContactManager::Error removeError = deleteContacts(boundRealRemoveIds, recordUnhandledChangeFlags); if (removeError != QContactManager::NoError) { if (!withinTransaction) { // only rollback if we created a transaction. rollbackTransaction(); } return removeError; } } // remove any aggregates which no longer aggregate any contacts. QContactManager::Error removeError = removeChildlessAggregates(&realRemoveIds); if (removeError != QContactManager::NoError) { if (!withinTransaction) { // only rollback the transaction if we created it rollbackTransaction(); } return removeError; } // And notify of any removals. if (realRemoveIds.size() > 0) { // update our "regenerate list" by purging deleted contacts foreach (const QContactId &removedId, realRemoveIds) { aggregatesOfRemoved.removeAll(ContactId::databaseId(removedId)); } } // Now regenerate our remaining aggregates as required. if (aggregatesOfRemoved.size() > 0) { QContactManager::Error writeError = regenerateAggregates(aggregatesOfRemoved, DetailList(), true); if (writeError != QContactManager::NoError) { if (!withinTransaction) { // only rollback the transaction if we created it rollbackTransaction(); } return writeError; } } foreach (const QContactId &id, realRemoveIds) { m_removedIds.insert(id); } foreach (const QContactCollectionId &rccid, removeChangedCollectionIds) { m_collectionContactsChanged.insert(rccid); } // Success! If we created a transaction, commit. if (!withinTransaction && !commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after removal")); return QContactManager::UnspecifiedError; } return error; } template void insert(QMap &map, const char *name) { map.insert(T::Type, name); } #define PREFIX_LENGTH 8 #define STRINGIZE(T) #T #define INSERT(map,T) insert(map, STRINGIZE(T) + PREFIX_LENGTH) QMap getDetailTypeNames() { QMap rv; INSERT(rv, QContactAddress); INSERT(rv, QContactAnniversary); INSERT(rv, QContactAvatar); INSERT(rv, QContactBirthday); INSERT(rv, QContactDisplayLabel); INSERT(rv, QContactEmailAddress); INSERT(rv, QContactExtendedDetail); INSERT(rv, QContactFamily); INSERT(rv, QContactFavorite); INSERT(rv, QContactGender); INSERT(rv, QContactGeoLocation); INSERT(rv, QContactGlobalPresence); INSERT(rv, QContactGuid); INSERT(rv, QContactHobby); INSERT(rv, QContactName); INSERT(rv, QContactNickname); INSERT(rv, QContactNote); INSERT(rv, QContactOnlineAccount); INSERT(rv, QContactOrganization); INSERT(rv, QContactPhoneNumber); INSERT(rv, QContactPresence); INSERT(rv, QContactRingtone); INSERT(rv, QContactSyncTarget); INSERT(rv, QContactTag); INSERT(rv, QContactTimestamp); INSERT(rv, QContactType); INSERT(rv, QContactUrl); INSERT(rv, QContactVersion); // Our extensions: INSERT(rv, QContactDeactivated); INSERT(rv, QContactOriginMetadata); INSERT(rv, QContactStatusFlags); return rv; } #undef INSERT #undef STRINGIZE #undef PREFIX_LENGTH template QContactDetail::DetailType detailType() { return T::Type; } QContactDetail::DetailType detailType(const QContactDetail &detail) { return detail.type(); } const char *detailTypeName(QContactDetail::DetailType type) { static const QMap names(getDetailTypeNames()); QMap::const_iterator it = names.find(type); if (it != names.end()) { return *it; } return 0; } template const char *detailTypeName() { return detailTypeName(T::Type); } QString detailTypeName(const QContactDetail &detail) { return QString::fromLatin1(detailTypeName(detail.type())); } static ContactWriter::DetailList getIdentityDetailTypes() { // The list of types for details that identify a contact ContactWriter::DetailList rv; rv << detailType() << detailType() << detailType(); return rv; } static ContactWriter::DetailList getUnpromotedDetailTypes() { // The list of types for details that are not promoted to an aggregate ContactWriter::DetailList rv(getIdentityDetailTypes()); rv << detailType(); rv << detailType(); rv << detailType(); rv << detailType(); rv << detailType(); return rv; } static ContactWriter::DetailList getAbsolutelyUnpromotedDetailTypes() { // The list of types for details that are not promoted to an aggregate, even if promotion is forced ContactWriter::DetailList rv; rv << detailType(); rv << detailType(); rv << detailType(); rv << detailType(); return rv; } static ContactWriter::DetailList getPresenceUpdateDetailTypes() { // The list of types for details whose changes constitute presence updates ContactWriter::DetailList rv; rv << detailType(); rv << detailType(); rv << detailType(); return rv; } template static bool detailListContains(const ContactWriter::DetailList &list) { return list.contains(detailType()); } static bool detailListContains(const ContactWriter::DetailList &list, QContactDetail::DetailType type) { return list.contains(type); } static bool detailListContains(const ContactWriter::DetailList &list, const QContactDetail &detail) { return list.contains(detailType(detail)); } bool removeCommonDetails(ContactsDatabase &db, quint32 contactId, const QString &typeName, QContactManager::Error *error) { const QString statement(QStringLiteral("DELETE FROM Details WHERE contactId = :contactId AND detail = :detail")); ContactsDatabase::Query query(db.prepare(statement)); query.bindValue(0, contactId); query.bindValue(1, typeName); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to remove common detail for %1").arg(typeName)); *error = QContactManager::UnspecifiedError; return false; } return true; } template bool ContactWriter::removeCommonDetails( quint32 contactId, QContactManager::Error *error) { return ::removeCommonDetails(m_database, contactId, detailTypeName(), error); } template QVariant detailValue(const T &detail, F field) { return detail.value(field); } /* Steps: - begin transaction - apply deletions for contacts and details according to changeFlags & !unhandledChangeFlags i.e. delete ONLY IF changeFlags has isDeleted AND unhandledChangeFlags does NOT have isDeleted to ensure that we report the deletion properly during the next fetch. - for every Contact in the list: set changeFlags = unhandledChangeFlags, unhandledChangeFlags = 0 - for every Detail in each contact: set changeFlags = unhandledChangeFlags, unhandledChangeFlags = 0 - end transaction. */ QContactManager::Error ContactWriter::clearChangeFlags(const QList &contactIds, bool withinTransaction) { QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); QVariantList boundIds; for (const QContactId &id : contactIds) { boundIds.append(ContactId::databaseId(id)); } if (!withinTransaction && !beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while clearing contact change flags")); return QContactManager::UnspecifiedError; } // first, purge any deleted contacts specified in the list. QContactManager::Error error = removeContacts(boundIds, true); if (error != QContactManager::NoError) { rollbackTransaction(); return error; } // second, purge any deleted details of contacts specified in the list. error = removeDetails(boundIds, true); if (error != QContactManager::NoError) { if (!withinTransaction) { rollbackTransaction(); } return error; } // do it in batches, otherwise the query can fail due to too many bound values. for (int i = 0; i < boundIds.size(); i += 167) { const QVariantList cids = boundIds.mid(i, qMin(boundIds.size() - i, 167)); // third, clear any added/modified change flags for contacts specified in the list. const QString statement(QStringLiteral("UPDATE Contacts SET changeFlags = unhandledChangeFlags, unhandledChangeFlags = 0 WHERE contactId = :contactId")); ContactsDatabase::Query query(m_database.prepare(statement)); query.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(query)) { query.reportError("Failed to clear contact change flags"); if (!withinTransaction) { rollbackTransaction(); } return QContactManager::UnspecifiedError; } // fourth, clear any added/modified change flags for details of contacts specified in the list. const QString detstatement(QStringLiteral("UPDATE Details SET changeFlags = unhandledChangeFlags, unhandledChangeFlags = 0 WHERE contactId = :contactId")); ContactsDatabase::Query detquery(m_database.prepare(detstatement)); detquery.bindValue(QStringLiteral(":contactId"), cids); if (!ContactsDatabase::executeBatch(detquery)) { detquery.reportError("Failed to clear detail change flags"); if (!withinTransaction) { rollbackTransaction(); } return QContactManager::UnspecifiedError; } } if (!withinTransaction && !commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after clearing contact change flags")); return QContactManager::UnspecifiedError; } return QContactManager::NoError; } /* Steps: - begin transaction - set Collection.recordUnhandledChangeFlags = false - apply deletion to the collection according to its changeFlags - apply deletions for contacts and details according to changeFlags & !unhandledChangeFlags i.e. delete ONLY IF changeFlags has isDeleted AND unhandledChangeFlags does NOT have isDeleted to ensure that we report the deletion properly during the next fetch. - for every Contact in the collection: set changeFlags = unhandledChangeFlags, unhandledChangeFlags = 0 - for every Detail in the contact: set changeFlags = unhandledChangeFlags, unhandledChangeFlags = 0 - end transaction. */ QContactManager::Error ContactWriter::clearChangeFlags(const QContactCollectionId &collectionId, bool withinTransaction) { QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); if (!withinTransaction && !beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while clearing collection change flags")); return QContactManager::UnspecifiedError; } const QString statement(QStringLiteral("SELECT contactId FROM Contacts WHERE collectionId = :collectionId")); ContactsDatabase::Query query(m_database.prepare(statement)); query.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collectionId)); QContactManager::Error err = QContactManager::NoError; QList contactIds; if (!ContactsDatabase::execute(query)) { query.reportError("Failed to retrieve contacts in collection while clearing change flags"); err = QContactManager::UnspecifiedError; } else while (query.next()) { contactIds.append(ContactId::apiId(query.value(0), m_managerUri)); } if (contactIds.size()) { err = clearChangeFlags(contactIds, true); } if (err == QContactManager::NoError) { err = removeCollection(collectionId, true /* only purge if delete flag is set */); } if (err == QContactManager::NoError) { const QString clearFlagsStatement(QStringLiteral( " UPDATE Collections SET" " changeFlags = 0" " WHERE collectionId = :collectionId" )); ContactsDatabase::Query clearQuery(m_database.prepare(clearFlagsStatement)); clearQuery.bindValue(QStringLiteral(":collectionId"), ContactCollectionId::databaseId(collectionId)); if (!ContactsDatabase::execute(clearQuery)) { clearQuery.reportError("Failed to clear collection change flags"); err = QContactManager::UnspecifiedError; } } if (err != QContactManager::NoError && !withinTransaction) { rollbackTransaction(); } else if (err == QContactManager::NoError && !withinTransaction && !commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after clearing contact change flags")); err = QContactManager::UnspecifiedError; } return err; } /* This method returns collections associated with the specified accountId or applicationName which have been added, modified, or deleted. For the purposes of this method, a collection is only considered modified if its metadata has changed. Changes to the content of the collection (i.e. contact additions, modifications, or deletions) are ignored for the purposes of this method. Fetch all collections whose COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID value is the specified accountId, and whose COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME value is the specified applicationName. If the specified accountId value is zero, it matches on applicationName only, and vice versa. Append any collection which has ChangeFlags::IsDeleted to deletedCollections. Append any collection which has ChangeFlags::IsAdded (and not IsDeleted) to addedCollections. Append any collection which has ChangeFlags::IsModified (and not IsAdded or IsDeleted) to modifiedCollections. */ QContactManager::Error ContactWriter::fetchCollectionChanges( int accountId, const QString &applicationName, QList *addedCollections, QList *modifiedCollections, QList *deletedCollections, QList *unmodifiedCollections) { return m_reader->fetchCollections(accountId, applicationName, addedCollections, modifiedCollections, deletedCollections, unmodifiedCollections); } /* Steps: - begin transaction. - set Collection.recordUnhandledChangeFlags = true any subsequent "normal" updates to a contact in the collection will result in both changeFlags and unhandledChangeFlags being set for it. we will report these "unhandled" changes during the next sync cycle. - clear Contact.unhandledChangeFlags, and all Detail.unhandledChangeFlags it seems counter-intuitive, but it's basically saying: the previous "unhandled" changes have now been handled as a result of the fetch. doing this prevents us from reporting the SAME CHANGE TWICE, in subsequent fetch calls. - retrieve all Contact + Detail data, including the changeFlags field. - end transaction. - return the Contact+Detail info to caller via the outparams. */ QContactManager::Error ContactWriter::fetchContactChanges( const QContactCollectionId &collectionId, QList *addedContacts, QList *modifiedContacts, QList *deletedContacts, QList *unmodifiedContacts) { QContactManager::Error error = QContactManager::NoError; const quint32 dbColId = ContactCollectionId::databaseId(collectionId); QMutexLocker locker(m_database.accessMutex()); if (!beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while fetching contact changes")); error = QContactManager::UnspecifiedError; } if (error == QContactManager::NoError) { // set Collection.recordUnhandledChangeFlags = true const QString setRecordUnhandledChangeFlags(QStringLiteral( " UPDATE Collections SET" " recordUnhandledChangeFlags = 1" " WHERE collectionId = :collectionId;" )); ContactsDatabase::Query query(m_database.prepare(setRecordUnhandledChangeFlags)); query.bindValue(":collectionId", dbColId); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to set collection.recordUnhandledChangeFlags while fetching contact changes"); error = QContactManager::UnspecifiedError; } } if (error == QContactManager::NoError) { // clear Contact.unhandledChangeFlags const QString clearUnhandledChangeFlags(QStringLiteral( " UPDATE Contacts SET" " unhandledChangeFlags = 0" " WHERE collectionId = :collectionId" )); ContactsDatabase::Query query(m_database.prepare(clearUnhandledChangeFlags)); query.bindValue(":collectionId", dbColId); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to clear contact.unhandledChangeFlags while fetching contact changes"); error = QContactManager::UnspecifiedError; } } if (error == QContactManager::NoError) { // clear Detail.unhandledChangeFlags const QString clearUnhandledChangeFlags(QStringLiteral( " UPDATE Details SET" " unhandledChangeFlags = 0" " WHERE contactId IN (" " SELECT ContactId" " FROM Contacts" " WHERE collectionId = :collectionId" " )" )); ContactsDatabase::Query query(m_database.prepare(clearUnhandledChangeFlags)); query.bindValue(":collectionId", dbColId); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to clear contact.unhandledChangeFlags while fetching contact changes"); error = QContactManager::UnspecifiedError; } } if (error == QContactManager::NoError) { // retrieve all contact+detail data. // this fetch should NOT strip out the added/modified/deleted info. error = m_reader->fetchContacts(collectionId, addedContacts, modifiedContacts, deletedContacts, unmodifiedContacts); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to fetch contact changes for collection %1").arg(dbColId)); } } if (error != QContactManager::NoError) { rollbackTransaction(); } else if (!commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after sync contacts fetch")); error = QContactManager::UnspecifiedError; } return error; } /* Steps: - begin transaction. - read the current db state of the contact. if it's deleted, skip / don't apply. - the input contact should contain change flags to specify which details should be added/modified/removed. apply changes as best as possible, but "keep" the unhandled changes. resolve conflicts according to the conflictResolutionPolicy. - if clearChangeFlags is true, call clearChangeFlags(collectionId). - end transaction. */ QContactManager::Error ContactWriter::storeChanges( QHash * /* added contacts */> *addedCollections, QHash * /* added/modified/deleted contacts */> *modifiedCollections, const QList &deletedCollections, QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy conflictResolutionPolicy, bool clearChangeFlags) { Q_UNUSED(conflictResolutionPolicy); // TODO. QMutexLocker locker(m_database.accessMutex()); if (!beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction for store changes")); return QContactManager::UnspecifiedError; } QContactManager::Error error = QContactManager::NoError; QList touchedCollections; // handle additions if (addedCollections) { QHash *>::iterator ait = addedCollections->begin(), aend = addedCollections->end(); for ( ; ait != aend; ++ait) { QContactCollection *collection = ait.key(); if (!collection->id().isNull()) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Invalid attempt to add an already-existing collection %1 with id %2 within store changes") .arg(collection->metaData(QContactCollection::KeyName).toString(), QString::fromLatin1(collection->id().localId()))); error = QContactManager::BadArgumentError; break; } QList collections; collections.append(*collection); error = save(&collections, nullptr, true, true); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to save added collection %1 within store changes") .arg(collection->metaData(QContactCollection::KeyName).toString())); break; } *collection = collections.first(); touchedCollections.append(collection->id()); QList *addedContacts = ait.value(); QList::iterator cit = addedContacts->begin(), cend = addedContacts->end(); for ( ; cit != cend; ++cit) { cit->setCollectionId(collection->id()); } error = save(addedContacts, QList(), nullptr, nullptr, true, false, true); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to save added contacts for added collection %1 within store changes") .arg(collection->metaData(QContactCollection::KeyName).toString())); break; } } } // handle modifications if (error == QContactManager::NoError && modifiedCollections) { QHash *>::iterator mit = modifiedCollections->begin(), mend = modifiedCollections->end(); for ( ; mit != mend; ++mit) { QContactCollection *collection = mit.key(); if (collection->id().isNull()) { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Invalid attempt to modify a non-added collection %1 within store changes") .arg(collection->metaData(QContactCollection::KeyName).toString())); error = QContactManager::BadArgumentError; break; } touchedCollections.append(collection->id()); QList collections; collections.append(*collection); error = save(&collections, nullptr, true, true); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to save modified collection %1 within store changes") .arg(QString::fromLatin1(collection->id().localId()))); break; } *collection = collections.first(); QList *contacts = mit.value(); QList addedContacts; QList modifiedContacts; QList deletedContacts; QList deletedContactIds; // for every modified contact, determine the change type. { QList::iterator mcit = contacts->begin(), mcend = contacts->end(); for ( ; mcit != mcend; ++mcit) { const QContactStatusFlags &flags = mcit->detail(); if (flags.testFlag(QContactStatusFlags::IsDeleted)) { deletedContacts.append(*mcit); deletedContactIds.append(mcit->id()); } else if (flags.testFlag(QContactStatusFlags::IsAdded)) { addedContacts.append(*mcit); } else if (flags.testFlag(QContactStatusFlags::IsModified)) { modifiedContacts.append(*mcit); } else { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Ignoring unchanged contact %1 within modified collection %2 within store changes") .arg(QString::fromLatin1(mcit->id().localId())) .arg(QString::fromLatin1(collection->id().localId()))); } } } // now apply the changes // first, contact additions if (addedContacts.size()) { QList::iterator cit = addedContacts.begin(), cend = addedContacts.end(); for ( ; cit != cend; ++cit) { cit->setCollectionId(collection->id()); } error = save(&addedContacts, QList(), nullptr, nullptr, true, false, true); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to save added contacts for modified collection %1 within store changes") .arg(QString::fromLatin1(collection->id().localId()))); break; } } // then contact modifications if (modifiedContacts.size()) { QList::iterator cit = modifiedContacts.begin(), cend = modifiedContacts.end(); for ( ; cit != cend; ++cit) { cit->setCollectionId(collection->id()); } error = save(&modifiedContacts, QList(), nullptr, nullptr, true, false, true); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to save added contacts for modified collection %1 within store changes") .arg(QString::fromLatin1(collection->id().localId()))); break; } } // finally contact deletions if (deletedContactIds.size()) { error = remove(deletedContactIds, nullptr, true, true); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to delete deleted contacts for modified collection %1 within store changes") .arg(QString::fromLatin1(collection->id().localId()))); break; } } // update the input parameter with the potentially modified values. // this is important primarily for additions, which get updated ids. contacts->clear(); contacts->append(addedContacts); contacts->append(modifiedContacts); contacts->append(deletedContacts); } } // handle deletions if (error == QContactManager::NoError && deletedCollections.size()) { error = remove(deletedCollections, nullptr, true, true); touchedCollections.append(deletedCollections); } // clear change flags (including purging items marked for deletion) if required. if (clearChangeFlags) { for (const QContactCollectionId &touchedCollection : touchedCollections) { error = this->clearChangeFlags(touchedCollection, true); if (error != QContactManager::NoError) { break; } } } if (error != QContactManager::NoError) { rollbackTransaction(); } else if (!commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after store changes")); error = QContactManager::UnspecifiedError; } return error; } bool ContactWriter::storeOOB(const QString &scope, const QMap &values) { QMutexLocker locker(m_database.accessMutex()); if (values.isEmpty()) return true; if (!beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while storing OOB")); return false; } QStringList tuples; QVariantList dataValues; const QChar colon(QChar::fromLatin1(':')); const QString bindString(QStringLiteral("(?,?,?)")); QMap::const_iterator it = values.constBegin(), end = values.constEnd(); for ( ; it != end; ++it) { tuples.append(bindString); dataValues.append(scope + colon + it.key()); // If the data is large, compress it to reduce the IO cost const QVariant &var(it.value()); if (var.type() == static_cast(QMetaType::QByteArray)) { const QByteArray uncompressed(var.value()); if (uncompressed.size() > 512) { // Test the entropy of this data, if it is unlikely to compress significantly, don't try if (entropy(uncompressed.constBegin() + 256, uncompressed.constBegin() + 512, 256) < 0.85) { dataValues.append(QVariant(qCompress(uncompressed))); dataValues.append(1); continue; } } } else if (var.type() == static_cast(QMetaType::QString)) { const QString uncompressed(var.value()); if (uncompressed.size() > 256) { dataValues.append(QVariant(qCompress(uncompressed.toUtf8()))); dataValues.append(2); continue; } } // No compression: dataValues.append(var); dataValues.append(0); } QString statement(QStringLiteral("INSERT OR REPLACE INTO OOB (name, value, compressed) VALUES %1").arg(tuples.join(","))); QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare OOB insert:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(statement)); } else { foreach (const QVariant &v, dataValues) { query.addBindValue(v); } if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to insert OOB: %1") .arg(query.lastError().text())); } else { if (!commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after storing OOB")); return false; } return true; } } rollbackTransaction(); return false; } bool ContactWriter::removeOOB(const QString &scope, const QStringList &keys) { QMutexLocker locker(m_database.accessMutex()); if (!beginTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while removing OOB")); return false; } QVariantList keyNames; QString statement(QStringLiteral("DELETE FROM OOB WHERE name ")); if (keys.isEmpty()) { statement.append(QStringLiteral("LIKE '%1%%'").arg(scope)); } else { const QChar colon(QChar::fromLatin1(':')); QString keyList; foreach (const QString &key, keys) { keyNames.append(scope + colon + key); keyList.append(keyList.isEmpty() ? QStringLiteral("?") : QStringLiteral(",?")); } statement.append(QStringLiteral("IN (%1)").arg(keyList)); } QSqlQuery query(m_database); query.setForwardOnly(true); if (!query.prepare(statement)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to prepare OOB remove:\n%1\nQuery:\n%2") .arg(query.lastError().text()) .arg(statement)); } else { foreach (const QVariant &name, keyNames) { query.addBindValue(name); } if (!ContactsDatabase::execute(query)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to query OOB: %1") .arg(query.lastError().text())); } else { if (!commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit database after removing OOB")); return false; } return true; } } rollbackTransaction(); return false; } QMap contextTypes() { QMap rv; rv.insert(QContactDetail::ContextHome, QStringLiteral("Home")); rv.insert(QContactDetail::ContextWork, QStringLiteral("Work")); rv.insert(QContactDetail::ContextOther, QStringLiteral("Other")); return rv; } QString contextString(int type) { static const QMap types(contextTypes()); QMap::const_iterator it = types.find(type); if (it != types.end()) { return *it; } return QString(); } QVariant detailContexts(const QContactDetail &detail) { static const QString separator = QStringLiteral(";"); QStringList contexts; foreach (int context, detail.contexts()) { contexts.append(contextString(context)); } return QVariant(contexts.join(separator)); } quint32 writeCommonDetails(ContactsDatabase &db, quint32 contactId, quint32 detailId, const QContactDetail &detail, bool syncable, bool wasLocal, bool aggregateContact, bool recordUnhandledChangeFlags, const QString &typeName, QContactManager::Error *error) { const QString statement(detailId == 0 ? QStringLiteral( " INSERT INTO Details (" " contactId," " detail," " detailUri," " linkedDetailUris," " contexts," " accessConstraints," " provenance," " modifiable," " nonexportable," " changeFlags," " unhandledChangeFlags," " created," " modified)" " VALUES (" " :contactId," " :detail," " :detailUri," " :linkedDetailUris," " :contexts," " :accessConstraints," " :provenance," " :modifiable," " :nonexportable," " %1," " %2," " :created," " :modified)" ).arg(aggregateContact ? QStringLiteral("0") : QStringLiteral("1")) // ChangeFlags::IsAdded .arg((aggregateContact || !recordUnhandledChangeFlags) ? QStringLiteral("0") : QStringLiteral("1")) : QStringLiteral( " UPDATE Details SET" " detail = :detail," " detailUri = :detailUri," " linkedDetailUris = :linkedDetailUris," " contexts = :contexts," " accessConstraints = :accessConstraints," " provenance = :provenance," " modifiable = :modifiable," " nonexportable = :nonexportable" " %1 %2," " modified = :modified" " WHERE contactId = :contactId AND detailId = :detailId") .arg(aggregateContact ? QString() : QStringLiteral(", ChangeFlags = ChangeFlags | 2")) // ChangeFlags::IsModified .arg((aggregateContact || !recordUnhandledChangeFlags) ? QString() : QStringLiteral(", UnhandledChangeFlags = UnhandledChangeFlags | 2"))); ContactsDatabase::Query query(db.prepare(statement)); const QVariant detailUri = detailValue(detail, QContactDetail::FieldDetailUri); const QVariant linkedDetailUris = QVariant(detail.linkedDetailUris().join(QStringLiteral(";"))); const QVariant contexts = detailContexts(detail); const QVariant accessConstraints = static_cast(detail.accessConstraints()); const QVariant provenance = aggregateContact ? detailValue(detail, QContactDetail::FieldProvenance) : QVariant(); const QVariant modifiable = wasLocal ? true : (syncable && detail.hasValue(QContactDetail__FieldModifiable) ? detailValue(detail, QContactDetail__FieldModifiable) : QVariant()); const QVariant nonexportable = detailValue(detail, QContactDetail__FieldNonexportable); const QVariant modified = aggregateContact ? detailValue(detail, QContactDetail__FieldModified) : ContactsDatabase::dateTimeString(QDateTime::currentDateTimeUtc()); if (detailId > 0) { query.bindValue(":detailId", detailId); } else { query.bindValue(":created", modified); } query.bindValue(":contactId", contactId); query.bindValue(":detail", typeName); query.bindValue(":detailUri", detailUri); query.bindValue(":linkedDetailUris", linkedDetailUris); query.bindValue(":contexts", contexts); query.bindValue(":accessConstraints", accessConstraints); query.bindValue(":provenance", provenance); query.bindValue(":modifiable", modifiable); query.bindValue(":nonexportable", nonexportable); query.bindValue(":modified", modified); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to write common details for %1\ndetailUri: %2, linkedDetailUris: %3") .arg(typeName) .arg(detailUri.value()) .arg(linkedDetailUris.value())); *error = QContactManager::UnspecifiedError; return 0; } return detailId == 0 ? query.lastInsertId().value() : detailId; } template quint32 ContactWriter::writeCommonDetails( quint32 contactId, quint32 detailId, const T &detail, bool syncable, bool wasLocal, bool aggregateContact, bool recordUnhandledChangeFlags, QContactManager::Error *error) { return ::writeCommonDetails( m_database, contactId, detailId, detail, syncable, wasLocal, aggregateContact, recordUnhandledChangeFlags, detailTypeName(), error); } // Define the type that another type is generated from template struct GeneratorType { typedef T type; }; template<> struct GeneratorType { typedef QContactPresence type; }; QContactDetail::DetailType generatorType(QContactDetail::DetailType type) { if (type == QContactGlobalPresence::Type) return QContactPresence::Type; return type; } bool deleteDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, const QString &typeName, bool recordUnhandledChangeFlags, QContactManager::Error *error) { const QString deleteDetailStatement(QStringLiteral( "UPDATE Details SET" " ChangeFlags = ChangeFlags | 4" // ChangeFlags::IsDeleted " %1" " WHERE detailId = :detailId" " AND contactId = :contactId").arg(recordUnhandledChangeFlags ? QStringLiteral(", unhandledChangeFlags = unhandledChangeFlags | 4") : QString())); ContactsDatabase::Query query(db.prepare(deleteDetailStatement)); query.bindValue(":contactId", contactId); query.bindValue(":detailId", detailId); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to delete existing detail of type %1 with id %2 for contact %3").arg(typeName).arg(detailId).arg(contactId)); *error = QContactManager::UnspecifiedError; return false; } return true; } template struct RemoveStatement {}; template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Addresses WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Anniversaries WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Avatars WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Birthdays WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM DisplayLabels WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM EmailAddresses WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Families WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Favorites WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Genders WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM GeoLocations WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM GlobalPresences WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Guids WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Hobbies WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Names WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Nicknames WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Notes WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM OnlineAccounts WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Organizations WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM PhoneNumbers WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Presences WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Ringtones WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM SyncTargets WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Tags WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM Urls WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM OriginMetadata WHERE contactId = :contactId")); template<> struct RemoveStatement { static const QString statement; }; const QString RemoveStatement::statement(QStringLiteral("DELETE FROM ExtendedDetails WHERE contactId = :contactId")); bool removeSpecificDetails(ContactsDatabase &db, quint32 contactId, const QString &statement, const QString &typeName, QContactManager::Error *error) { ContactsDatabase::Query query(db.prepare(statement)); query.bindValue(0, contactId); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to remove existing details of type %1 for contact %2").arg(typeName).arg(contactId)); *error = QContactManager::UnspecifiedError; return false; } return true; } template bool removeSpecificDetails(ContactsDatabase &db, quint32 contactId, QContactManager::Error *error) { return removeSpecificDetails(db, contactId, RemoveStatement::statement, detailTypeName(), error); } static void adjustAggregateDetailProperties(QContactDetail &detail, const QContact &constituent) { // Modify this detail URI to preserve uniqueness - the result must not clash with the // URI in the constituent's copy (there won't be any other aggregator of the same detail). // Also, since the detail URI only needs to be unique-per-contact, if an aggregate // aggregates two contacts, each of those contacts may have a detail which has a particular // detail URI. Thus, to disambiguate, we need to prefix with the constituent contact id. // If a detail URI is modified for aggregation, we need to insert a prefix const QString aggregateTag(QStringLiteral("aggregate")); const QString prefix(QStringLiteral("%1-%2:").arg(aggregateTag).arg(ContactId::databaseId(constituent))); QString detailUri = detail.detailUri(); if (!detailUri.isEmpty() && !detailUri.startsWith(prefix)) { if (detailUri.startsWith(aggregateTag)) { // Remove any invalid aggregate prefix that may have been previously stored int index = detailUri.indexOf(QChar::fromLatin1(':')); detailUri = detailUri.mid(index + 1); } detail.setDetailUri(prefix + detailUri); } QStringList linkedDetailUris = detail.linkedDetailUris(); if (!linkedDetailUris.isEmpty()) { QStringList::iterator it = linkedDetailUris.begin(), end = linkedDetailUris.end(); for ( ; it != end; ++it) { QString &linkedUri(*it); if (!linkedUri.isEmpty() && !linkedUri.startsWith(prefix)) { if (linkedUri.startsWith(aggregateTag)) { // Remove any invalid aggregate prefix that may have been previously stored int index = linkedUri.indexOf(QChar::fromLatin1(':')); linkedUri = linkedUri.mid(index + 1); } linkedUri.insert(0, prefix); } } detail.setLinkedDetailUris(linkedDetailUris); } } namespace { QStringList subTypeList(const QList &subTypes) { QStringList rv; foreach (int subType, subTypes) { rv.append(QString::number(subType)); } return rv; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactAddress &detail) { const QString statement(update ? QStringLiteral( " UPDATE Addresses SET" " street = :street," " postOfficeBox = :postOfficeBox," " region = :region," " locality = :locality," " postCode = :postCode," " country = :country," " subTypes = :subTypes" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Addresses (" " detailId," " contactId," " street," " postOfficeBox," " region," " locality," " postCode," " country," " subTypes)" " VALUES (" " :detailId," " :contactId," " :street," " :postOfficeBox," " :region," " :locality," " :postCode," " :country," " :subTypes)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactAddress T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":street", detail.value(T::FieldStreet).trimmed()); query.bindValue(":postOfficeBox", detail.value(T::FieldPostOfficeBox).trimmed()); query.bindValue(":region", detail.value(T::FieldRegion).trimmed()); query.bindValue(":locality", detail.value(T::FieldLocality).trimmed()); query.bindValue(":postCode", detail.value(T::FieldPostcode).trimmed()); query.bindValue(":country", detail.value(T::FieldCountry).trimmed()); query.bindValue(":subTypes", subTypeList(detail.subTypes()).join(QStringLiteral(";"))); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactAnniversary &detail) { const QString statement(update ? QStringLiteral( " UPDATE Anniversaries SET" " originalDateTime = :originalDateTime," " calendarId = :calendarId," " subType = :subType," " event = :event" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Anniversaries (" " detailId," " contactId," " originalDateTime," " calendarId," " subType," " event)" " VALUES (" " :detailId," " :contactId," " :originalDateTime," " :calendarId," " :subType," " :event)" )); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactAnniversary T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":originalDateTime", detailValue(detail, T::FieldOriginalDate)); query.bindValue(":calendarId", detailValue(detail, T::FieldCalendarId)); query.bindValue(":subType", detail.hasValue(T::FieldSubType) ? QString::number(detail.subType()) : QString()); query.bindValue(":event", detail.value(T::FieldEvent).trimmed()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactAvatar &detail) { const QString statement(update ? QStringLiteral( " UPDATE Avatars SET" " imageUrl = :imageUrl," " videoUrl = :videoUrl," " avatarMetadata = :avatarMetadata" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Avatars (" " detailId," " contactId," " imageUrl," " videoUrl," " avatarMetadata)" " VALUES (" " :detailId," " :contactId," " :imageUrl," " :videoUrl," " :avatarMetadata)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactAvatar T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":imageUrl", detail.value(T::FieldImageUrl).trimmed()); query.bindValue(":videoUrl", detail.value(T::FieldVideoUrl).trimmed()); query.bindValue(":avatarMetadata", detailValue(detail, QContactAvatar::FieldMetaData)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactBirthday &detail) { const QString statement(update ? QStringLiteral( " UPDATE Birthdays SET" " birthday = :birthday," " calendarId = :calendarId" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Birthdays (" " detailId," " contactId," " birthday," " calendarId)" " VALUES (" " :detailId," " :contactId," " :birthday," " :calendarId)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactBirthday T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":birthday", detailValue(detail, T::FieldBirthday)); query.bindValue(":calendarId", detailValue(detail, T::FieldCalendarId)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactDisplayLabel &detail) { const QString statement(update ? QStringLiteral( " UPDATE DisplayLabels SET" " displayLabel = :displayLabel," " displayLabelGroup = :displayLabelGroup," " displayLabelGroupSortOrder = :displayLabelGroupSortOrder" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO DisplayLabels (" " detailId," " contactId," " displayLabel," " displayLabelGroup," " displayLabelGroupSortOrder)" " VALUES (" " :detailId," " :contactId," " :displayLabel," " :displayLabelGroup," " :displayLabelGroupSortOrder)")); ContactsDatabase::Query query(db.prepare(statement)); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":displayLabel", detail.label()); query.bindValue(":displayLabelGroup", detail.value(QContactDisplayLabel__FieldLabelGroup)); query.bindValue(":displayLabelGroupSortOrder", detail.value(QContactDisplayLabel__FieldLabelGroupSortOrder)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactEmailAddress &detail) { const QString statement(update ? QStringLiteral( " UPDATE EmailAddresses SET" " emailAddress = :emailAddress," " lowerEmailAddress = :lowerEmailAddress" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO EmailAddresses (" " detailId," " contactId," " emailAddress," " lowerEmailAddress)" " VALUES (" " :detailId," " :contactId," " :emailAddress," " :lowerEmailAddress)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactEmailAddress T; const QString address(detail.value(T::FieldEmailAddress).trimmed()); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":emailAddress", address); query.bindValue(":lowerEmailAddress", address.toLower()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactFamily &detail) { const QString statement(update ? QStringLiteral( " UPDATE Families SET" " spouse = :spouse," " children = :children" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Families (" " detailId," " contactId," " spouse," " children)" " VALUES (" " :detailId," " :contactId," " :spouse," " :children)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactFamily T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":spouse", detail.value(T::FieldSpouse).trimmed()); query.bindValue(":children", detail.value(T::FieldChildren).join(QStringLiteral(";"))); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactFavorite &detail) { const QString statement(update ? QStringLiteral( " UPDATE Favorites SET" " isFavorite = :isFavorite" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Favorites (" " detailId," " contactId," " isFavorite)" " VALUES (" " :detailId," " :contactId," " :isFavorite)")); ContactsDatabase::Query query(db.prepare(statement)); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":isFavorite", detail.isFavorite()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactGender &detail) { const QString statement(update ? QStringLiteral( " UPDATE Genders SET" " gender = :gender" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Genders (" " detailId," " contactId," " gender)" " VALUES (" " :detailId," " :contactId," " :gender)")); ContactsDatabase::Query query(db.prepare(statement)); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":gender", QString::number(static_cast(detail.gender()))); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactGeoLocation &detail) { const QString statement(update ? QStringLiteral( " UPDATE GeoLocations SET" " label = :label," " latitude = :latitude," " longitude = :longitude," " accuracy = :accuracy," " altitude = :altitude," " altitudeAccuracy = :altitudeAccuracy," " heading = :heading," " speed = :speed," " timestamp = :timestamp)" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO GeoLocations (" " detailId," " contactId," " label," " latitude," " longitude," " accuracy," " altitude," " altitudeAccuracy," " heading," " speed," " timestamp)" " VALUES (" " :detailId," " :contactId," " :label," " :latitude," " :longitude," " :accuracy," " :altitude," " :altitudeAccuracy," " :heading," " :speed," " :timestamp)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactGeoLocation T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":label", detail.value(T::FieldLabel).trimmed()); query.bindValue(":latitude", detail.value(T::FieldLatitude)); query.bindValue(":longitude", detail.value(T::FieldLongitude)); query.bindValue(":accuracy", detail.value(T::FieldAccuracy)); query.bindValue(":altitude", detail.value(T::FieldAltitude)); query.bindValue(":altitudeAccuracy", detail.value(T::FieldAltitudeAccuracy)); query.bindValue(":heading", detail.value(T::FieldHeading)); query.bindValue(":speed", detail.value(T::FieldSpeed)); query.bindValue(":timestamp", ContactsDatabase::dateTimeString(detail.value(T::FieldTimestamp).toUTC())); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactGlobalPresence &detail) { const QString statement(update ? QStringLiteral( " UPDATE GlobalPresences SET" " presenceState = :presenceState," " timestamp = :timestamp," " nickname = :nickname," " customMessage = :customMessage," " presenceStateText = :presenceStateText," " presenceStateImageUrl = :presenceStateImageUrl" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO GlobalPresences (" " detailId," " contactId," " presenceState," " timestamp," " nickname," " customMessage," " presenceStateText," " presenceStateImageUrl)" " VALUES (" " :detailId," " :contactId," " :presenceState," " :timestamp," " :nickname," " :customMessage," " :presenceStateText," " :presenceStateImageUrl)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactGlobalPresence T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":presenceState", detailValue(detail, T::FieldPresenceState)); query.bindValue(":timestamp", ContactsDatabase::dateTimeString(detail.value(T::FieldTimestamp).toUTC())); query.bindValue(":nickname", detail.value(T::FieldNickname).trimmed()); query.bindValue(":customMessage", detail.value(T::FieldCustomMessage).trimmed()); query.bindValue(":presenceStateText", detail.value(T::FieldPresenceStateText).trimmed()); query.bindValue(":presenceStateImageUrl", detail.value(T::FieldPresenceStateImageUrl).trimmed()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactGuid &detail) { const QString statement(update ? QStringLiteral( " UPDATE Guids SET" " guid = :guid" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Guids (" " detailId," " contactId," " guid)" " VALUES (" " :detailId," " :contactId," " :guid)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactGuid T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":guid", detailValue(detail, T::FieldGuid)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactHobby &detail) { const QString statement(update ? QStringLiteral( " UPDATE Hobbies SET" " hobby = :hobby" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Hobbies (" " detailId," " contactId," " hobby)" " VALUES (" " :detailId," " :contactId," " :hobby)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactHobby T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":hobby", detailValue(detail, T::FieldHobby)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactName &detail) { const QString statement(update ? QStringLiteral( " UPDATE Names SET" " firstName = :firstName," " lowerFirstName = :lowerFirstName," " lastName = :lastName," " lowerLastName = :lowerLastName," " middleName = :middleName," " prefix = :prefix," " suffix = :suffix," " customLabel = :customLabel" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Names (" " detailId," " contactId," " firstName," " lowerFirstName," " lastName," " lowerLastName," " middleName," " prefix," " suffix," " customLabel)" " VALUES (" " :detailId," " :contactId," " :firstName," " :lowerFirstName," " :lastName," " :lowerLastName," " :middleName," " :prefix," " :suffix," " :customLabel)")); ContactsDatabase::Query query(db.prepare(statement)); const QString firstName(detail.value(QContactName::FieldFirstName).trimmed()); const QString lastName(detail.value(QContactName::FieldLastName).trimmed()); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":firstName", firstName); query.bindValue(":lowerFirstName", firstName.toLower()); query.bindValue(":lastName", lastName); query.bindValue(":lowerLastName", lastName.toLower()); query.bindValue(":middleName", detail.value(QContactName::FieldMiddleName).trimmed()); query.bindValue(":prefix", detail.value(QContactName::FieldPrefix).trimmed()); query.bindValue(":suffix", detail.value(QContactName::FieldSuffix).trimmed()); query.bindValue(":customLabel", detail.value(QContactName::FieldCustomLabel).trimmed()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactNickname &detail) { const QString statement(update ? QStringLiteral( " UPDATE Nicknames SET" " nickname = :nickname," " lowerNickname = :lowerNickname" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Nicknames (" " detailId," " contactId," " nickname," " lowerNickname)" " VALUES (" " :detailId," " :contactId," " :nickname," " :lowerNickname)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactNickname T; const QString nickname(detail.value(T::FieldNickname).trimmed()); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":nickname", nickname); query.bindValue(":lowerNickname", nickname.toLower()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactNote &detail) { const QString statement(update ? QStringLiteral( " UPDATE Notes SET" " note = :note" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Notes (" " detailId," " contactId," " note)" " VALUES (" " :detailId," " :contactId," " :note)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactNote T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":note", detailValue(detail, T::FieldNote)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactOnlineAccount &detail) { const QString statement(update ? QStringLiteral( " UPDATE OnlineAccounts SET" " accountUri = :accountUri," " lowerAccountUri = :lowerAccountUri," " protocol = :protocol," " serviceProvider = :serviceProvider," " capabilities = :capabilities," " subTypes = :subTypes," " accountPath = :accountPath," " accountIconPath = :accountIconPath," " enabled = :enabled," " accountDisplayName = :accountDisplayName," " serviceProviderDisplayName = :serviceProviderDisplayName" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO OnlineAccounts (" " detailId," " contactId," " accountUri," " lowerAccountUri," " protocol," " serviceProvider," " capabilities," " subTypes," " accountPath," " accountIconPath," " enabled," " accountDisplayName," " serviceProviderDisplayName)" " VALUES (" " :detailId," " :contactId," " :accountUri," " :lowerAccountUri," " :protocol," " :serviceProvider," " :capabilities," " :subTypes," " :accountPath," " :accountIconPath," " :enabled," " :accountDisplayName," " :serviceProviderDisplayName)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactOnlineAccount T; const QString uri(detail.value(T::FieldAccountUri).trimmed()); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":accountUri", uri); query.bindValue(":lowerAccountUri", uri.toLower()); query.bindValue(":protocol", QString::number(detail.protocol())); query.bindValue(":serviceProvider", detailValue(detail, T::FieldServiceProvider)); query.bindValue(":capabilities", detailValue(detail, T::FieldCapabilities).value().join(QStringLiteral(";"))); query.bindValue(":subTypes", subTypeList(detail.subTypes()).join(QStringLiteral(";"))); query.bindValue(":accountPath", detailValue(detail, QContactOnlineAccount__FieldAccountPath)); query.bindValue(":accountIconPath", detailValue(detail, QContactOnlineAccount__FieldAccountIconPath)); query.bindValue(":enabled", detailValue(detail, QContactOnlineAccount__FieldEnabled)); query.bindValue(":accountDisplayName", detailValue(detail, QContactOnlineAccount__FieldAccountDisplayName)); query.bindValue(":serviceProviderDisplayName", detailValue(detail, QContactOnlineAccount__FieldServiceProviderDisplayName)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactOrganization &detail) { const QString statement(update ? QStringLiteral( " UPDATE Organizations SET" " name = :name," " role = :role," " title = :title," " location = :location," " department = :department," " logoUrl = :logoUrl," " assistantName = :assistantName" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Organizations (" " detailId," " contactId," " name," " role," " title," " location," " department," " logoUrl," " assistantName)" " VALUES (" " :detailId," " :contactId," " :name," " :role," " :title," " :location," " :department," " :logoUrl," " :assistantName)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactOrganization T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":name", detail.value(T::FieldName).trimmed()); query.bindValue(":role", detail.value(T::FieldRole).trimmed()); query.bindValue(":title", detail.value(T::FieldTitle).trimmed()); query.bindValue(":location", detail.value(T::FieldLocation).trimmed()); query.bindValue(":department", detail.department().join(QStringLiteral(";"))); query.bindValue(":logoUrl", detail.value(T::FieldLogoUrl).trimmed()); query.bindValue(":assistantName", detail.value(T::FieldAssistantName).trimmed()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactPhoneNumber &detail) { const QString statement(update ? QStringLiteral( " UPDATE PhoneNumbers SET" " phoneNumber = :phoneNumber," " subTypes = :subTypes," " normalizedNumber = :normalizedNumber" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO PhoneNumbers (" " detailId," " contactId," " phoneNumber," " subTypes," " normalizedNumber)" " VALUES (" " :detailId," " :contactId," " :phoneNumber," " :subTypes," " :normalizedNumber)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactPhoneNumber T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":phoneNumber", detail.value(T::FieldNumber).trimmed()); query.bindValue(":subTypes", subTypeList(detail.subTypes()).join(QStringLiteral(";"))); query.bindValue(":normalizedNumber", QVariant(ContactsEngine::normalizedPhoneNumber(detail.number()))); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactPresence &detail) { const QString statement(update ? QStringLiteral( " UPDATE Presences SET" " presenceState = :presenceState," " timestamp = :timestamp," " nickname = :nickname," " customMessage = :customMessage," " presenceStateText = :presenceStateText," " presenceStateImageUrl = :presenceStateImageUrl" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Presences (" " detailId," " contactId," " presenceState," " timestamp," " nickname," " customMessage," " presenceStateText," " presenceStateImageUrl)" " VALUES (" " :detailId," " :contactId," " :presenceState," " :timestamp," " :nickname," " :customMessage," " :presenceStateText," " :presenceStateImageUrl)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactPresence T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":presenceState", detailValue(detail, T::FieldPresenceState)); query.bindValue(":timestamp", ContactsDatabase::dateTimeString(detail.value(T::FieldTimestamp).toUTC())); query.bindValue(":nickname", detail.value(T::FieldNickname).trimmed()); query.bindValue(":customMessage", detail.value(T::FieldCustomMessage).trimmed()); query.bindValue(":presenceStateText", detail.value(T::FieldPresenceStateText).trimmed()); query.bindValue(":presenceStateImageUrl", detail.value(T::FieldPresenceStateImageUrl).trimmed()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactRingtone &detail) { const QString statement(update ? QStringLiteral( " UPDATE Ringtones SET" " audioRingtone = :audioRingtone," " videoRingtone = :videoRingtone," " vibrationRingtone = :vibrationRingtone" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Ringtones (" " detailId," " contactId," " audioRingtone," " videoRingtone," " vibrationRingtone)" " VALUES (" " :detailId," " :contactId," " :audioRingtone," " :videoRingtone," " :vibrationRingtone)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactRingtone T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":audioRingtone", detail.value(T::FieldAudioRingtoneUrl).trimmed()); query.bindValue(":videoRingtone", detail.value(T::FieldVideoRingtoneUrl).trimmed()); query.bindValue(":vibrationRingtone", detail.value(T::FieldVibrationRingtoneUrl).trimmed()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactSyncTarget &detail) { const QString statement(update ? QStringLiteral( " UPDATE SyncTargets SET" " syncTarget = :syncTarget" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO SyncTargets (" " detailId," " contactId," " syncTarget)" " VALUES (" " :detailId," " :contactId," " :syncTarget)")); ContactsDatabase::Query query(db.prepare(statement)); query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":syncTarget", detail.syncTarget()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactTag &detail) { const QString statement(update ? QStringLiteral( " UPDATE Tags SET" " tag = :tag" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Tags (" " detailId," " contactId," " tag)" " VALUES (" " :detailId," " :contactId," " :tag)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactTag T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":tag", detail.value(T::FieldTag).trimmed()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactUrl &detail) { const QString statement(update ? QStringLiteral( " UPDATE Urls SET" " url = :url," " subTypes = :subTypes" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO Urls (" " detailId," " contactId," " url," " subTypes)" " VALUES (" " :detailId," " :contactId," " :url," " :subTypes)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactUrl T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":url", detail.value(T::FieldUrl).trimmed()); query.bindValue(":subTypes", detail.hasValue(T::FieldSubType) ? QString::number(detail.subType()) : QString()); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactOriginMetadata &detail) { const QString statement(update ? QStringLiteral( " UPDATE OriginMetadata SET" " id = :id," " groupId = :groupId," " enabled = :enabled" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO OriginMetadata (" " detailId," " contactId," " id," " groupId," " enabled)" " VALUES (" " :detailId," " :contactId," " :id," " :groupId," " :enabled)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactOriginMetadata T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":id", detailValue(detail, T::FieldId)); query.bindValue(":groupId", detailValue(detail, T::FieldGroupId)); query.bindValue(":enabled", detailValue(detail, T::FieldEnabled)); return query; } ContactsDatabase::Query bindDetail(ContactsDatabase &db, quint32 contactId, quint32 detailId, bool update, const QContactExtendedDetail &detail) { const QString statement(update ? QStringLiteral( " UPDATE ExtendedDetails SET" " name = :name," " data = :data" " WHERE detailId = :detailId" " AND contactId = :contactId") : QStringLiteral( " INSERT INTO ExtendedDetails (" " detailId," " contactId," " name," " data)" " VALUES (" " :detailId," " :contactId," " :name," " :data)")); ContactsDatabase::Query query(db.prepare(statement)); typedef QContactExtendedDetail T; query.bindValue(":detailId", detailId); query.bindValue(":contactId", contactId); query.bindValue(":name", detailValue(detail, T::FieldName)); const QVariant variantValue = detailValue(detail, T::FieldData); if (variantValue.isNull()) { query.bindValue(":data", variantValue); } else { QByteArray serialized; QBuffer buffer(&serialized); buffer.open(QIODevice::WriteOnly); QDataStream out(&buffer); out.setVersion(QDataStream::Qt_5_6); out << detailValue(detail, T::FieldData); query.bindValue(":data", serialized); } return query; } template void removeDuplicateDetails(QList *details) { for (int i = 0; i < details->size() - 1; ++i) { for (int j = details->size() - 1; j >= i+1; --j) { if (detailPairExactlyMatches( details->at(i), details->at(j), QtContactsSqliteExtensions::defaultIgnorableDetailFields(), QtContactsSqliteExtensions::defaultIgnorableCommonFields())) { details->removeAt(j); } } } } } template bool ContactWriter::writeDetails( quint32 contactId, const QtContactsSqliteExtensions::ContactDetailDelta &delta, QContact *contact, const DetailList &definitionMask, const QContactCollectionId &collectionId, bool syncable, bool wasLocal, bool uniqueDetail, bool recordUnhandledChangeFlags, QContactManager::Error *error) { if (!definitionMask.isEmpty() && // only a subset of detail types are being written !detailListContains(definitionMask) && // this type is not in the set !detailListContains::type>(definitionMask)) // this type's generator type is not in the set return true; const bool aggregateContact(ContactCollectionId::databaseId(collectionId) == ContactsDatabase::AggregateAddressbookCollectionId); if (delta.isValid) { // perform delta update. QList deletions(delta.deleted()); typename QList::iterator dit = deletions.begin(), dend = deletions.end(); for ( ; dit != dend; ++dit) { T &detail(*dit); const quint32 detailId = detail.value(QContactDetail__FieldDatabaseId).toUInt(); if (detailId == 0) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Invalid detail deletion specified for %1 in contact %2").arg(detailTypeName()).arg(contactId)); return false; } else if (!deleteDetail(m_database, contactId, detailId, detailTypeName(), recordUnhandledChangeFlags, error)) { return false; } } QList modifications(delta.modified()); typename QList::iterator mit = modifications.begin(), mend = modifications.end(); for ( ; mit != mend; ++mit) { T &detail(*mit); const quint32 detailId = detail.value(QContactDetail__FieldDatabaseId).toUInt(); if (detailId == 0) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Invalid detail modification specified for %1 in contact %2").arg(detailTypeName()).arg(contactId)); return false; } if (!writeCommonDetails(contactId, detailId, detail, syncable, wasLocal, aggregateContact, recordUnhandledChangeFlags, error)) { return false; } if (!aggregateContact) { // Insert the provenance value into the detail, now that we have it const QString provenance(QStringLiteral("%1:%2:%3").arg(ContactCollectionId::databaseId(collectionId)).arg(contactId).arg(detailId)); detail.setValue(QContactDetail::FieldProvenance, provenance); } ContactsDatabase::Query query = bindDetail(m_database, contactId, detailId, true, detail); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to update %1 detail %2 for contact %3").arg(detailTypeName()).arg(detailId).arg(contactId)); *error = QContactManager::UnspecifiedError; return false; } // the delta must be generated such that modifications re-use // the correct detail (with correct internal detailId), so that // this saveDetail() doesn't result in a new detail being added. contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); if (uniqueDetail) { break; } } QList additions(delta.added()); typename QList::iterator ait = additions.begin(), aend = additions.end(); for ( ; ait != aend; ++ait) { T &detail(*ait); const quint32 detailId = writeCommonDetails(contactId, 0, detail, syncable, wasLocal, aggregateContact, recordUnhandledChangeFlags, error); if (detailId == 0) { return false; } detail.setValue(QContactDetail__FieldDatabaseId, detailId); if (!aggregateContact) { // Insert the provenance value into the detail, now that we have it const QString provenance(QStringLiteral("%1:%2:%3").arg(ContactCollectionId::databaseId(collectionId)).arg(contactId).arg(detailId)); detail.setValue(QContactDetail::FieldProvenance, provenance); } ContactsDatabase::Query query = bindDetail(m_database, contactId, detailId, false, detail); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to add %1 detail %2 for contact %3").arg(detailTypeName()).arg(detailId).arg(contactId)); *error = QContactManager::UnspecifiedError; return false; } contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); if (uniqueDetail) { break; } } } else { // clobber all detail values for this contact. if (!removeSpecificDetails(m_database, contactId, error)) return false; if (!removeCommonDetails(contactId, error)) return false; QList contactDetails(contact->details()); if (aggregateContact) { removeDuplicateDetails(&contactDetails); } typename QList::iterator it = contactDetails.begin(), end = contactDetails.end(); for ( ; it != end; ++it) { T &detail(*it); const quint32 detailId = writeCommonDetails(contactId, 0, detail, syncable, wasLocal, aggregateContact, recordUnhandledChangeFlags, error); if (detailId == 0) { return false; } detail.setValue(QContactDetail__FieldDatabaseId, detailId); if (!aggregateContact) { // Insert the provenance value into the detail, now that we have it const QString provenance(QStringLiteral("%1:%2:%3").arg(ContactCollectionId::databaseId(collectionId)).arg(contactId).arg(detailId)); detail.setValue(QContactDetail::FieldProvenance, provenance); } ContactsDatabase::Query query = bindDetail(m_database, contactId, detailId, false, detail); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to write details for %1").arg(detailTypeName())); *error = QContactManager::UnspecifiedError; return false; } contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); if (uniqueDetail) { break; } } } return true; } static int presenceOrder(QContactPresence::PresenceState state) { #ifdef SORT_PRESENCE_BY_AVAILABILITY if (state == QContactPresence::PresenceAvailable) { return 0; } else if (state == QContactPresence::PresenceAway) { return 1; } else if (state == QContactPresence::PresenceExtendedAway) { return 2; } else if (state == QContactPresence::PresenceBusy) { return 3; } else if (state == QContactPresence::PresenceHidden) { return 4; } else if (state == QContactPresence::PresenceOffline) { return 5; } return 6; #else return static_cast(state); #endif } static bool betterPresence(const QContactPresence &detail, const QContactPresence &best) { if (best.isEmpty()) return true; QContactPresence::PresenceState detailState(detail.presenceState()); if (detailState == QContactPresence::PresenceUnknown) return false; return ((presenceOrder(detailState) < presenceOrder(best.presenceState())) || best.presenceState() == QContactPresence::PresenceUnknown); } QContactManager::Error ContactWriter::save( QList *contacts, const DetailList &definitionMask, QMap *aggregatesUpdated, QMap *errorMap, bool withinTransaction, bool withinAggregateUpdate, bool withinSyncUpdate) { QMutexLocker locker(withinTransaction ? nullptr : m_database.accessMutex()); if (contacts->isEmpty()) return QContactManager::NoError; // Check that all of the contacts have the same collectionId. // Note that empty == "local" for all intents and purposes. QContactCollectionId collectionId; if (!withinAggregateUpdate && !withinSyncUpdate) { foreach (const QContact &contact, *contacts) { // retrieve current contact's collectionId const QContactCollectionId currCollectionId = contact.collectionId().isNull() ? ContactCollectionId::apiId(ContactsDatabase::LocalAddressbookCollectionId, m_managerUri) : contact.collectionId(); if (collectionId.isNull()) { collectionId = currCollectionId; } // determine whether it's valid if (collectionId == ContactCollectionId::apiId(ContactsDatabase::AggregateAddressbookCollectionId, m_managerUri)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error: contacts from aggregate collection specified in batch save!")); return QContactManager::UnspecifiedError; } else if (collectionId != currCollectionId) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error: contacts from multiple collections specified in single batch save!")); return QContactManager::UnspecifiedError; } // Also verify the type of this contact const int contactType(contact.detail().type()); if (contactType != QContactType::TypeContact) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error: contact type %1 is not supported").arg(contactType)); return QContactManager::UnspecifiedError; } } } // If this is a non-sync update, and non-aggregate update, // then we may need to record the change as an "unhandled" change // if the collection is marked as such. // These "unhandled" changes occur between fetchChanges and storeChanges/clearChangeFlags // and need to be recorded for reporting in the next fetchChanges result. bool recordUnhandledChangeFlags = false; if (!withinSyncUpdate && !withinAggregateUpdate && m_reader->recordUnhandledChangeFlags(collectionId, &recordUnhandledChangeFlags) != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to determine recordUnhandledChangeFlags value for collection: %1").arg(QString::fromLatin1(collectionId.localId()))); return QContactManager::UnspecifiedError; } if (!withinTransaction && !beginTransaction()) { // only create a transaction if we're not within one already QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to begin database transaction while saving contacts")); return QContactManager::UnspecifiedError; } static const DetailList presenceUpdateDetailTypes(getPresenceUpdateDetailTypes()); bool presenceOnlyUpdate = false; if (definitionMask.contains(detailType())) { // If we only update presence/origin-metadata/online-account, we will report // this change as a presence change only presenceOnlyUpdate = true; foreach (const DetailList::value_type &type, definitionMask) { if (!presenceUpdateDetailTypes.contains(type)) { presenceOnlyUpdate = false; break; } } } bool possibleReactivation = false; QContactManager::Error worstError = QContactManager::NoError; QContactManager::Error err = QContactManager::NoError; for (int i = 0; i < contacts->count(); ++i) { QContact &contact = (*contacts)[i]; QContactId contactId = ContactId::apiId(contact); quint32 dbId = ContactId::databaseId(contactId); bool aggregateUpdated = false; if (dbId == 0) { err = create(&contact, definitionMask, true, withinAggregateUpdate, withinSyncUpdate, recordUnhandledChangeFlags); if (err == QContactManager::NoError) { contactId = ContactId::apiId(contact); dbId = ContactId::databaseId(contactId); m_addedIds.insert(contactId); } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error creating contact in collection: %2 : %3") .arg(ContactCollectionId::toString(contact.collectionId())).arg(err)); } } else { err = update(&contact, definitionMask, &aggregateUpdated, true, withinAggregateUpdate, withinSyncUpdate, recordUnhandledChangeFlags, presenceOnlyUpdate); if (err == QContactManager::NoError) { if (presenceOnlyUpdate) { m_presenceChangedIds.insert(contactId); } else { possibleReactivation = true; m_changedIds.insert(contactId); } } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error updating contact %1: %2").arg(ContactId::toString(contactId)).arg(err)); } } if (err == QContactManager::NoError) { if (aggregatesUpdated) { aggregatesUpdated->insert(i, aggregateUpdated); } const QContactCollectionId currCollectionId = contact.collectionId().isNull() ? ContactCollectionId::apiId(ContactsDatabase::LocalAddressbookCollectionId, m_managerUri) : contact.collectionId(); if (ContactCollectionId::databaseId(currCollectionId) != ContactsDatabase::AggregateAddressbookCollectionId && !m_suppressedCollectionIds.contains(currCollectionId)) { m_collectionContactsChanged.insert(currCollectionId); } } else { worstError = err; if (errorMap) { errorMap->insert(i, err); } } } if (m_database.aggregating() && !withinAggregateUpdate && possibleReactivation && worstError == QContactManager::NoError) { // Some contacts may need to have new aggregates created // if they previously had a QContactDeactivated detail // and this detail was removed (i.e. reactivated). QContactManager::Error aggregateError = aggregateOrphanedContacts(true, withinSyncUpdate); if (aggregateError != QContactManager::NoError) worstError = aggregateError; } if (!withinTransaction) { // only attempt to commit/rollback the transaction if we created it if (worstError != QContactManager::NoError) { // If anything failed at all, we need to rollback, so that we do not // have an inconsistent state between aggregate and constituent contacts // Any contacts we 'added' are not actually added - clear their IDs for (int i = 0; i < contacts->count(); ++i) { QContact &contact = (*contacts)[i]; const QContactId contactId = ContactId::apiId(contact); if (m_addedIds.contains(contactId)) { contact.setId(QContactId()); if (errorMap) { // We also need to report an error for this contact, even though there // is no true error preventing it from being updated errorMap->insert(i, QContactManager::LockedError); } } } rollbackTransaction(); return worstError; } else if (!commitTransaction()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to commit contacts")); return QContactManager::UnspecifiedError; } } return worstError; } template void appendDetailType(ContactWriter::DetailList *list) { list->append(T::Type); } static ContactWriter::DetailList allSupportedDetails() { ContactWriter::DetailList details; appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); return details; } static ContactWriter::DetailList allSingularDetails() { ContactWriter::DetailList details; appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); appendDetailType(&details); return details; } static QContactManager::Error enforceDetailConstraints(QContact *contact) { static const ContactWriter::DetailList supported(allSupportedDetails()); static const ContactWriter::DetailList singular(allSingularDetails()); QHash detailCounts; QSet detailUris; // look for unsupported detail data. foreach (const QContactDetail &det, contact->details()) { if (!detailListContains(supported, det)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Invalid detail type: %1 %2").arg(detailTypeName(det)).arg(det.type())); if (det.isEmpty()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Detail is also empty!")); } else { QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Dumping detail contents:")); dumpContactDetail(det); } QTCONTACTS_SQLITE_DEBUG(QString::fromLatin1("Dumping contact contents:")); dumpContact(*contact); return QContactManager::InvalidDetailError; } else { ++detailCounts[detailType(det)]; // Verify that detail URIs are unique within the contact const QString detailUri(det.detailUri()); if (!detailUri.isEmpty()) { if (detailUris.contains(detailUri)) { // This URI conflicts with one already present in the contact QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Detail URI conflict on: %1 %2 %3").arg(detailUri).arg(detailTypeName(det)).arg(det.type())); return QContactManager::InvalidDetailError; } detailUris.insert(detailUri); } } } // enforce uniqueness constraints foreach (const ContactWriter::DetailList::value_type &type, singular) { if (detailCounts[type] > 1) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Invalid count of detail type %1: %2").arg(detailTypeName(type)).arg(detailCounts[type])); return QContactManager::LimitReachedError; } } return QContactManager::NoError; } static bool promoteDetailType(QContactDetail::DetailType type, const ContactWriter::DetailList &definitionMask, bool forcePromotion) { static const ContactWriter::DetailList unpromotedDetailTypes(getUnpromotedDetailTypes()); static const ContactWriter::DetailList absolutelyUnpromotedDetailTypes(getAbsolutelyUnpromotedDetailTypes()); // Timestamp is promoted in every update if (type == QContactTimestamp::Type) return true; if (!definitionMask.isEmpty() && !detailListContains(definitionMask, type)) return false; // Some detail types are not promoted even if promotion is forced const ContactWriter::DetailList &unpromotedTypes(forcePromotion ? absolutelyUnpromotedDetailTypes : unpromotedDetailTypes); return !detailListContains(unpromotedTypes, type); } /* For every detail in a contact \a c, this function will check to see if an identical detail already exists in the \a aggregate contact. If not, the detail from \a c will be "promoted" (saved in) the \a aggregate contact. Note that QContactSyncTarget and QContactGuid details will NOT be promoted, nor will QContactDisplayLabel or QContactType details. */ static void promoteDetailsToAggregate(const QContact &contact, QContact *aggregate, const ContactWriter::DetailList &definitionMask, bool forcePromotion) { foreach (const QContactDetail &original, contact.details()) { if (!promoteDetailType(original.type(), definitionMask, forcePromotion)) { // skip this detail continue; } // promote this detail to the aggregate. Depending on uniqueness, // this consists either of composition or duplication. // Note: Composed (unique) details won't have any detailUri! if (detailType(original) == detailType()) { // name involves composition QContactName cname(original); QContactName aname(aggregate->detail()); if (!cname.prefix().isEmpty() && aname.prefix().isEmpty()) aname.setPrefix(cname.prefix()); if (!cname.firstName().isEmpty() && aname.firstName().isEmpty()) aname.setFirstName(cname.firstName()); if (!cname.middleName().isEmpty() && aname.middleName().isEmpty()) aname.setMiddleName(cname.middleName()); if (!cname.lastName().isEmpty() && aname.lastName().isEmpty()) aname.setLastName(cname.lastName()); if (!cname.suffix().isEmpty() && aname.suffix().isEmpty()) aname.setSuffix(cname.suffix()); QString customLabel = cname.value(QContactName::FieldCustomLabel); if (!customLabel.isEmpty() && aname.value(QContactName::FieldCustomLabel).isEmpty()) aname.setValue(QContactName::FieldCustomLabel, cname.value(QContactName::FieldCustomLabel)); aggregate->saveDetail(&aname, QContact::IgnoreAccessConstraints); } else if (detailType(original) == detailType()) { // timestamp involves composition // Note: From some sync sources, the creation timestamp will precede the existence of the local device. QContactTimestamp cts(original); QContactTimestamp ats(aggregate->detail()); if (cts.lastModified().isValid() && (!ats.lastModified().isValid() || cts.lastModified() > ats.lastModified())) { ats.setLastModified(cts.lastModified()); } if (cts.created().isValid() && !ats.created().isValid()) { ats.setCreated(cts.created()); } aggregate->saveDetail(&ats, QContact::IgnoreAccessConstraints); } else if (detailType(original) == detailType()) { // gender involves composition QContactGender cg(original); QContactGender ag(aggregate->detail()); // In Qtpim, uninitialized gender() does not default to GenderUnspecified... if (cg.gender() != QContactGender::GenderUnspecified && (ag.gender() != QContactGender::GenderMale && ag.gender() != QContactGender::GenderFemale)) { ag.setGender(cg.gender()); aggregate->saveDetail(&ag, QContact::IgnoreAccessConstraints); } } else if (detailType(original) == detailType()) { // favorite involves composition QContactFavorite cf(original); QContactFavorite af(aggregate->detail()); if ((cf.isFavorite() && !af.isFavorite()) || aggregate->details().isEmpty()) { af.setFavorite(cf.isFavorite()); aggregate->saveDetail(&af, QContact::IgnoreAccessConstraints); } } else if (detailType(original) == detailType()) { // birthday involves composition (at least, it's unique) QContactBirthday cb(original); QContactBirthday ab(aggregate->detail()); if (!ab.dateTime().isValid() || aggregate->details().isEmpty()) { ab.setDateTime(cb.dateTime()); aggregate->saveDetail(&ab, QContact::IgnoreAccessConstraints); } } else { // All other details involve duplication. // Only duplicate from contact to the aggregate if an identical detail doesn't already exist in the aggregate. QContactDetail det(original); bool needsPromote = true; foreach (const QContactDetail &ad, aggregate->details()) { if (detailsEquivalent(det, ad)) { needsPromote = false; break; } } if (needsPromote) { // all aggregate details are non-modifiable. QContactManagerEngine::setDetailAccessConstraints(&det, QContactDetail::ReadOnly | QContactDetail::Irremovable); det.setValue(QContactDetail__FieldModifiable, false); // Store the provenance of this promoted detail det.setValue(QContactDetail::FieldProvenance, original.value(QContactDetail::FieldProvenance)); adjustAggregateDetailProperties(det, contact); aggregate->saveDetail(&det, QContact::IgnoreAccessConstraints); } } } } /* This function is called when a new contact is created. The aggregate contacts are searched for a match, and the matching one updated if it exists; or a new aggregate is created. */ QContactManager::Error ContactWriter::updateOrCreateAggregate(QContact *contact, const DetailList &definitionMask, bool withinTransaction, bool withinSyncUpdate, bool createOnly, quint32 *aggregateContactId) { // 1) search for match // 2) if exists, update the existing aggregate (by default, non-clobber: // only update empty fields of details, or promote non-existent details. Never delete or replace details.) // 3) otherwise, create new aggregate, consisting of all details of contact, return. quint32 existingAggregateId = 0; QContact matchingAggregate; // We need to search to find an appropriate aggregate QString firstName; QString lastName; QString nickname; QVariantList phoneNumbers; QVariantList emailAddresses; QVariantList accountUris; QString syncTarget; QString excludeGender; foreach (const QContactName &detail, contact->details()) { firstName = detail.firstName().toLower(); lastName = detail.lastName().toLower(); break; } foreach (const QContactNickname &detail, contact->details()) { nickname = detail.nickname().toLower(); break; } foreach (const QContactPhoneNumber &detail, contact->details()) { phoneNumbers.append(ContactsEngine::normalizedPhoneNumber(detail.number())); } foreach (const QContactEmailAddress &detail, contact->details()) { emailAddresses.append(detail.emailAddress().toLower()); } foreach (const QContactOnlineAccount &detail, contact->details()) { accountUris.append(detail.accountUri().toLower()); } syncTarget = contact->detail().syncTarget(); const QContactGender gender(contact->detail()); if (gender.gender() == QContactGender::GenderMale) { excludeGender = QString::number(static_cast(QContactGender::GenderFemale)); } else if (gender.gender() == QContactGender::GenderFemale) { excludeGender = QString::number(static_cast(QContactGender::GenderMale)); } else { excludeGender = QStringLiteral("none"); } /* Aggregation heuristic. Search existing aggregate contacts, for matchability. The aggregate with the highest match score (over the threshold) represents the same "actual person". The newly saved contact then becomes a constituent of that aggregate. Note that individual contacts from the same sync collection can represent the same actual person (eg, Telepathy might provide buddies from different Jabber servers/rosters and thus if you have the same buddy on multiple services, they need to be aggregated together. Stages: 1) select all possible aggregate ids 2) join those ids on the tables of interest to get the data we match against 3) perform the heuristic matching, ordered by "best score" 4) select highest score; if over threshold, select that as aggregate. */ static const QString possibleAggregatesWhere(QStringLiteral( /* SELECT contactId FROM Contacts ... */ " WHERE Contacts.collectionId = 1" // AggregateAddressbookCollectionId " AND Contacts.contactId IN (" " SELECT contactId FROM Names" " WHERE COALESCE(:lastName, '') = ''" " OR COALESCE(lowerLastName, '') = ''" " OR lowerLastName = :lastName" " UNION" " SELECT contactId FROM Nicknames" " WHERE contactId NOT IN (SELECT contactId FROM Names))" " AND Contacts.contactId NOT IN (" " SELECT contactId FROM Genders" " WHERE gender = :excludeGender)" " AND contactId > 2" // exclude self contact " AND isDeactivated = 0" // exclude deactivated " AND contactId NOT IN (" " SELECT secondId FROM Relationships WHERE firstId = :contactId AND type = 'IsNot'" " UNION" " SELECT firstId FROM Relationships WHERE secondId = :contactId AND type = 'IsNot'" " )")); // Use a simple match algorithm, looking for exact matches on name fields, // or accumulating points for name matches (including partial matches of first name). // step one: build the temporary table which contains all "possible" aggregate contact ids. m_database.clearTemporaryContactIdsTable(possibleAggregatesTable); const QString orderBy = QStringLiteral("contactId ASC "); const QString where = possibleAggregatesWhere; QMap bindings; bindings.insert(":lastName", lastName); bindings.insert(":contactId", ContactId::databaseId(*contact)); bindings.insert(":excludeGender", excludeGender); if (!m_database.createTemporaryContactIdsTable(possibleAggregatesTable, QString(), where, orderBy, bindings)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error creating possibleAggregates temporary table")); return QContactManager::UnspecifiedError; } // step two: query matching data. const QString heuristicallyMatchData(QStringLiteral( " SELECT Matches.contactId, sum(Matches.score) AS total FROM (" " SELECT Names.contactId, 20 AS score FROM Names" " INNER JOIN temp.possibleAggregates ON Names.contactId = temp.possibleAggregates.contactId" " WHERE lowerLastName != '' AND lowerLastName = :lastName" " AND lowerFirstName != '' AND lowerFirstName = :firstName" " UNION" " SELECT Names.contactId, 15 AS score FROM Names" " INNER JOIN temp.possibleAggregates ON Names.contactId = temp.possibleAggregates.contactId" " WHERE COALESCE(lowerFirstName,'') = '' AND COALESCE(:firstName,'') = ''" " AND COALESCE(lowerLastName, '') = '' AND COALESCE(:lastName, '') = ''" " AND EXISTS (" " SELECT * FROM Nicknames" " WHERE Nicknames.contactId = Names.contactId" " AND lowerNickName = :nickname)" " UNION" " SELECT Nicknames.contactId, 15 AS score FROM Nicknames" " INNER JOIN temp.possibleAggregates ON Nicknames.contactId = temp.possibleAggregates.contactId" " WHERE lowerNickName = :nickname" " AND COALESCE(:firstName,'') = ''" " AND COALESCE(:lastName, '') = ''" " AND NOT EXISTS (" " SELECT * FROM Names WHERE Names.contactId = Nicknames.contactId )" " UNION" " SELECT Names.contactId, 12 AS score FROM Names" " INNER JOIN temp.possibleAggregates ON Names.contactId = temp.possibleAggregates.contactId" " WHERE (COALESCE(lowerLastName, '') = '' OR COALESCE(:lastName, '') = '')" " AND lowerFirstName != '' AND lowerFirstName = :firstName" " UNION" " SELECT Names.contactId, 12 AS score FROM Names" " INNER JOIN temp.possibleAggregates ON Names.contactId = temp.possibleAggregates.contactId" " WHERE lowerLastName != '' AND lowerLastName = :lastName" " AND (COALESCE(lowerFirstName, '') = '' OR COALESCE(:firstName, '') = '')" " UNION" " SELECT EmailAddresses.contactId, 3 AS score FROM EmailAddresses" " INNER JOIN temp.possibleAggregates ON EmailAddresses.contactId = temp.possibleAggregates.contactId" " INNER JOIN temp.matchEmailAddresses ON EmailAddresses.lowerEmailAddress = temp.matchEmailAddresses.value" " UNION" " SELECT PhoneNumbers.contactId, 3 AS score FROM PhoneNumbers" " INNER JOIN temp.possibleAggregates ON PhoneNumbers.contactId = temp.possibleAggregates.contactId" " INNER JOIN temp.matchPhoneNumbers ON PhoneNumbers.normalizedNumber = temp.matchPhoneNumbers.value" " UNION" " SELECT OnlineAccounts.contactId, 3 AS score FROM OnlineAccounts" " INNER JOIN temp.possibleAggregates ON OnlineAccounts.contactId = temp.possibleAggregates.contactId" " INNER JOIN temp.matchOnlineAccounts ON OnlineAccounts.lowerAccountUri = temp.matchOnlineAccounts.value" " UNION" " SELECT Nicknames.contactId, 1 AS score FROM Nicknames" " INNER JOIN temp.possibleAggregates ON Nicknames.contactId = temp.possibleAggregates.contactId" " WHERE lowerNickName != '' AND lowerNickName = :nickname" " ) AS Matches" " GROUP BY Matches.contactId" " ORDER BY total DESC" " LIMIT 1" )); m_database.clearTemporaryValuesTable(matchEmailAddressesTable); m_database.clearTemporaryValuesTable(matchPhoneNumbersTable); m_database.clearTemporaryValuesTable(matchOnlineAccountsTable); if (!m_database.createTemporaryValuesTable(matchEmailAddressesTable, emailAddresses) || !m_database.createTemporaryValuesTable(matchPhoneNumbersTable, phoneNumbers) || !m_database.createTemporaryValuesTable(matchOnlineAccountsTable, accountUris)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error creating possibleAggregates match tables")); return QContactManager::UnspecifiedError; } ContactsDatabase::Query query(m_database.prepare(heuristicallyMatchData)); query.bindValue(":firstName", firstName); query.bindValue(":lastName", lastName); query.bindValue(":nickname", nickname); if (!ContactsDatabase::execute(query)) { query.reportError("Error finding match for updated local contact"); return QContactManager::UnspecifiedError; } if (query.next()) { const quint32 aggregateId = query.value(0); const quint32 score = query.value(1); static const quint32 MinimumMatchScore = 15; if (score >= MinimumMatchScore) { existingAggregateId = aggregateId; } } if (!existingAggregateId) { // need to create an aggregating contact first. matchingAggregate.setCollectionId(ContactCollectionId::apiId(ContactsDatabase::AggregateAddressbookCollectionId, m_managerUri)); } else if (!createOnly) { // aggregate already exists. QList readIds; readIds.append(existingAggregateId); QContactFetchHint hint; hint.setOptimizationHints(QContactFetchHint::NoRelationships); QList readList; QContactManager::Error readError = m_reader->readContacts(QStringLiteral("CreateAggregate"), &readList, readIds, hint); if (readError != QContactManager::NoError || readList.size() < 1) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read aggregate contact %1 during regenerate").arg(existingAggregateId)); return QContactManager::UnspecifiedError; } matchingAggregate = readList.at(0); } QContactManager::Error err = QContactManager::NoError; QMap errorMap; QContactId matchingAggregateId; if (existingAggregateId && createOnly) { // the caller has specified that we should not update existing aggregates. // this is because it will manually regenerate the aggregates themselves, // with specific detail promotion order (e.g. prefer local contact details). matchingAggregateId = QContactId(ContactId::apiId(existingAggregateId, m_managerUri)); } else { // whether it's an existing or new contact, we promote details. // TODO: promote non-Aggregates relationships! promoteDetailsToAggregate(*contact, &matchingAggregate, definitionMask, false); // now save in database. QList saveContactList; saveContactList.append(matchingAggregate); err = save(&saveContactList, DetailList(), 0, &errorMap, withinTransaction, true, false); // we're updating (or creating) the aggregate if (err != QContactManager::NoError) { if (!existingAggregateId) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Could not create new aggregate contact")); } else { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Could not update existing aggregate contact")); } return err; } matchingAggregateId = saveContactList.at(0).id(); } { // add the relationship and save in the database. // Note: we DON'T use the existing save(relationshipList, ...) function // as it does (expensive) aggregate regeneration which we have already // done above (via the detail promotion and aggregate save). // Instead, we simply add the "aggregates" relationship directly. const QString insertRelationship(QStringLiteral( " INSERT INTO Relationships (firstId, secondId, type)" " VALUES (:firstId, :secondId, :type)" )); ContactsDatabase::Query query(m_database.prepare(insertRelationship)); query.bindValue(":firstId", ContactId::databaseId(matchingAggregateId)); query.bindValue(":secondId", ContactId::databaseId(*contact)); query.bindValue(":type", relationshipString(QContactRelationship::Aggregates)); if (!ContactsDatabase::execute(query)) { query.reportError("Error inserting Aggregates relationship"); err = QContactManager::UnspecifiedError; } } if (err == QContactManager::NoError) { if (aggregateContactId) { *aggregateContactId = ContactId::databaseId(matchingAggregateId); } } else { // if the aggregation relationship fails, the entire save has failed. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to save aggregation relationship!")); if (!existingAggregateId) { // clean up the newly created contact. QList removeList; removeList.append(matchingAggregateId); QContactManager::Error cleanupErr = remove(removeList, &errorMap, withinTransaction, withinSyncUpdate); if (cleanupErr != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Unable to cleanup newly created aggregate contact!")); } } } return err; } /* This function is called as part of the "remove contacts" codepath. Any aggregate contacts which still exist after the remove operation which used to aggregate a contact which was removed during the operation needs to be regenerated (as some details may no longer be valid). If the operation fails, it's not a huge issue - we don't need to rollback the database. It simply means that the existing aggregates may contain some stale data. */ QContactManager::Error ContactWriter::regenerateAggregates(const QList &aggregateIds, const DetailList &definitionMask, bool withinTransaction) { static const DetailList identityDetailTypes(getIdentityDetailTypes()); // for each aggregate contact: // 1) get the contacts it aggregates // 2) build unique details via composition (name / timestamp / gender / favorite - NOT synctarget or guid) // 3) append non-unique details // In all cases, we "prefer" the 'local' contact's data (if it exists) QList aggregatesToSave; QSet aggregatesToSaveIds; QVariantList aggregatesToRemove; foreach (quint32 aggId, aggregateIds) { const QContactId apiId(ContactId::apiId(aggId, m_managerUri)); if (aggregatesToSaveIds.contains(apiId)) { continue; } QList readIds; readIds.append(aggId); { const QString findConstituentsForAggregate(QStringLiteral( " SELECT secondId FROM Relationships" " WHERE firstId = :aggregateId AND type = 'Aggregates'" " AND secondId NOT IN (SELECT contactId FROM Contacts WHERE changeFlags >= 4)" )); ContactsDatabase::Query query(m_database.prepare(findConstituentsForAggregate)); query.bindValue(":aggregateId", aggId); if (!ContactsDatabase::execute(query)) { query.reportError(QStringLiteral("Failed to find constituent contacts for aggregate %1 during regenerate").arg(aggId)); return QContactManager::UnspecifiedError; } while (query.next()) { readIds.append(query.value(0)); } } if (readIds.size() == 1) { // only the aggregate? QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Existing aggregate %1 should already have been removed - aborting regenerate").arg(aggId)); return QContactManager::UnspecifiedError; } QContactFetchHint hint; hint.setOptimizationHints(QContactFetchHint::NoRelationships); QList readList; QContactManager::Error readError = m_reader->readContacts(QStringLiteral("RegenerateAggregate"), &readList, readIds, hint); if (readError != QContactManager::NoError || readList.size() <= 1 || ContactCollectionId::databaseId(readList.at(0).collectionId()) != ContactsDatabase::AggregateAddressbookCollectionId) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read constituent contacts for aggregate %1 during regenerate").arg(aggId)); return QContactManager::UnspecifiedError; } // See if there are any constituents to aggregate bool activeConstituent = false; for (int i = 1; i < readList.size(); ++i) { // start from 1 to skip aggregate const QContact &curr(readList.at(i)); if (curr.details().count() == 0) { activeConstituent = true; break; } } if (!activeConstituent) { // No active constituents - we need to remove this aggregate aggregatesToRemove.append(QVariant(aggId)); continue; } QContact originalAggregateContact = readList.at(0); QContact aggregateContact; aggregateContact.setId(originalAggregateContact.id()); aggregateContact.setCollectionId(originalAggregateContact.collectionId()); // Copy any existing fields not affected by this update foreach (const QContactDetail &detail, originalAggregateContact.details()) { if (detailListContains(identityDetailTypes, detail) || !promoteDetailType(detail.type(), definitionMask, false)) { // Copy this detail to the new aggregate QContactDetail newDetail(detail); if (!aggregateContact.saveDetail(&newDetail, QContact::IgnoreAccessConstraints)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Contact: %1 Failed to copy existing detail:") .arg(ContactId::toString(aggregateContact)) << detail); } } } // Step two: search for the "local" contacts and promote their details first bool foundFirstLocal = false; for (int i = 1; i < readList.size(); ++i) { // start from 1 to skip aggregate QContact curr = readList.at(i); if (curr.details().count()) continue; if (ContactCollectionId::databaseId(curr.collectionId()) != ContactsDatabase::LocalAddressbookCollectionId) continue; if (!foundFirstLocal) { foundFirstLocal = true; const QList currDetails = curr.details(); for (int j = 0; j < currDetails.size(); ++j) { QContactDetail currDet = currDetails.at(j); if (promoteDetailType(currDet.type(), definitionMask, false)) { // unconditionally promote this detail to the aggregate. adjustAggregateDetailProperties(currDet, curr); aggregateContact.saveDetail(&currDet, QContact::IgnoreAccessConstraints); } } } else { promoteDetailsToAggregate(curr, &aggregateContact, definitionMask, false); } } // Step Three: promote data from details of other related contacts for (int i = 1; i < readList.size(); ++i) { // start from 1 to skip aggregate QContact curr = readList.at(i); if (curr.details().count()) continue; if (ContactCollectionId::databaseId(curr.collectionId()) == ContactsDatabase::LocalAddressbookCollectionId) { continue; // already promoted the "local" contact's details. } // need to promote this contact's details to the aggregate promoteDetailsToAggregate(curr, &aggregateContact, definitionMask, false); } // we save the updated aggregates to database all in a batch at the end. aggregatesToSave.append(aggregateContact); aggregatesToSaveIds.insert(ContactId::apiId(aggregateContact)); } if (!aggregatesToSave.isEmpty()) { QMap errorMap; QContactManager::Error writeError = save(&aggregatesToSave, definitionMask, 0, &errorMap, withinTransaction, true, false); // we're updating aggregates. if (writeError != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to write updated aggregate contacts during regenerate. definitionMask:") << definitionMask); return writeError; } } if (!aggregatesToRemove.isEmpty()) { QContactManager::Error removeError = removeContacts(aggregatesToRemove); if (removeError != QContactManager::NoError) { return removeError; } } return QContactManager::NoError; } QContactManager::Error ContactWriter::removeChildlessAggregates(QList *removedIds) { QVariantList aggregateIds; const QString childlessAggregateIds(QStringLiteral( " SELECT contactId FROM Contacts" " WHERE collectionId = 1" // AggregateAddressbookCollectionId " AND contactId NOT IN (" " SELECT DISTINCT firstId FROM Relationships" " WHERE type = 'Aggregates'" " AND secondId NOT IN (" " SELECT contactId FROM Contacts WHERE changeFlags >= 4" // ChangeFlags::IsDeleted " )" " )" )); ContactsDatabase::Query query(m_database.prepare(childlessAggregateIds)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch childless aggregate contact ids during remove"); return QContactManager::UnspecifiedError; } while (query.next()) { quint32 aggregateId = query.value(0); aggregateIds.append(aggregateId); removedIds->append(ContactId::apiId(aggregateId, m_managerUri)); } if (aggregateIds.size() > 0) { QContactManager::Error removeError = removeContacts(aggregateIds); if (removeError != QContactManager::NoError) { return removeError; } } return QContactManager::NoError; } QContactManager::Error ContactWriter::aggregateOrphanedContacts(bool withinTransaction, bool withinSyncUpdate) { QList contactIds; { const QString orphanContactIds(QStringLiteral( " SELECT contactId FROM Contacts" " WHERE isDeactivated = 0" " AND changeFlags < 4" // ChangeFlags::IsDeleted " AND collectionId IN (" " SELECT collectionId FROM Collections WHERE aggregable = 1" " )" " AND contactId NOT IN (" " SELECT DISTINCT secondId FROM Relationships WHERE type = 'Aggregates'" " )" )); ContactsDatabase::Query query(m_database.prepare(orphanContactIds)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch orphan aggregate contact ids during remove"); return QContactManager::UnspecifiedError; } while (query.next()) { contactIds.append(query.value(0)); } } if (contactIds.size() > 0) { QContactFetchHint hint; hint.setOptimizationHints(QContactFetchHint::NoRelationships); QList readList; QContactManager::Error readError = m_reader->readContacts(QStringLiteral("AggregateOrphaned"), &readList, contactIds, hint); if (readError != QContactManager::NoError || readList.size() != contactIds.size()) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read orphaned contacts for aggregation")); return QContactManager::UnspecifiedError; } QList::iterator it = readList.begin(), end = readList.end(); for ( ; it != end; ++it) { QContact &orphan(*it); QContactManager::Error error = updateOrCreateAggregate(&orphan, DetailList(), withinTransaction, withinSyncUpdate); if (error != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to create aggregate for orphaned contact: %1").arg(ContactId::toString(orphan))); return error; } } } return QContactManager::NoError; } static bool updateGlobalPresence(QContact *contact) { QContactGlobalPresence globalPresence = contact->detail(); const QList details = contact->details(); if (details.isEmpty()) { // No presence - remove global presence if present if (!globalPresence.isEmpty()) { contact->removeDetail(&globalPresence); } return true; } QContactPresence bestPresence; foreach (const QContactPresence &detail, details) { if (betterPresence(detail, bestPresence)) { bestPresence = detail; } } globalPresence.setPresenceState(bestPresence.presenceState()); globalPresence.setPresenceStateText(bestPresence.presenceStateText()); globalPresence.setTimestamp(bestPresence.timestamp()); globalPresence.setNickname(bestPresence.nickname()); globalPresence.setCustomMessage(bestPresence.customMessage()); contact->saveDetail(&globalPresence, QContact::IgnoreAccessConstraints); return true; } static bool updateTimestamp(QContact *contact, bool setCreationTimestamp) { QContactTimestamp timestamp = contact->detail(); QDateTime createdTime = timestamp.created().toUTC(); QDateTime modifiedTime = QDateTime::currentDateTimeUtc(); // always clobber last modified timestamp. timestamp.setLastModified(modifiedTime); if (setCreationTimestamp && !createdTime.isValid()) { timestamp.setCreated(modifiedTime); } return contact->saveDetail(×tamp, QContact::IgnoreAccessConstraints); } QContactManager::Error ContactWriter::create(QContact *contact, const DetailList &definitionMask, bool withinTransaction, bool withinAggregateUpdate, bool withinSyncUpdate, bool recordUnhandledChangeFlags) { // If not specified, this contact is a "local device" contact bool contactIsLocal = false; const QContactCollectionId localAddressbookId(ContactCollectionId::apiId(ContactsDatabase::LocalAddressbookCollectionId, m_managerUri)); if (contact->collectionId().isNull()) { contact->setCollectionId(localAddressbookId); } // If this contact is local, ensure it has a GUID for import/export stability if (contact->collectionId() == localAddressbookId) { contactIsLocal = true; QContactGuid guid = contact->detail(); if (guid.guid().isEmpty()) { guid.setGuid(QUuid::createUuid().toString()); contact->saveDetail(&guid, QContact::IgnoreAccessConstraints); } } if (definitionMask.isEmpty() || detailListContains(definitionMask) || detailListContains(definitionMask)) { // update the global presence (display label may be derived from it) updateGlobalPresence(contact); } // update the display label for this contact m_engine.regenerateDisplayLabel(*contact, &m_displayLabelGroupsChanged); // update the timestamp if necessary (aggregate contacts should have a composed timestamp value) if (!m_database.aggregating() || (contact->collectionId() != ContactCollectionId::apiId(ContactsDatabase::AggregateAddressbookCollectionId, m_managerUri))) { // only update the timestamp for "normal" modifications, not updates caused by sync, // as we should retain the revision timestamp for synced contacts. if (!withinSyncUpdate) { updateTimestamp(contact, true); } } QContactManager::Error writeErr = enforceDetailConstraints(contact); if (writeErr != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Contact failed detail constraints")); return writeErr; } quint32 contactId = 0; { ContactsDatabase::Query query(bindContactDetails(*contact, withinSyncUpdate || withinAggregateUpdate, recordUnhandledChangeFlags)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to create contact"); return QContactManager::UnspecifiedError; } contactId = query.lastInsertId().toUInt(); } writeErr = write(contactId, QContact(), contact, definitionMask, recordUnhandledChangeFlags); if (writeErr == QContactManager::NoError) { // successfully saved all data. Update id. contact->setId(ContactId::apiId(contactId, m_managerUri)); if (m_database.aggregating() && !withinAggregateUpdate) { // and either update the aggregate contact (if it exists) or create a new one // (unless it is an aggregate contact, or should otherwise not be aggregated). bool aggregable = contactIsLocal; // local contacts are always aggregable. if (!aggregable) { writeErr = collectionIsAggregable(contact->collectionId(), &aggregable); if (writeErr != QContactManager::NoError) { contact->setId(QContactId()); // reset to null id as the transaction will rolled back. return writeErr; } } if (aggregable) { writeErr = setAggregate(contact, contactId, false, definitionMask, withinTransaction, withinSyncUpdate); if (writeErr != QContactManager::NoError) { contact->setId(QContactId()); // reset to null id as the transaction will rolled back. return writeErr; } } } } if (writeErr != QContactManager::NoError) { // error occurred. Remove the failed entry. const QString removeContact(QStringLiteral( " DELETE FROM Contacts WHERE contactId = :contactId" )); ContactsDatabase::Query query(m_database.prepare(removeContact)); query.bindValue(":contactId", contactId); if (!ContactsDatabase::execute(query)) { query.reportError("Unable to remove stale contact after failed save"); } } return writeErr; } QContactManager::Error ContactWriter::update(QContact *contact, const DetailList &definitionMask, bool *aggregateUpdated, bool withinTransaction, bool withinAggregateUpdate, bool withinSyncUpdate, bool recordUnhandledChangeFlags, bool transientUpdate) { *aggregateUpdated = false; quint32 contactId = ContactId::databaseId(*contact); int exists = 0; int changeFlags = 0; QContactCollectionId oldCollectionId; { const QString checkContactExists(QStringLiteral( " SELECT COUNT(contactId), collectionId, changeFlags FROM Contacts WHERE contactId = :contactId" )); ContactsDatabase::Query query(m_database.prepare(checkContactExists)); query.bindValue(0, contactId); if (!ContactsDatabase::execute(query) || !query.next()) { query.reportError("Failed to check contact existence"); return QContactManager::UnspecifiedError; } else { exists = query.value(0); oldCollectionId = ContactCollectionId::apiId(query.value(1), m_managerUri); changeFlags = query.value(2); } } if (!exists) { return QContactManager::DoesNotExistError; } if (ContactCollectionId::databaseId(oldCollectionId) == ContactsDatabase::LocalAddressbookCollectionId && contact->collectionId().isNull()) { contact->setCollectionId(oldCollectionId); } if (!oldCollectionId.isNull() && contact->collectionId() != oldCollectionId) { // they are attempting to manually change the collectionId of a contact QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot manually change collectionId: %1 to %2") .arg(ContactCollectionId::databaseId(oldCollectionId)).arg(ContactCollectionId::databaseId(contact->collectionId()))); return QContactManager::UnspecifiedError; } // check to see if this is an attempted undeletion. QContactManager::Error writeError = QContactManager::NoError; if (changeFlags >= ContactsDatabase::IsDeleted) { QList undeleteDetails = contact->details(); if (undeleteDetails.size() == 0) { // the only modification we allow to deleted contacts is undeletion. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot modify deleted contact: %1").arg(contactId)); return QContactManager::DoesNotExistError; } // undelete the contact. writeError = undeleteContacts(QVariantList() << contactId, recordUnhandledChangeFlags); if (writeError != QContactManager::NoError) { return writeError; } // regenerate the undeleted contact data from the database. QContactFetchHint hint; hint.setOptimizationHints(QContactFetchHint::NoRelationships); QList undeletedList; QContactManager::Error readError = m_reader->readContacts(QStringLiteral("RegenerateUndeleted"), &undeletedList, QList() << contactId, hint); if (readError != QContactManager::NoError || undeletedList.size() != 1) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read undeleted contact data for regenerate: %1").arg(contactId)); return QContactManager::UnspecifiedError; } *contact = undeletedList.first(); // if the database is aggregating, fall through, as we may need to // recreate or regenerate the aggregate, below. if (!m_database.aggregating()) { return writeError; } } else { writeError = enforceDetailConstraints(contact); if (writeError != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Contact failed detail constraints")); return writeError; } // update the modification timestamp (aggregate contacts should have a composed timestamp value) if (!m_database.aggregating() || (contact->collectionId() != ContactCollectionId::apiId(ContactsDatabase::AggregateAddressbookCollectionId, m_managerUri))) { // only update the timestamp for "normal" modifications, not updates caused by sync, // as we should retain the revision timestamp for synced contacts. if (!withinSyncUpdate) { updateTimestamp(contact, false); } } if (m_database.aggregating() && (!withinAggregateUpdate && oldCollectionId == ContactCollectionId::apiId(ContactsDatabase::AggregateAddressbookCollectionId, m_managerUri))) { // Attempting to update an aggregate contact directly. // This codepath should not be possible, and if hit // is always a result of a bug in qtcontacts-sqlite. QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Error: direct modification of aggregate contact %1").arg(contactId)); return QContactManager::UnspecifiedError; } if (definitionMask.isEmpty() || detailListContains(definitionMask) || detailListContains(definitionMask)) { // update the global presence (display label may be derived from it) updateGlobalPresence(contact); } // update the display label for this contact m_engine.regenerateDisplayLabel(*contact, &m_displayLabelGroupsChanged); // Can this update be transient, or does it need to be durable? if (transientUpdate) { // Instead of updating the database, store these minor changes only to the transient store QList transientDetails; foreach (const QContactDetail &detail, contact->details()) { if (definitionMask.contains(detail.type()) || definitionMask.contains(generatorType(detail.type()))) { // Only store the details indicated by the detail type mask transientDetails.append(detail); } } const QDateTime lastModified(contact->detail().lastModified()); if (!m_database.setTransientDetails(contactId, lastModified, transientDetails)) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Could not perform transient update; fallback to durable update")); transientUpdate = false; } } if (!transientUpdate) { QList oldContacts; if (!withinAggregateUpdate) { // read the existing contact data from the database, to perform delta detection. QContactManager::Error readOldContactError = m_reader->readContacts(QStringLiteral("UpdateContact"), &oldContacts, QList() << contactId, QContactFetchHint()); if (readOldContactError != QContactManager::NoError || oldContacts.size() != 1) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to read existing data during update for contact: %1").arg(contactId)); return QContactManager::UnspecifiedError; } } // This update invalidates any details that may be present in the transient store m_database.removeTransientDetails(contactId); // Store updated details to the database { ContactsDatabase::Query query(bindContactDetails(*contact, withinSyncUpdate || withinAggregateUpdate, recordUnhandledChangeFlags, definitionMask, contactId)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to update contact"); return QContactManager::UnspecifiedError; } } writeError = write(contactId, withinAggregateUpdate ? QContact() : oldContacts.first(), contact, definitionMask, recordUnhandledChangeFlags); } } if (m_database.aggregating() && writeError == QContactManager::NoError) { if (oldCollectionId != ContactCollectionId::apiId(ContactsDatabase::AggregateAddressbookCollectionId, m_managerUri)) { bool aggregable = false; writeError = collectionIsAggregable(contact->collectionId(), &aggregable); if (writeError != QContactManager::NoError) { return writeError; } if (aggregable) { const QString findAggregateForContact(QStringLiteral( " SELECT DISTINCT firstId FROM Relationships" " WHERE type = 'Aggregates' AND secondId = :localId" )); ContactsDatabase::Query query(m_database.prepare(findAggregateForContact)); query.bindValue(":localId", contactId); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to fetch aggregator contact ids during update"); return QContactManager::UnspecifiedError; } QList aggregatesOfUpdated; while (query.next()) { aggregatesOfUpdated.append(query.value(0)); } if (aggregatesOfUpdated.size() > 0) { writeError = regenerateAggregates(aggregatesOfUpdated, definitionMask, withinTransaction); } else if (oldCollectionId == ContactCollectionId::apiId(ContactsDatabase::LocalAddressbookCollectionId, m_managerUri)) { writeError = setAggregate(contact, contactId, true, definitionMask, withinTransaction, withinSyncUpdate); } if (writeError != QContactManager::NoError) { return writeError; } *aggregateUpdated = true; } } } return writeError; } QContactManager::Error ContactWriter::collectionIsAggregable(const QContactCollectionId &collectionId, bool *aggregable) { *aggregable = false; const QString contactShouldBeAggregated(QStringLiteral( " SELECT aggregable FROM Collections WHERE collectionId = :collectionId" )); ContactsDatabase::Query query(m_database.prepare(contactShouldBeAggregated)); query.bindValue(":collectionId", ContactCollectionId::databaseId(collectionId)); if (!ContactsDatabase::execute(query)) { query.reportError("Failed to determine aggregability during update"); return QContactManager::UnspecifiedError; } if (query.next()) { *aggregable = query.value(0); } return QContactManager::NoError; } QContactManager::Error ContactWriter::setAggregate(QContact *contact, quint32 contactId, bool update, const DetailList &definitionMask, bool withinTransaction, bool withinSyncUpdate) { quint32 aggregateId = 0; const bool createOnly = true; QContactManager::Error writeErr = updateOrCreateAggregate(contact, definitionMask, withinTransaction, withinSyncUpdate, createOnly, &aggregateId); if ((writeErr == QContactManager::NoError) && (update || (aggregateId < contactId))) { // The aggregate pre-dates the new contact - it probably had a local constituent already. // We must regenerate the aggregate, because the precedence order of the details may have changed. writeErr = regenerateAggregates(QList() << aggregateId, definitionMask, withinTransaction); if (writeErr != QContactManager::NoError) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Failed to regenerate aggregate contact %1 for local insertion").arg(aggregateId)); } } return writeErr; } QContactManager::Error ContactWriter::write( quint32 contactId, const QContact &oldContact, QContact *contact, const DetailList &definitionMask, bool recordUnhandledChangeFlags) { // Does this contact belong to a synced addressbook? const QContactCollectionId collectionId = contact->collectionId(); const bool wasLocal = false; // XXXXXXXXXXXXXXXXXXXX TODO fixme? const bool syncable = (ContactCollectionId::databaseId(collectionId) != ContactsDatabase::AggregateAddressbookCollectionId) && (ContactCollectionId::databaseId(collectionId) != ContactsDatabase::LocalAddressbookCollectionId); // if the oldContact doesn't match this one, // don't perform delta detection and update; // instead, clobber all detail values for this contact. const bool performDeltaDetection = ContactId::databaseId(oldContact) == contactId; const QtContactsSqliteExtensions::ContactDetailDelta delta = performDeltaDetection ? QtContactsSqliteExtensions::determineContactDetailDelta( oldContact.details(), contact->details()) : QtContactsSqliteExtensions::ContactDetailDelta(); QContactManager::Error error = QContactManager::NoError; if (writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, true, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, true, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, true, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, true, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, true, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, true, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) && writeDetails(contactId, delta, contact, definitionMask, collectionId, syncable, wasLocal, false, recordUnhandledChangeFlags, &error) ) { return QContactManager::NoError; } return error; } ContactsDatabase::Query ContactWriter::bindContactDetails(const QContact &contact, bool keepChangeFlags, bool recordUnhandledChangeFlags, const DetailList &definitionMask, quint32 contactId) { const QString insertContact(QStringLiteral( " INSERT INTO Contacts (" " collectionId," " created," " modified," " hasPhoneNumber," " hasEmailAddress," " hasOnlineAccount," " isOnline," " isDeactivated," " changeFlags," " unhandledChangeFlags)" " VALUES (" " :collectionId," " :created," " :modified," " :hasPhoneNumber," " :hasEmailAccount," " :hasOnlineAccount," " :isOnline," " :isDeactivated," " %1," " %2)" ).arg(keepChangeFlags ? 0 : 1) // if addition is due to sync, don't set Added flag. Aggregates don't get flags either. .arg((!keepChangeFlags && recordUnhandledChangeFlags) ? 1 : 0)); const QString updateContact(QStringLiteral( " UPDATE Contacts SET" " collectionId = :collectionId," " created = :created," " modified = :modified," " hasPhoneNumber = CASE WHEN :phoneKnown = 1 THEN :phone ELSE hasPhoneNumber END," " hasEmailAddress = CASE WHEN :emailKnown = 1 THEN :email ELSE hasEmailAddress END," " hasOnlineAccount = CASE WHEN :accountKnown = 1 THEN :account ELSE hasOnlineAccount END," " isOnline = CASE WHEN :onlineKnown = 1 THEN :online ELSE isOnline END," " isDeactivated = CASE WHEN :deactivatedKnown = 1 THEN :deactivated ELSE isDeactivated END," " changeFlags = %1," " unhandledChangeFlags = %2" " WHERE contactId = :contactId;" ).arg(keepChangeFlags ? QStringLiteral("changeFlags") // if modification is due to sync, don't set Modified flag. Aggregates don't get flags either. : QStringLiteral("changeFlags | 2")) // ChangeFlags::IsModified .arg((!keepChangeFlags && recordUnhandledChangeFlags) ? QStringLiteral("unhandledChangeFlags | 2") : QStringLiteral("unhandledChangeFlags"))); const bool update(contactId != 0); ContactsDatabase::Query query(m_database.prepare(update ? updateContact : insertContact)); int col = 0; const quint32 collectionId = ContactCollectionId::databaseId(contact.collectionId()) > 0 ? ContactCollectionId::databaseId(contact.collectionId()) : static_cast(ContactsDatabase::LocalAddressbookCollectionId); query.bindValue(col++, collectionId); const QContactTimestamp timestamp = contact.detail(); query.bindValue(col++, ContactsDatabase::dateTimeString(timestamp.value(QContactTimestamp::FieldCreationTimestamp).toUTC())); query.bindValue(col++, ContactsDatabase::dateTimeString(timestamp.value(QContactTimestamp::FieldModificationTimestamp).toUTC())); // Does this contact contain the information needed to update hasPhoneNumber? bool hasPhoneNumberKnown = definitionMask.isEmpty() || detailListContains(definitionMask); bool hasPhoneNumber = hasPhoneNumberKnown ? !contact.detail().isEmpty() : false; bool hasEmailAddressKnown = definitionMask.isEmpty() || detailListContains(definitionMask); bool hasEmailAddress = hasEmailAddressKnown ? !contact.detail().isEmpty() : false; bool hasOnlineAccountKnown = definitionMask.isEmpty() || detailListContains(definitionMask); bool hasOnlineAccount = hasOnlineAccountKnown ? !contact.detail().isEmpty() : false; // isOnline is true if any presence details are not offline/unknown bool isOnlineKnown = definitionMask.isEmpty() || detailListContains(definitionMask); bool isOnline = false; foreach (const QContactPresence &presence, contact.details()) { if (presence.presenceState() >= QContactPresence::PresenceAvailable && presence.presenceState() <= QContactPresence::PresenceExtendedAway) { isOnline = true; break; } } // isDeactivated is true if the contact contains QContactDeactivated bool isDeactivatedKnown = definitionMask.isEmpty() || detailListContains(definitionMask); bool isDeactivated = isDeactivatedKnown ? !contact.details().isEmpty() : false; if (isDeactivated) { // TODO: should we also disallow deactivation of local addressbook contacts? if (collectionId == ContactsDatabase::AggregateAddressbookCollectionId) { isDeactivated = false; QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("Cannot set deactivated for collection: %1").arg(collectionId)); } } if (update) { query.bindValue(col++, hasPhoneNumberKnown); query.bindValue(col++, hasPhoneNumber); query.bindValue(col++, hasEmailAddressKnown); query.bindValue(col++, hasEmailAddress); query.bindValue(col++, hasOnlineAccountKnown); query.bindValue(col++, hasOnlineAccount); query.bindValue(col++, isOnlineKnown); query.bindValue(col++, isOnline); query.bindValue(col++, isDeactivatedKnown); query.bindValue(col++, isDeactivated); query.bindValue(col++, contactId); } else { query.bindValue(col++, hasPhoneNumber); query.bindValue(col++, hasEmailAddress); query.bindValue(col++, hasOnlineAccount); query.bindValue(col++, isOnline); query.bindValue(col++, isDeactivated); } return query; } ContactsDatabase::Query ContactWriter::bindCollectionDetails(const QContactCollection &collection) { const QString insertCollection(QStringLiteral( " INSERT INTO Collections (" " aggregable," " name," " description," " color," " secondaryColor," " image," " applicationName," " accountId," " remotePath," " changeFlags)" " VALUES (" " :aggregable," " :name," " :description," " :color," " :secondaryColor," " :image," " :applicationName," " :accountId," " :remotePath," " 1)" // ChangeFlags::IsAdded )); const QString updateCollection(QStringLiteral( " UPDATE Collections SET" " aggregable = :aggregable," " name = :name," " description = :description," " color = :color," " secondaryColor = :secondaryColor," " image = :image," " applicationName = :applicationName," " accountId = :accountId," " remotePath = :remotePath," " changeFlags = changeFlags | 2" // ChangeFlags::IsModified " WHERE collectionId = :collectionId;" )); const bool update(ContactCollectionId::isValid(collection)); ContactsDatabase::Query query(m_database.prepare(update ? updateCollection : insertCollection)); query.bindValue(":aggregable", collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE).isNull() ? true : collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE).toBool()); query.bindValue(":name", collection.metaData(QContactCollection::KeyName).toString()); query.bindValue(":description", collection.metaData(QContactCollection::KeyDescription).toString()); query.bindValue(":color", collection.metaData(QContactCollection::KeyColor).toString()); query.bindValue(":secondaryColor", collection.metaData(QContactCollection::KeySecondaryColor).toString()); query.bindValue(":image", collection.metaData(QContactCollection::KeyImage).toString()); query.bindValue(":applicationName", collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString()); query.bindValue(":accountId", collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt()); query.bindValue(":remotePath", collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString()); if (update) { query.bindValue(":collectionId", ContactCollectionId::databaseId(collection)); } return query; } ContactsDatabase::Query ContactWriter::bindCollectionMetadataDetails(const QContactCollection &collection, int *count) { const QString insertMetadata(QStringLiteral( " INSERT OR REPLACE INTO CollectionsMetadata (" " collectionId," " key," " value)" " VALUES (" " :collectionId," " :key," " :value)" )); QVariantList boundIds; QVariantList boundKeys; QVariantList boundValues; const QVariantMap extendedMetadata = collection.extendedMetaData(); for (QVariantMap::const_iterator it = extendedMetadata.constBegin(); it != extendedMetadata.constEnd(); it++) { // store the key/value pairs which we haven't stored already in the Collections table if (it.key() != COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE && it.key() != COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME && it.key() != COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID && it.key() != COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH) { boundIds.append(ContactCollectionId::databaseId(collection.id())); boundKeys.append(it.key()); boundValues.append(it.value()); } } ContactsDatabase::Query query(m_database.prepare(insertMetadata)); query.bindValue(":collectionId", boundIds); query.bindValue(":key", boundKeys); query.bindValue(":value", boundValues); *count = boundValues.size(); return query; } qtcontacts-sqlite-0.3.19/src/engine/contactwriter.h000066400000000000000000000246151436373107600224050ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2019 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTSSQLITE_CONTACTWRITER #define QTCONTACTSSQLITE_CONTACTWRITER #include "contactsdatabase.h" #include "contactnotifier.h" #include "contactid_p.h" #include "../extensions/qtcontacts-extensions.h" #include "../extensions/qcontactoriginmetadata.h" #include "../extensions/contactmanagerengine.h" #include "../extensions/contactdelta.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class ProcessMutex; class ContactsEngine; class ContactReader; class ContactWriter { public: typedef QList DetailList; ContactWriter(ContactsEngine &engine, ContactsDatabase &database, ContactNotifier *notifier, ContactReader *reader); ~ContactWriter(); QContactManager::Error save( QList *contacts, const DetailList &definitionMask, QMap *aggregateUpdated, QMap *errorMap, bool withinTransaction, bool withinAggregateUpdate, bool withinSyncUpdate); QContactManager::Error remove(const QList &contactIds, QMap *errorMap, bool withinTransaction, bool withinSyncUpdate); QContactManager::Error setIdentity(ContactsDatabase::Identity identity, QContactId contactId); QContactManager::Error save( const QList &relationships, QMap *errorMap, bool withinTransaction, bool withinAggregateUpdate); QContactManager::Error remove( const QList &relationships, QMap *errorMap, bool withinTransaction); QContactManager::Error save( QList *collections, QMap *errorMap, bool withinTransaction, bool withinSyncUpdate); QContactManager::Error remove( const QList &collectionIds, QMap *errorMap, bool withinTransaction, bool withinSyncUpdate); QList transientDetails(quint32 contactId) const; bool storeTransientDetails(quint32 contactId, const QList &details); void removeTransientDetails(quint32 contactId); QContactManager::Error clearChangeFlags(const QList &contactIds, bool withinTransaction); QContactManager::Error clearChangeFlags(const QContactCollectionId &collectionId, bool withinTransaction); QContactManager::Error fetchCollectionChanges( int accountId, const QString &applicationName, QList *addedCollections, QList *modifiedCollections, QList *deletedCollections, QList *unmodifiedCollections); QContactManager::Error fetchContactChanges( const QContactCollectionId &collectionId, QList *addedContacts, QList *modifiedContacts, QList *deletedContacts, QList *unmodifiedContacts); QContactManager::Error storeChanges( QHash * /* added contacts */> *addedCollections, QHash * /* added/modified/deleted contacts */> *modifiedCollections, const QList &deletedCollections, QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy conflictResolutionPolicy, bool clearChangeFlags); bool storeOOB(const QString &scope, const QMap &values); bool removeOOB(const QString &scope, const QStringList &keys); private: bool beginTransaction(); bool commitTransaction(); void rollbackTransaction(); QContactManager::Error create(QContact *contact, const DetailList &definitionMask, bool withinTransaction, bool withinAggregateUpdate, bool withinSyncUpdate, bool recordUnhandledChangeFlags); QContactManager::Error update(QContact *contact, const DetailList &definitionMask, bool *aggregateUpdated, bool withinTransaction, bool withinAggregateUpdate, bool withinSyncUpdate, bool recordUnhandledChangeFlags, bool transientUpdate); QContactManager::Error write(quint32 contactId, const QContact &oldContact, QContact *contact, const DetailList &definitionMask, bool recordUnhandledChangeFlags); QContactManager::Error saveRelationships(const QList &relationships, QMap *errorMap, bool withinAggregateUpdate); QContactManager::Error removeRelationships(const QList &relationships, QMap *errorMap); QContactManager::Error removeDetails(const QVariantList &contactIds, bool onlyIfFlagged = false); QContactManager::Error removeContacts(const QVariantList &ids, bool onlyIfFlagged = false); QContactManager::Error deleteContacts(const QVariantList &ids, bool recordUnhandledChangeFlags); QContactManager::Error undeleteContacts(const QVariantList &ids, bool recordUnhandledChangeFlags); QContactManager::Error saveCollection(QContactCollection *collection); QContactManager::Error removeCollection(const QContactCollectionId &collectionId, bool onlyIfFlagged); QContactManager::Error deleteCollection(const QContactCollectionId &collectionId); QContactManager::Error collectionIsAggregable(const QContactCollectionId &collectionId, bool *aggregable); QContactManager::Error setAggregate(QContact *contact, quint32 contactId, bool update, const DetailList &definitionMask, bool withinTransaction, bool withinSyncUpdate); QContactManager::Error updateOrCreateAggregate(QContact *contact, const DetailList &definitionMask, bool withinTransaction, bool withinSyncUpdate, bool createOnly = false, quint32 *aggregateContactId = 0); QContactManager::Error regenerateAggregates(const QList &aggregateIds, const DetailList &definitionMask, bool withinTransaction); QContactManager::Error removeChildlessAggregates(QList *realRemoveIds); QContactManager::Error aggregateOrphanedContacts(bool withinTransaction, bool withinSyncUpdate); ContactsDatabase::Query bindContactDetails(const QContact &contact, bool keepChangeFlags = false, bool recordUnhandledChangeFlags = false, const DetailList &definitionMask = DetailList(), quint32 contactId = 0); ContactsDatabase::Query bindCollectionDetails(const QContactCollection &collection); ContactsDatabase::Query bindCollectionMetadataDetails(const QContactCollection &collection, int *count); template bool writeDetails( quint32 contactId, const QtContactsSqliteExtensions::ContactDetailDelta &delta, QContact *contact, const DetailList &definitionMask, const QContactCollectionId &collectionId, bool syncable, bool wasLocal, bool uniqueDetail, bool recordUnhandledChangeFlags, QContactManager::Error *error); template quint32 writeCommonDetails( quint32 contactId, quint32 detailId, const T &detail, bool syncable, bool wasLocal, bool aggregateContact, bool recordUnhandledChangeFlags, QContactManager::Error *error); template bool removeCommonDetails(quint32 contactId, QContactManager::Error *error); ContactsEngine &m_engine; ContactsDatabase &m_database; ContactNotifier *m_notifier; ContactReader *m_reader; QString m_managerUri; bool m_displayLabelGroupsChanged; QSet m_addedIds; QSet m_removedIds; QSet m_changedIds; QSet m_presenceChangedIds; QSet m_suppressedCollectionIds; QSet m_collectionContactsChanged; QSet m_addedCollectionIds; QSet m_removedCollectionIds; QSet m_changedCollectionIds; }; #endif qtcontacts-sqlite-0.3.19/src/engine/conversion.cpp000066400000000000000000000310021436373107600222210ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * 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 "conversion_p.h" #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE namespace Conversion { // Note: all of this is only necessary because we remain compatible with databases // created for QtMobility Contacts, where various properties has string representations, // which were stored in string form int propertyValue(const QString &name, const QMap &propertyValues) { QMap::const_iterator it = propertyValues.find(name); if (it != propertyValues.end()) { return *it; } return -1; } QList propertyValueList(const QStringList &names, const QMap &propertyValues) { QList rv; foreach (const QString &name, names) { rv.append(propertyValue(name, propertyValues)); } return rv; } QString propertyName(int value, const QMap &propertyNames) { QMap::const_iterator it = propertyNames.find(value); if (it != propertyNames.end()) { return *it; } return QString(); } QStringList propertyNameList(const QList &values, const QMap &propertyNames) { QStringList list; foreach (int value, values) { list.append(propertyName(value, propertyNames)); } return list; } namespace OnlineAccount { static QMap subTypeValues() { QMap rv; rv.insert(QStringLiteral("Sip"), QContactOnlineAccount::SubTypeSip); rv.insert(QStringLiteral("SipVoip"), QContactOnlineAccount::SubTypeSipVoip); rv.insert(QStringLiteral("Impp"), QContactOnlineAccount::SubTypeImpp); rv.insert(QStringLiteral("VideoShare"), QContactOnlineAccount::SubTypeVideoShare); return rv; } static QMap subTypeNames() { QMap rv; rv.insert(QContactOnlineAccount::SubTypeSip, QStringLiteral("Sip")); rv.insert(QContactOnlineAccount::SubTypeSipVoip, QStringLiteral("SipVoip")); rv.insert(QContactOnlineAccount::SubTypeImpp, QStringLiteral("Impp")); rv.insert(QContactOnlineAccount::SubTypeVideoShare, QStringLiteral("VideoShare")); return rv; } static QMap protocolValues() { QMap rv; rv.insert(QStringLiteral("Unknown"), QContactOnlineAccount::ProtocolUnknown); rv.insert(QStringLiteral("Aim"), QContactOnlineAccount::ProtocolAim); rv.insert(QStringLiteral("Icq"), QContactOnlineAccount::ProtocolIcq); rv.insert(QStringLiteral("Irc"), QContactOnlineAccount::ProtocolIrc); rv.insert(QStringLiteral("Jabber"), QContactOnlineAccount::ProtocolJabber); rv.insert(QStringLiteral("Msn"), QContactOnlineAccount::ProtocolMsn); rv.insert(QStringLiteral("Qq"), QContactOnlineAccount::ProtocolQq); rv.insert(QStringLiteral("Skype"), QContactOnlineAccount::ProtocolSkype); rv.insert(QStringLiteral("Yahoo"), QContactOnlineAccount::ProtocolYahoo); return rv; } static QMap protocolNames() { QMap rv; rv.insert(QContactOnlineAccount::ProtocolUnknown, QStringLiteral("Unknown")); rv.insert(QContactOnlineAccount::ProtocolAim, QStringLiteral("Aim")); rv.insert(QContactOnlineAccount::ProtocolIcq, QStringLiteral("Icq")); rv.insert(QContactOnlineAccount::ProtocolIrc, QStringLiteral("Irc")); rv.insert(QContactOnlineAccount::ProtocolJabber, QStringLiteral("Jabber")); rv.insert(QContactOnlineAccount::ProtocolMsn, QStringLiteral("Msn")); rv.insert(QContactOnlineAccount::ProtocolQq, QStringLiteral("Qq")); rv.insert(QContactOnlineAccount::ProtocolSkype, QStringLiteral("Skype")); rv.insert(QContactOnlineAccount::ProtocolYahoo, QStringLiteral("Yahoo")); return rv; } QList subTypeList(const QStringList &names) { static const QMap subTypes(subTypeValues()); return propertyValueList(names, subTypes); } QStringList subTypeList(const QList &values) { static const QMap typeNames(subTypeNames()); return propertyNameList(values, typeNames); } int protocol(const QString &name) { static const QMap protocols(protocolValues()); return propertyValue(name, protocols); } QString protocol(int type) { static const QMap names(protocolNames()); return propertyName(type, names); } } namespace PhoneNumber { static QMap subTypeValues() { QMap rv; rv.insert(QStringLiteral("Landline"), QContactPhoneNumber::SubTypeLandline); rv.insert(QStringLiteral("Mobile"), QContactPhoneNumber::SubTypeMobile); rv.insert(QStringLiteral("Fax"), QContactPhoneNumber::SubTypeFax); rv.insert(QStringLiteral("Pager"), QContactPhoneNumber::SubTypePager); rv.insert(QStringLiteral("Voice"), QContactPhoneNumber::SubTypeVoice); rv.insert(QStringLiteral("Modem"), QContactPhoneNumber::SubTypeModem); rv.insert(QStringLiteral("Video"), QContactPhoneNumber::SubTypeVideo); rv.insert(QStringLiteral("Car"), QContactPhoneNumber::SubTypeCar); rv.insert(QStringLiteral("BulletinBoardSystem"), QContactPhoneNumber::SubTypeBulletinBoardSystem); rv.insert(QStringLiteral("MessagingCapable"), QContactPhoneNumber::SubTypeMessagingCapable); rv.insert(QStringLiteral("Assistant"), QContactPhoneNumber::SubTypeAssistant); rv.insert(QStringLiteral("DtmfMenu"), QContactPhoneNumber::SubTypeDtmfMenu); return rv; } static QMap subTypeNames() { QMap rv; rv.insert(QContactPhoneNumber::SubTypeLandline, QStringLiteral("Landline")); rv.insert(QContactPhoneNumber::SubTypeMobile, QStringLiteral("Mobile")); rv.insert(QContactPhoneNumber::SubTypeFax, QStringLiteral("Fax")); rv.insert(QContactPhoneNumber::SubTypePager, QStringLiteral("Pager")); rv.insert(QContactPhoneNumber::SubTypeVoice, QStringLiteral("Voice")); rv.insert(QContactPhoneNumber::SubTypeModem, QStringLiteral("Modem")); rv.insert(QContactPhoneNumber::SubTypeVideo, QStringLiteral("Video")); rv.insert(QContactPhoneNumber::SubTypeCar, QStringLiteral("Car")); rv.insert(QContactPhoneNumber::SubTypeBulletinBoardSystem, QStringLiteral("BulletinBoardSystem")); rv.insert(QContactPhoneNumber::SubTypeMessagingCapable, QStringLiteral("MessagingCapable")); rv.insert(QContactPhoneNumber::SubTypeAssistant, QStringLiteral("Assistant")); rv.insert(QContactPhoneNumber::SubTypeDtmfMenu, QStringLiteral("DtmfMenu")); return rv; } QList subTypeList(const QStringList &names) { static const QMap subTypes(subTypeValues()); return propertyValueList(names, subTypes); } QStringList subTypeList(const QList &values) { static const QMap typeNames(subTypeNames()); return propertyNameList(values, typeNames); } } namespace Address { static QMap subTypeValues() { QMap rv; rv.insert(QStringLiteral("Parcel"), QContactAddress::SubTypeParcel); rv.insert(QStringLiteral("Postal"), QContactAddress::SubTypePostal); rv.insert(QStringLiteral("Domestic"), QContactAddress::SubTypeDomestic); rv.insert(QStringLiteral("International"), QContactAddress::SubTypeInternational); return rv; } static QMap subTypeNames() { QMap rv; rv.insert(QContactAddress::SubTypeParcel, QStringLiteral("Parcel")); rv.insert(QContactAddress::SubTypePostal, QStringLiteral("Postal")); rv.insert(QContactAddress::SubTypeDomestic, QStringLiteral("Domestic")); rv.insert(QContactAddress::SubTypeInternational, QStringLiteral("International")); return rv; } QList subTypeList(const QStringList &names) { static const QMap subTypes(subTypeValues()); return propertyValueList(names, subTypes); } QStringList subTypeList(const QList &values) { static const QMap typeNames(subTypeNames()); return propertyNameList(values, typeNames); } } namespace Anniversary { static QMap subTypeValues() { QMap rv; rv.insert(QStringLiteral("Wedding"), QContactAnniversary::SubTypeWedding); rv.insert(QStringLiteral("Engagement"), QContactAnniversary::SubTypeEngagement); rv.insert(QStringLiteral("House"), QContactAnniversary::SubTypeHouse); rv.insert(QStringLiteral("Employment"), QContactAnniversary::SubTypeEmployment); rv.insert(QStringLiteral("Memorial"), QContactAnniversary::SubTypeMemorial); return rv; } static QMap subTypeNames() { QMap rv; rv.insert(QContactAnniversary::SubTypeWedding, QStringLiteral("Wedding")); rv.insert(QContactAnniversary::SubTypeEngagement, QStringLiteral("Engagement")); rv.insert(QContactAnniversary::SubTypeHouse, QStringLiteral("House")); rv.insert(QContactAnniversary::SubTypeEmployment, QStringLiteral("Employment")); rv.insert(QContactAnniversary::SubTypeMemorial, QStringLiteral("Memorial")); return rv; } int subType(const QString &name) { static const QMap subTypes(subTypeValues()); return propertyValue(name, subTypes); } QString subType(int type) { static const QMap subTypes(subTypeNames()); return propertyName(type, subTypes); } } namespace Url { static QMap subTypeValues() { QMap rv; rv.insert(QStringLiteral("HomePage"), QContactUrl::SubTypeHomePage); rv.insert(QStringLiteral("Blog"), QContactUrl::SubTypeBlog); rv.insert(QStringLiteral("Favourite"), QContactUrl::SubTypeFavourite); return rv; } static QMap subTypeNames() { QMap rv; rv.insert(QContactUrl::SubTypeHomePage, QStringLiteral("HomePage")); rv.insert(QContactUrl::SubTypeBlog, QStringLiteral("Blog")); rv.insert(QContactUrl::SubTypeFavourite, QStringLiteral("Favourite")); return rv; } int subType(const QString &name) { static const QMap subTypes(subTypeValues()); return propertyValue(name, subTypes); } QString subType(int type) { static const QMap subTypes(subTypeNames()); return propertyName(type, subTypes); } } namespace Gender { static QMap genderValues() { QMap rv; rv.insert(QStringLiteral("Male"), QContactGender::GenderMale); rv.insert(QStringLiteral("Female"), QContactGender::GenderFemale); rv.insert(QStringLiteral(""), QContactGender::GenderUnspecified); return rv; } static QMap genderNames() { QMap rv; rv.insert(QContactGender::GenderMale, QStringLiteral("Male")); rv.insert(QContactGender::GenderFemale, QStringLiteral("Female")); rv.insert(QContactGender::GenderUnspecified, QStringLiteral("")); return rv; } int gender(const QString &name) { static const QMap genders(genderValues()); return propertyValue(name, genders); } QString gender(int type) { static const QMap genders(genderNames()); return propertyName(type, genders); } } } qtcontacts-sqlite-0.3.19/src/engine/conversion_p.h000066400000000000000000000054771436373107600222260ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTSSQLITE_CONVERSION_P_H #define QTCONTACTSSQLITE_CONVERSION_P_H #include #include namespace Conversion { int propertyValue(const QString &name, const QMap &propertyValues); QList propertyValueList(const QStringList &names, const QMap &propertyValues); QString propertyName(int value, const QMap &propertyNames); QStringList propertyNameList(const QList &values, const QMap &propertyNames); namespace OnlineAccount { QList subTypeList(const QStringList &names); QStringList subTypeList(const QList &values); int protocol(const QString &name); QString protocol(int type); } namespace PhoneNumber { QList subTypeList(const QStringList &names); QStringList subTypeList(const QList &values); } namespace Address { QList subTypeList(const QStringList &names); QStringList subTypeList(const QList &values); } namespace Anniversary { int subType(const QString &name); QString subType(int value); } namespace Url { int subType(const QString &name); QString subType(int value); } namespace Gender { int gender(const QString &name); QString gender(int value); } } #endif qtcontacts-sqlite-0.3.19/src/engine/defaultdlggenerator.cpp000066400000000000000000000110701436373107600240610ustar00rootroot00000000000000/* * Copyright (C) 2019 Jolla Ltd. * * 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 "defaultdlggenerator.h" /* This display label group generator provides the default (fallback) group generation semantics, and should be the last generator used (i.e. if no other generator is valid in the current locale). The semantics it implements are as follows: 1) if the preferred name field data is empty, it falls back to display label data to generate the group. 2) if the first character of the preferred data is a digit (0..9) then the group is '#'. 3) if the first character of the preferred data is within 'A'..'Z' it returns that character. (TODO: perform a unidecode transliteration first.) 4) otherwise, the group is '?' For example, if the preferred detail is QContactName::Type and the preferred field is QContactName::FieldLastName, and the client passes in a contact with name "John Smith", then the first letter of the last name (in this case, 'S') will be returned as the group. */ DefaultDlgGenerator::DefaultDlgGenerator(QObject *parent) : QObject(parent) { } QString DefaultDlgGenerator::name() const { return QStringLiteral("default"); } int DefaultDlgGenerator::priority() const { return 0; } bool DefaultDlgGenerator::preferredForLocale(const QLocale &) const { return false; // this default plugin is the fallback, never preferred but always valid. } bool DefaultDlgGenerator::validForLocale(const QLocale &) const { return true; // this default plugin is the fallback, always valid. } QStringList DefaultDlgGenerator::displayLabelGroups() const { static QStringList groups { QStringLiteral("A"), QStringLiteral("B"), QStringLiteral("C"), QStringLiteral("D"), QStringLiteral("E"), QStringLiteral("F"), QStringLiteral("G"), QStringLiteral("H"), QStringLiteral("I"), QStringLiteral("J"), QStringLiteral("K"), QStringLiteral("L"), QStringLiteral("M"), QStringLiteral("N"), QStringLiteral("O"), QStringLiteral("P"), QStringLiteral("Q"), QStringLiteral("R"), QStringLiteral("S"), QStringLiteral("T"), QStringLiteral("U"), QStringLiteral("V"), QStringLiteral("W"), QStringLiteral("X"), QStringLiteral("Y"), QStringLiteral("Z"), QStringLiteral("#"), QStringLiteral("?") }; return groups; } QString DefaultDlgGenerator::displayLabelGroup(const QString &data) const { QString group; if (!data.isEmpty()) { QChar upperChar = data.at(0).toUpper(); ushort val = upperChar.unicode(); if (val >= 'A' && val <= 'Z') { group = QString(upperChar); } else if (data.at(0).isDigit()) { group = QStringLiteral("#"); } } if (group.isEmpty()) { // unknown group. put in "other" group '?' group = QStringLiteral("?"); } return group; } qtcontacts-sqlite-0.3.19/src/engine/defaultdlggenerator.h000066400000000000000000000046101436373107600235300ustar00rootroot00000000000000/* * Copyright (C) 2019 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef DEFAULTDLGGENERATOR_H #define DEFAULTDLGGENERATOR_H #include #include class DefaultDlgGenerator : public QObject, public QtContactsSqliteExtensions::DisplayLabelGroupGenerator { Q_OBJECT Q_INTERFACES(QtContactsSqliteExtensions::DisplayLabelGroupGenerator) public: DefaultDlgGenerator(QObject *parent = Q_NULLPTR); QString name() const Q_DECL_OVERRIDE; int priority() const Q_DECL_OVERRIDE; bool validForLocale(const QLocale &locale) const Q_DECL_OVERRIDE; bool preferredForLocale(const QLocale &locale) const Q_DECL_OVERRIDE; QString displayLabelGroup(const QString &data) const Q_DECL_OVERRIDE; QStringList displayLabelGroups() const Q_DECL_OVERRIDE; }; #endif // DEFAULTDLGGENERATOR_H qtcontacts-sqlite-0.3.19/src/engine/engine.pro000066400000000000000000000044751436373107600213350ustar00rootroot00000000000000include(../../config.pri) TEMPLATE = lib TARGET = qtcontacts_sqlite QT = core sql dbus # Error on undefined symbols QMAKE_LFLAGS += $$QMAKE_LFLAGS_NOUNDEF CONFIG += plugin hide_symbols PLUGIN_TYPE=contacts DESTDIR=$${PLUGIN_TYPE} packagesExist(mlite5) { PKGCONFIG += mlite5 # The `DEFINES` directive is already set in `config.pri` } else { warning("mlite not available. Display label groups will be generated from last name.") } # This should be passed on qmake command line isEmpty(PKGCONFIG_LIB) { PKGCONFIG_LIB = lib message("PKGCONFIG_LIB is unset, assuming $$PKGCONFIG_LIB") } CONFIG(load_icu) { PKGCONFIG += sqlite3 DEFINES += QTCONTACTS_SQLITE_LOAD_ICU } # we hardcode this for Qt4 as there's no GenericDataLocation offered by QDesktopServices DEFINES += 'QTCONTACTS_SQLITE_PRIVILEGED_DIR=\'\"privileged\"\'' DEFINES += 'QTCONTACTS_SQLITE_DATABASE_DIR=\'\"Contacts/qtcontacts-sqlite\"\'' DEFINES += 'QTCONTACTS_SQLITE_DATABASE_NAME=\'\"contacts.db\"\'' # we build a path like: /home/nemo/.local/share/system/Contacts/qtcontacts-sqlite/contacts.db # Use the option to sort presence state by availability DEFINES += SORT_PRESENCE_BY_AVAILABILITY INCLUDEPATH += \ ../extensions HEADERS += \ defaultdlggenerator.h \ memorytable_p.h \ semaphore_p.h \ trace_p.h \ conversion_p.h \ contactid_p.h \ contactsdatabase.h \ contactsengine.h \ contactstransientstore.h \ contactnotifier.h \ contactreader.h \ contactwriter.h \ ../extensions/contactmanagerengine.h SOURCES += \ defaultdlggenerator.cpp \ memorytable.cpp \ semaphore_p.cpp \ conversion.cpp \ contactid.cpp \ contactsdatabase.cpp \ contactsengine.cpp \ contactstransientstore.cpp \ contactsplugin.cpp \ contactnotifier.cpp \ contactreader.cpp \ contactwriter.cpp target.path = $$[QT_INSTALL_PLUGINS]/contacts INSTALLS += target PACKAGENAME=qtcontacts-sqlite-qt5-extensions headers.path = $${PREFIX}/include/$${PACKAGENAME} headers.files = ../extensions/* headers.depends = ../extensions/* INSTALLS += headers pkgconfig.path = $${PREFIX}/$${PKGCONFIG_LIB}/pkgconfig pkgconfig.files = ../$${PACKAGENAME}.pc INSTALLS += pkgconfig OTHER_FILES += plugin.json qtcontacts-sqlite-0.3.19/src/engine/memorytable.cpp000066400000000000000000000443321436373107600223660ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Contact: Matt Vogt * * 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 "memorytable_p.h" #include #include #include #include #include // Class to manage a table of key/value pairs in a memory buffer, using offsets rather // than addresses, to be suitable for placement in shared memory. // // The lower end of the available space holds a sorted index of keys to offsets; the // upper end of the space holds a heap allocated into variable sized blocks, growing // down toward the index. // // Deallocated blocks are added to a free list, from which they can be reallocated. // No defragmentation is currently performed; when allocation fails, it is required // that the content be migrated to a new memory buffer. No compaction is currently // performed. // // Key and value types are currently fixed as quint32/QByteArray, but could be changed // without much difficulty. namespace { template T extractData(const char *src, size_t len); // Functions to work with QByteArray (change for different value_type) size_t dataSize(const QByteArray &data) { return data.size(); } void insertData(char *dst, size_t len, const QByteArray &data) { std::memcpy(dst, data.constData(), len); } template<> QByteArray extractData(const char *src, size_t len) { return QByteArray::fromRawData(src, len); } // Structures used in the table management struct IndexElement { MemoryTable::key_type key; quint32 offset; }; struct Allocation { quint16 size; // size of the allocated block quint16 dataSize; // size of the stored data, when in use, or FreeBlock union { char data[1]; quint32 nextOffset; // offset of the next free block, when on the free list }; }; struct TableMetadata { quint32 size; // size of the table memory region quint32 count; // number of items quint32 freeOffset; // position of the free space quint32 freeList; // offset of the first free block IndexElement index[1]; }; bool operator<(const IndexElement &lhs, const MemoryTable::key_type &rhs) { return lhs.key < rhs; } template T roundUp(T n, T m) { T rv = n + m - 1; return rv - (rv % m); } template T roundDown(T n, T m) { return n - (n % m); } } class MemoryTablePrivate { public: typedef MemoryTable::key_type key_type; typedef MemoryTable::value_type value_type; typedef MemoryTable::Error Error; enum { FreeBlock = UINT_MAX }; static TableMetadata *metadata(MemoryTable *table); static const TableMetadata *metadata(const MemoryTable *table); static size_t count(const TableMetadata *table); static bool contains(const key_type &key, const TableMetadata *table); static const value_type value(const key_type &key, const TableMetadata *table); static Error insert(const key_type &key, const value_type &value, TableMetadata *table); static bool remove(const key_type &key, TableMetadata *table); static Error migrateTo(TableMetadata *other, const TableMetadata *table); static IndexElement *begin(TableMetadata *table); static IndexElement *end(TableMetadata *table); static const IndexElement *begin(const TableMetadata *table); static const IndexElement *end(const TableMetadata *table); static Allocation *allocationAt(quint32 offset, TableMetadata *table); static const Allocation *allocationAt(quint32 offset, const TableMetadata *table); static const value_type valueAt(quint32 offset, const TableMetadata *table); static key_type keyAtIndex(size_t index, const TableMetadata *table); static const value_type valueAtIndex(size_t index, const TableMetadata *table); static size_t freeSpace(const TableMetadata *table); static size_t requiredSpace(quint32 size); static quint32 allocate(quint32 size, TableMetadata *table, bool indexRequired); static void deallocate(quint32 offset, TableMetadata *table); static void updateValue(const value_type &value, quint32 valueSize, quint32 offset, TableMetadata *table); }; TableMetadata *MemoryTablePrivate::metadata(MemoryTable *memoryTable) { return reinterpret_cast(memoryTable->mBase); } const TableMetadata *MemoryTablePrivate::metadata(const MemoryTable *memoryTable) { return reinterpret_cast(memoryTable->mBase); } size_t MemoryTablePrivate::count(const TableMetadata *table) { return table->count; } bool MemoryTablePrivate::contains(const key_type &key, const TableMetadata *table) { const IndexElement *tableEnd = end(table); const IndexElement *position = std::lower_bound(begin(table), tableEnd, key); return (position != tableEnd && position->key == key); } const MemoryTablePrivate::value_type MemoryTablePrivate::value(const key_type &key, const TableMetadata *table) { const IndexElement *tableEnd = end(table); const IndexElement *position = std::lower_bound(begin(table), tableEnd, key); if (position == tableEnd || position->key != key) return value_type(); return valueAt(position->offset, table); } MemoryTablePrivate::Error MemoryTablePrivate::insert(const key_type &key, const value_type &value, TableMetadata *table) { const quint32 valueSize = dataSize(value); IndexElement *tableEnd = end(table); IndexElement *position = std::lower_bound(begin(table), tableEnd, key); if (position != tableEnd && position->key == key) { // This is a replacement - the item has an allocation already Allocation *allocation = allocationAt(position->offset, table); if (allocation->size < requiredSpace(valueSize)) { // Replace the existing allocation with a bigger one quint32 newOffset = allocate(valueSize, table, false); if (!newOffset) return MemoryTable::InsufficientSpace; deallocate(position->offset, table); position->offset = newOffset; } else { // Reuse this allocation // TODO: swap with a better fit from the free list? } } else { // This item needs to be added to the index if (table->count == std::numeric_limits::max()) return MemoryTable::InsufficientSpace; quint32 offset = allocate(valueSize, table, true); if (!offset) return MemoryTable::InsufficientSpace; // For index insertion, move the displaced elements of the index if (position != tableEnd) std::memmove(position + 1, position, (tableEnd - position) * sizeof(IndexElement)); ++(table->count); position->key = key; position->offset = offset; } Q_ASSERT(contains(key, table)); // Ensure that the data allocation does not overlap the index space Q_ASSERT((reinterpret_cast(table) + table->freeOffset) >= reinterpret_cast(end(table))); // Update the value stored at the position updateValue(value, valueSize, position->offset, table); return MemoryTable::NoError; } bool MemoryTablePrivate::remove(const key_type &key, TableMetadata *table) { IndexElement *tableEnd = end(table); IndexElement *position = std::lower_bound(begin(table), tableEnd, key); if (position == tableEnd || position->key != key) return false; deallocate(position->offset, table); std::memmove(position, position + 1, (tableEnd - position - 1) * sizeof(IndexElement)); --(table->count); Q_ASSERT(!contains(key, table)); return true; } MemoryTablePrivate::Error MemoryTablePrivate::migrateTo(TableMetadata *other, const TableMetadata *table) { // Copy all live elements to the other table const IndexElement *tableEnd(end(table)); for (const IndexElement *it = begin(table); it != tableEnd; ++it) { MemoryTable::Error error = insert((*it).key, valueAt((*it).offset, table), other); if (error != MemoryTable::NoError) return error; } return MemoryTable::NoError; } IndexElement *MemoryTablePrivate::begin(TableMetadata *table) { return &table->index[0]; } IndexElement *MemoryTablePrivate::end(TableMetadata *table) { return &table->index[table->count]; } const IndexElement *MemoryTablePrivate::begin(const TableMetadata *table) { return &table->index[0]; } const IndexElement *MemoryTablePrivate::end(const TableMetadata *table) { return &table->index[table->count]; } Allocation *MemoryTablePrivate::allocationAt(quint32 offset, TableMetadata *table) { return reinterpret_cast(reinterpret_cast(table) + offset); } const Allocation *MemoryTablePrivate::allocationAt(quint32 offset, const TableMetadata *table) { return reinterpret_cast(reinterpret_cast(table) + offset); } const MemoryTablePrivate::value_type MemoryTablePrivate::valueAt(quint32 offset, const TableMetadata *table) { const Allocation *allocation = allocationAt(offset, table); return extractData(allocation->data, allocation->dataSize); } MemoryTablePrivate::key_type MemoryTablePrivate::keyAtIndex(size_t index, const TableMetadata *table) { if (index >= table->count) return key_type(); return table->index[index].key; } const MemoryTablePrivate::value_type MemoryTablePrivate::valueAtIndex(size_t index, const TableMetadata *table) { if (index >= table->count) return key_type(); return valueAt(table->index[index].offset, table); } size_t MemoryTablePrivate::freeSpace(const TableMetadata *table) { // Free space lies between the index and the allocated blocks return table->freeOffset - ((table->count * sizeof(IndexElement)) + offsetof(TableMetadata, index)); } size_t MemoryTablePrivate::requiredSpace(quint32 size) { return std::max(sizeof(Allocation), offsetof(Allocation, data) + size); // overhead of Allocation + size } quint32 MemoryTablePrivate::allocate(quint32 size, TableMetadata *table, bool indexRequired) { const quint32 availableSpace = freeSpace(table); if (indexRequired) { // Even if satisfied from the free list, this allocation requires there to be space for expanded index if (availableSpace < sizeof(IndexElement)) { return 0; } } // Align the allocation so that the header is directly accessible quint32 allocationSize = static_cast(requiredSpace(size)); allocationSize = roundUp(allocationSize, static_cast(sizeof(quint32))); if (table->freeList) { // Try to reuse a freed block quint32 *bestOffset = 0; Allocation *bestBlock = 0; quint32 *freeOffset = &table->freeList; while (*freeOffset) { Allocation *freeBlock = allocationAt(*freeOffset, table); if (freeBlock->size >= allocationSize) { // This block is large enough if (!bestBlock || bestBlock->size > freeBlock->size) { // It's our best fit so far bestBlock = freeBlock; bestOffset = freeOffset; } } freeOffset = &freeBlock->nextOffset; } if (bestOffset) { // TODO: if this block is too large, should it be partitioned? quint32 rv = *bestOffset; *bestOffset = bestBlock->nextOffset; return rv; } } // Is there enough space for this allocation? if (availableSpace < (allocationSize + (indexRequired ? sizeof(IndexElement) : 0))) { return 0; } // Allocate the space immediately below the already-allocated space table->freeOffset -= allocationSize; Allocation *allocation = allocationAt(table->freeOffset, table); allocation->size = allocationSize; return table->freeOffset; } void MemoryTablePrivate::deallocate(quint32 offset, TableMetadata *table) { Allocation *allocation = allocationAt(offset, table); // TODO: attempt merge with adjoining blocks? // Add this block to the free list allocation->dataSize = static_cast(FreeBlock); allocation->nextOffset = table->freeList; table->freeList = offset; } void MemoryTablePrivate::updateValue(const value_type &value, quint32 valueSize, quint32 offset, TableMetadata *table) { Allocation *allocation = allocationAt(offset, table); Q_ASSERT(allocation->size >= requiredSpace(valueSize)); allocation->dataSize = valueSize; insertData(allocation->data, allocation->dataSize, value); } MemoryTable::MemoryTable(void *base, size_t size, bool initialize) : mBase(0) , mSize(0) { #ifndef __GNUG__ #error "Alignment testing required" #endif // base address must be aligned for the metadata struct if (!base) { qWarning() << "Invalid address for table:" << base; return; } if ((reinterpret_cast(base) % __alignof__(TableMetadata)) != 0) { qWarning() << "Invalid address alignment for table:" << base << "requires:" << __alignof__(TableMetadata); return; } // We must align the allocation space with the Allocation struct size_t managedSize = roundDown(size, __alignof__(Allocation)); if (managedSize <= sizeof(TableMetadata)) { qWarning() << "Invalid size alignment for table:" << base << "requires:" << __alignof__(Allocation); return; } TableMetadata *table = reinterpret_cast(base); if (initialize) { table->size = static_cast(managedSize); table->count = 0; table->freeOffset = table->size; table->freeList = 0; } else { if (table->size != managedSize) { qWarning() << "Invalid size for initialized table:" << table->size << "!=" << managedSize; return; } } mBase = base; mSize = table->size; } MemoryTable::~MemoryTable() { } bool MemoryTable::isValid() const { return mBase != 0; } size_t MemoryTable::count() const { if (!mBase) return 0u; return MemoryTablePrivate::count(MemoryTablePrivate::metadata(this)); } bool MemoryTable::contains(const key_type &key) const { if (!mBase) return false; return MemoryTablePrivate::contains(key, MemoryTablePrivate::metadata(this)); } MemoryTable::value_type MemoryTable::value(const key_type &key) const { if (!mBase) return value_type(); return MemoryTablePrivate::value(key, MemoryTablePrivate::metadata(this)); } MemoryTable::Error MemoryTable::insert(const key_type &key, const value_type &value) { if (!mBase) return NotAttached; return MemoryTablePrivate::insert(key, value, MemoryTablePrivate::metadata(this)); } bool MemoryTable::remove(const key_type &key) { if (!mBase) return false; return MemoryTablePrivate::remove(key, MemoryTablePrivate::metadata(this)); } MemoryTable::key_type MemoryTable::keyAt(size_t index) const { if (!mBase) return key_type(); return MemoryTablePrivate::keyAtIndex(index, MemoryTablePrivate::metadata(this)); } MemoryTable::value_type MemoryTable::valueAt(size_t index) const { if (!mBase) return value_type(); return MemoryTablePrivate::valueAtIndex(index, MemoryTablePrivate::metadata(this)); } MemoryTable::const_iterator MemoryTable::constBegin() const { return const_iterator(this, 0); } MemoryTable::const_iterator MemoryTable::constEnd() const { return const_iterator(this, count()); } MemoryTable::Error MemoryTable::migrateTo(MemoryTable &other) const { if (!mBase || !other.mBase) return NotAttached; return MemoryTablePrivate::migrateTo(MemoryTablePrivate::metadata(&other), MemoryTablePrivate::metadata(this)); } MemoryTable::const_iterator::const_iterator(const MemoryTable *table, quint32 position) : table(table) , position(position) { } MemoryTable::const_iterator::const_iterator() : table(0) , position(0) { } MemoryTable::const_iterator::const_iterator(const const_iterator &other) { *this = other; } MemoryTable::const_iterator &MemoryTable::const_iterator::operator=(const const_iterator &other) { table = other.table; position = other.position; return *this; } bool MemoryTable::const_iterator::operator==(const const_iterator &other) { return (table == other.table && position == other.position); } bool MemoryTable::const_iterator::operator!=(const const_iterator &other) { return !(*this == other); } MemoryTable::key_type MemoryTable::const_iterator::key() { if (!table) return MemoryTable::key_type(); return table->keyAt(position); } MemoryTable::value_type MemoryTable::const_iterator::value() { if (!table) return MemoryTable::value_type(); return table->valueAt(position); } const MemoryTable::const_iterator &MemoryTable::const_iterator::operator++() { ++position; return *this; } qtcontacts-sqlite-0.3.19/src/engine/memorytable_p.h000066400000000000000000000066331436373107600223540ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Contact: Matt Vogt * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef MEMORYTABLE_H #define MEMORYTABLE_H #include #include class MemoryTablePrivate; class MemoryTable { public: typedef quint32 key_type; typedef QByteArray value_type; class const_iterator : public std::iterator { friend class MemoryTable; protected: const_iterator(const MemoryTable *table, quint32 position); const MemoryTable *table; quint32 position; public: const_iterator(); const_iterator(const const_iterator &other); const_iterator &operator=(const const_iterator &other); bool operator==(const const_iterator &other); bool operator!=(const const_iterator &other); MemoryTable::key_type key(); MemoryTable::value_type value(); const const_iterator &operator++(); }; MemoryTable(void *base, size_t size, bool initialize); ~MemoryTable(); enum Error { NoError = 0, NotAttached, InsufficientSpace }; bool isValid() const; size_t count() const; bool contains(const key_type &key) const; value_type value(const key_type &key) const; Error insert(const key_type &key, const value_type &value); bool remove(const key_type &key); key_type keyAt(size_t index) const; value_type valueAt(size_t index) const; const_iterator constBegin() const; const_iterator constEnd() const; Error migrateTo(MemoryTable &other) const; private: MemoryTable(const MemoryTable &); MemoryTable &operator=(const MemoryTable &); friend class MemoryTablePrivate; void *mBase; size_t mSize; }; #endif qtcontacts-sqlite-0.3.19/src/engine/plugin.json000066400000000000000000000000651436373107600215260ustar00rootroot00000000000000{ "Keys": [ "org.nemomobile.contacts.sqlite" ] } qtcontacts-sqlite-0.3.19/src/engine/semaphore_p.cpp000066400000000000000000000126031436373107600223440ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * 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 "semaphore_p.h" #include "trace_p.h" #include #include #include #include #include #include #include namespace { // Defined as required for ::semun union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; }; void semaphoreError(const char *msg, const char *id, int error) { QTCONTACTS_SQLITE_WARNING(QString::fromLatin1("%1 %2: %3 (%4)").arg(msg).arg(id).arg(::strerror(error)).arg(error)); } int semaphoreInit(const char *id, size_t count, const int *initialValues) { int rv = -1; // It doesn't matter what proj_id we use, there are no other ftok uses on this ID key_t key = ::ftok(id, 1); rv = ::semget(key, count, 0); if (rv == -1) { if (errno != ENOENT) { semaphoreError("Unable to get semaphore", id, errno); } else { // The semaphore does not currently exist rv = ::semget(key, count, IPC_CREAT | IPC_EXCL | S_IRWXO | S_IRWXG | S_IRWXU); if (rv == -1) { if (errno == EEXIST) { // Someone else won the race to create the semaphore - retry get rv = ::semget(key, count, 0); } if (rv == -1) { semaphoreError("Unable to create semaphore", id, errno); } } else { // Set the initial value for (size_t i = 0; i < count; ++i) { union semun arg = { 0 }; arg.val = *initialValues++; int status = ::semctl(rv, static_cast(i), SETVAL, arg); if (status == -1) { rv = -1; semaphoreError("Unable to initialize semaphore", id, errno); } } } } } return rv; } bool semaphoreIncrement(int id, size_t index, bool wait, size_t ms, int value) { if (id == -1) { errno = 0; return false; } struct sembuf op; op.sem_num = index; op.sem_op = value; op.sem_flg = SEM_UNDO; if (!wait) { op.sem_flg |= IPC_NOWAIT; } struct timespec timeout; timeout.tv_sec = 0; timeout.tv_nsec = ms * 1000; do { int rv = ::semtimedop(id, &op, 1, (wait && ms > 0 ? &timeout : 0)); if (rv == 0) return true; } while (errno == EINTR); return false; } } Semaphore::Semaphore(const char *id, int initial) : m_identifier(id) , m_id(-1) { m_id = semaphoreInit(m_identifier.toUtf8().constData(), 1, &initial); } Semaphore::Semaphore(const char *id, size_t count, const int *initialValues) : m_identifier(id) , m_id(-1) { m_id = semaphoreInit(m_identifier.toUtf8().constData(), count, initialValues); } Semaphore::~Semaphore() { } bool Semaphore::isValid() const { return (m_id != -1); } bool Semaphore::decrement(size_t index, bool wait, size_t timeoutMs) { if (!semaphoreIncrement(m_id, index, wait, timeoutMs, -1)) { if (errno != EAGAIN || wait) { error("Unable to decrement semaphore", errno); } return false; } return true; } bool Semaphore::increment(size_t index, bool wait, size_t timeoutMs) { if (!semaphoreIncrement(m_id, index, wait, timeoutMs, 1)) { if (errno != EAGAIN || wait) { error("Unable to increment semaphore", errno); } return false; } return true; } int Semaphore::value(size_t index) const { if (m_id == -1) return -1; return ::semctl(m_id, index, GETVAL, 0); } void Semaphore::error(const char *msg, int error) { semaphoreError(msg, m_identifier.toUtf8().constData(), error); } qtcontacts-sqlite-0.3.19/src/engine/semaphore_p.h000066400000000000000000000043311436373107600220100ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTSSQLITE_SEMAPHORE_P #define QTCONTACTSSQLITE_SEMAPHORE_P #include class Semaphore { public: Semaphore(const char *identifier, int initial); Semaphore(const char *identifier, size_t count, const int *initialValues); ~Semaphore(); bool isValid() const; bool decrement(size_t index = 0, bool wait = true, size_t timeoutMs = 0); bool increment(size_t index = 0, bool wait = true, size_t timeoutMs = 0); int value(size_t index = 0) const; private: void error(const char *msg, int error); QString m_identifier; int m_id; }; #endif qtcontacts-sqlite-0.3.19/src/engine/trace_p.h000066400000000000000000000046511436373107600211300ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * Contact: Matthew Vogt * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef TRACE_P_H #define TRACE_P_H #include #include static bool qtcontacts_sqlite_debug_trace_enabled() { static const bool traceEnabled(!QString(QLatin1String(qgetenv("QTCONTACTS_SQLITE_TRACE"))).isEmpty()); return traceEnabled; } #define QTCONTACTS_SQLITE_DEBUG(msg) \ do { \ if (Q_UNLIKELY(qtcontacts_sqlite_debug_trace_enabled())) { \ qWarning() << msg; \ } \ } while (0) #define QTCONTACTS_SQLITE_WARNING(msg) \ do { \ qWarning() << msg; \ } while (0) #endif qtcontacts-sqlite-0.3.19/src/extensions/000077500000000000000000000000001436373107600202665ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/src/extensions/QContactChangesFetchRequest000066400000000000000000000000531436373107600255370ustar00rootroot00000000000000#include "./qcontactchangesfetchrequest.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactChangesSaveRequest000066400000000000000000000000521436373107600254030ustar00rootroot00000000000000#include "./qcontactchangessaverequest.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactClearChangeFlagsRequest000066400000000000000000000000571436373107600263320ustar00rootroot00000000000000#include "./qcontactclearchangeflagsrequest.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactCollectionChangesFetchRequest000066400000000000000000000000651436373107600275560ustar00rootroot00000000000000#include "./qcontactcollectionchangesfetchrequest.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactDeactivated000066400000000000000000000000431436373107600240600ustar00rootroot00000000000000#include "./qcontactdeactivated.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactDetailFetchRequest000066400000000000000000000000521436373107600253700ustar00rootroot00000000000000#include "./qcontactdetailfetchrequest.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactOriginMetadata000066400000000000000000000000461436373107600245360ustar00rootroot00000000000000#include "./qcontactoriginmetadata.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactStatusFlags000066400000000000000000000000431436373107600241030ustar00rootroot00000000000000#include "./qcontactstatusflags.h" qtcontacts-sqlite-0.3.19/src/extensions/QContactUndelete000066400000000000000000000000401436373107600234050ustar00rootroot00000000000000#include "./qcontactundelete.h" qtcontacts-sqlite-0.3.19/src/extensions/contactdelta.h000066400000000000000000000103471436373107600231110ustar00rootroot00000000000000/* * Copyright (C) 2014 - 2015 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef CONTACTDELTA_H #define CONTACTDELTA_H #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE namespace QtContactsSqliteExtensions { struct ContactDetailDelta { bool isValid = false; QList deletions; QList modifications; QList additions; template QList deleted() const { QList ret; for (QList::const_iterator it = deletions.constBegin(); it != deletions.constEnd(); ++it) { if (it->type() == T::Type) { ret.append(T(*it)); } } return ret; } template QList modified() const { QList ret; for (QList::const_iterator it = modifications.constBegin(); it != modifications.constEnd(); ++it) { if (it->type() == T::Type) { ret.append(T(*it)); } } return ret; } template QList added() const { QList ret; for (QList::const_iterator it = additions.constBegin(); it != additions.constEnd(); ++it) { if (it->type() == T::Type) { ret.append(T(*it)); } } return ret; } }; const QSet& defaultIgnorableDetailTypes(); const QHash >& defaultIgnorableDetailFields(); const QSet& defaultIgnorableCommonFields(); ContactDetailDelta determineContactDetailDelta( const QList &oldDetails, const QList &newDetails, const QSet &ignorableDetailTypes = defaultIgnorableDetailTypes(), const QHash > &ignorableDetailFields = defaultIgnorableDetailFields(), const QSet &ignorableCommonFields = defaultIgnorableCommonFields()); int exactContactMatchExistsInList( const QContact &aContact, const QList &list, const QSet &ignorableDetailTypes, const QHash > &ignorableDetailFields, const QSet &ignorableCommonFields, bool printDifferences = false); } #endif // CONTACTDELTA_H qtcontacts-sqlite-0.3.19/src/extensions/contactdelta_impl.h000066400000000000000000000703151436373107600241330ustar00rootroot00000000000000/* * Copyright (C) 2014 - 2016 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef CONTACTDELTA_IMPL_H #define CONTACTDELTA_IMPL_H #include #include #include #include #define QTCONTACTS_SQLITE_DELTA_DEBUG_LOG(msg) \ do { \ if (Q_UNLIKELY(qtcontacts_sqlite_delta_debug_trace_enabled())) { \ qDebug() << msg; \ } \ } while (0) #define QTCONTACTS_SQLITE_DELTA_DEBUG_DETAIL(detail) \ do { \ if (Q_UNLIKELY(qtcontacts_sqlite_delta_debug_trace_enabled())) { \ qWarning() << "++ ---------" << detail.type(); \ QMap values = detail.values(); \ foreach (int key, values.keys()) { \ qWarning() << " " << key << "=" << values.value(key); \ } \ } \ } while (0) QTCONTACTS_USE_NAMESPACE using namespace QtContactsSqliteExtensions; namespace { static bool qtcontacts_sqlite_delta_debug_trace_enabled() { static const bool traceEnabled(!QString(QLatin1String(qgetenv("QTCONTACTS_SQLITE_DELTA_TRACE"))).isEmpty()); return traceEnabled; } QSet getDefaultIgnorableDetailTypes() { // these details are either read-only or composed. // sync adapters may wish to add transient details here also, i.e.: // rv.insert(QContactDetail::TypeGlobalPresence); // rv.insert(QContactDetail::TypePresence); // other candidates to ignore include: // rv.insert(QContactDetail::TypeOnlineAccount); // rv.insert(QContactDetail::TypeDisplayLabel); // rv.insert(QContactDetail::TypeTimestamp); QSet rv; rv.insert(QContactDetail__TypeDeactivated); rv.insert(QContactDetail__TypeStatusFlags); return rv; } QHash > getDefaultIgnorableDetailFields() { QHash > rv; rv.insert(QContactDetail::TypePhoneNumber, { QContactPhoneNumber::FieldNormalizedNumber }); // Clients can specify their own ignorable fields depending on the semantics of their // sync service (eg, might not be able to handle some subtypes or contexts, etc) return rv; } QSet getDefaultIgnorableCommonFields() { return { QContactDetail::FieldProvenance, QContactDetail__FieldModifiable, QContactDetail__FieldNonexportable, QContactDetail__FieldChangeFlags, QContactDetail__FieldDatabaseId }; } void removeIgnorableDetailsFromList(QList *dets, const QSet &ignorableDetailTypes) { // ignore differences in certain detail types for (int i = dets->size() - 1; i >= 0; --i) { const QContactDetail::DetailType type(dets->at(i).type()); if (ignorableDetailTypes.contains(type)) { dets->removeAt(i); // we can ignore this detail altogether } } } void removeDatabaseIdsFromList(QList *dets) { for (QList::iterator it = dets->begin(); it != dets->end(); ++it) { it->removeValue(QContactDetail__FieldDatabaseId); } } void dumpContactDetail(const QContactDetail &d) { qWarning() << "++ ---------" << d.type(); QMap values = d.values(); foreach (int key, values.keys()) { qWarning() << " " << key << "=" << values.value(key); } } int scoreForValuePair(const QVariant &removal, const QVariant &addition) { // work around some variant-comparison issues. if (Q_UNLIKELY((((removal.type() == QVariant::String && addition.type() == QVariant::Invalid) ||(addition.type() == QVariant::String && removal.type() == QVariant::Invalid)) &&(removal.toString().isEmpty() && addition.toString().isEmpty())))) { // it could be that "invalid" variant is stored as an empty // string in database, if the field is a string field. // if so, ignore that - it's not a difference. return 0; } if (removal.canConvert >() && addition.canConvert >()) { // direct comparison of QVariant::fromValue > doesn't work // so instead, do the conversion and compare them manually. QList rlist = removal.value >(); QList llist = addition.value >(); return rlist == llist ? 0 : 1; } // the sync adaptor might return url data as a string. if (removal.type() == QVariant::Url && addition.type() == QVariant::String) { QUrl rurl = removal.toUrl(); QUrl aurl = QUrl(addition.toString()); return rurl == aurl ? 0 : 1; } else if (removal.type() == QVariant::String && addition.type() == QVariant::Url) { QUrl rurl = QUrl(removal.toString()); QUrl aurl = addition.toUrl(); return rurl == aurl ? 0 : 1; } // normal case. if they're different, increase the distance. return removal == addition ? 0 : 1; } // Given two details of the same type, determine a similarity score for them. int scoreForDetailPair( const QContactDetail &removal, const QContactDetail &addition, const QHash > &ignorableDetailFields, const QSet &ignorableCommonFields) { int score = 0; // distance QMap rvalues = removal.values(); QMap avalues = addition.values(); QList seenFields; foreach (int field, rvalues.keys()) { if (ignorableCommonFields.contains(field) || ignorableDetailFields.value(removal.type()).contains(field)) { continue; } seenFields.append(field); score += scoreForValuePair(rvalues.value(field), avalues.value(field)); } foreach (int field, avalues.keys()) { if (seenFields.contains(field) || ignorableCommonFields.contains(field) || ignorableDetailFields.value(addition.type()).contains(field)) { continue; } score += scoreForValuePair(rvalues.value(field), avalues.value(field)); } return score; } bool detailPairExactlyMatches( const QContactDetail &a, const QContactDetail &b, const QHash > &ignorableDetailFields, const QSet &ignorableCommonFields, bool printDifferences = false) { if (a.type() != b.type()) { return false; } // now ensure that all values match QMap avalues = a.values(); QMap bvalues = b.values(); foreach (int akey, avalues.keys()) { if (ignorableCommonFields.contains(akey) || ignorableDetailFields.value(a.type()).contains(akey)) { continue; } const QVariant avalue = avalues.value(akey); if (!bvalues.contains(akey)) { // this may still be ok if the avalue is NULL // or if the avalue is an empty string, or empty list, // as the database can sometimes return empty // string instead of NULL value. if (avalue.type() == QVariant::Invalid || (avalue.type() == QVariant::String && avalue.toString().isEmpty()) || (avalue.userType() == QMetaType::type("QList") && avalue.value >() == QList())) { // this is ok. } else { // a has a real value which b does not have. if (Q_UNLIKELY(printDifferences)) { QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("detail A of type" << a.type() << "has value which B does not have:" << akey << "=" << avalue); } return false; } } else { // b contains the same key, but do the values match? if (scoreForValuePair(avalue, bvalues.value(akey)) != 0) { if (Q_UNLIKELY(printDifferences)) { QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("detail A of type" << a.type() << "has value which differs from B:" << akey << "=" << avalue << "!=" << bvalues.value(akey)); } return false; } // yes, they match. bvalues.remove(akey); } } // if there are any non-empty/null values left in b, then // a and b do not exactly match. foreach (int bkey, bvalues.keys()) { if (ignorableCommonFields.contains(bkey) || ignorableDetailFields.value(b.type()).contains(bkey)) { continue; } const QVariant bvalue = bvalues.value(bkey); if (bvalue.type() == QVariant::Invalid || (bvalue.type() == QVariant::String && bvalue.toString().isEmpty()) || (bvalue.userType() == QMetaType::type("QList") && bvalue.value >() == QList())) { // this is ok. } else { // b has a real value which a does not have. if (Q_UNLIKELY(printDifferences)) { QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("detail B of type" << b.type() << "has value which A does not have:" << bkey << "=" << bvalue); } return false; } } return true; } int exactDetailMatchExistsInList( const QContactDetail &det, const QList &list, const QHash > &ignorableDetailFields, QSet ignorableCommonFields, bool printDifferences) { for (int i = 0; i < list.size(); ++i) { if (detailPairExactlyMatches(det, list[i], ignorableDetailFields, ignorableCommonFields, printDifferences)) { return i; // exact match at this index. } } return -1; } bool contactDetailsMatchExactly( const QList &aDetails, const QList &bDetails, const QHash > &ignorableDetailFields, QSet ignorableCommonFields, bool printDifferences = false) { // for it to be an exact match: // a) every detail in aDetails must exist in bDetails // b) no extra details can exist in bDetails if (aDetails.size() != bDetails.size()) { if (Q_UNLIKELY(printDifferences)) { // detail count differs, and continue the analysis to find out precisely what the differences are. QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("A has more details than B:" << aDetails.size() << ">" << bDetails.size()); } else { // detail count differs, return immediately. return false; } } QList nonMatchedADetails; QList nonMatchedBDetails = bDetails; bool allADetailsHaveMatches = true; foreach (const QContactDetail &aDetail, aDetails) { int exactMatchIndex = exactDetailMatchExistsInList( aDetail, nonMatchedBDetails, ignorableDetailFields, ignorableCommonFields, false); if (exactMatchIndex == -1) { // no exact match for this detail. allADetailsHaveMatches = false; if (Q_UNLIKELY(printDifferences)) { // we only record the difference if we're printing them. nonMatchedADetails.append(aDetail); } else { // we only break if we're not printing all differences. break; } } else { // found a match for this detail. // remove it from ldets so that duplicates in cdets // don't mess up our detection. nonMatchedBDetails.removeAt(exactMatchIndex); } } if (allADetailsHaveMatches && nonMatchedBDetails.size() == 0) { return true; // exact match } if (Q_UNLIKELY(printDifferences)) { Q_FOREACH (const QContactDetail &ad, nonMatchedADetails) { bool foundMatch = false; for (int i = 0; i < nonMatchedBDetails.size(); ++i) { const QContactDetail &bd = nonMatchedBDetails[i]; if (ad.type() == bd.type()) { // most likely a modification. foundMatch = true; QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("Detail modified from A to B:"); detailPairExactlyMatches(ad, bd, ignorableDetailFields, ignorableCommonFields, printDifferences); nonMatchedBDetails.removeAt(i); break; } } if (!foundMatch) { QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("New detail exists in contact A:"); QTCONTACTS_SQLITE_DELTA_DEBUG_DETAIL(ad); } } Q_FOREACH (const QContactDetail &bd, nonMatchedBDetails) { QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("New detail exists in contact B:"); QTCONTACTS_SQLITE_DELTA_DEBUG_DETAIL(bd); } } return false; } // move some information (modifiable, detail uris, provenance, database id) // from the old detail to the new detail. void constructModification(const QContactDetail &old, QContactDetail *update) { const QMap values = update->values(); const QMap oldValues = old.values(); for (int field : oldValues.keys()) { if (field == QContactDetail__FieldDatabaseId || (!values.contains(field) && field == QContactDetail__FieldModifiable && field == QContactDetail::FieldProvenance && field == QContactDetail::FieldDetailUri && field == QContactDetail::FieldLinkedDetailUris)) { update->setValue(field, oldValues.value(field)); } } } // Note: this implementation can be overridden if the sync adapter knows // more about how to determine modifications (eg persistent detail ids) QList determineModifications( QList *removalsOfThisType, QList *additionsOfThisType, const QHash > &ignorableDetailFields, const QSet &ignorableCommonFields) { QList modifications; QList finalRemovals; QList finalAdditions; QList > permutationsOfIndexes; QHash scoresForPermutations; QList remainingRemovals; QList remainingAdditions; for (int i = 0; i < additionsOfThisType->size(); ++i) { remainingAdditions.append(i); } QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("determining modifications from the given list of additions/removals for details of a particular type"); // for each possible permutation, determine its score. // lower is a closer match (ie, score == distance). for (int i = 0; i < removalsOfThisType->size(); ++i) { remainingRemovals.append(i); for (int j = 0; j < additionsOfThisType->size(); ++j) { // determine the score for the permutation int score = scoreForDetailPair(removalsOfThisType->at(i), additionsOfThisType->at(j), ignorableDetailFields, ignorableCommonFields); scoresForPermutations.insert(permutationsOfIndexes.size(), score); permutationsOfIndexes.append(QPair(i, j)); QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("score for permutation" << i << "," << j << "=" << score); } } while (remainingRemovals.size() > 0 && remainingAdditions.size() > 0) { int lowScore = 1000; int lowScorePermutationIdx = -1; foreach (int permutationIdx, scoresForPermutations.keys()) { QPair permutation = permutationsOfIndexes.at(permutationIdx); if (remainingRemovals.contains(permutation.first) && remainingAdditions.contains(permutation.second)) { // this permutation is still "possible". if (scoresForPermutations.value(permutationIdx) < lowScore) { lowScorePermutationIdx = permutationIdx; lowScore = scoresForPermutations.value(permutationIdx); } } } if (lowScorePermutationIdx != -1) { // we have a valid permutation which should be treated as a modification. QPair bestPermutation = permutationsOfIndexes.at(lowScorePermutationIdx); QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("have determined that permutation" << bestPermutation.first << "," << bestPermutation.second << "is a modification"); remainingRemovals.removeAll(bestPermutation.first); remainingAdditions.removeAll(bestPermutation.second); const QContactDetail old = removalsOfThisType->at(bestPermutation.first); QContactDetail update = additionsOfThisType->at(bestPermutation.second); constructModification(old, &update); modifications.append(update); } } // rebuild the return values, removing the permutations which were applied as modifications. foreach (int idx, remainingRemovals) finalRemovals.append(removalsOfThisType->at(idx)); foreach (int idx, remainingAdditions) finalAdditions.append(additionsOfThisType->at(idx)); // and return. *removalsOfThisType = finalRemovals; *additionsOfThisType = finalAdditions; return modifications; } // Given a list of removals and a list of additions, // attempt to transform removal+addition pairs into modifications // if the changes are minimal enough to be considered a modification. QList improveDelta( QList *removals, QList *additions, const QHash > &ignorableDetailFields, const QSet &ignorableCommonFields) { QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("improving delta, have:" << removals->size() << "removals," << additions->size() << "additions"); QList finalRemovals; QList finalAdditions; QList finalModifications; QMultiHash bucketedRemovals; QMultiHash bucketedAdditions; for (int i = 0; i < removals->size(); ++i) bucketedRemovals.insertMulti(removals->at(i).type(), removals->at(i)); for (int i = 0; i < additions->size(); ++i) bucketedAdditions.insertMulti(additions->at(i).type(), additions->at(i)); QSet seenTypes; foreach (int type, bucketedRemovals.uniqueKeys()) { QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("dealing with detail type:" << type); seenTypes.insert(type); QList removalsOfThisType = bucketedRemovals.values(type); QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("have" << removalsOfThisType.size() << "removals of this type"); QList additionsOfThisType = bucketedAdditions.values(type); QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("have" << additionsOfThisType.size() << "additions of this type"); QList modificationsOfThisType = determineModifications(&removalsOfThisType, &additionsOfThisType, ignorableDetailFields, ignorableCommonFields); QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("have" << modificationsOfThisType.size() << "modifications of this type - and now rCount =" << removalsOfThisType.size() << ", aCount =" << additionsOfThisType.size()); finalRemovals.append(removalsOfThisType); finalAdditions.append(additionsOfThisType); finalModifications.append(modificationsOfThisType); } foreach (int type, bucketedAdditions.uniqueKeys()) { if (!seenTypes.contains(type)) { finalAdditions.append(bucketedAdditions.values(type)); } } QTCONTACTS_SQLITE_DELTA_DEBUG_LOG("ended up with detail a/m/r:" << finalAdditions.size() << "/" << finalModifications.size() << "/" << finalRemovals.size()); *removals = finalRemovals; *additions = finalAdditions; return finalModifications; } typedef QMap DetailMap; DetailMap detailValues(const QContactDetail &detail, bool includeProvenance = true, bool includeModifiable = true) { DetailMap rv(detail.values()); if (!includeProvenance || !includeModifiable) { DetailMap::iterator it = rv.begin(); while (it != rv.end()) { if (!includeProvenance && it.key() == QContactDetail::FieldProvenance) { it = rv.erase(it); } else if (!includeModifiable && it.key() == QContactDetail__FieldModifiable) { it = rv.erase(it); } else { ++it; } } } return rv; } static bool variantEqual(const QVariant &lhs, const QVariant &rhs) { // 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 >()); } return (lhs == rhs); } static bool detailValuesEqual(const QContactDetail &lhs, const QContactDetail &rhs) { const DetailMap lhsValues(detailValues(lhs, false, false)); const DetailMap rhsValues(detailValues(rhs, false, false)); if (lhsValues.count() != rhsValues.count()) { return false; } // Because of map ordering, matching fields should be in the same order in both details DetailMap::const_iterator lit = lhsValues.constBegin(), lend = lhsValues.constEnd(); DetailMap::const_iterator rit = rhsValues.constBegin(); for ( ; lit != lend; ++lit, ++rit) { if (lit.key() != rit.key() || !variantEqual(*lit, *rit)) { return false; } } return true; } static bool detailsEquivalent(const QContactDetail &lhs, const QContactDetail &rhs) { // Same as operator== except ignores differences in certain field values if (lhs.type() != rhs.type()) return false; return detailValuesEqual(lhs, rhs); } } // namespace const QSet& QtContactsSqliteExtensions::defaultIgnorableDetailTypes() { static QSet types(getDefaultIgnorableDetailTypes()); return types; } const QHash >& QtContactsSqliteExtensions::defaultIgnorableDetailFields() { static QHash > fields(getDefaultIgnorableDetailFields()); return fields; } const QSet& QtContactsSqliteExtensions::defaultIgnorableCommonFields() { static QSet fields(getDefaultIgnorableCommonFields()); return fields; } ContactDetailDelta QtContactsSqliteExtensions::determineContactDetailDelta( const QList &oldDetails, const QList &newDetails, const QSet &ignorableDetailTypes, const QHash > &ignorableDetailFields, const QSet &ignorableCommonFields) { ContactDetailDelta delta; QList odets = oldDetails; QList ndets = newDetails; // XXXXXXXXX TODO: ensure Unique details (Guid / Name / etc) are unique removeIgnorableDetailsFromList(&odets, ignorableDetailTypes); removeIgnorableDetailsFromList(&ndets, ignorableDetailTypes); // ignore all exact matches, as they don't form part of the delta for (int i = odets.size() - 1; i >= 0; --i) { bool found = false; for (int j = ndets.size() - 1; j >= 0; --j) { if (detailPairExactlyMatches(odets.at(i), ndets.at(j), ignorableDetailFields, ignorableCommonFields)) { // found an exact match; this detail hasn't changed // or is a duplicate of an existing detail (in the // case where multiple constituents of an aggregate // have some identical details). ndets.removeAt(j); found = true; } } if (found) { odets.removeAt(i); } } // determine direct modifications by matching database id for (int i = odets.size() - 1; i >= 0; --i) { int idx = -1; const quint32 oDbId = odets.at(i).value(QContactDetail__FieldDatabaseId).toUInt(); const QContactDetail::DetailType otype = odets.at(i).type(); for (int j = ndets.size() - 1; j >= 0; --j) { const quint32 nDbId = ndets.at(j).value(QContactDetail__FieldDatabaseId).toUInt(); const QContactDetail::DetailType ntype = ndets.at(j).type(); if (oDbId > 0 && oDbId == nDbId && otype == ntype) { idx = j; break; } } if (idx != -1) { // found a direct modification. QContactDetail update = ndets.at(idx); constructModification(odets.at(i), &update); delta.modifications.append(update); odets.removeAt(i); ndets.removeAt(idx); } } // now determine which pairs of old+new details should be considered modifications delta.modifications.append(improveDelta(&odets, &ndets, ignorableDetailFields, ignorableCommonFields)); delta.deletions = odets; // any detail addition requires a new/clean database id. removeDatabaseIdsFromList(&ndets); delta.additions = ndets; delta.isValid = true; return delta; } int QtContactsSqliteExtensions::exactContactMatchExistsInList( const QContact &aContact, const QList &list, const QSet &ignorableDetailTypes, const QHash > &ignorableDetailFields, const QSet &ignorableCommonFields, bool printDifferences) { QList aDetails = aContact.details(); removeIgnorableDetailsFromList(&aDetails, ignorableDetailTypes); for (int i = 0; i < list.size(); ++i) { QList bDetails = list[i].details(); removeIgnorableDetailsFromList(&bDetails, ignorableDetailTypes); if (contactDetailsMatchExactly(aDetails, bDetails, ignorableDetailFields, ignorableCommonFields, printDifferences)) { return i; // exact match at this index. } } return -1; } #endif // CONTACTDELTA_IMPL_H qtcontacts-sqlite-0.3.19/src/extensions/contactmanagerengine.h000066400000000000000000000157221436373107600246220ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2019 Jolla Ltd. * Copyright (c) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef CONTACTMANAGERENGINE_H #define CONTACTMANAGERENGINE_H #include QT_BEGIN_NAMESPACE_CONTACTS class QContactDetailFetchRequest; class QContactChangesFetchRequest; class QContactCollectionChangesFetchRequest; class QContactChangesSaveRequest; class QContactClearChangeFlagsRequest; QT_END_NAMESPACE_CONTACTS QTCONTACTS_USE_NAMESPACE namespace QtContactsSqliteExtensions { /* * Parameters recognized by the qtcontact-sqlite engine include: * 'mergePresenceChanges' - if true, contact presence changes will be merged with other changes, * and reported via the contactsChanged signal. Otherwise presence * changes will be reported separately, via the contactsPresenceChanged * signal of the QContactManager's engine object. * 'nonprivileged' - if true, the engine will not attempt to use the privileged database * of contact details, which is not accessible to normal processes. Otherwise * the privileged database will be preferred if accessible. * 'autoTest' - if true, an alternate database path is accessed, separate to the * path used by non-auto-test applications */ class Q_DECL_EXPORT ContactManagerEngine : public QContactManagerEngine { Q_OBJECT public: enum ConflictResolutionPolicy { PreserveLocalChanges, PreserveRemoteChanges }; ContactManagerEngine() : m_nonprivileged(false), m_mergePresenceChanges(false), m_autoTest(false) {} void setNonprivileged(bool b) { m_nonprivileged = b; } void setMergePresenceChanges(bool b) { m_mergePresenceChanges = b; } void setAutoTest(bool b) { m_autoTest = b; } virtual bool clearChangeFlags(const QList &contactIds, QContactManager::Error *error) = 0; virtual bool clearChangeFlags(const QContactCollectionId &collectionId, QContactManager::Error *error) = 0; // doesn't cause a transaction virtual bool fetchCollectionChanges(int accountId, const QString &applicationName, QList *addedCollections, QList *modifiedCollections, QList *deletedCollections, QList *unmodifiedCollections, QContactManager::Error *error) = 0; // causes a transaction: sets Collection.recordUnhandledChangeFlags, clears Contact+Detail.unhandledChangeFlags virtual bool fetchContactChanges(const QContactCollectionId &collectionId, QList *addedContacts, QList *modifiedContacts, QList *deletedContacts, QList *unmodifiedContacts, QContactManager::Error *error) = 0; // causes a transaction virtual bool storeChanges(QHash * /* added contacts */> *addedCollections, QHash * /* added/modified/deleted contacts */> *modifiedCollections, const QList &deletedCollections, ConflictResolutionPolicy conflictResolutionPolicy, bool clearChangeFlags, QContactManager::Error *error) = 0; virtual bool fetchOOB(const QString &scope, const QString &key, QVariant *value) = 0; virtual bool fetchOOB(const QString &scope, const QStringList &keys, QMap *values) = 0; virtual bool fetchOOB(const QString &scope, QMap *values) = 0; virtual bool fetchOOBKeys(const QString &scope, QStringList *keys) = 0; virtual bool storeOOB(const QString &scope, const QString &key, const QVariant &value) = 0; virtual bool storeOOB(const QString &scope, const QMap &values) = 0; virtual bool removeOOB(const QString &scope, const QString &key) = 0; virtual bool removeOOB(const QString &scope, const QStringList &keys) = 0; virtual bool removeOOB(const QString &scope) = 0; virtual QStringList displayLabelGroups() = 0; virtual void requestDestroyed(QObject* request) = 0; virtual bool startRequest(QContactDetailFetchRequest* request) = 0; virtual bool startRequest(QContactCollectionChangesFetchRequest* request) = 0; virtual bool startRequest(QContactChangesFetchRequest* request) = 0; virtual bool startRequest(QContactChangesSaveRequest* request) = 0; virtual bool startRequest(QContactClearChangeFlagsRequest* request) = 0; virtual bool cancelRequest(QObject* request) = 0; virtual bool waitForRequestFinished(QObject* req, int msecs) = 0; Q_SIGNALS: void contactsPresenceChanged(const QList &contactsIds); void collectionContactsChanged(const QList &collectionIds); void displayLabelGroupsChanged(const QStringList &groups); protected: bool m_nonprivileged; bool m_mergePresenceChanges; bool m_autoTest; }; } #endif qtcontacts-sqlite-0.3.19/src/extensions/displaylabelgroupgenerator.h000066400000000000000000000060531436373107600260740ustar00rootroot00000000000000/* * Copyright (C) 2019 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef DISPLAYLABELGROUPGENERATOR_H #define DISPLAYLABELGROUPGENERATOR_H #include #include #include namespace QtContactsSqliteExtensions { /* A display label group generator is able to determine which "ribbon group" (bucket) a contact should be placed in. The most common bucket to place a contact in is usually the first letter of that contact's last name (so 'John Smith' would be placed into group 'S'). However, for languages other than English, other forms of grouping are required. */ class DisplayLabelGroupGenerator { public: virtual ~DisplayLabelGroupGenerator() {} virtual QString name() const = 0; virtual int priority() const = 0; // higher priority will be used before lower priority when generating label groups. virtual bool preferredForLocale(const QLocale &locale) const = 0; virtual bool validForLocale(const QLocale &locale) const = 0; virtual QString displayLabelGroup(const QString &data) const = 0; virtual QStringList displayLabelGroups() const = 0; }; } // namespace QtContactsSqliteExtensions #define QtContactsSqliteExtensions_DisplayLabelGroupGeneratorInterface_iid "org.nemomobile.qtcontacts-sqlite.extensions.DisplayLabelGroupGeneratorInterface" Q_DECLARE_INTERFACE(QtContactsSqliteExtensions::DisplayLabelGroupGenerator, QtContactsSqliteExtensions_DisplayLabelGroupGeneratorInterface_iid) #endif // DISPLAYLABELGROUPGENERATOR_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactchangesfetchrequest.h000066400000000000000000000057401436373107600262350ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCHANGESFETCHREQUEST_H #define QCONTACTCHANGESFETCHREQUEST_H #include #include #include #include #include QT_BEGIN_NAMESPACE_CONTACTS class QContactChangesFetchRequestPrivate; class QContactChangesFetchRequest : public QObject { Q_OBJECT Q_DISABLE_COPY(QContactChangesFetchRequest) Q_DECLARE_PRIVATE(QContactChangesFetchRequest) public: QContactChangesFetchRequest(QObject *parent = nullptr); ~QContactChangesFetchRequest() override; QContactManager *manager() const; void setManager(QContactManager *manager); QContactCollectionId collectionId() const; void setCollectionId(const QContactCollectionId &id); QContactAbstractRequest::State state() const; QContactManager::Error error() const; QList addedContacts() const; QList modifiedContacts() const; QList removedContacts() const; QList unmodifiedContacts() const; public Q_SLOTS: bool start(); bool cancel(); bool waitForFinished(int msecs = 0); Q_SIGNALS: void stateChanged(QContactAbstractRequest::State state); void resultsAvailable(); private: QScopedPointer d_ptr; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCHANGESFETCHREQUEST_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactchangesfetchrequest_impl.h000066400000000000000000000106271436373107600272560ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCHANGESFETCHREQUEST_IMPL_H #define QCONTACTCHANGESFETCHREQUEST_IMPL_H #include "./qcontactchangesfetchrequest_p.h" #include "./contactmanagerengine.h" #include QT_BEGIN_NAMESPACE_CONTACTS QContactChangesFetchRequest::QContactChangesFetchRequest(QObject *parent) : QObject(parent) , d_ptr(new QContactChangesFetchRequestPrivate( this, &QContactChangesFetchRequest::stateChanged, &QContactChangesFetchRequest::resultsAvailable)) { } QContactChangesFetchRequest::~QContactChangesFetchRequest() { } QContactManager *QContactChangesFetchRequest::manager() const { return d_ptr->manager.data(); } void QContactChangesFetchRequest::setManager(QContactManager *manager) { d_ptr->manager = manager; } QContactCollectionId QContactChangesFetchRequest::collectionId() const { return d_ptr->collectionId; } void QContactChangesFetchRequest::setCollectionId(const QContactCollectionId &id) { d_ptr->collectionId = id; } QContactAbstractRequest::State QContactChangesFetchRequest::state() const { return d_ptr->state; } QContactManager::Error QContactChangesFetchRequest::error() const { return d_ptr->error; } QList QContactChangesFetchRequest::addedContacts() const { return d_ptr->addedContacts; } QList QContactChangesFetchRequest::modifiedContacts() const { return d_ptr->modifiedContacts; } QList QContactChangesFetchRequest::removedContacts() const { return d_ptr->removedContacts; } QList QContactChangesFetchRequest::unmodifiedContacts() const { return d_ptr->unmodifiedContacts; } bool QContactChangesFetchRequest::start() { if (d_ptr->state == QContactAbstractRequest::ActiveState) { // Already executing. } else if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->startRequest(this); } return false; } bool QContactChangesFetchRequest::cancel() { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->cancelRequest(this); } return false; } bool QContactChangesFetchRequest::waitForFinished(int msecs) { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->waitForRequestFinished(this, msecs); } return false; } QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCHANGESFETCHREQUEST_IMPL_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactchangesfetchrequest_p.h000066400000000000000000000057201436373107600265520ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCHANGESFETCHREQUEST_P_H #define QCONTACTCHANGESFETCHREQUEST_P_H #include "./qcontactchangesfetchrequest.h" #include QT_BEGIN_NAMESPACE_CONTACTS class QContactChangesFetchRequestPrivate { public: static QContactChangesFetchRequestPrivate *get(QContactChangesFetchRequest *request) { return request->d_func(); } QContactChangesFetchRequestPrivate( QContactChangesFetchRequest *q, void (QContactChangesFetchRequest::*stateChanged)(QContactAbstractRequest::State state), void (QContactChangesFetchRequest::*resultsAvailable)()) : q_ptr(q) , stateChanged(stateChanged) , resultsAvailable(resultsAvailable) { } QContactChangesFetchRequest * const q_ptr; void (QContactChangesFetchRequest::* const stateChanged)(QContactAbstractRequest::State state); void (QContactChangesFetchRequest::* const resultsAvailable)(); QPointer manager; QContactCollectionId collectionId; QContactAbstractRequest::State state = QContactAbstractRequest::InactiveState; QContactManager::Error error = QContactManager::NoError; QList addedContacts; QList modifiedContacts; QList removedContacts; QList unmodifiedContacts; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCHANGESFETCHREQUEST_P_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactchangessaverequest.h000066400000000000000000000067571436373107600261130ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCHANGESSAVEREQUEST_H #define QCONTACTCHANGESSAVEREQUEST_H #include #include #include #include #include QT_BEGIN_NAMESPACE_CONTACTS class QContactChangesSaveRequestPrivate; class QContactChangesSaveRequest : public QObject { Q_OBJECT Q_DISABLE_COPY(QContactChangesSaveRequest) Q_DECLARE_PRIVATE(QContactChangesSaveRequest) public: enum ConflictResolutionPolicy { PreserveLocalChanges, PreserveRemoteChanges }; Q_ENUM(ConflictResolutionPolicy) QContactChangesSaveRequest(QObject *parent = nullptr); ~QContactChangesSaveRequest() override; QContactManager *manager() const; void setManager(QContactManager *manager); ConflictResolutionPolicy conflictResolutionPolicy() const; void setConflictResolutionPolicy(ConflictResolutionPolicy policy); bool clearChangeFlags() const; void setClearChangeFlags(bool clear); QHash > addedCollections() const; void setAddedCollections(const QHash > &added); QHash > modifiedCollections() const; void setModifiedCollections(const QHash > &modified); QList removedCollections() const; void setRemovedCollections(const QList &removed); QContactAbstractRequest::State state() const; QContactManager::Error error() const; public Q_SLOTS: bool start(); bool cancel(); bool waitForFinished(int msecs = 0); Q_SIGNALS: void stateChanged(QContactAbstractRequest::State state); void resultsAvailable(); private: QScopedPointer d_ptr; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCHANGESSAVEREQUEST_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactchangessaverequest_impl.h000066400000000000000000000121151436373107600271150ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCHANGESSAVEREQUEST_IMPL_H #define QCONTACTCHANGESSAVEREQUEST_IMPL_H #include "./qcontactchangessaverequest_p.h" #include "./contactmanagerengine.h" #include QT_BEGIN_NAMESPACE_CONTACTS QContactChangesSaveRequest::QContactChangesSaveRequest(QObject *parent) : QObject(parent) , d_ptr(new QContactChangesSaveRequestPrivate( this, &QContactChangesSaveRequest::stateChanged, &QContactChangesSaveRequest::resultsAvailable)) { } QContactChangesSaveRequest::~QContactChangesSaveRequest() { } QContactManager *QContactChangesSaveRequest::manager() const { return d_ptr->manager.data(); } void QContactChangesSaveRequest::setManager(QContactManager *manager) { d_ptr->manager = manager; } QContactChangesSaveRequest::ConflictResolutionPolicy QContactChangesSaveRequest::conflictResolutionPolicy() const { return d_ptr->policy; } void QContactChangesSaveRequest::setConflictResolutionPolicy(QContactChangesSaveRequest::ConflictResolutionPolicy policy) { d_ptr->policy = policy; } bool QContactChangesSaveRequest::clearChangeFlags() const { return d_ptr->clearChangeFlags; } void QContactChangesSaveRequest::setClearChangeFlags(bool clear) { d_ptr->clearChangeFlags = clear; } QHash > QContactChangesSaveRequest::addedCollections() const { return d_ptr->addedCollections; } void QContactChangesSaveRequest::setAddedCollections(const QHash > &added) { d_ptr->addedCollections = added; } QHash > QContactChangesSaveRequest::modifiedCollections() const { return d_ptr->modifiedCollections; } void QContactChangesSaveRequest::setModifiedCollections(const QHash > &modified) { d_ptr->modifiedCollections = modified; } QList QContactChangesSaveRequest::removedCollections() const { return d_ptr->removedCollections; } void QContactChangesSaveRequest::setRemovedCollections(const QList &removed) { d_ptr->removedCollections = removed; } QContactAbstractRequest::State QContactChangesSaveRequest::state() const { return d_ptr->state; } QContactManager::Error QContactChangesSaveRequest::error() const { return d_ptr->error; } bool QContactChangesSaveRequest::start() { if (d_ptr->state == QContactAbstractRequest::ActiveState) { // Already executing. } else if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->startRequest(this); } return false; } bool QContactChangesSaveRequest::cancel() { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->cancelRequest(this); } return false; } bool QContactChangesSaveRequest::waitForFinished(int msecs) { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->waitForRequestFinished(this, msecs); } return false; } QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCHANGESSAVEREQUEST_IMPL_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactchangessaverequest_p.h000066400000000000000000000061271436373107600264210ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCHANGESSAVEREQUEST_P_H #define QCONTACTCHANGESSAVEREQUEST_P_H #include "./qcontactchangessaverequest.h" #include QT_BEGIN_NAMESPACE_CONTACTS class QContactChangesSaveRequestPrivate { public: static QContactChangesSaveRequestPrivate *get(QContactChangesSaveRequest *request) { return request->d_func(); } QContactChangesSaveRequestPrivate( QContactChangesSaveRequest *q, void (QContactChangesSaveRequest::*stateChanged)(QContactAbstractRequest::State state), void (QContactChangesSaveRequest::*resultsAvailable)()) : q_ptr(q) , stateChanged(stateChanged) , resultsAvailable(resultsAvailable) { } QContactChangesSaveRequest * const q_ptr; void (QContactChangesSaveRequest::* const stateChanged)(QContactAbstractRequest::State state); void (QContactChangesSaveRequest::* const resultsAvailable)(); QPointer manager; QContactChangesSaveRequest::ConflictResolutionPolicy policy = QContactChangesSaveRequest::PreserveLocalChanges; bool clearChangeFlags = false; QHash > addedCollections; QHash > modifiedCollections; QList removedCollections; QContactAbstractRequest::State state = QContactAbstractRequest::InactiveState; QContactManager::Error error = QContactManager::NoError; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCHANGESSAVEREQUEST_P_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactclearchangeflagsrequest.h000066400000000000000000000056621436373107600270670ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCLEARCHANGEFLAGSREQUEST_H #define QCONTACTCLEARCHANGEFLAGSREQUEST_H #include #include #include #include #include QT_BEGIN_NAMESPACE_CONTACTS class QContactClearChangeFlagsRequestPrivate; class QContactClearChangeFlagsRequest : public QObject { Q_OBJECT Q_DISABLE_COPY(QContactClearChangeFlagsRequest) Q_DECLARE_PRIVATE(QContactClearChangeFlagsRequest) public: QContactClearChangeFlagsRequest(QObject *parent = nullptr); ~QContactClearChangeFlagsRequest() override; QContactManager *manager() const; void setManager(QContactManager *manager); QContactCollectionId collectionId() const; void setCollectionId(const QContactCollectionId &id); QList contactIds() const; void setContactIds(const QList &ids); QContactAbstractRequest::State state() const; QContactManager::Error error() const; public Q_SLOTS: bool start(); bool cancel(); bool waitForFinished(int msecs = 0); Q_SIGNALS: void stateChanged(QContactAbstractRequest::State state); void resultsAvailable(); private: QScopedPointer d_ptr; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCLEARCHANGEFLAGSREQUEST_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactclearchangeflagsrequest_impl.h000066400000000000000000000105351436373107600301030ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCLEARCHANGEFLAGSREQUEST_IMPL_H #define QCONTACTCLEARCHANGEFLAGSREQUEST_IMPL_H #include "./qcontactclearchangeflagsrequest_p.h" #include "./contactmanagerengine.h" #include QT_BEGIN_NAMESPACE_CONTACTS QContactClearChangeFlagsRequest::QContactClearChangeFlagsRequest(QObject *parent) : QObject(parent) , d_ptr(new QContactClearChangeFlagsRequestPrivate( this, &QContactClearChangeFlagsRequest::stateChanged, &QContactClearChangeFlagsRequest::resultsAvailable)) { } QContactClearChangeFlagsRequest::~QContactClearChangeFlagsRequest() { } QContactManager *QContactClearChangeFlagsRequest::manager() const { return d_ptr->manager.data(); } void QContactClearChangeFlagsRequest::setManager(QContactManager *manager) { d_ptr->manager = manager; } QContactCollectionId QContactClearChangeFlagsRequest::collectionId() const { return d_ptr->collectionId; } void QContactClearChangeFlagsRequest::setCollectionId(const QContactCollectionId &id) { d_ptr->contactIds.clear(); d_ptr->collectionId = id; } QList QContactClearChangeFlagsRequest::contactIds() const { return d_ptr->contactIds; } void QContactClearChangeFlagsRequest::setContactIds(const QList &ids) { d_ptr->collectionId = QContactCollectionId(); d_ptr->contactIds = ids; } QContactAbstractRequest::State QContactClearChangeFlagsRequest::state() const { return d_ptr->state; } QContactManager::Error QContactClearChangeFlagsRequest::error() const { return d_ptr->error; } bool QContactClearChangeFlagsRequest::start() { if (d_ptr->state == QContactAbstractRequest::ActiveState) { // Already executing. } else if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->startRequest(this); } return false; } bool QContactClearChangeFlagsRequest::cancel() { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->cancelRequest(this); } return false; } bool QContactClearChangeFlagsRequest::waitForFinished(int msecs) { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->waitForRequestFinished(this, msecs); } return false; } QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCLEARCHANGEFLAGSREQUEST_IMPL_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactclearchangeflagsrequest_p.h000066400000000000000000000056241436373107600274040ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCLEARCHANGEFLAGSREQUEST_P_H #define QCONTACTCLEARCHANGEFLAGSREQUEST_P_H #include "./qcontactclearchangeflagsrequest.h" #include QT_BEGIN_NAMESPACE_CONTACTS class QContactClearChangeFlagsRequestPrivate { public: static QContactClearChangeFlagsRequestPrivate *get(QContactClearChangeFlagsRequest *request) { return request->d_func(); } QContactClearChangeFlagsRequestPrivate( QContactClearChangeFlagsRequest *q, void (QContactClearChangeFlagsRequest::*stateChanged)(QContactAbstractRequest::State state), void (QContactClearChangeFlagsRequest::*resultsAvailable)()) : q_ptr(q) , stateChanged(stateChanged) , resultsAvailable(resultsAvailable) { } QContactClearChangeFlagsRequest * const q_ptr; void (QContactClearChangeFlagsRequest::* const stateChanged)(QContactAbstractRequest::State state); void (QContactClearChangeFlagsRequest::* const resultsAvailable)(); QContactCollectionId collectionId; QList contactIds; QPointer manager; QContactAbstractRequest::State state = QContactAbstractRequest::InactiveState; QContactManager::Error error = QContactManager::NoError; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCLEARCHANGEFLAGSREQUEST_P_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactcollectionchangesfetchrequest.h000066400000000000000000000062411436373107600303060ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCOLLECTIONCHANGESFETCHREQUEST_H #define QCONTACTCOLLECTIONCHANGESFETCHREQUEST_H #include #include #include #include #include QT_BEGIN_NAMESPACE_CONTACTS class QContactCollectionChangesFetchRequestPrivate; class QContactCollectionChangesFetchRequest : public QObject { Q_OBJECT Q_DISABLE_COPY(QContactCollectionChangesFetchRequest) Q_DECLARE_PRIVATE(QContactCollectionChangesFetchRequest) public: QContactCollectionChangesFetchRequest(QObject *parent = nullptr); ~QContactCollectionChangesFetchRequest() override; QContactManager *manager() const; void setManager(QContactManager *manager); int accountId() const; void setAccountId(int id); QString applicationName() const; void setApplicationName(const QString &name); QContactAbstractRequest::State state() const; QContactManager::Error error() const; QList addedCollections() const; QList modifiedCollections() const; QList removedCollections() const; QList unmodifiedCollections() const; public Q_SLOTS: bool start(); bool cancel(); bool waitForFinished(int msecs = 0); Q_SIGNALS: void stateChanged(QContactAbstractRequest::State state); void resultsAvailable(); private: QScopedPointer d_ptr; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCOLLECTIONCHANGESFETCHREQUEST_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactcollectionchangesfetchrequest_impl.h000066400000000000000000000115751436373107600313350ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCOLLECTIONCHANGESFETCHREQUEST_IMPL_H #define QCONTACTCOLLECTIONCHANGESFETCHREQUEST_IMPL_H #include "./qcontactcollectionchangesfetchrequest_p.h" #include "./contactmanagerengine.h" #include QT_BEGIN_NAMESPACE_CONTACTS QContactCollectionChangesFetchRequest::QContactCollectionChangesFetchRequest(QObject *parent) : QObject(parent) , d_ptr(new QContactCollectionChangesFetchRequestPrivate( this, &QContactCollectionChangesFetchRequest::stateChanged, &QContactCollectionChangesFetchRequest::resultsAvailable)) { } QContactCollectionChangesFetchRequest::~QContactCollectionChangesFetchRequest() { } QContactManager *QContactCollectionChangesFetchRequest::manager() const { return d_ptr->manager.data(); } void QContactCollectionChangesFetchRequest::setManager(QContactManager *manager) { d_ptr->manager = manager; } int QContactCollectionChangesFetchRequest::accountId() const { return d_ptr->accountId; } void QContactCollectionChangesFetchRequest::setAccountId(int id) { d_ptr->accountId = id; } QString QContactCollectionChangesFetchRequest::applicationName() const { return d_ptr->applicationName; } void QContactCollectionChangesFetchRequest::setApplicationName(const QString &name) { d_ptr->applicationName = name; } QContactAbstractRequest::State QContactCollectionChangesFetchRequest::state() const { return d_ptr->state; } QContactManager::Error QContactCollectionChangesFetchRequest::error() const { return d_ptr->error; } QList QContactCollectionChangesFetchRequest::addedCollections() const { return d_ptr->addedCollections; } QList QContactCollectionChangesFetchRequest::modifiedCollections() const { return d_ptr->modifiedCollections; } QList QContactCollectionChangesFetchRequest::removedCollections() const { return d_ptr->removedCollections; } QList QContactCollectionChangesFetchRequest::unmodifiedCollections() const { return d_ptr->unmodifiedCollections; } bool QContactCollectionChangesFetchRequest::start() { if (d_ptr->state == QContactAbstractRequest::ActiveState) { // Already executing. } else if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->startRequest(this); } return false; } bool QContactCollectionChangesFetchRequest::cancel() { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->cancelRequest(this); } return false; } bool QContactCollectionChangesFetchRequest::waitForFinished(int msecs) { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->waitForRequestFinished(this, msecs); } return false; } QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCOLLECTIONCHANGESFETCHREQUEST_IMPL_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactcollectionchangesfetchrequest_p.h000066400000000000000000000062351436373107600306300ustar00rootroot00000000000000/* * Copyright (c) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTCOLLECTIONCHANGESFETCHREQUEST_P_H #define QCONTACTCOLLECTIONCHANGESFETCHREQUEST_P_H #include "./qcontactcollectionchangesfetchrequest.h" #include QT_BEGIN_NAMESPACE_CONTACTS class QContactCollectionChangesFetchRequestPrivate { public: static QContactCollectionChangesFetchRequestPrivate *get(QContactCollectionChangesFetchRequest *request) { return request->d_func(); } QContactCollectionChangesFetchRequestPrivate( QContactCollectionChangesFetchRequest *q, void (QContactCollectionChangesFetchRequest::*stateChanged)(QContactAbstractRequest::State state), void (QContactCollectionChangesFetchRequest::*resultsAvailable)()) : q_ptr(q) , stateChanged(stateChanged) , resultsAvailable(resultsAvailable) { } QContactCollectionChangesFetchRequest * const q_ptr; void (QContactCollectionChangesFetchRequest::* const stateChanged)(QContactAbstractRequest::State state); void (QContactCollectionChangesFetchRequest::* const resultsAvailable)(); int accountId = 0; QString applicationName; QPointer manager; QContactAbstractRequest::State state = QContactAbstractRequest::InactiveState; QContactManager::Error error = QContactManager::NoError; QList addedCollections; QList modifiedCollections; QList removedCollections; QList unmodifiedCollections; }; QT_END_NAMESPACE_CONTACTS #endif // QCONTACTCOLLECTIONCHANGESFETCHREQUEST_P_H qtcontacts-sqlite-0.3.19/src/extensions/qcontactdeactivated.h000066400000000000000000000036221436373107600244540ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTDEACTIVATED_H #define QCONTACTDEACTIVATED_H #include QT_BEGIN_NAMESPACE_CONTACTS class QContactDeactivated : public QContactDetail { public: Q_DECLARE_CUSTOM_CONTACT_DETAIL(QContactDeactivated) }; QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactdeactivated_impl.h000066400000000000000000000036661436373107600255050ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTDEACTIVATED_IMPL_H #define QCONTACTDEACTIVATED_IMPL_H #include "qcontactdeactivated.h" #include "qtcontacts-extensions.h" QTCONTACTS_USE_NAMESPACE const QContactDetail::DetailType QContactDeactivated::Type(static_cast(QContactDetail__TypeDeactivated)); #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactdetailfetchrequest.h000066400000000000000000000062221436373107600260630ustar00rootroot00000000000000/* * Copyright (c) 2019 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTDETAILFETCHREQUEST_H #define QCONTACTDETAILFETCHREQUEST_H #include #include #include #include #include QT_BEGIN_NAMESPACE_CONTACTS class QContactDetailFetchRequestPrivate; class QContactDetailFetchRequest : public QObject { Q_OBJECT Q_DISABLE_COPY(QContactDetailFetchRequest) Q_DECLARE_PRIVATE(QContactDetailFetchRequest) public: QContactDetailFetchRequest(QObject *parent = nullptr); ~QContactDetailFetchRequest() override; QContactManager *manager() const; void setManager(QContactManager *manager); QContactDetail::DetailType type() const; void setType(QContactDetail::DetailType type); QList fields() const; void setFields(const QList fields); QContactFilter filter() const; void setFilter(const QContactFilter &filter); QList sorting() const; void setSorting(const QList &sorting); QContactFetchHint fetchHint() const; void setFetchHint(const QContactFetchHint &hint); QContactAbstractRequest::State state() const; QContactManager::Error error() const; QList details() const; public Q_SLOTS: bool start(); bool cancel(); bool waitForFinished(int msecs = 0); Q_SIGNALS: void stateChanged(QContactAbstractRequest::State state); void resultsAvailable(); private: QScopedPointer d_ptr; }; QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactdetailfetchrequest_impl.h000066400000000000000000000114221436373107600271020ustar00rootroot00000000000000/* * Copyright (c) 2019 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTDETAILFETCHREQUEST_IMPL_H #define QCONTACTDETAILFETCHREQUEST_IMPL_H #include "./qcontactdetailfetchrequest_p.h" #include "./contactmanagerengine.h" #include QT_BEGIN_NAMESPACE_CONTACTS QContactDetailFetchRequest::QContactDetailFetchRequest(QObject *parent) : QObject(parent) , d_ptr(new QContactDetailFetchRequestPrivate( this, &QContactDetailFetchRequest::stateChanged, &QContactDetailFetchRequest::resultsAvailable)) { } QContactDetailFetchRequest::~QContactDetailFetchRequest() { } QContactManager *QContactDetailFetchRequest::manager() const { return d_ptr->manager.data(); } void QContactDetailFetchRequest::setManager(QContactManager *manager) { d_ptr->manager = manager; } QContactDetail::DetailType QContactDetailFetchRequest::type() const { return d_ptr->type; } void QContactDetailFetchRequest::setType(QContactDetail::DetailType type) { d_ptr->type = type; } QList QContactDetailFetchRequest::fields() const { return d_ptr->fields; } void QContactDetailFetchRequest::setFields(const QList fields) { d_ptr->fields = fields; } QContactFilter QContactDetailFetchRequest::filter() const { return d_ptr->filter; } void QContactDetailFetchRequest::setFilter(const QContactFilter &filter) { d_ptr->filter = filter; } QList QContactDetailFetchRequest::sorting() const { return d_ptr->sorting; } void QContactDetailFetchRequest::setSorting(const QList &sorting) { d_ptr->sorting = sorting; } QContactFetchHint QContactDetailFetchRequest::fetchHint() const { return d_ptr->hint; } void QContactDetailFetchRequest::setFetchHint(const QContactFetchHint &hint) { d_ptr->hint = hint; } QContactAbstractRequest::State QContactDetailFetchRequest::state() const { return d_ptr->state; } QContactManager::Error QContactDetailFetchRequest::error() const { return d_ptr->error; } QList QContactDetailFetchRequest::details() const { return d_ptr->details; } bool QContactDetailFetchRequest::start() { if (d_ptr->state == QContactAbstractRequest::ActiveState) { // Already executing. } else if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->startRequest(this); } return false; } bool QContactDetailFetchRequest::cancel() { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->cancelRequest(this); } return false; } bool QContactDetailFetchRequest::waitForFinished(int msecs) { if (!d_ptr->manager) { // No manager. } else if (QtContactsSqliteExtensions::ContactManagerEngine * const engine = QtContactsSqliteExtensions::contactManagerEngine(*d_ptr->manager)) { return engine->waitForRequestFinished(this, msecs); } return false; } QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactdetailfetchrequest_p.h000066400000000000000000000056771436373107600264170ustar00rootroot00000000000000/* * Copyright (c) 2019 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTDETAILFETCHREQUEST_P_H #define QCONTACTDETAILFETCHREQUEST_P_H #include "./qcontactdetailfetchrequest.h" #include QT_BEGIN_NAMESPACE_CONTACTS class QContactDetailFetchRequestPrivate { public: static QContactDetailFetchRequestPrivate *get(QContactDetailFetchRequest *request) { return request->d_func(); } QContactDetailFetchRequestPrivate( QContactDetailFetchRequest *q, void (QContactDetailFetchRequest::*stateChanged)(QContactAbstractRequest::State state), void (QContactDetailFetchRequest::*resultsAvailable)()) : q_ptr(q) , stateChanged(stateChanged) , resultsAvailable(resultsAvailable) { } QContactDetailFetchRequest * const q_ptr; void (QContactDetailFetchRequest::* const stateChanged)(QContactAbstractRequest::State state); void (QContactDetailFetchRequest::* const resultsAvailable)(); QContactFilter filter; QContactFetchHint hint; QList sorting; QList fields; QList details; QPointer manager; QContactDetail::DetailType type = QContactDetail::TypeUndefined; QContactAbstractRequest::State state = QContactAbstractRequest::InactiveState; QContactManager::Error error = QContactManager::NoError; }; QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactoriginmetadata.h000066400000000000000000000045151436373107600251710ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTORIGINMETADATA_H #define QCONTACTORIGINMETADATA_H #include #include QT_BEGIN_NAMESPACE_CONTACTS class QContactOriginMetadata : public QContactDetail { public: Q_DECLARE_CUSTOM_CONTACT_DETAIL(QContactOriginMetadata) enum { FieldId = 0, FieldGroupId = 1, FieldEnabled = 2 }; void setId(const QString &s); QString id() const; void setGroupId(const QString &s); QString groupId() const; void setEnabled(bool b); bool enabled() const; static QContactDetailFilter matchId(const QString &s); static QContactDetailFilter matchGroupId(const QString &s); }; QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactoriginmetadata_impl.h000066400000000000000000000060431436373107600262100ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTORIGINMETADATA_IMPL_H #define QCONTACTORIGINMETADATA_IMPL_H #include "qcontactoriginmetadata.h" #include "qtcontacts-extensions.h" QTCONTACTS_USE_NAMESPACE void QContactOriginMetadata::setId(const QString &s) { setValue(FieldId, s); } QString QContactOriginMetadata::id() const { return value(FieldId); } void QContactOriginMetadata::setGroupId(const QString &s) { setValue(FieldGroupId, s); } QString QContactOriginMetadata::groupId() const { return value(FieldGroupId); } void QContactOriginMetadata::setEnabled(bool b) { setValue(FieldEnabled, QLatin1String(b ? "true" : "false")); } bool QContactOriginMetadata::enabled() const { return value(FieldEnabled); } QContactDetailFilter QContactOriginMetadata::matchId(const QString &s) { QContactDetailFilter filter; filter.setDetailType(QContactOriginMetadata::Type, FieldId); filter.setValue(s); filter.setMatchFlags(QContactFilter::MatchExactly); return filter; } QContactDetailFilter QContactOriginMetadata::matchGroupId(const QString &s) { QContactDetailFilter filter; filter.setDetailType(QContactOriginMetadata::Type, FieldGroupId); filter.setValue(s); filter.setMatchFlags(QContactFilter::MatchExactly); return filter; } const QContactDetail::DetailType QContactOriginMetadata::Type(static_cast(QContactDetail__TypeOriginMetadata)); #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactstatusflags.h000066400000000000000000000054641436373107600245450ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2014 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTSTATUSFLAGS_H #define QCONTACTSTATUSFLAGS_H #include #include QT_BEGIN_NAMESPACE_CONTACTS class QContactStatusFlags : public QContactDetail { public: Q_DECLARE_CUSTOM_CONTACT_DETAIL(QContactStatusFlags) enum { FieldFlags = 0 }; enum Flag { HasPhoneNumber = (1 << 0), HasEmailAddress = (1 << 1), HasOnlineAccount = (1 << 2), IsOnline = (1 << 3), IsDeactivated = (1 << 4), IsAdded = (1 << 5), IsModified = (1 << 6), IsDeleted = (1 << 7) }; Q_DECLARE_FLAGS(Flags, Flag) void setFlag(Flag flag, bool b); void setFlags(Flags flags); Flags flags() const; void setFlagsValue(quint64 value); quint64 flagsValue() const; bool testFlag(Flag flag) const; static QContactDetailFilter matchFlag(Flag flag, QContactFilter::MatchFlags matchFlags = QContactFilter::MatchContains); static QContactDetailFilter matchFlags(Flags flags, QContactFilter::MatchFlags matchFlags = QContactFilter::MatchContains); }; Q_DECLARE_OPERATORS_FOR_FLAGS(QContactStatusFlags::Flags) QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactstatusflags_impl.h000066400000000000000000000062461436373107600255650ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTSTATUSFLAGS_IMPL_H #define QCONTACTSTATUSFLAGS_IMPL_H #include "qcontactstatusflags.h" #include "qtcontacts-extensions.h" QTCONTACTS_USE_NAMESPACE void QContactStatusFlags::setFlag(Flag flag, bool b) { quint64 flagsValue = value(FieldFlags); if (b) { flagsValue |= static_cast(flag); } else { flagsValue &= ~(static_cast(flag)); } setFlagsValue(flagsValue); } void QContactStatusFlags::setFlags(Flags flags) { setFlagsValue(static_cast(flags)); } QContactStatusFlags::Flags QContactStatusFlags::flags() const { return Flags(static_cast(flagsValue())); } void QContactStatusFlags::setFlagsValue(quint64 value) { setValue(FieldFlags, value); } quint64 QContactStatusFlags::flagsValue() const { return value(FieldFlags); } bool QContactStatusFlags::testFlag(Flag flag) const { return flags().testFlag(flag); } QContactDetailFilter QContactStatusFlags::matchFlag(Flag flag, QContactFilter::MatchFlags matchFlags) { return QContactStatusFlags::matchFlags(Flags(flag), matchFlags); } QContactDetailFilter QContactStatusFlags::matchFlags(Flags flags, QContactFilter::MatchFlags matchFlags) { QContactDetailFilter filter; filter.setDetailType(QContactStatusFlags::Type, FieldFlags); filter.setValue(static_cast(flags)); filter.setMatchFlags(matchFlags); return filter; } const QContactDetail::DetailType QContactStatusFlags::Type(static_cast(QContactDetail__TypeStatusFlags)); #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactundelete.h000066400000000000000000000036261436373107600240100ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTUNDELETE_H #define QCONTACTUNDELETE_H #include QT_BEGIN_NAMESPACE_CONTACTS class QContactUndelete : public QContactDetail { public: Q_DECLARE_CUSTOM_CONTACT_DETAIL(QContactUndelete) }; QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/src/extensions/qcontactundelete_impl.h000066400000000000000000000036671436373107600250360ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QCONTACTUNDELETE_IMPL_H #define QCONTACTUNDELETE_IMPL_H #include "qcontactundelete.h" #include "qtcontacts-extensions.h" QTCONTACTS_USE_NAMESPACE const QContactDetail::DetailType QContactUndelete::Type(static_cast(QContactDetail__TypeUndelete)); #endif qtcontacts-sqlite-0.3.19/src/extensions/qtcontacts-extensions.h000066400000000000000000000150371436373107600250250ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2014 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTS_EXTENSIONS_H #define QTCONTACTS_EXTENSIONS_H #include #include #include #include #include #include #include #include #include // Defines the extended values supported by qtcontacts-sqlite QT_BEGIN_NAMESPACE_CONTACTS // In QContactDetail, we support some extra fields static const int QContactDetail__FieldModifiable = (QContactDetail::FieldLinkedDetailUris+2); static const int QContactDetail__FieldNonexportable = (QContactDetail::FieldLinkedDetailUris+3); static const int QContactDetail__FieldChangeFlags = (QContactDetail::FieldLinkedDetailUris+4); static const int QContactDetail__FieldUnhandledChangeFlags = (QContactDetail::FieldLinkedDetailUris+5); static const int QContactDetail__FieldDatabaseId = (QContactDetail::FieldLinkedDetailUris+6); static const int QContactDetail__FieldCreated = (QContactDetail::FieldLinkedDetailUris+7); static const int QContactDetail__FieldModified = (QContactDetail::FieldLinkedDetailUris+8); // The following change types can be reported for a detail when fetched via the synchronization plugin fetch API. static const int QContactDetail__ChangeFlag_IsAdded = 1 << 0; static const int QContactDetail__ChangeFlag_IsModified = 1 << 1; static const int QContactDetail__ChangeFlag_IsDeleted = 1 << 2; // In QContactDisplayLabel, we support the labelGroup property static const int QContactDisplayLabel__FieldLabelGroup = (QContactDisplayLabel::FieldLabel+1); static const int QContactDisplayLabel__FieldLabelGroupSortOrder = (QContactDisplayLabel::FieldLabel+2); // In QContactOnlineAccount we support the following properties: // AccountPath - identifying path value for the account // AccountIconPath - path to an icon indicating the service type of the account // Enabled - a boolean indicating whether or not the account is enabled for activity static const int QContactOnlineAccount__FieldAccountPath = (QContactOnlineAccount::FieldSubTypes+1); static const int QContactOnlineAccount__FieldAccountIconPath = (QContactOnlineAccount::FieldSubTypes+2); static const int QContactOnlineAccount__FieldEnabled = (QContactOnlineAccount::FieldSubTypes+3); static const int QContactOnlineAccount__FieldAccountDisplayName = (QContactOnlineAccount::FieldSubTypes+4); static const int QContactOnlineAccount__FieldServiceProviderDisplayName = (QContactOnlineAccount::FieldSubTypes+5); // We support the QContactOriginMetadata detail type static const QContactDetail::DetailType QContactDetail__TypeOriginMetadata = static_cast(QContactDetail::TypeVersion + 1); // We support the QContactStatusFlags detail type static const QContactDetail::DetailType QContactDetail__TypeStatusFlags = static_cast(QContactDetail::TypeVersion + 2); // We support the QContactDeactivated detail type static const QContactDetail::DetailType QContactDetail__TypeDeactivated = static_cast(QContactDetail::TypeVersion + 3); // We support the QContactUndelete detail type static const QContactDetail::DetailType QContactDetail__TypeUndelete = static_cast(QContactDetail::TypeVersion + 4); static const QString COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE = QString::fromLatin1("Aggregable"); static const QString COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME = QString::fromLatin1("ApplicationName"); static const QString COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID = QString::fromLatin1("AccountId"); static const QString COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH = QString::fromLatin1("RemotePath"); static const QString COLLECTION_EXTENDEDMETADATA_KEY_READONLY = QString::fromLatin1("ReadOnly"); QT_END_NAMESPACE_CONTACTS namespace QtContactsSqliteExtensions { QTCONTACTS_USE_NAMESPACE QContactCollectionId aggregateCollectionId(const QString &managerUri); QContactCollectionId localCollectionId(const QString &managerUri); QContactId apiContactId(quint32, const QString &managerUri); quint32 internalContactId(const QContactId &); enum NormalizePhoneNumberFlag { KeepPhoneNumberPunctuation = (1 << 0), KeepPhoneNumberDialString = (1 << 1), ValidatePhoneNumber = (1 << 2) }; Q_DECLARE_FLAGS(NormalizePhoneNumberFlags, NormalizePhoneNumberFlag) enum { DefaultMaximumPhoneNumberCharacters = 8 }; QString normalizePhoneNumber(const QString &input, NormalizePhoneNumberFlags flags); QString minimizePhoneNumber(const QString &input, int maxCharacters = DefaultMaximumPhoneNumberCharacters); class ContactManagerEngine; ContactManagerEngine *contactManagerEngine(QContactManager &manager); } Q_DECLARE_OPERATORS_FOR_FLAGS(QtContactsSqliteExtensions::NormalizePhoneNumberFlags) /* We define the name of the QCoreApplication property which holds our ContactsEngine */ #define CONTACT_MANAGER_ENGINE_PROP "qc_sqlite_extension_engine" #endif qtcontacts-sqlite-0.3.19/src/extensions/qtcontacts-extensions_impl.h000066400000000000000000000210201436373107600260330ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2014 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTS_EXTENSIONS_IMPL_H #define QTCONTACTS_EXTENSIONS_IMPL_H #include #include namespace { QString normalize(const QString &input, int flags, int maxCharacters) { // Allow '[' and ']' even though RFC3966 doesn't. // Also, even though RFC3966 explicitly disallows dialstring characters // such as DTMF pause etc, support those as per RFC2806 in order // to enable ITU-T V.250 style dialstring sequences. // Finally, convert ',' and ';' to 'p' and 'w' respectively for // consistency with defacto industry standards. static const QString allowedSeparators(QString::fromLatin1(" .-()[]")); static const QString dtmfChars(QString::fromLatin1("pPwWxX,;#*")); static const QString sipScheme(QString::fromLatin1("sips:")); static const QString hashControl(QString::fromLatin1("#31#")); static const QString starControl(QString::fromLatin1("*31#")); static const QChar plus(QChar::fromLatin1('+')); static const QChar colon(QChar::fromLatin1(':')); static const QChar at(QChar::fromLatin1('@')); // If this is a SIP URI (empty scheme means 'sips'), validate the identifier part QString number(input); if (number.startsWith(sipScheme) || number.startsWith(colon)) { int colonIndex = number.indexOf(colon); int atIndex = number.indexOf(at, colonIndex + 1); if (atIndex != -1) { number = number.mid(colonIndex + 1, (atIndex - colonIndex - 1)); } } QString subset; subset.reserve(number.length()); QChar initialChar; bool numericComponent = false; int firstDtmfIndex = -1; QString::const_iterator it = number.constBegin(), end = number.constEnd(); for ( ; it != end; ++it) { if ((*it).isDigit()) { // Convert to ASCII, capturing unicode digit values const QChar digit(QChar::fromLatin1('0' + (*it).digitValue())); subset.append(digit); numericComponent = true; if (initialChar.isNull()) { initialChar = digit; } } else if (*it == plus) { if (initialChar.isNull()) { // This is the start of the diallable number subset.append(*it); initialChar = *it; } else if (firstDtmfIndex != -1) { // Allow inside the DMTF section subset.append(*it); } else if (flags & QtContactsSqliteExtensions::ValidatePhoneNumber) { // Not valid in this location return QString(); } } else if (allowedSeparators.contains(*it)) { if (flags & QtContactsSqliteExtensions::KeepPhoneNumberPunctuation) { subset.append(*it); } } else if (dtmfChars.contains(*it)) { if ((*it).isLetter() && !numericComponent) { // Alphabetic DTMF chars can only occur after some numeric component if (flags & QtContactsSqliteExtensions::ValidatePhoneNumber) { return QString(); } else { // Skip this character, not valid in this position } } else { if ((flags & QtContactsSqliteExtensions::KeepPhoneNumberDialString) == 0) { // No need to continue accumulating if (flags & QtContactsSqliteExtensions::ValidatePhoneNumber) { // Ensure the remaining characters are permissible while (++it != end) { if ((!(*it).isDigit()) && !allowedSeparators.contains(*it) && !dtmfChars.contains(*it)) { // Invalid character return QString(); } } } break; } // Otherwise, continue with processing if (firstDtmfIndex == -1) { firstDtmfIndex = subset.length(); } if ((*it).toLower() == QChar::fromLatin1('x')) { // Accept 'x' and 'X', but convert them to 'p' in the normalized form subset.append(QChar::fromLatin1('p')); } else if (*it == QChar::fromLatin1(',')) { // Accept ',' but convert to 'p' in the normalized form subset.append(QChar::fromLatin1('p')); } else if (*it == QChar::fromLatin1(';')) { // Accept ';' but convert to 'w' in the normalized form subset.append(QChar::fromLatin1('w')); } else { subset.append(*it); } } } else if (flags & QtContactsSqliteExtensions::ValidatePhoneNumber) { // Invalid character return QString(); } } if ((flags & QtContactsSqliteExtensions::ValidatePhoneNumber) && (initialChar == plus) && (firstDtmfIndex != -1)) { // If this number starts with '+', it mustn't contain control codes if ((subset.indexOf(hashControl, firstDtmfIndex) != -1) || (subset.indexOf(starControl, firstDtmfIndex) != -1)) { return QString(); } } if (maxCharacters != -1) { int characters = 0; int index = (firstDtmfIndex == -1) ? (subset.length() - 1) : (firstDtmfIndex - 1); for ( ; index > 0; --index) { const QChar &c(subset.at(index)); if (c.isDigit() || c == plus) { if (++characters == maxCharacters) { // Only include the digits from here subset = subset.mid(index); break; } } } } return subset.trimmed(); } } namespace QtContactsSqliteExtensions { QContactCollectionId aggregateCollectionId(const QString &managerUri) { return QContactCollectionId(managerUri, QByteArrayLiteral("col-") + QByteArray::number(1)); } QContactCollectionId localCollectionId(const QString &managerUri) { return QContactCollectionId(managerUri, QByteArrayLiteral("col-") + QByteArray::number(2)); } QContactId apiContactId(quint32 iid, const QString &managerUri) { return QContactId(managerUri, QByteArrayLiteral("sql-") + QByteArray::number(iid)); } quint32 internalContactId(const QContactId &id) { const QByteArray localId = id.localId(); if (localId.startsWith(QByteArrayLiteral("sql-"))) { return localId.mid(4).toUInt(); } return 0; } QString normalizePhoneNumber(const QString &input, NormalizePhoneNumberFlags flags) { return normalize(input, flags, -1); } QString minimizePhoneNumber(const QString &input, int maxCharacters) { // Minimal form number should preserve DTMF dial string to differentiate PABX extensions return normalize(input, KeepPhoneNumberDialString, maxCharacters); } } #endif qtcontacts-sqlite-0.3.19/src/extensions/qtcontacts-extensions_manager_impl.h000066400000000000000000000050351436373107600275350ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef QTCONTACTS_EXTENSIONS_MANAGER_IMPL_H #define QTCONTACTS_EXTENSIONS_MANAGER_IMPL_H #include #include #include #include namespace QtContactsSqliteExtensions { ContactManagerEngine *contactManagerEngine(QContactManager &manager) { QCoreApplication *app = QCoreApplication::instance(); QList engines = app->property(CONTACT_MANAGER_ENGINE_PROP).toList(); for (QVariant &v : engines) { QContactManagerEngine *engine = static_cast(v.value()); if (engine && engine->managerName() == manager.managerName()) { return static_cast(engine); } } //ContactManagerEngine *cme = dynamic_cast(QContactManagerData::managerData(mgr)->m_engine); return 0; } } #endif qtcontacts-sqlite-0.3.19/src/extensions/twowaycontactsyncadaptor.h000066400000000000000000000172251436373107600256240ustar00rootroot00000000000000/* * Copyright (C) 2014 - 2017 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef TWOWAYCONTACTSYNCADAPTOR_H #define TWOWAYCONTACTSYNCADAPTOR_H #include #include #include #include #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE namespace QtContactsSqliteExtensions { class TwoWayContactSyncAdaptorPrivate; class TwoWayContactSyncAdaptor { public: enum ErrorHandlingMode { ExitUponError, ContinueAfterError }; TwoWayContactSyncAdaptor(int accountId = 0, const QString &applicationName = QString()); TwoWayContactSyncAdaptor(int accountId, const QString &applicationName, const QMap ¶ms); TwoWayContactSyncAdaptor(int accountId, const QString &applicationName, QContactManager &mananger); virtual ~TwoWayContactSyncAdaptor(); void setManager(QContactManager &manager); // step two: start complete sync cycle // - determine collection metadata changes made on remote server // - determine collection metadata changes made on local device // - for each locally-existent collection (which was not deleted remotely), // trigger per-collection sync cycle. virtual bool startSync(ErrorHandlingMode mode = ExitUponError); virtual bool determineRemoteCollections(); virtual void remoteCollectionsDetermined( // if the plugin doesn't support retrieving remote deltas const QList &remoteCollections); virtual bool determineRemoteCollectionChanges( const QList &locallyAddedCollections, const QList &locallyModifiedCollections, const QList &locallyRemovedCollections, const QList &locallyUnmodifiedCollections, QContactManager::Error *error); virtual void remoteCollectionChangesDetermined( const QList &remotelyAddedCollections, const QList &remotelyModifiedCollections, const QList &remotelyRemovedCollections, const QList &remotelyUnmodifiedCollections); // step three: delete remotely-deleted collections from local database virtual bool storeRemoteCollectionDeletionsLocally(const QList &collectionIds); // step four: perform per-collection sync cycle // - if the collection was deleted locally, push the deletion to the server // - if the collection was added locally, push it (and its contents) to the server // - otherwise (modified or unmodified): // - determine per-collection contact changes made on local device // - determine per-collection contact changes made on remote server // - calculate "final result" by performing conflict resolution etc to the two change sets // - push local collection metadata changes to remote server // - push "final result" contact data to remote server // - save "final result" contact data + collection metadata changes (incl ctag) to local. virtual void startCollectionSync(const QContactCollection &collection, int changeFlag = 0); // hrm, need the following cases: added locally, modified locally, deleted locally, added remotely. virtual bool deleteRemoteCollection(const QContactCollection &collection); virtual void remoteCollectionDeleted(const QContactCollection &collection); virtual bool determineRemoteContacts(const QContactCollection &collection); virtual void remoteContactsDetermined( // if the plugin doesn't support retrieving remote deltas const QContactCollection &collection, const QList &contacts); virtual bool determineRemoteContactChanges( const QContactCollection &collection, const QList &localAddedContacts, const QList &localModifiedContacts, const QList &localDeletedContacts, const QList &localUnmodifiedContacts, QContactManager::Error *error); virtual void remoteContactChangesDetermined( const QContactCollection &collection, const QList &remotelyAddedContacts, const QList &remotelyModifiedContacts, const QList &remotelyRemovedContacts); virtual bool storeLocalChangesRemotely( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts); virtual void localChangesStoredRemotely( // params will include updated ctag/etags etc. const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts); virtual void storeRemoteChangesLocally( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts); // if the account is deleted, the sync plugin needs to purge all related collections. bool removeAllCollections(); const QContactManager &contactManager() const; QContactManager &contactManager(); protected: friend class TwoWayContactSyncAdaptorPrivate; void performNextQueuedOperation(); struct IgnorableDetailsAndFields { QSet detailTypes; QHash > detailFields; QSet commonFields; }; virtual IgnorableDetailsAndFields ignorableDetailsAndFields() const; virtual QContact resolveConflictingChanges(const QContact &local, const QContact &remote, bool *identical); virtual void syncFinishedSuccessfully(); virtual void syncFinishedWithError(); virtual void syncOperationError(); TwoWayContactSyncAdaptorPrivate *d; }; } // namespace QtContactsSqliteExtensions #endif // TWOWAYCONTACTSYNCADAPTOR_H qtcontacts-sqlite-0.3.19/src/extensions/twowaycontactsyncadaptor_impl.h000066400000000000000000001734751436373107600266570ustar00rootroot00000000000000/* * Copyright (C) 2014 - 2017 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef TWOWAYCONTACTSYNCADAPTOR_IMPL_H #define TWOWAYCONTACTSYNCADAPTOR_IMPL_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define QTCONTACTS_SQLITE_TWCSA_DEBUG_LOG(msg) \ do { \ if (Q_UNLIKELY(qtcontacts_sqlite_twcsa_debug_trace_enabled())) { \ qDebug() << msg; \ } \ } while (0) namespace QtContactsSqliteExtensions { static bool qtcontacts_sqlite_twcsa_debug_trace_enabled() { static const bool traceEnabled(!QString(QLatin1String(qgetenv("QTCONTACTS_SQLITE_TWCSA_TRACE"))).isEmpty()); return traceEnabled; } // QDataStream encoding version to use in OOB storage. // Don't change this without scheduling a migration for stored data! // (which can be done in contactsdatabase.cpp) const QDataStream::Version STREAM_VERSION = QDataStream::Qt_5_1; class TwoWayContactSyncAdaptorPrivate { public: TwoWayContactSyncAdaptorPrivate( TwoWayContactSyncAdaptor *q, int accountId, const QString &applicationName); TwoWayContactSyncAdaptorPrivate( TwoWayContactSyncAdaptor *q, int accountId, const QString &applicationName, const QMap ¶ms); TwoWayContactSyncAdaptorPrivate( TwoWayContactSyncAdaptor *q, int accountId, const QString &applicationName, QContactManager &manager); ~TwoWayContactSyncAdaptorPrivate(); struct CollectionChanges { QList addedCollections; QList modifiedCollections; QList removedCollections; QList unmodifiedCollections; }; struct ContactChanges { QList addedContacts; QList modifiedContacts; QList removedContacts; QList unmodifiedContacts; }; enum CollectionSyncOperationType { Unmodified = 0, LocalAddition, LocalModification, LocalDeletion, RemoteAddition, RemoteModification }; struct CollectionSyncOperation { QContactCollection collection; CollectionSyncOperationType operationType; }; CollectionChanges m_collectionChanges; QHash m_localContactChanges; QHash m_remoteContactChanges; QList m_syncOperations; TwoWayContactSyncAdaptor *m_q = nullptr; QContactManager *m_manager = nullptr; ContactManagerEngine *m_engine = nullptr; QString m_oobScope; QString m_applicationName; int m_accountId = 0; bool m_deleteManager = false; bool m_busy = false; bool m_errorOccurred = false; bool m_continueAfterError = false; }; } QTCONTACTS_USE_NAMESPACE using namespace QtContactsSqliteExtensions; /*! * \class TwoWayContactSyncAdaptor * \brief TwoWayContactSyncAdaptor provides an interface which * contact sync plugins can implement in order to correctly * synchronize contact data between a remote datastore and * the local device contacts database. * * A contact sync plugin which implements this interface must * provide implementations for at least the following methods: * \list * \li determineRemoteCollections() * \li determineRemoteContacts() * \li deleteRemoteCollection() * \li storeLocalChangesRemotely() * \li syncFinishedSuccessfully() * \li syncFinishedWithError() * \endlist * * If the contact sync plugin is able to determine precisely what * has changed in the remote datastore since the last sync (e.g. * via ctag or syncToken which can be stored as metadata in the * collection), then it can also implement the following methods: * \list * \li determineRemoteCollectionChanges() * \li determineRemoteContactChanges() * \endlist * * Finally, the plugin can define its own conflict resolution * semantics by implementing: * \list * \li resolveConflictingChanges() * \endlist * * Note that this interface is provided merely as a convenience; * a contact sync plugin which doesn't wish to utilize this * interface may instead use the sync transaction API offered * by the ContactManagerEngine directly. */ Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(QList) namespace { void registerTypes() { static bool registered = false; if (!registered) { registered = true; qRegisterMetaType >(); qRegisterMetaTypeStreamOperators >(); qRegisterMetaType >(); qRegisterMetaType >(); } } // Input must be UTC QString dateTimeString(const QDateTime &qdt) { return QLocale::c().toString(qdt, QStringLiteral("yyyy-MM-ddThh:mm:ss.zzz")); } QDateTime fromDateTimeString(const QString &s) { QDateTime rv(QLocale::c().toDateTime(s, QStringLiteral("yyyy-MM-ddThh:mm:ss.zzz"))); rv.setTimeSpec(Qt::UTC); return rv; } QMap checkParams(const QMap ¶ms) { QMap rv(params); const QString presenceKey(QStringLiteral("mergePresenceChanges")); if (!rv.contains(presenceKey)) { // Don't report presence changes rv.insert(presenceKey, QStringLiteral("false")); } return rv; } void modifyContactDetail(const QContactDetail &original, const QContactDetail &modified, QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy conflictPolicy, QContactDetail *recipient) { // Apply changes field-by-field DetailMap originalValues(detailValues(original, false)); DetailMap modifiedValues(detailValues(modified, false)); DetailMap::const_iterator mit = modifiedValues.constBegin(), mend = modifiedValues.constEnd(); for ( ; mit != mend; ++mit) { const int field(mit.key()); const QVariant originalValue(originalValues[field]); originalValues.remove(field); const QVariant currentValue(recipient->value(field)); if (!variantEqual(currentValue, originalValue)) { // The local value has changed since this data was exported if (conflictPolicy == QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges) { // Ignore this remote change continue; } } // Update the result value recipient->setValue(field, mit.value()); } DetailMap::const_iterator oit = originalValues.constBegin(), oend = originalValues.constEnd(); for ( ; oit != oend; ++oit) { // Any previously existing values that are no longer present should be removed const int field(oit.key()); const QVariant originalValue(oit.value()); const QVariant currentValue(recipient->value(field)); if (!variantEqual(currentValue, originalValue)) { // The local value has changed since this data was exported if (conflictPolicy == QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges) { // Ignore this remote removal continue; } } recipient->removeValue(field); } // set the modifiable flag to true unless the sync adapter has set it explicitly if (!recipient->values().contains(QContactDetail__FieldModifiable)) { recipient->setValue(QContactDetail__FieldModifiable, true); } } void removeEquivalentDetails(QList &original, QList &updated, QList &equivalent) { // Determine which details are in the update contact which aren't in the database contact: // Detail order is not defined, so loop over the entire set for each, removing matches or // superset details (eg, backend added a field (like lastModified to timestamp) on previous save) QList::iterator oit = original.begin(), oend = original.end(); while (oit != oend) { QList::iterator uit = updated.begin(), uend = updated.end(); while (uit != uend) { if (detailsEquivalent(*oit, *uit)) { // These details match - remove from the lists uit = updated.erase(uit); break; } ++uit; } if (uit != uend) { // We found a match oit = original.erase(oit); } else { ++oit; } } } } TwoWayContactSyncAdaptorPrivate::TwoWayContactSyncAdaptorPrivate( TwoWayContactSyncAdaptor *q, int accountId, const QString &applicationName) : m_q(q) , m_applicationName(applicationName) , m_accountId(accountId) , m_deleteManager(false) { registerTypes(); } TwoWayContactSyncAdaptorPrivate::TwoWayContactSyncAdaptorPrivate( TwoWayContactSyncAdaptor *q, int accountId, const QString &applicationName, const QMap ¶ms) : m_q(q) , m_manager(new QContactManager(QStringLiteral("org.nemomobile.contacts.sqlite"), checkParams(params))) , m_engine(contactManagerEngine(*m_manager)) , m_applicationName(applicationName) , m_accountId(accountId) , m_deleteManager(true) { registerTypes(); } TwoWayContactSyncAdaptorPrivate::TwoWayContactSyncAdaptorPrivate( TwoWayContactSyncAdaptor *q, int accountId, const QString &applicationName, QContactManager &manager) : m_q(q) , m_manager(&manager) , m_engine(contactManagerEngine(*m_manager)) , m_applicationName(applicationName) , m_accountId(accountId) , m_deleteManager(false) { registerTypes(); } TwoWayContactSyncAdaptorPrivate::~TwoWayContactSyncAdaptorPrivate() { if (m_deleteManager) { delete m_manager; } } TwoWayContactSyncAdaptor::TwoWayContactSyncAdaptor( int accountId, const QString &applicationName) : d(new TwoWayContactSyncAdaptorPrivate(this, accountId, applicationName)) { } TwoWayContactSyncAdaptor::TwoWayContactSyncAdaptor( int accountId, const QString &applicationName, const QMap ¶ms) : d(new TwoWayContactSyncAdaptorPrivate(this, accountId, applicationName, params)) { } TwoWayContactSyncAdaptor::TwoWayContactSyncAdaptor( int accountId, const QString &applicationName, QContactManager &manager) : d(new TwoWayContactSyncAdaptorPrivate(this, accountId, applicationName, manager)) { } TwoWayContactSyncAdaptor::~TwoWayContactSyncAdaptor() { delete d; } void TwoWayContactSyncAdaptor::setManager(QContactManager &manager) { d->m_manager = &manager; d->m_engine = contactManagerEngine(manager); d->m_deleteManager = false; } bool TwoWayContactSyncAdaptor::startSync(ErrorHandlingMode mode) { if (!d) { qWarning() << "Sync adaptor not initialised!"; return false; } if (!d->m_engine) { qWarning() << "Sync adaptor manager not set!"; return false; } if (d->m_busy) { qWarning() << "Sync adaptor for application: " << d->m_applicationName << " for account: " << d->m_accountId << " is already busy!"; return false; } QTCONTACTS_SQLITE_TWCSA_DEBUG_LOG( QStringLiteral("Starting contacts sync by application: %1 for account: %2") .arg(d->m_applicationName).arg(d->m_accountId).toUtf8()); d->m_busy = true; d->m_continueAfterError = mode == TwoWayContactSyncAdaptor::ContinueAfterError; QContactManager::Error err = QContactManager::NoError; if (!d->m_engine->fetchCollectionChanges( d->m_accountId, d->m_applicationName, &d->m_collectionChanges.addedCollections, &d->m_collectionChanges.modifiedCollections, &d->m_collectionChanges.removedCollections, &d->m_collectionChanges.unmodifiedCollections, &err)) { qWarning() << "Unable to fetch collection changes for application: " << d->m_applicationName << " for account: " << d->m_accountId << " - " << err; d->m_busy = false; syncFinishedWithError(); return false; } if (!determineRemoteCollectionChanges( d->m_collectionChanges.addedCollections, d->m_collectionChanges.modifiedCollections, d->m_collectionChanges.removedCollections, d->m_collectionChanges.unmodifiedCollections, &err)) { if (err != QContactManager::NotSupportedError) { qWarning() << "Unable to determine remote collection changes for application: " << d->m_applicationName << " for account: " << d->m_accountId << " - " << err; d->m_busy = false; syncFinishedWithError(); return false; } else if (!determineRemoteCollections()) { qWarning() << "Unable to determine remote collections for application: " << d->m_applicationName << " for account: " << d->m_accountId << " - " << err; d->m_busy = false; syncFinishedWithError(); return false; } } return true; } bool TwoWayContactSyncAdaptor::determineRemoteCollectionChanges( const QList &locallyAddedCollections, const QList &locallyModifiedCollections, const QList &locallyRemovedCollections, const QList &locallyUnmodifiedCollections, QContactManager::Error *error) { // By default, we assume that the plugin is unable to determine // a precise delta of what collection metadata has changed on // the remote server. // If this assumption is incorrect, the plugin should override // this method to perform the appropriate requests and then // invoke remoteCollectionChangesDetermined() once complete. *error = QContactManager::NotSupportedError; return false; } bool TwoWayContactSyncAdaptor::determineRemoteCollections() { // The plugin must implement this method to retrieve // information about addressbooks on the remote server, // and then invoke remoteCollectionsDetermined() once complete // (or syncOperationError() if an error occurred). qWarning() << "TWCSA::determineRemoteCollections(): implementation missing"; return false; } // match by id and then by remote path static QContactCollectionId findMatchingCollection(const QContactCollection &remoteCollection, const QList &localCollections) { for (const QContactCollection &localCollection : localCollections) { if (!remoteCollection.id().isNull() && remoteCollection.id() == localCollection.id()) { return localCollection.id(); } else if (remoteCollection.id().isNull() && !remoteCollection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString().isEmpty() && remoteCollection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString() == localCollection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString()) { return localCollection.id(); } } return QContactCollectionId(); } static QContactCollection remoteCollectionWithId(const QContactCollection &collection, const QContactCollectionId &id) { QContactCollection ret(collection); if (collection.id().isNull()) { ret.setId(id); } return ret; } void TwoWayContactSyncAdaptor::remoteCollectionsDetermined( const QList &remoteCollections) { // we determine the remote collection delta by inspection, // comparing them to the local collections fetched earlier. QList remotelyAddedCollections; QList remotelyModifiedCollections; QList remotelyRemovedCollections; QList remotelyUnmodifiedCollections; QSet seenLocalCollections; for (const QContactCollection &remoteCollection : remoteCollections) { // attempt to find matching local collection. // if we find a matching local collection which was modified, // we assume that it was unmodified on the remote side. // otherwise, we assume that it was modified on the remote side. // client plugins can override this method if they // a way to precisely determine change ordering / resolution, // although in that case they're probably better off just // implementing determineRemoteCollectionChanges() directly. bool foundMatch = false; // could be one of the added collections, if the previous sync cycle // was aborted after the local collection addition was pushed to the server. QList::iterator ait = d->m_collectionChanges.addedCollections.begin(), aend = d->m_collectionChanges.addedCollections.end(); while (ait != aend && !foundMatch) { if ((!remoteCollection.id().isNull() && remoteCollection.id() == ait->id()) || (remoteCollection.id().isNull() && !remoteCollection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString().isEmpty() && remoteCollection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString() == ait->extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString())) { foundMatch = true; seenLocalCollections.insert(ait->id()); remotelyUnmodifiedCollections.append(remoteCollectionWithId(remoteCollection, ait->id())); d->m_collectionChanges.modifiedCollections.append(*ait); ait = d->m_collectionChanges.addedCollections.erase(ait); } else { ++ait; } } // also check locally modified collections for matches. if (!foundMatch) { const QContactCollectionId matchingLocalModifiedCollectionId = findMatchingCollection( remoteCollection, d->m_collectionChanges.modifiedCollections); if (!matchingLocalModifiedCollectionId.isNull()) { foundMatch = true; remotelyUnmodifiedCollections.append(remoteCollectionWithId(remoteCollection, matchingLocalModifiedCollectionId)); seenLocalCollections.insert(matchingLocalModifiedCollectionId); } } // also check locally removed collections for matches. if (!foundMatch) { const QContactCollectionId matchingLocalRemovedCollectionId = findMatchingCollection( remoteCollection, d->m_collectionChanges.removedCollections); if (!matchingLocalRemovedCollectionId.isNull()) { foundMatch = true; remotelyUnmodifiedCollections.append(remoteCollectionWithId(remoteCollection, matchingLocalRemovedCollectionId)); seenLocalCollections.insert(matchingLocalRemovedCollectionId); } } // finally, check locally unmodified collections for matches. if (!foundMatch) { const QContactCollectionId matchingLocalUnmodifiedCollectionId = findMatchingCollection( remoteCollection, d->m_collectionChanges.unmodifiedCollections); if (!matchingLocalUnmodifiedCollectionId.isNull()) { foundMatch = true; remotelyModifiedCollections.append(remoteCollectionWithId(remoteCollection, matchingLocalUnmodifiedCollectionId)); seenLocalCollections.insert(matchingLocalUnmodifiedCollectionId); } } // no matching local collection was found, must have been a remote addition. if (!foundMatch) { if (remoteCollection.id().isNull()) { remotelyAddedCollections.append(remoteCollection); } else { qWarning() << "Error: manual delta detection found remote collection addition, but collection already has id:" << QString::fromLatin1(remoteCollection.id().localId()) << " : " << remoteCollection.metaData(QContactCollection::KeyName).toString(); } } } // now determine remotely removed collections. for (const QContactCollection &col : d->m_collectionChanges.modifiedCollections) { if (!seenLocalCollections.contains(col.id())) { remotelyRemovedCollections.append(col); } } for (const QContactCollection &col : d->m_collectionChanges.unmodifiedCollections) { if (!seenLocalCollections.contains(col.id())) { remotelyRemovedCollections.append(col); } } remoteCollectionChangesDetermined( remotelyAddedCollections, remotelyModifiedCollections, remotelyRemovedCollections, remotelyUnmodifiedCollections); } void TwoWayContactSyncAdaptor::remoteCollectionChangesDetermined( const QList &remotelyAddedCollections, const QList &remotelyModifiedCollections, const QList &remotelyRemovedCollections, const QList &remotelyUnmodifiedCollections) { QSet handledCollectionIds; // Construct a queue of sync operations to be completed one-at-a-time. // Note that the order in which we handle each of these change-sets // is important (as e.g. a collection which is remotely modified // may also appear as an unmodified local collection, and thus we // need to ensure we enqueue only one operation for the collection). for (const QContactCollection &col : remotelyRemovedCollections) { // we will remove these directly from local storage, at the end. // mark them as handled, so that we don't attempt to sync them. handledCollectionIds.insert(col.id()); } for (const QContactCollection &col : d->m_collectionChanges.removedCollections) { if (!handledCollectionIds.contains(col.id())) { handledCollectionIds.insert(col.id()); d->m_syncOperations.append({col, TwoWayContactSyncAdaptorPrivate::LocalDeletion}); } } for (const QContactCollection &col : remotelyModifiedCollections) { if (!handledCollectionIds.contains(col.id())) { handledCollectionIds.insert(col.id()); d->m_syncOperations.append({col, TwoWayContactSyncAdaptorPrivate::RemoteModification}); } } for (const QContactCollection &col : d->m_collectionChanges.modifiedCollections) { if (!handledCollectionIds.contains(col.id())) { handledCollectionIds.insert(col.id()); d->m_syncOperations.append({col, TwoWayContactSyncAdaptorPrivate::LocalModification}); } } for (const QContactCollection &col : d->m_collectionChanges.unmodifiedCollections) { if (!handledCollectionIds.contains(col.id())) { handledCollectionIds.insert(col.id()); d->m_syncOperations.append({col, TwoWayContactSyncAdaptorPrivate::Unmodified}); } } for (const QContactCollection &col : d->m_collectionChanges.addedCollections) { if (!handledCollectionIds.contains(col.id())) { handledCollectionIds.insert(col.id()); d->m_syncOperations.append({col, TwoWayContactSyncAdaptorPrivate::LocalAddition}); } } for (const QContactCollection &col : remotelyUnmodifiedCollections) { if (!handledCollectionIds.contains(col.id())) { handledCollectionIds.insert(col.id()); d->m_syncOperations.append({col, TwoWayContactSyncAdaptorPrivate::Unmodified}); } } for (const QContactCollection &col : remotelyAddedCollections) { d->m_syncOperations.append({col, TwoWayContactSyncAdaptorPrivate::RemoteAddition}); } QList remotelyRemovedCollectionIds; for (const QContactCollection &col : remotelyRemovedCollections) { remotelyRemovedCollectionIds.append(col.id()); } if (remotelyRemovedCollectionIds.size() && !storeRemoteCollectionDeletionsLocally(remotelyRemovedCollectionIds)) { qWarning() << "Failed to store remote deletion of collections to local database!"; syncOperationError(); } else { performNextQueuedOperation(); } } bool TwoWayContactSyncAdaptor::storeRemoteCollectionDeletionsLocally( const QList &collectionIds) { QContactManager::Error err = QContactManager::NoError; return d->m_engine->storeChanges(nullptr, nullptr, collectionIds, ContactManagerEngine::PreserveLocalChanges, true, &err); } void TwoWayContactSyncAdaptor::performNextQueuedOperation() { if (d->m_syncOperations.isEmpty()) { d->m_busy = false; if (d->m_errorOccurred) { syncFinishedWithError(); } else { syncFinishedSuccessfully(); } } else { TwoWayContactSyncAdaptorPrivate::CollectionSyncOperation op = d->m_syncOperations.takeFirst(); startCollectionSync(op.collection, op.operationType); } } void TwoWayContactSyncAdaptor::startCollectionSync(const QContactCollection &collection, int changeFlag) { TwoWayContactSyncAdaptorPrivate::CollectionSyncOperationType opType = static_cast(changeFlag); QTCONTACTS_SQLITE_TWCSA_DEBUG_LOG( QStringLiteral("Performing sync operation %1 on contacts collection %2 with application: %3 for account: %4") .arg(opType) .arg(collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString().isEmpty() ? QString::fromLatin1(collection.id().localId()) : collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString()) .arg(d->m_applicationName) .arg(d->m_accountId).toUtf8()); if (opType == TwoWayContactSyncAdaptorPrivate::LocalDeletion) { if (!deleteRemoteCollection(collection)) { qWarning() << "Failed to push the local deletion of the collection " << QString::fromLatin1(collection.id().localId()) << " for application " << d->m_applicationName << " for account " << d->m_accountId; syncOperationError(); } return; } if (opType == TwoWayContactSyncAdaptorPrivate::LocalAddition) { // no need to determine remote changes for a collection which doesn't exist remotely yet. // instead, just determine the local contacts and store them remotely. QList addedContacts; QContactManager::Error err = QContactManager::NoError; if (!d->m_engine->fetchContactChanges(collection.id(), &addedContacts, nullptr, nullptr, nullptr, &err)) { qWarning() << "Failed to fetch contacts for locally added collection " << QString::fromLatin1(collection.id().localId()) << " for application " << d->m_applicationName << " for account " << d->m_accountId; syncOperationError(); } else if (!storeLocalChangesRemotely(collection, addedContacts, QList(), QList())) { qWarning() << "Unable to store local changes remotely for locally added collection " << collection.metaData(QContactCollection::KeyName).toString() << "for application: " << d->m_applicationName << " for account: " << d->m_accountId; syncOperationError(); } return; } if (opType == TwoWayContactSyncAdaptorPrivate::RemoteAddition) { // no need to determine local changes for a collection which doesn't exist locally yet. // instead, just determine the remote contacts and then store them locally. if (!determineRemoteContacts(collection)) { qWarning() << "Unable to determine remote contacts for remotely added collection " << collection.metaData(QContactCollection::KeyName).toString() << "for application: " << d->m_applicationName << " for account: " << d->m_accountId; syncOperationError(); } return; } // otherwise, there are both local and remote contact changes to determine and apply. QList addedContacts, modifiedContacts, removedContacts, unmodifiedContacts; QContactManager::Error err = QContactManager::NoError; if (!d->m_engine->fetchContactChanges(collection.id(), &addedContacts, &modifiedContacts, &removedContacts, &unmodifiedContacts, &err)) { qWarning() << "Failed to fetch contacts for locally represented collection " << QString::fromLatin1(collection.id().localId()) << " for application " << d->m_applicationName << " for account " << d->m_accountId; syncOperationError(); return; } d->m_localContactChanges.insert(collection.id(), { addedContacts, modifiedContacts, removedContacts, unmodifiedContacts }); if (!determineRemoteContactChanges(collection, addedContacts, modifiedContacts, removedContacts, unmodifiedContacts, &err)) { if (err != QContactManager::NotSupportedError) { qWarning() << "Unable to determine remote changes for collection " << QString::fromLatin1(collection.id().localId()) << " for application: " << d->m_applicationName << " for account: " << d->m_accountId << " - " << err; syncOperationError(); } else if (!determineRemoteContacts(collection)) { qWarning() << "Unable to determine remote contacts for collection " << QString::fromLatin1(collection.id().localId()) << "for application: " << d->m_applicationName << " for account: " << d->m_accountId; syncOperationError(); } } } bool TwoWayContactSyncAdaptor::deleteRemoteCollection(const QContactCollection &collection) { // The plugin must implement this method to delete // a remote addressbook from the server, // and then invoke remoteCollectionDeleted() when complete // (or syncOperationError() if an error occurred). qWarning() << "TWCSA::deleteRemoteCollection(): implementation missing"; return false; } void TwoWayContactSyncAdaptor::remoteCollectionDeleted(const QContactCollection &collection) { QContactManager::Error error = QContactManager::NoError; if (!d->m_engine->clearChangeFlags(collection.id(), &error)) { qWarning() << "Failed to clear change flags for collection " << QString::fromLatin1(collection.id().localId()) << "for application: " << d->m_applicationName << " for account: " << d->m_accountId << " after pushing local deletion to remote."; syncOperationError(); } else { performNextQueuedOperation(); } } bool TwoWayContactSyncAdaptor::determineRemoteContacts(const QContactCollection &collection) { // The plugin must implement this method to retrieve // information about contacts in an addressbook on the remote server, // and call remoteContactsDetermined() once complete // (or syncOperationError() if an error occurred). qWarning() << "TWCSA::determineRemoteContacts(): implementation missing"; return false; } static QContact findMatchingContact(const QContact &contact, const QList &contacts) { for (const QContact &c : contacts) { if (!contact.id().isNull() && contact.id() == c.id()) { return c; } else if (contact.id().isNull()) { if (!contact.detail().guid().isEmpty() && contact.detail().guid() == c.detail().guid()) { return c; } else if (!contact.detail().syncTarget().isEmpty() && contact.detail().syncTarget() == c.detail().syncTarget()) { return c; } } } return QContact(); } static QContact contactWithId(const QContact &contact, const QContactId &id) { QContact ret = contact; if (contact.id().isNull()) { ret.setId(id); } return ret; } void TwoWayContactSyncAdaptor::remoteContactsDetermined( const QContactCollection &collection, const QList &contacts) { if (!d->m_localContactChanges.contains(collection.id())) { // must have been a remote collection addition. // every contact here will be considered an addition. remoteContactChangesDetermined(collection, contacts, QList(), QList()); return; } QList remoteAdditions; QList remoteModifications; QList remoteDeletions; QSet handledContactIds; TwoWayContactSyncAdaptorPrivate::ContactChanges &localChanges(d->m_localContactChanges[collection.id()]); for (const QContact &contact : contacts) { bool matchFound = false; // first, search local additions for a matching contact. // this can happen if an error occurred after the local additions were // successfully pushed to the server, during the previous sync cycle. QList::iterator ait = localChanges.addedContacts.begin(), aend = localChanges.addedContacts.end(); while (ait != aend && !matchFound) { if (!contact.id().isNull() && contact.id() == ait->id()) { matchFound = true; } else if (contact.id().isNull()) { if (!contact.detail().guid().isEmpty() && contact.detail().guid() == ait->detail().guid()) { matchFound = true; } else if (!contact.detail().syncTarget().isEmpty() && contact.detail().syncTarget() == ait->detail().syncTarget()) { matchFound = true; } } if (matchFound) { // treat the matching local addition as a remote modification instead. // TODO: perform per-detail delta detection, to determine precise changes. handledContactIds.insert(ait->id()); localChanges.modifiedContacts.append(*ait); remoteModifications.append(contactWithId(contact, ait->id())); ait = localChanges.addedContacts.erase(ait); } else { ++ait; } } if (!matchFound) { const QContact c = findMatchingContact(contact, localChanges.removedContacts); if (!c.id().isNull()) { // this contact will be deleted locally anyway. treat as remote unmodified. matchFound = true; handledContactIds.insert(c.id()); } } if (!matchFound) { const QContact c = findMatchingContact(contact, localChanges.modifiedContacts); if (!c.id().isNull()) { // assume that the remote contact is unmodified, so the local // change will be preserved. // TODO: perform per-detail delta detection, to determine precise changes. matchFound = true; handledContactIds.insert(c.id()); } } if (!matchFound) { const QContact c = findMatchingContact(contact, localChanges.unmodifiedContacts); if (!c.id().isNull()) { // assume that the remote contact is modified. // TODO: perform per-detail delta detection, to determine precise changes. matchFound = true; handledContactIds.insert(c.id()); remoteModifications.append(contactWithId(contact, c.id())); } } if (!matchFound) { if (contact.id().isNull()) { remoteAdditions.append(contact); } else { qWarning() << "Error: manual delta detection found remote contact addition, but contact already has id:" << QString::fromLatin1(contact.id().localId()); } } } // now check the local modified/unmodified contacts: // any which we haven't seen, must have been deleted remotely. QList::iterator it = localChanges.modifiedContacts.begin(), end = localChanges.modifiedContacts.end(); while (it != end) { if (!handledContactIds.contains(it->id())) { handledContactIds.insert(it->id()); remoteDeletions.append(*it); it = localChanges.modifiedContacts.erase(it); } else { ++it; } } it = localChanges.unmodifiedContacts.begin(); end = localChanges.unmodifiedContacts.end(); while (it != end) { if (!handledContactIds.contains(it->id())) { handledContactIds.insert(it->id()); remoteDeletions.append(*it); it = localChanges.unmodifiedContacts.erase(it); } else { ++it; } } remoteContactChangesDetermined( collection, remoteAdditions, remoteModifications, remoteDeletions); } bool TwoWayContactSyncAdaptor::determineRemoteContactChanges( const QContactCollection &collection, const QList &localAddedContacts, const QList &localModifiedContacts, const QList &localDeletedContacts, const QList &localUnmodifiedContacts, QContactManager::Error *error) { // By default, we assume that the plugin is unable to determine // a precise delta of what contacts have changed on // the remote server. // If this assumption is incorrect, the plugin should override // this method to perform the appropriate requests and then // invoke remoteContactChangesDetermined() once complete. *error = QContactManager::NotSupportedError; return false; } static void setContactChangeFlags(QContact &c, QContactStatusFlags::Flag flag) { QContactStatusFlags flags = c.detail(); if (flag == QContactStatusFlags::IsAdded) { flags.setFlag(QContactStatusFlags::IsAdded, true); flags.setFlag(QContactStatusFlags::IsModified, false); flags.setFlag(QContactStatusFlags::IsDeleted, false); } else if (flag == QContactStatusFlags::IsModified) { flags.setFlag(QContactStatusFlags::IsAdded, false); flags.setFlag(QContactStatusFlags::IsModified, true); flags.setFlag(QContactStatusFlags::IsDeleted, false); } else if (flag == QContactStatusFlags::IsDeleted) { flags.setFlag(QContactStatusFlags::IsAdded, false); flags.setFlag(QContactStatusFlags::IsModified, false); flags.setFlag(QContactStatusFlags::IsDeleted, true); } c.saveDetail(&flags, QContact::IgnoreAccessConstraints); } void TwoWayContactSyncAdaptor::remoteContactChangesDetermined( const QContactCollection &collection, const QList &remotelyAddedContacts, const QList &remotelyModifiedContacts, const QList &remotelyRemovedContacts) { // if this is not a pure remote-collection-addition, then we may have // local changes which need to be applied remotely. bool haveLocalChanges = false; QSet handledContactIds; QList remoteAdditions, remoteModifications, remoteRemovals; if (!collection.id().isNull() && d->m_localContactChanges.contains(collection.id())) { TwoWayContactSyncAdaptorPrivate::ContactChanges &localChanges(d->m_localContactChanges[collection.id()]); haveLocalChanges = !localChanges.addedContacts.isEmpty() || !localChanges.removedContacts.isEmpty(); // resolve conflicts between local and remote changes QList localModifications; for (const QContact &c : localChanges.modifiedContacts) { bool foundMatch = false; for (const QContact &r : remotelyModifiedContacts) { if (c.id() == r.id()) { bool identical = false; QContact resolved = resolveConflictingChanges(c, r, &identical); if (!identical) { haveLocalChanges = true; localModifications.append(resolved); QContact modified = resolved; setContactChangeFlags(modified, QContactStatusFlags::IsModified); remoteModifications.append(modified); } handledContactIds.insert(c.id()); foundMatch = true; break; } } if (!foundMatch) { haveLocalChanges = true; localModifications.append(c); } } localChanges.modifiedContacts = localModifications; } // set the appropriate change flags on the remote changes (which will be applied locally). for (const QContact &r : remotelyAddedContacts) { QContact added = r; setContactChangeFlags(added, QContactStatusFlags::IsAdded); remoteAdditions.append(added); } for (const QContact &r : remotelyRemovedContacts) { QContact deleted = r; setContactChangeFlags(deleted, QContactStatusFlags::IsDeleted); remoteRemovals.append(deleted); } for (const QContact &r : remotelyModifiedContacts) { if (!handledContactIds.contains(r.id())) { bool foundMatch = false; for (const QContact &c : d->m_localContactChanges[collection.id()].unmodifiedContacts) { if (c.id() == r.id()) { foundMatch = true; handledContactIds.insert(c.id()); bool identical = false; QContact resolved = resolveConflictingChanges(c, r, &identical); if (!identical) { setContactChangeFlags(resolved, QContactStatusFlags::IsModified); remoteModifications.append(resolved); } break; } } if (!foundMatch) { QContact modified = r; setContactChangeFlags(modified, QContactStatusFlags::IsModified); remoteModifications.append(modified); } } } if (collection.id().isNull() || !haveLocalChanges) { // no local changes exist to push to the server. storeRemoteChangesLocally(collection, remoteAdditions, remoteModifications, remoteRemovals); } else { // cache the remote changes (which need to be applied locally) // while we push the local changes to the server. TwoWayContactSyncAdaptorPrivate::ContactChanges remoteChanges; remoteChanges.addedContacts = remoteAdditions; remoteChanges.modifiedContacts = remoteModifications; remoteChanges.removedContacts = remoteRemovals; d->m_remoteContactChanges.insert(collection.id(), remoteChanges); const TwoWayContactSyncAdaptorPrivate::ContactChanges &localChanges(d->m_localContactChanges[collection.id()]); if (!storeLocalChangesRemotely( collection, localChanges.addedContacts, localChanges.modifiedContacts, localChanges.removedContacts)) { qWarning() << "Failed to push local changes to remote server for collection " << QString::fromLatin1(collection.id().localId()) << "for application: " << d->m_applicationName << " for account: " << d->m_accountId; syncOperationError(); } } // we no longer need the cache of local changes for this collection. if (!collection.id().isNull()) { d->m_localContactChanges.remove(collection.id()); } } bool TwoWayContactSyncAdaptor::storeLocalChangesRemotely( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { // The plugin must implement this method to store // information about contacts to an addressbook on the remote server, // and then call localChangesStoredRemotely() once complete // (or syncOperationError() if an error occurred). qWarning() << "TWCSA::storeLocalChangesRemotely(): implementation missing"; return false; } void TwoWayContactSyncAdaptor::localChangesStoredRemotely( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts) { // here we get back the updated collection and contacts // (e.g. may have updated ctag and etag values). { TwoWayContactSyncAdaptorPrivate::ContactChanges &remoteChanges(d->m_remoteContactChanges[collection.id()]); // every local addition cannot previously have been represented // in the remote change set. Thus, we can mark this as a // remote modification (i.e. with updated etag / guid / etc). for (const QContact &c : addedContacts) { QContact modified = c; setContactChangeFlags(modified, QContactStatusFlags::IsModified); remoteChanges.modifiedContacts.append(modified); } // a local modification might already be represented // as a remote modification, in which case we need to replace it. for (const QContact &c : modifiedContacts) { bool foundMatch = false; QList::iterator it = remoteChanges.modifiedContacts.begin(), end = remoteChanges.modifiedContacts.end(); while (it != end) { if (c.id() == it->id()) { foundMatch = true; QContact modified = c; setContactChangeFlags(modified, QContactStatusFlags::IsModified); *it = modified; // overwrite with the updated content break; } it++; } if (!foundMatch) { QContact modified = c; setContactChangeFlags(modified, QContactStatusFlags::IsModified); remoteChanges.modifiedContacts.append(modified); } } // store the final results locally. storeRemoteChangesLocally(collection, remoteChanges.addedContacts, remoteChanges.modifiedContacts, remoteChanges.removedContacts); } // we no longer need the cache of remote changes for this collection. d->m_remoteContactChanges.remove(collection.id()); } void TwoWayContactSyncAdaptor::storeRemoteChangesLocally( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { if (collection.id().isNull()) { // remote collection addition. Q_ASSERT(modifiedContacts.isEmpty()); Q_ASSERT(deletedContacts.isEmpty()); QHash*> remotelyAddedCollections; QContactCollection remotelyAddedCollection = collection; QList remotelyAddedContacts = addedContacts; remotelyAddedCollections.insert(&remotelyAddedCollection, &remotelyAddedContacts); QContactManager::Error err = QContactManager::NoError; if (!d->m_engine->storeChanges(&remotelyAddedCollections, nullptr, QList(), ContactManagerEngine::PreserveLocalChanges, true, &err)) { qWarning() << "Failed to store remotely added collection to local database for collection " << collection.metaData(QContactCollection::KeyName).toString() << "for application: " << d->m_applicationName << " for account: " << d->m_accountId; syncOperationError(); return; } } else { // update existing collection contents. QHash*> remotelyModifiedCollections; QList changes = addedContacts + modifiedContacts + deletedContacts; QContactCollection remotelyModifiedCollection = collection; remotelyModifiedCollections.insert(&remotelyModifiedCollection, &changes); QContactManager::Error err = QContactManager::NoError; if (!d->m_engine->storeChanges(nullptr, &remotelyModifiedCollections, QList(), ContactManagerEngine::PreserveLocalChanges, true, &err)) { qWarning() << "Failed to store remote collection modifications to local database for collection " << QString::fromLatin1(collection.id().localId()) << "for application: " << d->m_applicationName << " for account: " << d->m_accountId; syncOperationError(); return; } } performNextQueuedOperation(); } bool TwoWayContactSyncAdaptor::removeAllCollections() { if (d->m_busy) { qWarning() << Q_FUNC_INFO << "busy with ongoing sync! cannot remove collections!"; return false; } if (!d->m_engine) { qWarning() << Q_FUNC_INFO << "no connection to qtcontacts-sqlite"; return false; } d->m_busy = true; const QList allCollections = contactManager().collections(); QList removeCollectionIds; for (const QContactCollection &collection : allCollections) { if (collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == d->m_accountId && collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString() == d->m_applicationName) { removeCollectionIds.append(collection.id()); } } QContactManager::Error err = QContactManager::NoError; if (!d->m_engine->storeChanges( nullptr, nullptr, removeCollectionIds, ContactManagerEngine::PreserveRemoteChanges, true, &err)) { qWarning() << "Failed to remove contact addressbooks for " << d->m_applicationName << " for deleted account:" << d->m_accountId; d->m_busy = false; return false; } d->m_busy = false; return true; } const QContactManager &TwoWayContactSyncAdaptor::contactManager() const { return *d->m_manager; } QContactManager &TwoWayContactSyncAdaptor::contactManager() { return *d->m_manager; } // Note: this implementation can be overridden if the sync adapter knows // that the remote service doesn't support some detail or field types, // and thus these details and fields should not be inspected during // conflict resolution. TwoWayContactSyncAdaptor::IgnorableDetailsAndFields TwoWayContactSyncAdaptor::ignorableDetailsAndFields() const { TwoWayContactSyncAdaptor::IgnorableDetailsAndFields ignorable; // Note: we may still upsync these ignorable details+fields, just don't look at them during delta detection. // We need to do this, otherwise there can be infinite loops caused due to spurious differences between the // in-memory version (QContact) and the exportable version (vCard) resulting in ETag updates server-side. // The downside is that changes to these details will not be upsynced unless another change also occurs. QSet ignorableDetailTypes = defaultIgnorableDetailTypes(); ignorableDetailTypes.insert(QContactDetail::TypeGender); // ignore differences in X-GENDER field when detecting delta. ignorableDetailTypes.insert(QContactDetail::TypeFavorite); // ignore differences in X-FAVORITE field when detecting delta. ignorableDetailTypes.insert(QContactDetail::TypeAvatar); // ignore differences in PHOTO field when detecting delta. QHash > ignorableDetailFields = defaultIgnorableDetailFields(); ignorableDetailFields[QContactDetail::TypeAddress] << QContactAddress::FieldSubTypes; // and ADR subtypes ignorableDetailFields[QContactDetail::TypePhoneNumber] << QContactPhoneNumber::FieldSubTypes; // and TEL number subtypes ignorableDetailFields[QContactDetail::TypeUrl] << QContactUrl::FieldSubType; // and URL subtype ignorable.detailTypes = ignorableDetailTypes; ignorable.detailFields = ignorableDetailFields; ignorable.commonFields = defaultIgnorableCommonFields(); return ignorable; } // Note: this implementation can be overridden if the sync adapter knows // more about how to resolve conflicts (eg persistent detail ids) QContact TwoWayContactSyncAdaptor::resolveConflictingChanges( const QContact &local, const QContact &remote, bool *identical) { // first, remove duplicate details from both a and b. bool detailIsDuplicate = false; QList ldets = local.details(), rdets = remote.details(); QList nonDupLdets, nonDupRdets; while (!ldets.isEmpty()) { detailIsDuplicate = false; QContactDetail d = ldets.takeLast(); Q_FOREACH (const QContactDetail &otherd, ldets) { if (otherd == d) { detailIsDuplicate = true; break; } } if (!detailIsDuplicate) { nonDupLdets.append(d); } } while (!rdets.isEmpty()) { detailIsDuplicate = false; QContactDetail d = rdets.takeLast(); Q_FOREACH (const QContactDetail &otherd, rdets) { if (otherd == d) { detailIsDuplicate = true; break; } } if (!detailIsDuplicate) { nonDupRdets.append(d); } } // second, attempt to apply the flagged modifications from local to the resolved contact. // any details which remain in the remote detail list should also be saved in the resolved contact. QContact resolved, localWithoutDeletedDetails; for (int i = nonDupLdets.size() - 1; i >= 0; --i) { QContactDetail &ldet(nonDupLdets[i]); const quint32 localDetailDbId = ldet.value(QContactDetail__FieldDatabaseId).toUInt(); const int localDetailChangeFlag = ldet.value(QContactDetail__FieldChangeFlags).toInt(); if ((localDetailChangeFlag & QContactDetail__ChangeFlag_IsDeleted) == 0) { localWithoutDeletedDetails.saveDetail(&ldet, QContact::IgnoreAccessConstraints); } // apply detail additions directly. if (((localDetailChangeFlag & QContactDetail__ChangeFlag_IsAdded) > 0) && ((localDetailChangeFlag & QContactDetail__ChangeFlag_IsDeleted) == 0)) { ldet.removeValue(QContactDetail__FieldChangeFlags); resolved.saveDetail(&ldet, QContact::IgnoreAccessConstraints); continue; } // if the sync adapter provides the persistent detail database ids // as detail field values, we can apply modifications and deletions directly. if (((localDetailChangeFlag & QContactDetail__ChangeFlag_IsModified) > 0) || ((localDetailChangeFlag & QContactDetail__ChangeFlag_IsDeleted) > 0)) { for (int j = nonDupRdets.size() - 1; j >= 0; --j) { const QContactDetail &rdet(nonDupRdets[j]); const quint32 remoteDetailDbId = rdet.value(QContactDetail__FieldDatabaseId).toUInt(); if (ldet.type() == rdet.type() && (localDetailDbId > 0 && localDetailDbId == remoteDetailDbId)) { if ((localDetailChangeFlag & QContactDetail__ChangeFlag_IsModified) > 0) { // note: this will clobber the remote detail if it was also modified. ldet.removeValue(QContactDetail__FieldChangeFlags); nonDupRdets.replace(j, ldet); } else { // QContactDetail__ChangeFlag_IsDeleted nonDupRdets.removeAt(j); } break; } } } } // any remaining details from the remote should also be stored into the resolved contact. // note that we need to ensure that unique details (name etc) are replaced if they already exist. const QSet uniqueDetailTypes { QContactDetail::TypeDisplayLabel, QContactDetail::TypeGender, QContactDetail::TypeGlobalPresence, QContactDetail::TypeGuid, QContactDetail::TypeName, QContactDetail::TypeSyncTarget, QContactDetail::TypeTimestamp, }; for (int j = nonDupRdets.size() - 1; j >= 0; --j) { QContactDetail &rdet(nonDupRdets[j]); if (uniqueDetailTypes.contains(rdet.type()) && resolved.details(rdet.type()).size()) { QContactDetail existing = resolved.detail(rdet.type()); existing.setValues(rdet.values()); resolved.saveDetail(&existing, QContact::IgnoreAccessConstraints); } else { resolved.saveDetail(&rdet, QContact::IgnoreAccessConstraints); } } // set the id as appropriate into the resolved contact. resolved.setId(local.id()); resolved.setCollectionId(local.collectionId()); // after applying the delta from the local to the remote as best we can, // check to see if the resolved contact is identical to the local contact // (after removing deleted details from the local contact). TwoWayContactSyncAdaptor::IgnorableDetailsAndFields ignorable = ignorableDetailsAndFields(); *identical = exactContactMatchExistsInList( resolved, QList() << localWithoutDeletedDetails, ignorable.detailTypes, ignorable.detailFields, ignorable.commonFields, true) >= 0; return resolved; } void TwoWayContactSyncAdaptor::syncFinishedSuccessfully() { // The plugin must implement this method appropriately. // Usually this will mean emitting some signal which is // handled by the sync framework, etc. qWarning() << "TWCSA::syncFinishedSuccessfully(): implementation missing"; } void TwoWayContactSyncAdaptor::syncFinishedWithError() { // The plugin must implement this method appropriately. // Usually this will mean emitting some signal which is // handled by the sync framework, etc. qWarning() << "TWCSA::syncFinishedWithError(): implementation missing"; } void TwoWayContactSyncAdaptor::syncOperationError() { // Plugins should invoke this if the most recent operation // failed (e.g. network request, etc). d->m_errorOccurred = true; if (d->m_continueAfterError) { performNextQueuedOperation(); } else { d->m_busy = false; syncFinishedWithError(); } } #endif // TWOWAYCONTACTSYNCADAPTOR_IMPL_H qtcontacts-sqlite-0.3.19/src/qtcontacts-sqlite-qt5-extensions.pc000066400000000000000000000003421436373107600250000ustar00rootroot00000000000000prefix=/usr includedir=${prefix}/include/qtcontacts-sqlite-qt5-extensions Name: qtcontacts-sqlite-qt5-extensions Description: QtContacts extensions implemented by qtcontacts-sqlite-qt5 Version: 0.3.0 Cflags: -I${includedir} qtcontacts-sqlite-0.3.19/src/src.pro000066400000000000000000000042171436373107600174040ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = engine EXTENSION_DETAILS = \ extensions/QContactDeactivated \ extensions/qcontactdeactivated.h \ extensions/qcontactdeactivated_impl.h \ extensions/QContactUndelete \ extensions/qcontactundelete.h \ extensions/qcontactundelete_impl.h \ extensions/QContactOriginMetadata \ extensions/qcontactoriginmetadata.h \ extensions/qcontactoriginmetadata_impl.h \ extensions/QContactStatusFlags \ extensions/qcontactstatusflags.h \ extensions/qcontactstatusflags_impl.h EXTENSION_REQUESTS = \ extensions/QContactDetailFetchRequest \ extensions/qcontactdetailfetchrequest.h \ extensions/qcontactdetailfetchrequest_p.h \ extensions/qcontactdetailfetchrequest_impl.h \ extensions/QContactCollectionChangesFetchRequest \ extensions/qcontactcollectionchangesfetchrequest.h \ extensions/qcontactcollectionchangesfetchrequest_p.h \ extensions/qcontactcollectionchangesfetchrequest_impl.h \ extensions/QContactChangesFetchRequest \ extensions/qcontactchangesfetchrequest.h \ extensions/qcontactchangesfetchrequest_p.h \ extensions/qcontactchangesfetchrequest_impl.h \ extensions/QContactClearChangeFlagsRequest \ extensions/qcontactclearchangeflagsrequest.h \ extensions/qcontactclearchangeflagsrequest_p.h \ extensions/qcontactclearchangeflagsrequest_impl.h \ extensions/QContactChangesSaveRequest \ extensions/qcontactchangessaverequest.h \ extensions/qcontactchangessaverequest_p.h \ extensions/qcontactchangessaverequest_impl.h EXTENSION_PLUGIN_INTERFACES = \ extensions/displaylabelgroupgenerator.h \ extensions/twowaycontactsyncadaptor.h \ extensions/twowaycontactsyncadaptor_impl.h \ extensions/twowaycontactsyncadapter.h \ extensions/twowaycontactsyncadapter_impl.h OTHER_FILES = \ extensions/contactmanagerengine.h \ extensions/contactdelta.h \ extensions/contactdelta_impl.h \ extensions/qtcontacts-extensions.h \ extensions/qtcontacts-extensions_impl.h \ extensions/qtcontacts-extensions_manager_impl.h \ $$EXTENSION_DETAILS \ $$EXTENSION_REQUESTS \ $$EXTENSION_PLUGIN_INTERFACES qtcontacts-sqlite-0.3.19/tests/000077500000000000000000000000001436373107600164425ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/.gitignore000066400000000000000000000004541436373107600204350ustar00rootroot00000000000000auto/aggregation/tst_aggregation auto/database/tst_database auto/memorytable/tst_memorytable auto/phonenumber/tst_phonenumber auto/qcontactmanager/tst_qcontactmanager auto/qcontactmanagerfiltering/tst_qcontactmanagerfiltering benchmarks/deltadetection/deltadetection benchmarks/fetchtimes/fetchtimes qtcontacts-sqlite-0.3.19/tests/auto/000077500000000000000000000000001436373107600174125ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/aggregation/000077500000000000000000000000001436373107600217015ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/aggregation/aggregation.pro000066400000000000000000000004531436373107600247140ustar00rootroot00000000000000TARGET = tst_aggregation include(../../common.pri) INCLUDEPATH += \ ../../../src/engine/ HEADERS += \ ../../../src/engine/contactid_p.h \ ../../../src/extensions/contactmanagerengine.h \ ../../util.h SOURCES += \ ../../../src/engine/contactid.cpp \ tst_aggregation.cpp \ qtcontacts-sqlite-0.3.19/tests/auto/aggregation/tst_aggregation.cpp000066400000000000000000006710011436373107600255730ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2019 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * 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." */ #define QT_STATICPLUGIN #include "../../util.h" #include "qtcontacts-extensions.h" #include static const QString aggregatesRelationship(relationshipString(QContactRelationship::Aggregates)); namespace { static const char *addedColAccumulationSlot = SLOT(addColAccumulationSlot(QList)); static const char *changedColAccumulationSlot = SLOT(chgColAccumulationSlot(QList)); static const char *removedColAccumulationSlot = SLOT(remColAccumulationSlot(QList)); static const char *addedAccumulationSlot = SLOT(addAccumulationSlot(QList)); static const char *changedAccumulationSlot = SLOT(chgAccumulationSlot(QList)); static const char *removedAccumulationSlot = SLOT(remAccumulationSlot(QList)); QString detailProvenance(const QContactDetail &detail) { return detail.value(QContactDetail::FieldProvenance); } QString detailProvenanceContact(const QContactDetail &detail) { // The contact element is the first part up to ':' const QString provenance(detailProvenance(detail)); return provenance.left(provenance.indexOf(QChar::fromLatin1(':'))); } } class tst_Aggregation : public QObject { Q_OBJECT public: tst_Aggregation(); virtual ~tst_Aggregation(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); public slots: void addColAccumulationSlot(const QList &ids); void chgColAccumulationSlot(const QList &ids); void remColAccumulationSlot(const QList &ids); void addAccumulationSlot(const QList &ids); void chgAccumulationSlot(const QList &ids); void remAccumulationSlot(const QList &ids); private slots: void createSingleLocal(); void createMultipleLocal(); void createSingleLocalAndSingleSync(); void createNonAggregable(); void updateSingleLocal(); void updateSingleAggregate(); void updateAggregateOfLocalAndSync(); void updateAggregateOfLocalAndModifiableSync(); void compositionPrefersLocal(); void uniquenessConstraints(); void removeSingleLocal(); void removeSingleAggregate(); void alterRelationships(); void aggregationHeuristic_data(); void aggregationHeuristic(); void regenerateAggregate(); void detailUris(); void correctDetails(); void batchSemantics(); void customSemantics(); void changeLogFiltering(); void deactivationSingle(); void deactivationMultiple(); void deletionSingle(); void deletionMultiple(); void deletionCollections(); void testOOB(); private: void waitForSignalPropagation(); QContactManager *m_cm; QSet m_addColAccumulatedIds; QSet m_chgColAccumulatedIds; QSet m_remColAccumulatedIds; QSet m_createdColIds; QSet m_addAccumulatedIds; QSet m_chgAccumulatedIds; QSet m_remAccumulatedIds; QSet m_createdIds; QByteArray aggregateAddressbookId() { return QtContactsSqliteExtensions::aggregateCollectionId(m_cm->managerUri()).localId(); } QByteArray localAddressbookId() { return QtContactsSqliteExtensions::localCollectionId(m_cm->managerUri()).localId(); } }; tst_Aggregation::tst_Aggregation() : m_cm(0) { QMap parameters; parameters.insert(QString::fromLatin1("autoTest"), QString::fromLatin1("true")); parameters.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("true")); m_cm = new QContactManager(QString::fromLatin1("org.nemomobile.contacts.sqlite"), parameters); QTest::qWait(250); // creating self contact etc will cause some signals to be emitted. ignore them. connect(m_cm, collectionsAddedSignal, this, addedColAccumulationSlot); connect(m_cm, collectionsChangedSignal, this, changedColAccumulationSlot); connect(m_cm, collectionsRemovedSignal, this, removedColAccumulationSlot); connect(m_cm, contactsAddedSignal, this, addedAccumulationSlot); connect(m_cm, contactsChangedSignal, this, changedAccumulationSlot); connect(m_cm, contactsRemovedSignal, this, removedAccumulationSlot); } tst_Aggregation::~tst_Aggregation() { } void tst_Aggregation::initTestCase() { registerIdType(); /* Make sure the DB is empty */ QContactCollectionFilter allCollections; m_cm->removeContacts(m_cm->contactIds(allCollections)); waitForSignalPropagation(); } void tst_Aggregation::init() { m_addColAccumulatedIds.clear(); m_chgColAccumulatedIds.clear(); m_remColAccumulatedIds.clear(); m_createdColIds.clear(); m_addAccumulatedIds.clear(); m_chgAccumulatedIds.clear(); m_remAccumulatedIds.clear(); m_createdIds.clear(); } void tst_Aggregation::cleanupTestCase() { } void tst_Aggregation::cleanup() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; waitForSignalPropagation(); if (!m_createdIds.isEmpty()) { // purge them one at a time, to avoid "contacts from different collections in single batch" errors. for (const QContactId &cid : m_createdIds) { QContact doomed = m_cm->contact(cid); if (!doomed.id().isNull() && doomed.collectionId().localId() != aggregateAddressbookId()) { if (!m_cm->removeContact(cid)) { qWarning() << "Failed to cleanup:" << QString::fromLatin1(cid.localId()); } cme->clearChangeFlags(QList() << cid, &err); } } m_createdIds.clear(); } if (!m_createdColIds.isEmpty()) { for (const QContactCollectionId &colId : m_createdColIds.toList()) { m_cm->removeCollection(colId); cme->clearChangeFlags(colId, &err); } m_createdColIds.clear(); } cme->clearChangeFlags(QContactCollectionId(m_cm->managerUri(), localAddressbookId()), &err); waitForSignalPropagation(); } void tst_Aggregation::waitForSignalPropagation() { // Signals are routed via DBUS, so we need to wait for them to arrive QTest::qWait(50); } void tst_Aggregation::addColAccumulationSlot(const QList &ids) { foreach (const QContactCollectionId &id, ids) { m_addColAccumulatedIds.insert(id); m_createdColIds.insert(id); } } void tst_Aggregation::chgColAccumulationSlot(const QList &ids) { foreach (const QContactCollectionId &id, ids) { m_chgColAccumulatedIds.insert(id); } } void tst_Aggregation::remColAccumulationSlot(const QList &ids) { foreach (const QContactCollectionId &id, ids) { m_remColAccumulatedIds.insert(id); } } void tst_Aggregation::addAccumulationSlot(const QList &ids) { foreach (const QContactId &id, ids) { m_addAccumulatedIds.insert(id); m_createdIds.insert(id); } } void tst_Aggregation::chgAccumulationSlot(const QList &ids) { foreach (const QContactId &id, ids) { m_chgAccumulatedIds.insert(id); } } void tst_Aggregation::remAccumulationSlot(const QList &ids) { foreach (const QContactId &id, ids) { m_remAccumulatedIds.insert(id); } } void tst_Aggregation::createSingleLocal() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); int addSpyCount = 0; // now add a new local contact (no collection specified == automatically local) QContact alice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("1234567"); alice.saveDetail(&aph); QContactGender ag; ag.setGender(QContactGender::GenderFemale); alice.saveDetail(&ag); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 2); // should have added local + aggregate QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("1234567")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // Test the provenance of details QContactPhoneNumber localDetail(localAlice.detail()); QContactPhoneNumber aggregateDetail(aggregateAlice.detail()); QVERIFY(!detailProvenance(localDetail).isEmpty()); QCOMPARE(detailProvenance(aggregateDetail), detailProvenance(localDetail)); // A local contact should have a GUID, which is not promoted to the aggregate QVERIFY(!localAlice.detail().guid().isEmpty()); QVERIFY(aggregateAlice.detail().guid().isEmpty()); // Verify that gender is promoted QCOMPARE(localAlice.detail().gender(), QContactGender::GenderFemale); QCOMPARE(aggregateAlice.detail().gender(), QContactGender::GenderFemale); } void tst_Aggregation::createMultipleLocal() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); int addSpyCount = 0; // now add two new local contacts (no collectionId specified == automatically local) QContact alice; QContact bob; QContactName an, bn; an.setFirstName("Alice2"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); bn.setFirstName("Bob2"); bn.setMiddleName("The"); bn.setLastName("Destroyer"); bob.saveDetail(&bn); QContactPhoneNumber aph, bph; aph.setNumber("234567"); alice.saveDetail(&aph); bph.setNumber("765432"); bob.saveDetail(&bph); // add an explicit GUID to Bob const QString bobGuid("I am Bob"); QContactGuid bg; bg.setGuid(bobGuid); bob.saveDetail(&bg); QList saveList; saveList << alice << bob; m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContacts(&saveList)); QTRY_VERIFY(addSpy.count() > addSpyCount); // should have added local + aggregate for each alice = saveList.at(0); bob = saveList.at(1); QTRY_COMPARE(m_addAccumulatedIds.size(), 4); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(bob))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 2); // 2 extra aggregate contacts aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 4); // should have added local + aggregate for each allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact localBob; QContact aggregateAlice; QContact aggregateBob; bool foundLocalAlice = false; bool foundAggregateAlice = false; bool foundLocalBob = false; bool foundAggregateBob = false; foreach (const QContact &curr, allContacts) { QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice2") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("234567")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } else if (currName.firstName() == QLatin1String("Bob2") && currName.middleName() == QLatin1String("The") && currName.lastName() == QLatin1String("Destroyer") && currPhn.number() == QLatin1String("765432")) { if (curr.collectionId().localId() == localAddressbookId()) { localBob = curr; foundLocalBob = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateBob = curr; foundAggregateBob = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(foundLocalBob); QVERIFY(foundAggregateBob); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(!localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateBob.id())); QVERIFY(!aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localBob.id())); QVERIFY(localBob.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateBob.id())); QVERIFY(aggregateBob.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localBob.id())); QVERIFY(!localBob.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(!aggregateBob.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // Test the provenance of details QContactPhoneNumber localAliceDetail(localAlice.detail()); QContactPhoneNumber aggregateAliceDetail(aggregateAlice.detail()); QVERIFY(!detailProvenance(localAliceDetail).isEmpty()); QCOMPARE(detailProvenance(aggregateAliceDetail), detailProvenance(localAliceDetail)); QContactPhoneNumber localBobDetail(localBob.detail()); QContactPhoneNumber aggregateBobDetail(aggregateBob.detail()); QVERIFY(!detailProvenance(localBobDetail).isEmpty()); QCOMPARE(detailProvenance(aggregateBobDetail), detailProvenance(localBobDetail)); QVERIFY(detailProvenance(localBobDetail) != detailProvenance(localAliceDetail)); // Verify that the local consituents have GUIDs, but the aggregates don't QVERIFY(!localAlice.detail().guid().isEmpty()); QVERIFY(!localBob.detail().guid().isEmpty()); QCOMPARE(localBob.detail().guid(), bobGuid); QVERIFY(aggregateAlice.detail().guid().isEmpty()); QVERIFY(aggregateBob.detail().guid().isEmpty()); } void tst_Aggregation::createSingleLocalAndSingleSync() { // here we create a local contact, and then save it // and then we create a "sync" contact, which should "match" it. // It should be related to the aggregate created for the sync. QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); QSignalSpy chgSpy(m_cm, contactsChangedSignal); int addSpyCount = 0; int chgSpyCount = 0; // now add a new local contact (no collectionId specified == automatically local) QContact alice; QContactName an; an.setFirstName("Alice3"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("34567"); alice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice@test.com"); alice.saveDetail(&aem); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); // should have added local + aggregate QTRY_COMPARE(m_addAccumulatedIds.size(), 2); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice3") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("34567") && currEm.emailAddress() == QLatin1String("alice@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // now add the doppleganger from another sync source (remote addressbook) QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&remoteAddressbook)); QContact syncAlice; syncAlice.setCollectionId(remoteAddressbook.id()); QContactName san; san.setFirstName(an.firstName()); san.setMiddleName(an.middleName()); san.setLastName(an.lastName()); syncAlice.saveDetail(&san); QContactPhoneNumber saph; saph.setNumber(aph.number()); syncAlice.saveDetail(&saph); QContactEmailAddress saem; saem.setEmailAddress(aem.emailAddress()); syncAlice.saveDetail(&saem); QContactHobby sah; // this is a "new" detail which doesn't appear in the local contact. sah.setHobby(QLatin1String("tennis")); syncAlice.saveDetail(&sah); QContactSyncTarget sast; sast.setSyncTarget(QLatin1String("test")); syncAlice.saveDetail(&sast); // DON'T clear the m_addAccumulatedIds list here. // DO clear the m_chgAccumulatedIds list here, though. chgSpyCount = chgSpy.count(); m_chgAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&syncAlice)); QTRY_VERIFY(addSpy.count() > addSpyCount); // should have added test but not an aggregate - aggregate already exists QTRY_VERIFY(chgSpy.count() > chgSpyCount); // should have updated the aggregate QTRY_COMPARE(m_addAccumulatedIds.size(), 3); QTRY_COMPARE(m_chgAccumulatedIds.size(), 1); // the aggregate should have been updated (with the hobby) QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(localAlice))); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(syncAlice))); QVERIFY(m_chgAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount); // no extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 1); // should have added test but not an aggregate allCount = m_cm->contactIds(allCollections).size(); allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact testAlice; bool foundTestAlice = false; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice3") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("34567") && currEm.emailAddress() == QLatin1String("alice@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else if (curr.collectionId() == remoteAddressbook.id()) { testAlice = curr; foundTestAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundTestAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(testAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(testAlice.id())); // Verify the propagation of details QContactHobby localDetail(localAlice.detail()); QContactHobby testDetail(testAlice.detail()); QContactHobby aggregateDetail(aggregateAlice.detail()); QCOMPARE(testDetail.value(QContactHobby::FieldHobby), QLatin1String("tennis")); // came from here QVERIFY(!detailProvenance(testDetail).isEmpty()); QCOMPARE(aggregateDetail.value(QContactHobby::FieldHobby), QLatin1String("tennis")); // aggregated to here QCOMPARE(detailProvenance(aggregateDetail), detailProvenance(testDetail)); QCOMPARE(localDetail.value(QContactHobby::FieldHobby), QString()); // local shouldn't get it QVERIFY(detailProvenance(localDetail).isEmpty()); } void tst_Aggregation::createNonAggregable() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); int addSpyCount = 0; // add a non-aggregable addressbook (e.g. application-specific addressbook). QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, false); QVERIFY(m_cm->saveCollection(&testAddressbook)); // now add a new non-aggregable contact QContact alice; alice.setCollectionId(testAddressbook.id()); QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("34567"); alice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice@test.com"); alice.saveDetail(&aem); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 1); // just 1, since no aggregate should be generated. QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount); aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 1); // should have added non-aggregable allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact testAlice; bool foundTestAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("34567") && currEm.emailAddress() == QLatin1String("alice@test.com")) { if (curr.collectionId() == testAddressbook.id()) { testAlice = curr; foundTestAlice = true; } else { foundAggregateAlice = true; } } } QVERIFY(foundTestAlice); QVERIFY(!foundAggregateAlice); // should not be found, no aggregate should have been generated for it. QCOMPARE(testAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).size(), 0); // now add a new local contact (no collectionId specified == automatically local) QContact localAlice; QContactName lan; lan.setFirstName("Alice"); lan.setMiddleName("In"); lan.setLastName("Wonderland"); localAlice.saveDetail(&lan); QContactHobby lah; lah.setHobby("tennis"); localAlice.saveDetail(&lah); QContactEmailAddress laem; laem.setEmailAddress("alice@test.com"); localAlice.saveDetail(&laem); QVERIFY(m_cm->saveContact(&localAlice)); QTRY_VERIFY(addSpy.count() > addSpyCount); // should have added local + aggregate QTRY_COMPARE(m_addAccumulatedIds.size(), 3); // testAlice, localAlice, aggAlice. QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(localAlice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact locAlice, aggAlice; bool foundLocalAlice = false; foundTestAlice = false; foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currEm.emailAddress() == QLatin1String("alice@test.com")) { if (curr.collectionId() == testAddressbook.id()) { testAlice = curr; foundTestAlice = true; } else if (curr.collectionId().localId() == localAddressbookId()) { locAlice = curr; foundLocalAlice = true; } else { aggAlice = curr; foundAggregateAlice = true; } } } // ensure that we have now found all contacts QVERIFY(foundTestAlice); QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); // ensure the local contact contains the content we expect. QCOMPARE(locAlice.detail().firstName(), localAlice.detail().firstName()); QCOMPARE(locAlice.detail().middleName(), localAlice.detail().middleName()); QCOMPARE(locAlice.detail().lastName(), localAlice.detail().lastName()); QCOMPARE(locAlice.detail().emailAddress(), localAlice.detail().emailAddress()); QCOMPARE(locAlice.detail().hobby(), localAlice.detail().hobby()); QVERIFY(locAlice.detail().number().isEmpty()); // ensure that the aggregate contact contains the content we expect. QCOMPARE(aggAlice.detail().firstName(), localAlice.detail().firstName()); QCOMPARE(aggAlice.detail().middleName(), localAlice.detail().middleName()); QCOMPARE(aggAlice.detail().lastName(), localAlice.detail().lastName()); QCOMPARE(aggAlice.detail().emailAddress(), localAlice.detail().emailAddress()); QCOMPARE(aggAlice.detail().hobby(), localAlice.detail().hobby()); QVERIFY(aggAlice.detail().number().isEmpty()); // and that it aggregates only localAlice QVERIFY(aggAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(!aggAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(testAlice.id())); // now modify the local contact; this shouldn't result in testAlice details being aggregated into the aggregate. QContactNickname lnick; lnick.setNickname("Ally"); localAlice = locAlice; QVERIFY(localAlice.saveDetail(&lnick)); QVERIFY(m_cm->saveContact(&localAlice)); aggAlice = m_cm->contact(aggAlice.id()); QCOMPARE(aggAlice.detail().nickname(), localAlice.detail().nickname()); QVERIFY(aggAlice.detail().number().isEmpty()); // now modify the test contact; this shouldn't result in testAlice details being aggregated into the aggregate. QContactAvatar tav; tav.setImageUrl(QUrl(QStringLiteral("img://alice.in.wonderland.tld/avatar.png"))); QVERIFY(testAlice.saveDetail(&tav)); QVERIFY(m_cm->saveContact(&testAlice)); aggAlice = m_cm->contact(aggAlice.id()); QCOMPARE(aggAlice.detail().nickname(), localAlice.detail().nickname()); QVERIFY(aggAlice.detail().number().isEmpty()); QVERIFY(aggAlice.detail().imageUrl().isEmpty()); // nor should the relationships have changed. QVERIFY(aggAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(!aggAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(testAlice.id())); } void tst_Aggregation::updateSingleLocal() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); QSignalSpy chgSpy(m_cm, contactsChangedSignal); int addSpyCount = 0; int chgSpyCount = 0; // now add a new local contact (no synctarget specified == automatically local) QContact alice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("4567"); alice.saveDetail(&aph); QContactHobby ah; ah.setHobby("tennis"); alice.saveDetail(&ah); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 2); // should have added local + aggregate QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); QContactHobby currHobby = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("4567") && currHobby.hobby() == QLatin1String("tennis")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // now update local alice. The aggregate should get updated also. QContactEmailAddress ae; // add an email address. ae.setEmailAddress("alice4@test.com"); QVERIFY(localAlice.saveDetail(&ae)); QContactHobby rah = localAlice.detail(); // remove a hobby QVERIFY(localAlice.removeDetail(&rah)); QContactPhoneNumber maph = localAlice.detail(); // modify a phone number maph.setNumber("4444"); QVERIFY(localAlice.saveDetail(&maph)); chgSpyCount = chgSpy.count(); m_chgAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&localAlice)); QTRY_VERIFY(chgSpy.count() > chgSpyCount); QTRY_VERIFY(m_chgAccumulatedIds.contains(ContactId::apiId(localAlice))); QTRY_VERIFY(m_chgAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); // reload them, and compare. localAlice = m_cm->contact(retrievalId(localAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QCOMPARE(localAlice.details().size(), 1); QCOMPARE(localAlice.details().size(), 1); QCOMPARE(localAlice.details().size(), 0); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(aggregateAlice.details().size(), 0); QCOMPARE(localAlice.detail().value(QContactEmailAddress::FieldEmailAddress), QString::fromLatin1("alice4@test.com")); QVERIFY(!detailProvenance(localAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.detail().value(QContactEmailAddress::FieldEmailAddress), QString::fromLatin1("alice4@test.com")); QCOMPARE(detailProvenance(aggregateAlice.detail()), detailProvenance(localAlice.detail())); QCOMPARE(localAlice.detail().value(QContactPhoneNumber::FieldNumber), QString::fromLatin1("4444")); QVERIFY(!detailProvenance(localAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.detail().value(QContactPhoneNumber::FieldNumber), QString::fromLatin1("4444")); QCOMPARE(detailProvenance(aggregateAlice.detail()), detailProvenance(localAlice.detail())); QVERIFY(localAlice.detail().value(QContactHobby::FieldHobby).isEmpty()); QVERIFY(aggregateAlice.detail().value(QContactHobby::FieldHobby).isEmpty()); // now do an update with a definition mask. We need to be certain that no masked details were lost. ae = localAlice.detail(); ae.setEmailAddress("alice4@test4.com"); QVERIFY(localAlice.saveDetail(&ae)); aph = localAlice.detail(); QVERIFY(localAlice.removeDetail(&aph)); // removed, but since we don't include phone number in the definitionMask, shouldn't be applied QList saveList; saveList << localAlice; QVERIFY(m_cm->saveContacts(&saveList, DetailList() << detailType())); // reload them, and compare. localAlice = m_cm->contact(retrievalId(localAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QCOMPARE(localAlice.detail().value(QContactEmailAddress::FieldEmailAddress), QString::fromLatin1("alice4@test4.com")); QCOMPARE(localAlice.details().size(), 1); QCOMPARE(aggregateAlice.detail().value(QContactEmailAddress::FieldEmailAddress), QString::fromLatin1("alice4@test4.com")); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(localAlice.detail().value(QContactPhoneNumber::FieldNumber), QString::fromLatin1("4444")); QCOMPARE(localAlice.details().size(), 1); QCOMPARE(aggregateAlice.detail().value(QContactPhoneNumber::FieldNumber), QString::fromLatin1("4444")); QCOMPARE(aggregateAlice.details().size(), 1); } // we now require updates to occur to constituent contacts; // any attempt to save to an aggregate contact will result in an error. void tst_Aggregation::updateSingleAggregate() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); QSignalSpy chgSpy(m_cm, contactsChangedSignal); int addSpyCount = 0; int chgSpyCount = 0; // now add a new local contact (no synctarget specified == automatically local) QContact alice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("567"); alice.saveDetail(&aph); QContactHobby ah; ah.setHobby("tennis"); alice.saveDetail(&ah); QContactNickname ak; ak.setNickname("Ally"); alice.saveDetail(&ak); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 2); // should have added local + aggregate QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); QContactHobby currHobby = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("567") && currHobby.hobby() == QLatin1String("tennis")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // now attempt update aggregate alice. We expect the operation to fail. QContactEmailAddress ae; // add an email address ae.setEmailAddress("alice5@test.com"); aggregateAlice.saveDetail(&ae); QContactHobby rah = aggregateAlice.detail(); // remove a hobby aggregateAlice.removeDetail(&rah); QContactPhoneNumber maph = aggregateAlice.detail(); // modify a phone number maph.setNumber("555"); aggregateAlice.saveDetail(&maph); chgSpyCount = chgSpy.count(); m_chgAccumulatedIds.clear(); QVERIFY(!m_cm->saveContact(&aggregateAlice)); QTest::qWait(250); QCOMPARE(chgSpy.count(), chgSpyCount); QVERIFY(!m_chgAccumulatedIds.contains(ContactId::apiId(localAlice))); QVERIFY(!m_chgAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); // reload them, and compare. ensure that no changes have occurred. localAlice = m_cm->contact(retrievalId(localAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QCOMPARE(localAlice.details().size(), 0); QCOMPARE(localAlice.details().size(), 1); QCOMPARE(localAlice.details().size(), 1); QCOMPARE(localAlice.details().size(), 1); QCOMPARE(aggregateAlice.details().size(), 0); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(localAlice.detail().value(QContactPhoneNumber::FieldNumber), QLatin1String("567")); QVERIFY(!detailProvenance(localAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.detail().value(QContactPhoneNumber::FieldNumber), QLatin1String("567")); QCOMPARE(detailProvenance(aggregateAlice.detail()), detailProvenance(localAlice.detail())); QCOMPARE(localAlice.detail().value(QContactHobby::FieldHobby), QLatin1String("tennis")); QVERIFY(!detailProvenance(localAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.detail().value(QContactHobby::FieldHobby), QLatin1String("tennis")); QCOMPARE(detailProvenance(aggregateAlice.detail()), detailProvenance(localAlice.detail())); QCOMPARE(localAlice.detail().value(QContactNickname::FieldNickname), QLatin1String("Ally")); QVERIFY(!detailProvenance(localAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.detail().value(QContactNickname::FieldNickname), QLatin1String("Ally")); QCOMPARE(detailProvenance(aggregateAlice.detail()), detailProvenance(localAlice.detail())); } // we now require updates to occur to constituent contacts; // any attempt to save to an aggregate contact will result in an error. void tst_Aggregation::updateAggregateOfLocalAndSync() { QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&remoteAddressbook)); // local alice QContact alice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("PromotedLand"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("11111"); alice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("aliceP@test.com"); alice.saveDetail(&aem); QContactNickname ak; ak.setNickname("Ally"); alice.saveDetail(&ak); QVERIFY(m_cm->saveContact(&alice)); // sync alice QContact syncAlice; syncAlice.setCollectionId(remoteAddressbook.id()); QContactName san; san.setFirstName(an.firstName()); san.setMiddleName(an.middleName()); san.setLastName(an.lastName()); syncAlice.saveDetail(&san); QContactEmailAddress saem; saem.setEmailAddress(aem.emailAddress()); syncAlice.saveDetail(&saem); QContactHobby sah; // this is a "new" detail which doesn't appear in the local contact. sah.setHobby(QLatin1String("tennis")); syncAlice.saveDetail(&sah); QContactNote sanote; // this is a "new" detail which doesn't appear in the local contact. sanote.setNote(QLatin1String("noteworthy note")); syncAlice.saveDetail(&sanote); QContactSyncTarget sast; sast.setSyncTarget(QLatin1String("test")); syncAlice.saveDetail(&sast); QVERIFY(m_cm->saveContact(&syncAlice)); // now grab the aggregate alice QContactRelationshipFilter aggf; setFilterContactId(aggf, alice.id()); aggf.setRelatedContactRole(QContactRelationship::Second); setFilterType(aggf, QContactRelationship::Aggregates); QList allAggregatesOfAlice = m_cm->contacts(aggf); QCOMPARE(allAggregatesOfAlice.size(), 1); QContact aggregateAlice = allAggregatesOfAlice.at(0); // now ensure that any attempt to modify the aggregate directly will fail. QCOMPARE(aggregateAlice.details().size(), 1); // comes from the local contact QContactPhoneNumber maph = aggregateAlice.detail(); QVERIFY((maph.accessConstraints() & QContactDetail::Irremovable) && (maph.accessConstraints() & QContactDetail::ReadOnly)); maph.setNumber("11115"); QVERIFY(!aggregateAlice.saveDetail(&maph)); QCOMPARE(aggregateAlice.details().size(), 1); // there are two, but since the values were identical, should only have one! QContactEmailAddress mem = aggregateAlice.detail(); QVERIFY((mem.accessConstraints() & QContactDetail::Irremovable) && (mem.accessConstraints() & QContactDetail::ReadOnly)); mem.setEmailAddress("aliceP2@test.com"); QVERIFY(!aggregateAlice.saveDetail(&mem)); QCOMPARE(aggregateAlice.details().size(), 1); // comes from the sync contact QContactHobby rah = aggregateAlice.detail(); QVERIFY(rah.accessConstraints() & QContactDetail::Irremovable); QVERIFY(rah.accessConstraints() & QContactDetail::ReadOnly); QVERIFY(!aggregateAlice.removeDetail(&rah)); // this should be irremovable, due to constraint on synced details QContactNote man = aggregateAlice.detail(); QVERIFY(man.accessConstraints() & QContactDetail::Irremovable); QVERIFY(man.accessConstraints() & QContactDetail::ReadOnly); man.setNote("modified note"); QVERIFY(!aggregateAlice.saveDetail(&man)); // this should be read only, due to constraint on synced details // but the attempted modifications should fail, due to modifying an aggregate. QVERIFY(!m_cm->saveContact(&aggregateAlice)); // re-retrieve and ensure we get what we expect. aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QCOMPARE(aggregateAlice.details().size(), 1); // comes from the local contact QVERIFY(!detailProvenance(aggregateAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.details().size(), 1); // comes from the local contact QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), detailProvenanceContact(aggregateAlice.detail())); QCOMPARE(aggregateAlice.detail().value(QContactPhoneNumber::FieldNumber), QString::fromLatin1("11111")); QCOMPARE(aggregateAlice.details().size(), 1); // comes from the sync contact QVERIFY(!detailProvenance(aggregateAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.detail().value(QContactHobby::FieldHobby), QString::fromLatin1("tennis")); QCOMPARE(aggregateAlice.details().size(), 1); // comes from the sync contact QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), detailProvenanceContact(aggregateAlice.detail())); QVERIFY(detailProvenanceContact(aggregateAlice.detail()) != detailProvenanceContact(aggregateAlice.detail())); QCOMPARE(aggregateAlice.detail().value(QContactNote::FieldNote), QString::fromLatin1("noteworthy note")); QList aaems = aggregateAlice.details(); QCOMPARE(aaems.size(), 1); // values should be unchanged (and identical). QCOMPARE(aaems.at(0).emailAddress(), QLatin1String("aliceP@test.com")); } void tst_Aggregation::updateAggregateOfLocalAndModifiableSync() { QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&remoteAddressbook)); QContactCollection remoteAddressbook2; remoteAddressbook2.setMetaData(QContactCollection::KeyName, QStringLiteral("trial")); remoteAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); remoteAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); remoteAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/trial"); QVERIFY(m_cm->saveCollection(&remoteAddressbook2)); // local alice QContact alice; { QContactName name; name.setFirstName("Alice"); name.setMiddleName("In"); name.setLastName("PromotedLand"); alice.saveDetail(&name); QContactNickname nickname; nickname.setNickname("Ally"); alice.saveDetail(&nickname); QContactPhoneNumber aph; aph.setNumber("11111"); alice.saveDetail(&aph); } QVERIFY(m_cm->saveContact(&alice)); const QContactName &localName(alice.detail()); // first syncTarget alice QContact testAlice; testAlice.setCollectionId(remoteAddressbook.id()); { QContactName name; name.setFirstName(localName.firstName()); name.setMiddleName(localName.middleName()); name.setLastName(localName.lastName()); testAlice.saveDetail(&name); QContactRingtone ringtone; ringtone.setAudioRingtoneUrl(QUrl("http://example.org/crickets.mp3")); testAlice.saveDetail(&ringtone); QContactEmailAddress emailAddress; emailAddress.setEmailAddress("aliceP@test.com"); emailAddress.setValue(QContactDetail__FieldModifiable, true); testAlice.saveDetail(&emailAddress); QContactNote note; note.setNote("noteworthy note"); note.setValue(QContactDetail__FieldModifiable, true); testAlice.saveDetail(¬e); QContactHobby hobby; hobby.setHobby("tennis"); hobby.setValue(QContactDetail__FieldModifiable, false); testAlice.saveDetail(&hobby); QContactSyncTarget syncTarget; syncTarget.setSyncTarget("test"); testAlice.saveDetail(&syncTarget); QVERIFY(m_cm->saveContact(&testAlice)); } // second syncTarget alice QContact trialAlice; trialAlice.setCollectionId(remoteAddressbook2.id()); { QContactName name; name.setFirstName(localName.firstName()); name.setMiddleName(localName.middleName()); name.setLastName(localName.lastName()); trialAlice.saveDetail(&name); QContactTag tag; tag.setTag("Fiction"); trialAlice.saveDetail(&tag); QContactEmailAddress emailAddress; emailAddress.setEmailAddress("alice@example.org"); emailAddress.setValue(QContactDetail__FieldModifiable, true); trialAlice.saveDetail(&emailAddress); QContactOrganization organization; organization.setRole("CEO"); organization.setValue(QContactDetail__FieldModifiable, true); trialAlice.saveDetail(&organization); QContactSyncTarget syncTarget; syncTarget.setSyncTarget("trial"); trialAlice.saveDetail(&syncTarget); QVERIFY(m_cm->saveContact(&trialAlice)); } // now grab the aggregate alice QContact aggregateAlice; { QContactRelationshipFilter filter; setFilterContactId(filter, alice.id()); filter.setRelatedContactRole(QContactRelationship::Second); setFilterType(filter, QContactRelationship::Aggregates); QList allAggregatesOfAlice = m_cm->contacts(filter); QCOMPARE(allAggregatesOfAlice.size(), 1); aggregateAlice = allAggregatesOfAlice.at(0); } // Verify the aggregate state QCOMPARE(aggregateAlice.details().size(), 1); QVERIFY(!detailProvenance(aggregateAlice.detail()).isEmpty()); // Nickname found only in the local contact const QString localContact(detailProvenanceContact(aggregateAlice.detail())); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), localContact); QCOMPARE(aggregateAlice.details().size(), 1); QVERIFY(!detailProvenance(aggregateAlice.detail()).isEmpty()); QVERIFY(detailProvenanceContact(aggregateAlice.detail()) != localContact); // Ringtone found only in the 'test' contact const QString teabContact(detailProvenanceContact(aggregateAlice.detail())); QCOMPARE(aggregateAlice.details().size(), 2); QVERIFY(!detailProvenance(aggregateAlice.details().at(0)).isEmpty()); QVERIFY(detailProvenanceContact(aggregateAlice.details().at(0)) != localContact); QVERIFY(!detailProvenance(aggregateAlice.details().at(1)).isEmpty()); QVERIFY(detailProvenanceContact(aggregateAlice.details().at(1)) != localContact); QVERIFY(detailProvenanceContact(aggregateAlice.details().at(0)) != detailProvenanceContact(aggregateAlice.details().at(1))); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), teabContact); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), teabContact); QCOMPARE(aggregateAlice.details().size(), 1); QVERIFY(!detailProvenance(aggregateAlice.detail()).isEmpty()); QVERIFY(detailProvenanceContact(aggregateAlice.detail()) != localContact); QVERIFY(detailProvenanceContact(aggregateAlice.detail()) != teabContact); // Tag found only in the 'trial' contact const QString trialContact(detailProvenanceContact(aggregateAlice.detail())); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), trialContact); // Test the modifiability of the details // Aggregate details are not modifiable QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.details().at(0).value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.details().at(1).value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); // The test contact should have some modifiable fields testAlice = m_cm->contact(retrievalId(testAlice)); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), true); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), true); // The trial contact should also have some modifiable fields trialAlice = m_cm->contact(retrievalId(trialAlice)); QCOMPARE(trialAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(trialAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(trialAlice.detail().value(QContactDetail__FieldModifiable).toBool(), true); QCOMPARE(trialAlice.detail().value(QContactDetail__FieldModifiable).toBool(), true); // Aggregate details which are promoted even from modifiable details are readonly QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.details().at(0).accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.details().at(1).accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); // now ensure that attempts to modify the aggregate contact fail as expected. { // locally-originated detail QContactPhoneNumber phoneNumber = aggregateAlice.detail(); phoneNumber.setNumber("22222"); QVERIFY(!aggregateAlice.saveDetail(&phoneNumber)); // sync constituent details foreach (QContactEmailAddress emailAddress, aggregateAlice.details()) { if (emailAddress.emailAddress() == QString::fromLatin1("aliceP@test.com")) { emailAddress.setEmailAddress("aliceP2@test.com"); QVERIFY(!aggregateAlice.saveDetail(&emailAddress)); } else { emailAddress.setEmailAddress("alice2@example.org"); QVERIFY(!aggregateAlice.saveDetail(&emailAddress)); } } // sync constituent detail which is modifiable in constituent QContactNote note = aggregateAlice.detail(); QVERIFY(!aggregateAlice.removeDetail(¬e)); // sync constituent detail which is modifiable in constituent QContactOrganization organization = aggregateAlice.detail(); QVERIFY(!aggregateAlice.removeDetail(&organization)); // sync constituent detail which is non-modifiable in constituent QContactHobby hobby = aggregateAlice.detail(); hobby.setHobby("crochet"); QVERIFY(!aggregateAlice.saveDetail(&hobby)); } QVERIFY(!m_cm->saveContact(&aggregateAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); // ensure that no changes have occurred. QCOMPARE(aggregateAlice.details().size(), 1); QVERIFY(!detailProvenance(aggregateAlice.detail()).isEmpty()); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), localContact); QCOMPARE(aggregateAlice.details().at(0).number(), QString::fromLatin1("11111")); QList aaeas = aggregateAlice.details(); QCOMPARE(aaeas.size(), 2); if (aaeas.at(0).emailAddress() == QString::fromLatin1("aliceP@test.com")) { QCOMPARE(detailProvenanceContact(aaeas.at(0)), teabContact); QCOMPARE(detailProvenanceContact(aaeas.at(1)), trialContact); QCOMPARE(aaeas.at(1).emailAddress(), QString::fromLatin1("alice@example.org")); } else { QCOMPARE(detailProvenanceContact(aaeas.at(0)), trialContact); QCOMPARE(aaeas.at(0).emailAddress(), QString::fromLatin1("alice@example.org")); QCOMPARE(detailProvenanceContact(aaeas.at(1)), teabContact); QCOMPARE(aaeas.at(1).emailAddress(), QString::fromLatin1("aliceP@test.com")); } QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), teabContact); QCOMPARE(aggregateAlice.details().at(0).note(), QString::fromLatin1("noteworthy note")); QList aahs = aggregateAlice.details(); QCOMPARE(aahs.size(), 1); QCOMPARE(aggregateAlice.details().at(0).hobby(), QString::fromLatin1("tennis")); QCOMPARE(detailProvenanceContact(aahs.at(0)), teabContact); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(detailProvenanceContact(aggregateAlice.detail()), trialContact); QCOMPARE(aggregateAlice.details().at(0).role(), QString::fromLatin1("CEO")); // Modifiability should be unaffected // Aggregate details are not modifiable QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.details().at(0).value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.details().at(1).value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(aggregateAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); // The test contact should have some modifiable fields testAlice = m_cm->contact(retrievalId(testAlice)); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), true); QCOMPARE(testAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); // The trial contact should also have some modifiable fields trialAlice = m_cm->contact(retrievalId(trialAlice)); QCOMPARE(trialAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(trialAlice.detail().value(QContactDetail__FieldModifiable).toBool(), false); QCOMPARE(trialAlice.detail().value(QContactDetail__FieldModifiable).toBool(), true); // Aggregate details which are promoted from modifiable details are still readonly QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.details().at(0).accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.details().at(1).accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.details().at(0).accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); QVERIFY((aggregateAlice.detail().accessConstraints() & QContactDetail::ReadOnly) > 0); } void tst_Aggregation::compositionPrefersLocal() { // Composed details should prefer the values of the local, where present QContactCollectionFilter allCollections; // Create the addressbook collections QContactCollection testCollection1, testCollection2, testCollection3; testCollection1.setMetaData(QContactCollection::KeyName, QStringLiteral("test1")); testCollection1.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testCollection2.setMetaData(QContactCollection::KeyName, QStringLiteral("test2")); testCollection2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testCollection3.setMetaData(QContactCollection::KeyName, QStringLiteral("test3")); testCollection3.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testCollection3.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testCollection3.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test3"); QVERIFY(m_cm->saveCollection(&testCollection1)); QVERIFY(m_cm->saveCollection(&testCollection2)); QVERIFY(m_cm->saveCollection(&testCollection3)); // These contacts should all be aggregated together QContact abContact1, abContact2, abContact3, localContact; QContactName n1; n1.setPrefix(QLatin1String("Supt.")); n1.setFirstName(QLatin1String("Link")); n1.setMiddleName(QLatin1String("Alice")); n1.setLastName(QLatin1String("CompositionTester")); abContact1.saveDetail(&n1); abContact1.setCollectionId(testCollection1.id()); QVERIFY(m_cm->saveContact(&abContact1)); QContactName n2; n2.setFirstName(QLatin1String("Link")); n2.setMiddleName(QLatin1String("Bob")); n2.setLastName(QLatin1String("CompositionTester")); localContact.saveDetail(&n2); QVERIFY(m_cm->saveContact(&localContact)); QContactName n3; n3.setFirstName(QLatin1String("Link")); n3.setMiddleName(QLatin1String("Charlie")); n3.setLastName(QLatin1String("CompositionTester")); n3.setSuffix(QLatin1String("Esq.")); abContact2.saveDetail(&n3); abContact2.setCollectionId(testCollection2.id()); QVERIFY(m_cm->saveContact(&abContact2)); // Add a contact via synchronization QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactName n4; n4.setFirstName(QLatin1String("Link")); n4.setMiddleName(QLatin1String("Donatella")); n4.setLastName(QLatin1String("CompositionTester")); abContact3.saveDetail(&n4); QContactStatusFlags flags; flags.setFlag(QContactStatusFlags::IsAdded, true); abContact3.saveDetail(&flags, QContact::IgnoreAccessConstraints); QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy policy(QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges); QContactManager::Error err; QHash *> additions; QHash *> modifications; QList modifiedContacts; modifiedContacts.append(abContact3); // in this case, the change is an addition. modifications.insert(&testCollection3, &modifiedContacts); QVERIFY(cme->storeChanges( &additions, // not adding any collections &modifications, QList(), // not deleting any collections policy, true, &err)); QList allContacts = m_cm->contacts(allCollections); QContact abc1, abc2, abc3, l, a; // addressbook contacts 1,2,3, local, aggregate. foreach (const QContact &curr, allContacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Link") && currName.lastName() == QLatin1String("CompositionTester")) { if (curr.collectionId() == testCollection1.id()) { abc1 = curr; } else if (curr.collectionId() == testCollection2.id()) { abc2 = curr; } else if (curr.collectionId() == testCollection3.id()) { abc3 = curr; } else if (curr.collectionId().localId() == localAddressbookId()) { l = curr; } else if (curr.collectionId().localId() == aggregateAddressbookId()) { a = curr; } } } QVERIFY(abc1.id() != QContactId()); QVERIFY(abc2.id() != QContactId()); QVERIFY(abc3.id() != QContactId()); QVERIFY(l.id() != QContactId()); QVERIFY(a.id() != QContactId()); QVERIFY(abc1.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(a.id())); QVERIFY(a.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(abc1.id())); QVERIFY(abc2.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(a.id())); QVERIFY(a.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(abc2.id())); QVERIFY(abc3.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(a.id())); QVERIFY(a.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(abc3.id())); QVERIFY(l.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(a.id())); QVERIFY(a.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(l.id())); // The name of the local contact should be prioritized in aggregation QContactName name(a.detail()); QCOMPARE(name.middleName(), n2.middleName()); // The name elements unspecified by the local should be filled by other constituents in indeterminate order QCOMPARE(name.prefix(), n1.prefix()); QCOMPARE(name.suffix(), n3.suffix()); // Change the names in non-local constituents n1 = abc1.detail(); n1.setPrefix(QLatin1String("Dr.")); n1.setMiddleName(QLatin1String("Enzo")); abc1.saveDetail(&n1); QVERIFY(m_cm->saveContact(&abc1)); // Update with a definition mask n3 = abc2.detail(); n3.setMiddleName(QLatin1String("Francois")); n3.setSuffix(QLatin1String("MBA")); abc2.saveDetail(&n3); QList saveList; saveList.append(abc2); QVERIFY(m_cm->saveContacts(&saveList, QList() << QContactName::Type)); a = m_cm->contact(retrievalId(a)); name = a.detail(); QCOMPARE(name.middleName(), n2.middleName()); QCOMPARE(name.prefix(), n1.prefix()); QCOMPARE(name.suffix(), n3.suffix()); // Update with a definition mask not including name (should not update, but local still has priority) QContactName n5 = abc2.detail(); n5.setMiddleName(QLatin1String("Guillermo")); n5.setSuffix(QLatin1String("Ph.D")); abc2.saveDetail(&n5); saveList.clear(); saveList.append(abc2); QVERIFY(m_cm->saveContacts(&saveList, QList() << QContactAvatar::Type)); a = m_cm->contact(retrievalId(a)); name = a.detail(); QCOMPARE(name.middleName(), n2.middleName()); QCOMPARE(name.prefix(), n1.prefix()); QCOMPARE(name.suffix(), n3.suffix()); // Update via synchronization QContact modified; { QList addedContacts; QList modifiedContacts; QList deletedContacts; QList unmodifiedContacts; QVERIFY(cme->fetchContactChanges( testCollection3.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 1); modified = unmodifiedContacts.first(); } n4 = modified.detail(); n4.setMiddleName(QLatin1String("Hector")); modified.saveDetail(&n4); // mark the contact as modified for the sync operation. flags = modified.detail(); flags.setFlag(QContactStatusFlags::IsModified, true); modified.saveDetail(&flags, QContact::IgnoreAccessConstraints); modifications.clear(); modifiedContacts.clear(); modifiedContacts.append(modified); modifications.insert(&testCollection3, &modifiedContacts); QVERIFY(cme->storeChanges( &additions, // not adding any collections &modifications, QList(), // not deleting any collections policy, true, &err)); a = m_cm->contact(retrievalId(a)); l = m_cm->contact(retrievalId(l)); // The sync update will not update the local. // Since the local data is preferred for aggregation, the aggregate will not update. name = a.detail(); QCOMPARE(name.middleName(), n2.middleName()); QCOMPARE(name.prefix(), n1.prefix()); QCOMPARE(name.suffix(), n3.suffix()); name = l.detail(); QCOMPARE(name.middleName(), n2.middleName()); // unchanged // Local changes override other changes n2 = l.detail(); n2.setPrefix(QLatin1String("Monsignor")); n2.setMiddleName(QLatin1String("Isaiah")); l.saveDetail(&n2); QVERIFY(m_cm->saveContact(&l)); a = m_cm->contact(retrievalId(a)); name = a.detail(); QCOMPARE(name.middleName(), n2.middleName()); QCOMPARE(name.prefix(), n2.prefix()); QCOMPARE(name.suffix(), n3.suffix()); // Local details should still be preferred name = a.detail(); QCOMPARE(name.middleName(), n2.middleName()); QCOMPARE(name.prefix(), n2.prefix()); QCOMPARE(name.suffix(), n3.suffix()); } void tst_Aggregation::uniquenessConstraints() { QContactCollectionFilter allCollections; // create a valid local contact. An aggregate should be generated. QContact localAlice; QContactName an; an.setFirstName("Uniqueness"); an.setLastName("Constraints"); QVERIFY(localAlice.saveDetail(&an)); QContactEmailAddress aem; aem.setEmailAddress("uniqueness@test.com"); QVERIFY(localAlice.saveDetail(&aem)); QContactGuid ag; ag.setGuid("first-unique-guid"); QVERIFY(localAlice.saveDetail(&ag)); QContactFavorite afav; afav.setFavorite(false); QVERIFY(localAlice.saveDetail(&afav)); QVERIFY(m_cm->saveContact(&localAlice)); QList allContacts = m_cm->contacts(allCollections); QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Uniqueness") && currName.lastName() == QLatin1String("Constraints") && currEm.emailAddress() == QLatin1String("uniqueness@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); // test uniqueness constraint of favorite detail. QCOMPARE(aggregateAlice.details().size(), 1); afav = localAlice.detail(); QContactFavorite afav2; afav2.setFavorite(true); QVERIFY(localAlice.saveDetail(&afav2)); // this actually creates a second (in memory) favorite detail QCOMPARE(localAlice.details().size(), 2); QVERIFY(!m_cm->saveContact(&localAlice)); // should fail, as Favorite is unique QVERIFY(localAlice.removeDetail(&afav2)); afav = localAlice.detail(); afav.setFavorite(true); QVERIFY(localAlice.saveDetail(&afav)); QCOMPARE(localAlice.details().size(), 1); QVERIFY(m_cm->saveContact(&localAlice)); // should succeed. QCOMPARE(localAlice.details().size(), 1); QVERIFY(m_cm->contact(retrievalId(aggregateAlice)).detail().isFavorite()); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); // test uniqueness constraint of birthday detail. QDateTime aliceBirthday = QLocale::c().toDateTime("25/12/1950 01:23:45", "dd/MM/yyyy hh:mm:ss"); QCOMPARE(aggregateAlice.details().size(), 0); QContactBirthday abd; abd.setDateTime(aliceBirthday); QVERIFY(localAlice.saveDetail(&abd)); QCOMPARE(localAlice.details().size(), 1); QVERIFY(m_cm->saveContact(&localAlice)); // now save another, should fail. QContactBirthday anotherBd; anotherBd.setDateTime(QDateTime::currentDateTime()); QVERIFY(localAlice.saveDetail(&anotherBd)); QCOMPARE(localAlice.details().size(), 2); QVERIFY(!m_cm->saveContact(&localAlice)); // should fail, uniqueness. QVERIFY(localAlice.removeDetail(&anotherBd)); QVERIFY(m_cm->saveContact(&localAlice)); // back to just one, should succeed. QVERIFY(m_cm->contact(retrievalId(aggregateAlice)).detail().date() == aliceBirthday.date()); // now save a different birthday in another contact aggregated into alice. QContactCollection testCollection1; testCollection1.setMetaData(QContactCollection::KeyName, QStringLiteral("test1")); QVERIFY(m_cm->saveCollection(&testCollection1)); QContact testsyncAlice; testsyncAlice.setCollectionId(testCollection1.id()); QContactBirthday tsabd; tsabd.setDateTime(aliceBirthday.addDays(-5)); testsyncAlice.saveDetail(&tsabd); QContactName tsaname; tsaname.setFirstName(an.firstName()); tsaname.setLastName(an.lastName()); testsyncAlice.saveDetail(&tsaname); QContactEmailAddress tsaem; tsaem.setEmailAddress(aem.emailAddress()); testsyncAlice.saveDetail(&tsaem); QContactNote tsanote; tsanote.setNote("noteworthy note"); testsyncAlice.saveDetail(&tsanote); QContactSyncTarget tsast; tsast.setSyncTarget("test1"); testsyncAlice.saveDetail(&tsast); QVERIFY(m_cm->saveContact(&testsyncAlice)); // should get aggregated into aggregateAlice. aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); // reload QCOMPARE(aggregateAlice.details().size(), 1); // should still only have one birthday - local should take precedence. QCOMPARE(aggregateAlice.detail().date(), aliceBirthday.date()); QCOMPARE(aggregateAlice.detail().note(), tsanote.note()); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); localAlice = m_cm->contact(retrievalId(localAlice)); // test uniqueness constraint of name detail. QVERIFY(localAlice.details().size() == 1); QContactName anotherName; anotherName.setFirstName("Testing"); QVERIFY(localAlice.saveDetail(&anotherName)); QCOMPARE(localAlice.details().size(), 2); QVERIFY(!m_cm->saveContact(&localAlice)); QVERIFY(localAlice.removeDetail(&anotherName)); QCOMPARE(localAlice.details().size(), 1); anotherName = localAlice.detail(); anotherName.setMiddleName("Middle"); QVERIFY(localAlice.saveDetail(&anotherName)); QVERIFY(m_cm->saveContact(&localAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); localAlice = m_cm->contact(retrievalId(localAlice)); QCOMPARE(aggregateAlice.detail().firstName(), localAlice.detail().firstName()); QCOMPARE(aggregateAlice.detail().middleName(), localAlice.detail().middleName()); QCOMPARE(aggregateAlice.detail().lastName(), localAlice.detail().lastName()); // test uniqueness (and non-promotion) constraint of sync target. QVERIFY(aggregateAlice.details().size() == 0); QContactSyncTarget tsast2; tsast2.setSyncTarget("uniqueness"); QVERIFY(testsyncAlice.saveDetail(&tsast2)); QCOMPARE(testsyncAlice.details().size(), 2); QVERIFY(!m_cm->saveContact(&testsyncAlice)); // uniqueness constraint fails. QVERIFY(testsyncAlice.removeDetail(&tsast2)); QCOMPARE(testsyncAlice.details().size(), 1); tsast2 = testsyncAlice.detail(); tsast2.setSyncTarget("uniqueness"); QVERIFY(testsyncAlice.saveDetail(&tsast2)); QVERIFY(m_cm->saveContact(&testsyncAlice)); // should now succeed. aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QVERIFY(aggregateAlice.details().size() == 0); // but not promoted to aggregate. localAlice = m_cm->contact(retrievalId(localAlice)); QVERIFY(localAlice.details().size() == 0); // and localAlice should never be affected by operations to testsyncAlice. // test uniqueness constraint of timestamp detail. // Timestamp is a bit special, since if no values exist, we don't synthesise it, // even though it exists in the main table. QDateTime testDt = QDateTime::currentDateTime(); bool hasCreatedTs = false; if (testsyncAlice.details().size() == 0) { QContactTimestamp firstTs; firstTs.setCreated(testDt); QVERIFY(testsyncAlice.saveDetail(&firstTs)); QVERIFY(m_cm->saveContact(&testsyncAlice)); hasCreatedTs = true; } aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QVERIFY(aggregateAlice.details().size() == 1); QContactTimestamp ats; ats.setLastModified(testDt); QVERIFY(testsyncAlice.saveDetail(&ats)); QCOMPARE(testsyncAlice.details().size(), 2); QVERIFY(!m_cm->saveContact(&testsyncAlice)); QVERIFY(testsyncAlice.removeDetail(&ats)); QCOMPARE(testsyncAlice.details().size(), 1); ats = testsyncAlice.detail(); ats.setLastModified(testDt); QVERIFY(testsyncAlice.saveDetail(&ats)); QDateTime beforeWrite(QDateTime::currentDateTimeUtc()); QTest::qWait(11); QVERIFY(m_cm->saveContact(&testsyncAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QVERIFY(aggregateAlice.details().size() == 1); QVERIFY(aggregateAlice.detail().lastModified() >= beforeWrite); QVERIFY(aggregateAlice.detail().lastModified() <= QDateTime::currentDateTimeUtc()); if (hasCreatedTs) { QCOMPARE(aggregateAlice.detail().created(), testDt); } // GUID is no longer a singular detail QVERIFY(localAlice.details().size() == 1); QContactGuid ag2; ag2.setGuid("second-unique-guid"); QVERIFY(localAlice.saveDetail(&ag2)); QCOMPARE(localAlice.details().size(), 2); QVERIFY(m_cm->saveContact(&localAlice)); localAlice = m_cm->contact(retrievalId(localAlice)); QCOMPARE(localAlice.details().size(), 2); // GUIDs are not promoted aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QCOMPARE(aggregateAlice.details().size(), 0); } void tst_Aggregation::removeSingleLocal() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); int oldAggCount = aggCount; int oldAllCount = allCount; // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); QSignalSpy remSpy(m_cm, contactsRemovedSignal); int addSpyCount = 0; int remSpyCount = 0; // now add a new local contact (no collectionId specified == automatically local) QContact alice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("67"); alice.saveDetail(&aph); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 2); // should have added local + aggregate QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("67")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // now add another local contact. QContact bob; QContactName bn; bn.setFirstName("Bob7"); bn.setMiddleName("The"); bn.setLastName("Constructor"); QContactPhoneNumber bp; bp.setNumber("777"); bob.saveDetail(&bn); bob.saveDetail(&bp); QVERIFY(m_cm->saveContact(&bob)); // we should have an extra aggregate (bob's) now too aggCount = m_cm->contactIds().size(); // now remove local alice. We expect that the "orphan" aggregate alice will also be removed. remSpyCount = remSpy.count(); m_remAccumulatedIds.clear(); QVERIFY(m_cm->removeContact(removalId(localAlice))); QTRY_VERIFY(remSpy.count() > remSpyCount); QTRY_VERIFY(m_remAccumulatedIds.contains(ContactId::apiId(localAlice))); QTRY_VERIFY(m_remAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); // alice's aggregate contact should have been removed, bob's should not have. QCOMPARE(m_cm->contactIds().size(), (aggCount-1)); // but bob should not have been removed. QVERIFY(m_cm->contactIds(allCollections).contains(ContactId::apiId(bob))); QList stillExisting = m_cm->contacts(allCollections); bool foundBob = false; foreach (const QContact &c, stillExisting) { if (c.id() == bob.id()) { foundBob = true; break; } } QVERIFY(foundBob); // now remove bob. QVERIFY(m_cm->removeContact(removalId(bob))); QVERIFY(!m_cm->contactIds(allCollections).contains(ContactId::apiId(bob))); // should be back to our original counts int newAggCount = m_cm->contactIds().size(); int newAllCount = m_cm->contactIds(allCollections).size(); QCOMPARE(newAggCount, oldAggCount); QCOMPARE(newAllCount, oldAllCount); } void tst_Aggregation::removeSingleAggregate() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); int oldAggCount = aggCount; int oldAllCount = allCount; // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); QSignalSpy remSpy(m_cm, contactsRemovedSignal); int addSpyCount = 0; int remSpyCount = 0; // now add a new local contact (no collectionId specified == automatically local) QContact alice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("7"); alice.saveDetail(&aph); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 2); // should have added local + aggregate QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("7")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // now add another local contact. QContact bob; QContactName bn; bn.setFirstName("Bob7"); bn.setMiddleName("The"); bn.setLastName("Constructor"); QContactPhoneNumber bp; bp.setNumber("777"); bob.saveDetail(&bn); bob.saveDetail(&bp); QVERIFY(m_cm->saveContact(&bob)); // we should have an extra aggregate (bob's) now too aggCount = m_cm->contactIds().size(); // now attempt to remove aggregate alice - should fail. remSpyCount = remSpy.count(); m_remAccumulatedIds.clear(); QVERIFY(!m_cm->removeContact(removalId(aggregateAlice))); QTest::qWait(50); QCOMPARE(remSpy.count(), remSpyCount); QVERIFY(!m_remAccumulatedIds.contains(ContactId::apiId(localAlice))); QVERIFY(!m_remAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); // now attempt to remove local alice - should succeed, and her "orphan" aggregate should be removed also. QVERIFY(m_cm->removeContact(removalId(localAlice))); QTRY_VERIFY(remSpy.count() > remSpyCount); QTRY_VERIFY(m_remAccumulatedIds.contains(ContactId::apiId(localAlice))); QTRY_VERIFY(m_remAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); // alice's aggregate contact should have been removed, bob's should not have. QCOMPARE(m_cm->contactIds().size(), (aggCount-1)); // and bob should not have been removed. QVERIFY(m_cm->contactIds(allCollections).contains(ContactId::apiId(bob))); QList stillExisting = m_cm->contacts(allCollections); bool foundBob = false; foreach (const QContact &c, stillExisting) { if (c.id() == bob.id()) { foundBob = true; break; } } QVERIFY(foundBob); // now remove bob. QVERIFY(m_cm->removeContact(removalId(bob))); QVERIFY(!m_cm->contactIds(allCollections).contains(ContactId::apiId(bob))); // should be back to our original counts int newAggCount = m_cm->contactIds().size(); int newAllCount = m_cm->contactIds(allCollections).size(); QCOMPARE(newAggCount, oldAggCount); QCOMPARE(newAllCount, oldAllCount); } void tst_Aggregation::alterRelationships() { QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); QSignalSpy remSpy(m_cm, contactsRemovedSignal); int addSpyCount = 0; int remSpyCount = 0; // add two test collections QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContactCollection trialAddressbook; trialAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("trial")); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/trial"); QVERIFY(m_cm->saveCollection(&trialAddressbook)); // now add two new contacts (with different collection ids) QContact alice; alice.setCollectionId(testAddressbook.id()); QContactName an; an.setMiddleName("Alice"); an.setFirstName("test"); an.setLastName("alterRelationships"); alice.saveDetail(&an); // Add a detail with non-empty detail URI - during the alteration, a duplicate // of the linked detail URI will exist in each aggregate, until the obsolete // aggregate is removed QContactPhoneNumber ap; ap.setNumber("1234567"); ap.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); ap.setDetailUri("alice-alterRelationships-phone"); alice.saveDetail(&ap); QContact bob; bob.setCollectionId(trialAddressbook.id()); QContactName bn; bn.setMiddleName("Bob"); bn.setLastName("alterRelationships"); bob.saveDetail(&bn); QContactPhoneNumber bp; bp.setNumber("2345678"); bp.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); bp.setDetailUri("bob-alterRelationships-phone"); bob.saveDetail(&bp); QContactEmailAddress bem; bem.setEmailAddress("bob@wonderland.tld"); bob.saveDetail(&bem); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QVERIFY(m_cm->saveContact(&bob)); QTRY_VERIFY(addSpy.count() >= addSpyCount + 2); QTRY_COMPARE(m_addAccumulatedIds.size(), 4); // should have added locals + aggregates QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(bob))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 2); // 2 extra aggregate contacts aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 4); // should have added 2 normal + 2 aggregates allCount = m_cm->contactIds(allCollections).size(); QContact localAlice; QContact localBob; QContact aggregateAlice; QContact aggregateBob; QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. foreach (const QContact &curr, allContacts) { QContactName currName = curr.detail(); if (currName.middleName() == QLatin1String("Alice") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == testAddressbook.id()) { localAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } else if (currName.middleName() == QLatin1String("Bob") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == trialAddressbook.id()) { localBob = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateBob = curr; } } } QVERIFY(localAlice.id() != QContactId()); QVERIFY(localBob.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(aggregateBob.id() != QContactId()); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(localBob.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateBob.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(aggregateBob.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localBob.id())); // Remove the aggregation relationship for Bob QContactRelationship relationship; relationship = makeRelationship(QContactRelationship::Aggregates, aggregateBob.id(), localBob.id()); QVERIFY(m_cm->removeRelationship(relationship)); // The childless aggregate should have been removed QTRY_VERIFY(remSpy.count() > remSpyCount); QTRY_COMPARE(m_remAccumulatedIds.size(), 1); QVERIFY(m_remAccumulatedIds.contains(ContactId::apiId(aggregateBob))); remSpyCount = remSpy.count(); // A new aggregate should have been generated QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 5); addSpyCount = addSpy.count(); // Verify the relationships QContactId oldAggregateBobId = aggregateBob.id(); localAlice = QContact(); localBob = QContact(); aggregateAlice = QContact(); aggregateBob = QContact(); allContacts = m_cm->contacts(allCollections); foreach (const QContact &curr, allContacts) { QContactName currName = curr.detail(); if (currName.middleName() == QLatin1String("Alice") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == testAddressbook.id()) { localAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } else if (currName.middleName() == QLatin1String("Bob") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == trialAddressbook.id()) { localBob = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateBob = curr; } } } QVERIFY(localAlice.id() != QContactId()); QVERIFY(localBob.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(aggregateBob.id() != QContactId()); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(localBob.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateBob.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(aggregateBob.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localBob.id())); QVERIFY(aggregateBob.id() != oldAggregateBobId); // Aggregate localBob into aggregateAlice relationship = makeRelationship(QContactRelationship::Aggregates, aggregateAlice.id(), localBob.id()); QVERIFY(m_cm->saveRelationship(&relationship)); // Remove the relationship between localBob and aggregateBob relationship = makeRelationship(QContactRelationship::Aggregates, aggregateBob.id(), localBob.id()); QVERIFY(m_cm->removeRelationship(relationship)); // The childless aggregate should have been removed QTRY_VERIFY(remSpy.count() > remSpyCount); QTRY_COMPARE(m_remAccumulatedIds.size(), 2); QVERIFY(m_remAccumulatedIds.contains(ContactId::apiId(aggregateBob))); remSpyCount = remSpy.count(); // No new aggregate should have been generated waitForSignalPropagation(); QCOMPARE(addSpy.count(), addSpyCount); QCOMPARE(m_addAccumulatedIds.size(), 5); // Verify the relationships localAlice = QContact(); localBob = QContact(); aggregateAlice = QContact(); aggregateBob = QContact(); allContacts = m_cm->contacts(allCollections); foreach (const QContact &curr, allContacts) { QContactName currName = curr.detail(); if (currName.middleName() == QLatin1String("Alice") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == testAddressbook.id()) { localAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } else if (currName.middleName() == QLatin1String("Bob") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == trialAddressbook.id()) { localBob = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateBob = curr; } } } QVERIFY(localAlice.id() != QContactId()); QVERIFY(localBob.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QCOMPARE(aggregateBob.id(), QContactId()); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(localBob.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localBob.id())); // ensure that Bob's details have been promoted to the aggregateAlice contact. QCOMPARE(aggregateAlice.detail().emailAddress(), localBob.detail().emailAddress()); QCOMPARE(aggregateAlice.details().size(), 2); // Change Bob to have the same first and last name details as Alice bn = localBob.detail(); bn.setFirstName("test"); localBob.saveDetail(&bn); QVERIFY(m_cm->saveContact(&localBob)); // Test removing a relationship from a multi-child aggregate relationship = makeRelationship(QContactRelationship::Aggregates, aggregateAlice.id(), localAlice.id()); QVERIFY(m_cm->removeRelationship(relationship)); // No aggregate will be removed waitForSignalPropagation(); QCOMPARE(remSpy.count(), remSpyCount); QCOMPARE(m_remAccumulatedIds.size(), 2); // No new aggregate should have been generated, since the aggregation process will find // the existing aggregate as the best candidate (due to same first/last name) // Note - this test is failing with qt4; the match-finding query is failing to find the // existing match, due to some error in binding values that I can't work out right now... QCOMPARE(addSpy.count(), addSpyCount); QCOMPARE(m_addAccumulatedIds.size(), 5); // Verify that the relationships are unchanged localAlice = m_cm->contact(retrievalId(localAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // Create an IsNot relationship to prevent re-aggregation relationship = makeRelationship(QString::fromLatin1("IsNot"), aggregateAlice.id(), localAlice.id()); QVERIFY(m_cm->saveRelationship(&relationship)); // Now remove the aggregation again relationship = makeRelationship(QContactRelationship::Aggregates, aggregateAlice.id(), localAlice.id()); QVERIFY(m_cm->removeRelationship(relationship)); // No aggregate will be removed waitForSignalPropagation(); QCOMPARE(remSpy.count(), remSpyCount); QCOMPARE(m_remAccumulatedIds.size(), 2); // A new aggregate should have been generated, since the aggregation can't use the existing match QTRY_VERIFY(addSpy.count() > addSpyCount); QTRY_COMPARE(m_addAccumulatedIds.size(), 6); addSpyCount = addSpy.count(); // Verify that the relationships are updated localAlice = m_cm->contact(retrievalId(localAlice)); aggregateAlice = m_cm->contact(retrievalId(aggregateAlice)); QCOMPARE(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).size(), 1); QVERIFY(!localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(!aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // ensure that each aggregate contains the details from the respective constituent contact. aggregateBob = aggregateAlice; // this one now only aggregates Bob QCOMPARE(aggregateBob.details().size(), 1); QCOMPARE(aggregateBob.detail().number(), localBob.detail().number()); QCOMPARE(aggregateBob.details().size(), 1); QCOMPARE(aggregateBob.detail().emailAddress(), localBob.detail().emailAddress()); aggregateAlice = m_cm->contact(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).first()); QCOMPARE(aggregateAlice.details().size(), 1); QCOMPARE(aggregateAlice.detail().number(), localAlice.detail().number()); QCOMPARE(aggregateAlice.details().size(), 0); // finally, create two new local addresbook contacts, // which we will later manually aggregate together. QContact localEdmond; QContactName en; en.setMiddleName("Edmond"); en.setFirstName("test"); en.setLastName("alterRelationshipsLocal"); localEdmond.saveDetail(&en); QContactPhoneNumber ep; ep.setNumber("8765432"); ep.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); ep.setDetailUri("edmond-alterRelationships-phone"); localEdmond.saveDetail(&ep); QContactHobby eh; eh.setHobby("Surfing"); localEdmond.saveDetail(&eh); QContactGuid eg; eg.setGuid("alterRelationships-Edmond"); localEdmond.saveDetail(&eg); QContact localFred; QContactName fn; fn.setMiddleName("Fred"); fn.setFirstName("trial"); fn.setLastName("alterRelationshipsLocal"); localFred.saveDetail(&fn); QContactPhoneNumber fp; fp.setNumber("9876543"); fp.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); fp.setDetailUri("fred-alterRelationships-phone"); localFred.saveDetail(&fp); QContactHobby fh; fh.setHobby("Bowling"); localFred.saveDetail(&fh); QContactGuid fg; fg.setGuid("alterRelationships-Fred"); localFred.saveDetail(&fg); QVERIFY(m_cm->saveContact(&localEdmond)); QVERIFY(m_cm->saveContact(&localFred)); // fetch the contacts, ensure we have what we expect. QContact aggregateEdmond, aggregateFred; allContacts = m_cm->contacts(allCollections); foreach (const QContact &curr, allContacts) { QContactName currName = curr.detail(); if (currName.middleName() == QLatin1String("Alice") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == testAddressbook.id()) { localAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } else if (currName.middleName() == QLatin1String("Bob") && currName.lastName() == QLatin1String("alterRelationships")) { if (curr.collectionId() == trialAddressbook.id()) { localBob = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateBob = curr; } } else if (currName.middleName() == QLatin1String("Edmond") && currName.lastName() == QLatin1String("alterRelationshipsLocal")) { if (curr.collectionId().localId() == localAddressbookId()) { localEdmond = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateEdmond = curr; } } else if (currName.middleName() == QLatin1String("Fred") && currName.lastName() == QLatin1String("alterRelationshipsLocal")) { if (curr.collectionId().localId() == localAddressbookId()) { localFred = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateFred = curr; } } } QCOMPARE(localEdmond.collectionId().localId(), localAddressbookId()); QCOMPARE(localFred.collectionId().localId(), localAddressbookId()); QCOMPARE(aggregateEdmond.collectionId().localId(), aggregateAddressbookId()); QCOMPARE(aggregateFred.collectionId().localId(), aggregateAddressbookId()); remSpyCount = remSpy.count(); // Aggregate localFred into aggregateEdmond relationship = makeRelationship(QContactRelationship::Aggregates, aggregateEdmond.id(), localFred.id()); QVERIFY(m_cm->saveRelationship(&relationship)); // Remove the relationship between localFred and aggregateFred relationship = makeRelationship(QContactRelationship::Aggregates, aggregateFred.id(), localFred.id()); QVERIFY(m_cm->removeRelationship(relationship)); // The childless aggregate should have been removed QTRY_VERIFY(remSpy.count() > remSpyCount); QVERIFY(m_remAccumulatedIds.contains(ContactId::apiId(aggregateFred))); remSpyCount = remSpy.count(); // Reload the aggregateEdmond, and ensure that it has the required details. aggregateEdmond = m_cm->contact(aggregateEdmond.id()); QCOMPARE(aggregateEdmond.details().size(), 2); QCOMPARE(aggregateEdmond.details().size(), 2); QVERIFY((aggregateEdmond.details().at(0).number() == ep.number() || aggregateEdmond.details().at(1).number() == ep.number()) && (aggregateEdmond.details().at(0).number() == fp.number() || aggregateEdmond.details().at(1).number() == fp.number())); QVERIFY((aggregateEdmond.details().at(0).hobby() == eh.hobby() || aggregateEdmond.details().at(1).hobby() == eh.hobby()) && (aggregateEdmond.details().at(0).hobby() == fh.hobby() || aggregateEdmond.details().at(1).hobby() == fh.hobby())); } void tst_Aggregation::aggregationHeuristic_data() { QTest::addColumn("shouldAggregate"); QTest::addColumn("aFirstName"); QTest::addColumn("aMiddleName"); QTest::addColumn("aLastName"); QTest::addColumn("aNickname"); QTest::addColumn("aGender"); QTest::addColumn("aPhoneNumber"); QTest::addColumn("aEmailAddress"); QTest::addColumn("aOnlineAccount"); QTest::addColumn("bFirstName"); QTest::addColumn("bMiddleName"); QTest::addColumn("bLastName"); QTest::addColumn("bNickname"); QTest::addColumn("bGender"); QTest::addColumn("bPhoneNumber"); QTest::addColumn("bEmailAddress"); QTest::addColumn("bOnlineAccount"); // shared details / family members QTest::newRow("shared email") << false // husband and wife, sharing email, should not get aggregated << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "" << "gumboots@test.com" << "" << "Jillian" << "Anastacia Faith" << "Gumboots" << "Jilly" << "unspecified" << "" << "gumboots@test.com" << ""; QTest::newRow("shared phone") << false // husband and wife, sharing phone, should not get aggregated << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "" << "" << "Jillian" << "Anastacia Faith" << "Gumboots" << "Jilly" << "unspecified" << "111992888337" << "" << ""; QTest::newRow("shared phone+email") << false // husband and wife, sharing phone+email, should not get aggregated << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "" << "Jillian" << "Anastacia Faith" << "Gumboots" << "Jilly" << "unspecified" << "111992888337" << "gumboots@test.com" << ""; QTest::newRow("shared phone+email+account") << false // husband and wife, sharing phone+email+account, should not get aggregated << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "gumboots@familysocial" << "Jillian" << "Anastacia Faith" << "Gumboots" << "Jilly" << "unspecified" << "111992888337" << "gumboots@test.com" << "gumboots@familysocial"; // different contactable details / same name QTest::newRow("match name, different p/e/a") << true // identical name match is enough to match the contact << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; QTest::newRow("match name insentive, different p/e/a") << true << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "frederick" << "william preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; QTest::newRow("match hyphenated name, different p/e/a") << true << "Frederick-Albert" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick-Albert" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; QTest::newRow("match hyphenated name insensitive, different p/e/a") << true << "Frederick-Albert" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "frederick-albert" << "william preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; // identical contacts should be aggregated QTest::newRow("identical, complete") << true << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount"; QTest::newRow("identical, -fname") << true << "" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount"; QTest::newRow("identical, -mname") << true << "Frederick" << "" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount"; QTest::newRow("identical, -lname") << true << "Frederick" << "William Preston" << "" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount"; QTest::newRow("identical, -nick") << true << "Frederick" << "William Preston" << "Gumboots" << "" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "Gumboots" << "" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount"; QTest::newRow("identical, -phone") << true << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "" << "gumboots@test.com" << "freddy00001@socialaccount"; QTest::newRow("identical, -email") << true << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "" << "freddy00001@socialaccount"; QTest::newRow("identical, -account") << true << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "" << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << ""; QTest::newRow("identical, diff nick") << true << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "Gumboots" << "Ricky" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount"; // f/l name differences should stop aggregation. middle name doesn't count in the aggregation heuristic. QTest::newRow("fname different") << false << "Frederick" << "" << "Gumboots" << "" << "unspecified" << "111992888337" << "" << "" << "Jillian" << "" << "Gumboots" << "" << "unspecified" << "999118222773" << "" << ""; QTest::newRow("lname different") << false << "Frederick" << "" << "Gumboots" << "" << "unspecified" << "111992888337" << "" << "" << "Frederick" << "" << "Galoshes" << "" << "unspecified" << "999118222773" << "" << ""; // similarities in name, different contactable details QTest::newRow("similar name, different p/e/a") << false // Only the last names match; not enough << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "" << "" << "Gumboots" << "" << "unspecified" << "999118222773" << "anastacia@test.net" << "agumboots@coolsocial"; // Gender differences prevent aggregation QTest::newRow("no gender specified") << true << "Sam" << "" << "Gumboots" << "Freddy" << "unspecified" << "" << "" << "" << "Sam" << "" << "Gumboots" << "Freddy" << "unspecified" << "" << "" << ""; QTest::newRow("one gender specified male") << true << "Sam" << "" << "Gumboots" << "Freddy" << "Male" << "" << "" << "" << "Sam" << "" << "Gumboots" << "Freddy" << "unspecified" << "" << "" << ""; QTest::newRow("one gender specified female") << true << "Sam" << "" << "Gumboots" << "Freddy" << "Female" << "" << "" << "" << "Sam" << "" << "Gumboots" << "Freddy" << "unspecified" << "" << "" << ""; QTest::newRow("gender match male") << true << "Sam" << "" << "Gumboots" << "Freddy" << "Male" << "" << "" << "" << "Sam" << "" << "Gumboots" << "Freddy" << "Male" << "" << "" << ""; QTest::newRow("gender match female") << true << "Sam" << "" << "Gumboots" << "Freddy" << "Female" << "" << "" << "" << "Sam" << "" << "Gumboots" << "Freddy" << "Female" << "" << "" << ""; QTest::newRow("gender mismatch") << false << "Sam" << "" << "Gumboots" << "Freddy" << "Male" << "" << "" << "" << "Sam" << "" << "Gumboots" << "Freddy" << "Female" << "" << "" << ""; // Nicknames should cause aggregation in the absence of real names QTest::newRow("nickname match") << true << "" << "" << "" << "Freddy" << "unspecified" << "" << "" << "" << "" << "" << "" << "Freddy" << "unspecified" << "" << "" << ""; QTest::newRow("nickname mismatch") << false << "" << "" << "" << "Freddy" << "unspecified" << "" << "" << "" << "" << "" << "" << "Buster" << "unspecified" << "" << "" << ""; QTest::newRow("nickname match with firstname") << false << "Frederick" << "" << "" << "Freddy" << "unspecified" << "" << "" << "" << "" << "" << "" << "Freddy" << "unspecified" << "" << "" << ""; QTest::newRow("nickname match with lastname") << false << "" << "" << "Gumboots" << "Freddy" << "unspecified" << "" << "" << "" << "" << "" << "" << "Freddy" << "unspecified" << "" << "" << ""; QTest::newRow("lname without detail match") << false << "" << "" << "Gumboots" << "" << "unspecified" << "" << "" << "" << "" << "" << "Gumboots" << "" << "unspecified" << "" << "" << ""; QTest::newRow("lname using phonenumber") << true << "" << "" << "Gumboots" << "" << "unspecified" << "111992888337" << "" << "" << "" << "" << "Gumboots" << "" << "unspecified" << "111992888337" << "" << ""; QTest::newRow("lname using multiple phonenumbers") << true << "" << "" << "Gumboots" << "" << "unspecified" << "111992888337" << "" << "" << "" << "" << "Gumboots" << "" << "unspecified" << "111992888338|111992888337" << "" << ""; QTest::newRow("lname using email address") << true << "" << "" << "Gumboots" << "" << "unspecified" << "" << "gumboots@test.com" << "" << "" << "" << "Gumboots" << "" << "unspecified" << "" << "gumboots@test.com" << ""; QTest::newRow("lname using multiple email addresses") << true << "" << "" << "Gumboots" << "" << "unspecified" << "" << "gumboots@test.com" << "" << "" << "" << "Gumboots" << "" << "unspecified" << "" << "wellingtons@test.com|gumboots@test.com" << ""; QTest::newRow("lname using account uri") << true << "" << "" << "Gumboots" << "" << "unspecified" << "" << "" << "freddy00001@socialaccount" << "" << "" << "Gumboots" << "" << "unspecified" << "" << "" << "freddy00001@socialaccount"; QTest::newRow("lname using multiple account uris") << true << "" << "" << "Gumboots" << "" << "unspecified" << "" << "" << "freddy00001@socialaccount" << "" << "" << "Gumboots" << "" << "unspecified" << "" << "" << "freddy11111@socialaccount|freddy00001@socialaccount"; // partial name matches are no longer aggregated QTest::newRow("partial match name, different p/e/a") << false << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Fred" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; QTest::newRow("partial match name insentive, different p/e/a") << false << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "fred" << "william preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; QTest::newRow("partial match hyphenated name, different p/e/a") << false << "Frederick-Albert" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "Frederick" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; QTest::newRow("partial match hyphenated name insensitive, different p/e/a") << false << "Frederick-Albert" << "William Preston" << "Gumboots" << "Freddy" << "unspecified" << "111992888337" << "gumboots@test.com" << "freddy00001@socialaccount" << "frederick" << "william preston" << "Gumboots" << "Freddy" << "unspecified" << "999118222773" << "freddy@test.net" << "fgumboots@coolsocial"; } void tst_Aggregation::aggregationHeuristic() { // this test exists to validate the findMatchingAggregate query. QFETCH(bool, shouldAggregate); QFETCH(QString, aFirstName); QFETCH(QString, aMiddleName); QFETCH(QString, aLastName); QFETCH(QString, aNickname); QFETCH(QString, aGender); QFETCH(QString, aPhoneNumber); QFETCH(QString, aEmailAddress); QFETCH(QString, aOnlineAccount); QFETCH(QString, bFirstName); QFETCH(QString, bMiddleName); QFETCH(QString, bLastName); QFETCH(QString, bNickname); QFETCH(QString, bGender); QFETCH(QString, bPhoneNumber); QFETCH(QString, bEmailAddress); QFETCH(QString, bOnlineAccount); // add two test collections QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContactCollection trialAddressbook; trialAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("trial")); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/trial"); QVERIFY(m_cm->saveCollection(&trialAddressbook)); for (int i = 0; i < 2; ++i) { QContact a, b; QContactName aname, bname; QContactNickname anick, bnick; QContactGender agen, bgen; QContactPhoneNumber aphn, bphn; QContactEmailAddress aem, bem; QContactOnlineAccount aoa, boa; // construct a a.setCollectionId(testAddressbook.id()); if (!aFirstName.isEmpty() || !aMiddleName.isEmpty() || !aLastName.isEmpty()) { aname.setFirstName(aFirstName); aname.setMiddleName(aMiddleName); aname.setLastName(aLastName); a.saveDetail(&aname); } if (!aNickname.isEmpty()) { anick.setNickname(aNickname); a.saveDetail(&anick); } if (aGender != QString::fromLatin1("unspecified")) { agen.setGender(aGender == QString::fromLatin1("Male") ? QContactGender::GenderMale : QContactGender::GenderFemale); a.saveDetail(&agen); } if (!aPhoneNumber.isEmpty()) { aphn.setNumber(aPhoneNumber); a.saveDetail(&aphn); } if (!aEmailAddress.isEmpty()) { aem.setEmailAddress(aEmailAddress); a.saveDetail(&aem); } if (!aOnlineAccount.isEmpty()) { aoa.setAccountUri(aOnlineAccount); a.saveDetail(&aoa); } // construct b b.setCollectionId(trialAddressbook.id()); if (!bFirstName.isEmpty() || !bMiddleName.isEmpty() || !bLastName.isEmpty()) { bname.setFirstName(bFirstName); bname.setMiddleName(bMiddleName); bname.setLastName(bLastName); b.saveDetail(&bname); } if (!bNickname.isEmpty()) { bnick.setNickname(bNickname); b.saveDetail(&bnick); } if (bGender != QString::fromLatin1("unspecified")) { bgen.setGender(bGender == QString::fromLatin1("Male") ? QContactGender::GenderMale : QContactGender::GenderFemale); b.saveDetail(&bgen); } if (!bPhoneNumber.isEmpty()) { foreach (QString number, bPhoneNumber.split(QString::fromLatin1("|"))){ bphn = QContactPhoneNumber(); bphn.setNumber(number); b.saveDetail(&bphn); } } if (!bEmailAddress.isEmpty()) { foreach (QString address, bEmailAddress.split(QString::fromLatin1("|"))){ bem = QContactEmailAddress(); bem.setEmailAddress(address); b.saveDetail(&bem); } } if (!bOnlineAccount.isEmpty()) { foreach (QString address, bOnlineAccount.split(QString::fromLatin1("|"))){ bphn = QContactOnlineAccount(); boa.setAccountUri(address); b.saveDetail(&boa); } } // Now perform the saves and see if we get some aggregation as required. int count = m_cm->contactIds().count(); QVERIFY(m_cm->saveContact(i == 0 ? &a : &b)); QCOMPARE(m_cm->contactIds().count(), (count+1)); QVERIFY(m_cm->saveContact(i == 0 ? &b : &a)); QCOMPARE(m_cm->contactIds().count(), shouldAggregate ? (count+1) : (count+2)); QVERIFY(m_cm->removeContact(a.id())); QVERIFY(m_cm->removeContact(b.id())); } } void tst_Aggregation::regenerateAggregate() { // here we create a local contact, and then save it // and then we create a "synced" contact, which should "match" it. // It should be related to the aggregate created for the sync. // We then remove the synced contact, which should cause the aggregate // to be "regenerated" from the remaining aggregated contacts // (which in this case, is just the local contact). QContactCollectionFilter allCollections; int aggCount = m_cm->contactIds().size(); int allCount = m_cm->contactIds(allCollections).size(); // set up some signal spies QSignalSpy addSpy(m_cm, contactsAddedSignal); QSignalSpy chgSpy(m_cm, contactsChangedSignal); int addSpyCount = 0; int chgSpyCount = 0; // now add a new local contact (no collectionId specified == automatically local) QContact alice; QContactName an; an.setFirstName("Alice8"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("88888"); alice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice8@test.com"); alice.saveDetail(&aem); m_addAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&alice)); QTRY_VERIFY(addSpy.count() > addSpyCount); // should have added local + aggregate QTRY_COMPARE(m_addAccumulatedIds.size(), 2); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(alice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount + 1); // 1 extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 2); // should have added local + aggregate allCount = m_cm->contactIds(allCollections).size(); QList allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact localAlice; QContact aggregateAlice; bool foundLocalAlice = false; bool foundAggregateAlice = false; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice8") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("88888") && currEm.emailAddress() == QLatin1String("alice8@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); // now add the doppleganger from another sync source QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContact syncAlice; syncAlice.setCollectionId(testAddressbook.id()); QContactName san; san.setFirstName(an.firstName()); san.setMiddleName(an.middleName()); san.setLastName(an.lastName()); syncAlice.saveDetail(&san); QContactPhoneNumber saph; saph.setNumber(aph.number()); syncAlice.saveDetail(&saph); QContactEmailAddress saem; saem.setEmailAddress(aem.emailAddress()); syncAlice.saveDetail(&saem); QContactHobby sah; // this is a "new" detail which doesn't appear in the local contact. sah.setHobby(QLatin1String("tennis")); syncAlice.saveDetail(&sah); // DON'T clear the m_addAccumulatedIds list here. // DO clear the m_chgAccumulatedIds list here, though. chgSpyCount = chgSpy.count(); m_chgAccumulatedIds.clear(); QVERIFY(m_cm->saveContact(&syncAlice)); QTRY_VERIFY(addSpy.count() > addSpyCount); // should have added test but not an aggregate - aggregate already exists QTRY_VERIFY(chgSpy.count() > chgSpyCount); // should have updated the aggregate QTRY_COMPARE(m_addAccumulatedIds.size(), 3); QTRY_COMPARE(m_chgAccumulatedIds.size(), 1); // the aggregate should have been updated (with the hobby) QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(localAlice))); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); QVERIFY(m_addAccumulatedIds.contains(ContactId::apiId(syncAlice))); QVERIFY(m_chgAccumulatedIds.contains(ContactId::apiId(aggregateAlice))); addSpyCount = addSpy.count(); QCOMPARE(m_cm->contactIds().size(), aggCount); // no extra aggregate contact aggCount = m_cm->contactIds().size(); QCOMPARE(m_cm->contactIds(allCollections).size(), allCount + 1); // should have added test but not an aggregate allCount = m_cm->contactIds(allCollections).size(); allContacts = m_cm->contacts(allCollections); QCOMPARE(allContacts.size(), allCount); // should return as many contacts as contactIds. QContact testAlice; bool foundTestAlice = false; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice8") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("88888") && currEm.emailAddress() == QLatin1String("alice8@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { QCOMPARE(curr.detail().value(QContactHobby::FieldHobby), QString()); // local shouldn't get it localAlice = curr; foundLocalAlice = true; } else if (curr.collectionId() == testAddressbook.id()) { QCOMPARE(curr.detail().value(QContactHobby::FieldHobby), QLatin1String("tennis")); // came from here testAlice = curr; foundTestAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); QCOMPARE(curr.detail().value(QContactHobby::FieldHobby), QLatin1String("tennis")); // aggregated to here aggregateAlice = curr; foundAggregateAlice = true; } } } QVERIFY(foundLocalAlice); QVERIFY(foundTestAlice); QVERIFY(foundAggregateAlice); QVERIFY(localAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(testAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(localAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(testAlice.id())); // now remove the "test" sync contact QVERIFY(m_cm->removeContact(removalId(testAlice))); QVERIFY(!m_cm->contactIds(allCollections).contains(ContactId::apiId(testAlice))); // should have been removed // but the other contacts should NOT have been removed QVERIFY(m_cm->contactIds(allCollections).contains(ContactId::apiId(localAlice))); QVERIFY(m_cm->contactIds(allCollections).contains(ContactId::apiId(aggregateAlice))); // reload them, and ensure that the "hobby" detail has been removed from the aggregate allContacts = m_cm->contacts(allCollections); foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice8") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("88888") && currEm.emailAddress() == QLatin1String("alice8@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { QCOMPARE(curr.detail().value(QContactHobby::FieldHobby), QString()); localAlice = curr; foundLocalAlice = true; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); QCOMPARE(curr.detail().value(QContactHobby::FieldHobby), QString()); aggregateAlice = curr; foundAggregateAlice = true; } } } } void tst_Aggregation::detailUris() { QContactCollectionFilter allCollections; // save alice. Some details will have a detailUri or linkedDetailUris QContact alice; QContactName an; an.setFirstName("Alice9"); an.setMiddleName("In"); an.setLastName("Wonderland"); alice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("99999"); aph.setDetailUri("alice9PhoneNumberDetailUri"); alice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice9@test.com"); aem.setLinkedDetailUris("alice9PhoneNumberDetailUri"); alice.saveDetail(&aem); QVERIFY(m_cm->saveContact(&alice)); QList allContacts = m_cm->contacts(allCollections); QContact localAlice; QContact aggregateAlice; foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice9") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("99999") && currEm.emailAddress() == QLatin1String("alice9@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } QVERIFY(!localAlice.id().isNull()); QVERIFY(!aggregateAlice.id().isNull()); // now check to ensure that the detail uris and links were updated correctly // in the aggregate. Those uris need to be unique in the database. QCOMPARE(localAlice.detail().detailUri(), QLatin1String("alice9PhoneNumberDetailUri")); QVERIFY(aggregateAlice.detail().detailUri().startsWith(QStringLiteral("aggregate-%1:").arg(QString::fromLatin1(localAlice.id().localId()).remove(0,4)))); QVERIFY(aggregateAlice.detail().detailUri().endsWith(QLatin1String(":alice9PhoneNumberDetailUri"))); QCOMPARE(localAlice.detail().linkedDetailUris(), QStringList() << QLatin1String("alice9PhoneNumberDetailUri")); QCOMPARE(aggregateAlice.detail().linkedDetailUris().count(), 1); QVERIFY(aggregateAlice.detail().linkedDetailUris().at(0).startsWith(QStringLiteral("aggregate-%1:").arg(QString::fromLatin1(localAlice.id().localId()).remove(0,4)))); QVERIFY(aggregateAlice.detail().linkedDetailUris().at(0).endsWith(QLatin1String(":alice9PhoneNumberDetailUri"))); // try to add another detail with a conflicting detail URI. // this should cause save to fail, as the detail URI must be unique within the contact. QContact failAlice(alice); QContactTag at; at.setTag("fail"); at.setDetailUri("alice9PhoneNumberDetailUri"); failAlice.saveDetail(&at); QCOMPARE(m_cm->saveContact(&failAlice), false); // now perform an update of the local contact. This should also trigger regeneration of the aggregate. QContactHobby ah; ah.setHobby("tennis"); ah.setDetailUri("alice9HobbyDetailUri"); localAlice.saveDetail(&ah); QVERIFY(m_cm->saveContact(&localAlice)); // reload them both allContacts = m_cm->contacts(allCollections); localAlice = QContact(); aggregateAlice = QContact(); foreach (const QContact &curr, allContacts) { QContactEmailAddress currEm = curr.detail(); QContactPhoneNumber currPhn = curr.detail(); QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice9") && currName.middleName() == QLatin1String("In") && currName.lastName() == QLatin1String("Wonderland") && currPhn.number() == QLatin1String("99999") && currEm.emailAddress() == QLatin1String("alice9@test.com")) { if (curr.collectionId().localId() == localAddressbookId()) { localAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } QVERIFY(!localAlice.id().isNull()); QVERIFY(!aggregateAlice.id().isNull()); // now check to ensure that the detail uris and links were updated correctly // in the aggregate. Those uris need to be unique in the database. QCOMPARE(localAlice.detail().detailUri(), QLatin1String("alice9PhoneNumberDetailUri")); QVERIFY(aggregateAlice.detail().detailUri().startsWith(QStringLiteral("aggregate-%1:").arg(QString::fromLatin1(localAlice.id().localId()).remove(0,4)))); QVERIFY(aggregateAlice.detail().detailUri().endsWith(QLatin1String(":alice9PhoneNumberDetailUri"))); QCOMPARE(localAlice.detail().linkedDetailUris(), QStringList() << QLatin1String("alice9PhoneNumberDetailUri")); QCOMPARE(aggregateAlice.detail().linkedDetailUris().count(), 1); QVERIFY(aggregateAlice.detail().linkedDetailUris().at(0).startsWith(QStringLiteral("aggregate-%1:").arg(QString::fromLatin1(localAlice.id().localId()).remove(0,4)))); QVERIFY(aggregateAlice.detail().linkedDetailUris().at(0).endsWith(QLatin1String(":alice9PhoneNumberDetailUri"))); QCOMPARE(localAlice.detail().detailUri(), QLatin1String("alice9HobbyDetailUri")); QVERIFY(aggregateAlice.detail().detailUri().startsWith(QStringLiteral("aggregate-%1:").arg(QString::fromLatin1(localAlice.id().localId()).remove(0,4)))); QVERIFY(aggregateAlice.detail().detailUri().endsWith(QLatin1String(":alice9HobbyDetailUri"))); // now create a contact in a separate collection // which should get aggregated into the same contact. // because the aggregate already exists, its id will // be lower than the id of the new constituent, and // thus the regenerateAggregates() codepath should be hit. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContact syncAlice; syncAlice.setCollectionId(testAddressbook.id()); QContactName san; san.setFirstName("Alice9"); san.setMiddleName("In"); san.setLastName("Wonderland"); syncAlice.saveDetail(&san); QContactPhoneNumber saph; saph.setNumber("999111999"); // try re-using the same detail uri; save should succeed, // as it needs only be unique within the contact. // thus, the aggregation promotion should mutate the // detail uri appropriately to ensure that no conflict occurs. saph.setDetailUri("alice9PhoneNumberDetailUri"); syncAlice.saveDetail(&saph); QVERIFY(syncAlice.id().isNull()); QVERIFY(m_cm->saveContact(&syncAlice)); QVERIFY(!syncAlice.id().isNull()); } void tst_Aggregation::correctDetails() { QContact a, b, c, d; QContactName an, bn, cn, dn; QContactPhoneNumber ap, bp, cp, dp; QContactEmailAddress ae, be, ce, de; QContactHobby ah, bh, ch, dh; an.setFirstName("a"); an.setLastName("A"); bn.setFirstName("b"); bn.setLastName("B"); cn.setFirstName("c"); cn.setLastName("C"); dn.setFirstName("d"); dn.setLastName("D"); ap.setNumber("123"); bp.setNumber("234"); cp.setNumber("345"); dp.setNumber("456"); ae.setEmailAddress("a@test.com"); be.setEmailAddress("b@test.com"); ce.setEmailAddress("c@test.com"); de.setEmailAddress("d@test.com"); ah.setHobby("soccer"); bh.setHobby("tennis"); ch.setHobby("squash"); a.saveDetail(&an); a.saveDetail(&ap); a.saveDetail(&ae); a.saveDetail(&ah); b.saveDetail(&bn); b.saveDetail(&bp); b.saveDetail(&be); b.saveDetail(&bh); c.saveDetail(&cn); c.saveDetail(&cp); c.saveDetail(&ce); c.saveDetail(&ch); d.saveDetail(&dn); d.saveDetail(&dp); d.saveDetail(&de); QList saveList; saveList << a << b << c << d; m_cm->saveContacts(&saveList); QContactCollectionFilter allCollections; QList allContacts = m_cm->contacts(allCollections); QVERIFY(allContacts.size() >= saveList.size()); // at least that amount, maybe more (aggregates) for (int i = 0; i < allContacts.size(); ++i) { QContact curr = allContacts.at(i); bool needsComparison = true; QContact xpct; if (curr.detail().value(QContactName::FieldFirstName) == a.detail().value(QContactName::FieldFirstName)) { xpct = a; } else if (curr.detail().value(QContactName::FieldFirstName) == b.detail().value(QContactName::FieldFirstName)) { xpct = b; } else if (curr.detail().value(QContactName::FieldFirstName) == c.detail().value(QContactName::FieldFirstName)) { xpct = c; } else if (curr.detail().value(QContactName::FieldFirstName) == d.detail().value(QContactName::FieldFirstName)) { xpct = d; } else { needsComparison = false; } if (needsComparison) { //qWarning() << "actual:" << i // << curr.detail().value(QContactSyncTarget::FieldSyncTarget) // << curr.detail().value(QContactName::FieldFirstName) // << curr.detail().value(QContactName::FieldLastName) // << curr.detail().value(QContactPhoneNumber::FieldNumber) // << curr.detail().value(QContactEmailAddress::FieldEmailAddress) // << curr.detail().value(QContactHobby::FieldHobby); //qWarning() << "expected:" << i // << xpct.detail().value(QContactSyncTarget::FieldSyncTarget) // << xpct.detail().value(QContactName::FieldFirstName) // << xpct.detail().value(QContactName::FieldLastName) // << xpct.detail().value(QContactPhoneNumber::FieldNumber) // << xpct.detail().value(QContactEmailAddress::FieldEmailAddress) // << xpct.detail().value(QContactHobby::FieldHobby); QCOMPARE(curr.detail().value(QContactPhoneNumber::FieldNumber), xpct.detail().value(QContactPhoneNumber::FieldNumber)); QCOMPARE(curr.detail().value(QContactEmailAddress::FieldEmailAddress), xpct.detail().value(QContactEmailAddress::FieldEmailAddress)); QCOMPARE(curr.detail().value(QContactHobby::FieldHobby), xpct.detail().value(QContactHobby::FieldHobby)); } } } void tst_Aggregation::batchSemantics() { // TODO: the following comment is no longer true; we still apply batch semantics rules // for simplification of possible cases, however // for performance reasons, the engine assumes: // 1) collectionId of all contacts in a batch save must be the same // 2) no two contacts from the same collection should be aggregated together QContactCollectionFilter allCollections; QList allContacts = m_cm->contacts(allCollections); int allContactsCount = allContacts.size(); QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContactCollection trialAddressbook; trialAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("trial")); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/trial"); QVERIFY(m_cm->saveCollection(&trialAddressbook)); QContact a, b, c; b.setCollectionId(testAddressbook.id()); c.setCollectionId(trialAddressbook.id()); QContactName aname, bname, cname; aname.setFirstName("a"); aname.setLastName("batch"); bname.setFirstName("b"); bname.setLastName("batch"); cname.setFirstName("c"); cname.setLastName("batch"); a.saveDetail(&aname); b.saveDetail(&bname); c.saveDetail(&cname); // a) batch save should fail due to different collection ids. QList saveList; saveList << a << b << c; QVERIFY(!m_cm->saveContacts(&saveList)); // b) same as (a) c.setCollectionId(testAddressbook.id()); // move addressbooks. saveList.clear(); saveList << a << b << c; QVERIFY(!m_cm->saveContacts(&saveList)); // c) same as (a) although in this case, local / empty are considered identical b.setCollectionId(QContactCollectionId()); saveList.clear(); saveList << a << b << c; QVERIFY(!m_cm->saveContacts(&saveList)); // d) now it should succeed. c.setCollectionId(QContactCollectionId()); saveList.clear(); saveList << a << b << c; QVERIFY(m_cm->saveContacts(&saveList)); allContacts = m_cm->contacts(allCollections); int newContactsCount = allContacts.size() - allContactsCount; QCOMPARE(newContactsCount, 6); // 3 local, 3 aggregate // Now we test the semantic of "two contacts from the same collection should get aggregated if they match" QContact d, e; d.setCollectionId(trialAddressbook.id()); e.setCollectionId(trialAddressbook.id()); QContactName dname, ename; dname.setFirstName("d"); dname.setLastName("batch"); ename.setFirstName("d"); ename.setLastName("batch"); d.saveDetail(&dname); e.saveDetail(&ename); saveList.clear(); saveList << d << e; QVERIFY(m_cm->saveContacts(&saveList)); allContacts = m_cm->contacts(allCollections); newContactsCount = allContacts.size() - allContactsCount; QCOMPARE(newContactsCount, 9); // 5 local, 4 aggregate - d and e should have been aggregated into one. } void tst_Aggregation::customSemantics() { // the qtcontacts-sqlite engine defines some custom semantics // 1) avatars have a custom "AvatarMetadata" field // 2) self contact cannot be changed, and its id will always be "1" (aggregate=2) // ensure that the AvatarMetadata field is supported. QContact alice; QContactName an; an.setFirstName("Alice"); alice.saveDetail(&an); QContactAvatar aa; aa.setImageUrl(QUrl(QString::fromLatin1("test.png"))); aa.setValue(QContactAvatar::FieldMetaData, "cover"); alice.saveDetail(&aa); QVERIFY(m_cm->saveContact(&alice)); QContact aliceReloaded = m_cm->contact(retrievalId(alice)); QCOMPARE(aliceReloaded.detail().value(QContactName::FieldFirstName), QLatin1String("Alice")); QCOMPARE(QUrl(aliceReloaded.detail().value(QContactAvatar::FieldImageUrl)).toString(), QUrl(QString::fromLatin1("test.png")).toString()); QCOMPARE(aliceReloaded.detail().value(QContactAvatar::FieldMetaData), QLatin1String("cover")); // test the self contact semantics QCOMPARE(m_cm->selfContactId(), ContactId::apiId(2, m_cm->managerUri())); QVERIFY(!m_cm->setSelfContactId(ContactId::apiId(alice))); // ensure we cannot delete the self contact. QVERIFY(!m_cm->removeContact(ContactId::apiId(1, m_cm->managerUri()))); QVERIFY(!m_cm->removeContact(ContactId::apiId(2, m_cm->managerUri()))); QVERIFY(m_cm->removeContact(removalId(alice))); } void tst_Aggregation::changeLogFiltering() { // The qtcontacts-sqlite engine automatically adds creation timestamp // if not already set. It always clobbers (updates) modification timestamp. QTest::qWait(1); // wait for millisecond change, to ensure unique timestamps for saved contacts. QDateTime startTime = QDateTime::currentDateTimeUtc(); QDateTime minus5 = startTime.addDays(-5); QDateTime minus3 = startTime.addDays(-3); QDateTime minus2 = startTime.addDays(-2); // 1) if provided, creation timestamp should not be overwritten. // if not provided, modification timestamp should be set by the backend. QContact a; QContactName an; an.setFirstName("Alice"); a.saveDetail(&an); QContactTimestamp at; at.setCreated(minus5); a.saveDetail(&at); QTest::qWait(1); QDateTime justPrior = QDateTime::currentDateTimeUtc(); QVERIFY(m_cm->saveContact(&a)); a = m_cm->contact(retrievalId(a)); at = a.detail(); QCOMPARE(at.created(), minus5); QVERIFY(at.lastModified() >= justPrior); QVERIFY(at.lastModified() <= QDateTime::currentDateTimeUtc()); // 2) even if modified timestamp is provided, it should be updated by the backend. at.setLastModified(minus2); a.saveDetail(&at); QTest::qWait(1); justPrior = QDateTime::currentDateTimeUtc(); QVERIFY(m_cm->saveContact(&a)); a = m_cm->contact(retrievalId(a)); at = a.detail(); QCOMPARE(at.created(), minus5); QVERIFY(at.lastModified() >= justPrior); QVERIFY(at.lastModified() <= QDateTime::currentDateTimeUtc()); // 3) created timestamp should only be generated on creation, not normal save. at.setCreated(QDateTime()); a.saveDetail(&at); QTest::qWait(1); justPrior = QDateTime::currentDateTimeUtc(); QVERIFY(m_cm->saveContact(&a)); a = m_cm->contact(retrievalId(a)); at = a.detail(); QCOMPARE(at.created(), QDateTime()); QVERIFY(at.lastModified() >= justPrior); QVERIFY(at.lastModified() <= QDateTime::currentDateTimeUtc()); // Generate a timestamp which is before b's created timestamp. QTest::qWait(1); QDateTime beforeBCreated = QDateTime::currentDateTimeUtc(); QContact b; QContactName bn; bn.setFirstName("Bob"); b.saveDetail(&bn); QTest::qWait(1); justPrior = QDateTime::currentDateTimeUtc(); QVERIFY(m_cm->saveContact(&b)); b = m_cm->contact(retrievalId(b)); QContactTimestamp bt = b.detail(); QVERIFY(bt.created() >= justPrior); QVERIFY(bt.created() <= QDateTime::currentDateTimeUtc()); QVERIFY(bt.lastModified() >= justPrior); QVERIFY(bt.lastModified() <= QDateTime::currentDateTimeUtc()); // Generate a timestamp which is after b's lastModified timestamp but which // will be before a's lastModified timestamp due to the upcoming save. QTest::qWait(1); QDateTime betweenTime = QDateTime::currentDateTimeUtc(); // 4) ensure filtering works as expected. // First, ensure timestamps are filterable; // invalid date times are always included in filtered results. at.setCreated(minus5); a.saveDetail(&at); QTest::qWait(1); justPrior = QDateTime::currentDateTimeUtc(); QVERIFY(m_cm->saveContact(&a)); a = m_cm->contact(retrievalId(a)); at = a.detail(); QCOMPARE(at.created(), minus5); QVERIFY(at.lastModified() >= justPrior); QVERIFY(at.lastModified() <= QDateTime::currentDateTimeUtc()); QContactCollectionFilter localFilter; localFilter.setCollectionId(QContactCollectionId(m_cm->managerUri(), localAddressbookId())); QContactCollectionFilter aggFilter; aggFilter.setCollectionId(QContactCollectionId(m_cm->managerUri(), aggregateAddressbookId())); QContactIntersectionFilter cif; QContactChangeLogFilter clf; clf.setEventType(QContactChangeLogFilter::EventAdded); clf.setSince(beforeBCreated); // should contain b, but not a as a's creation time was days-5 cif.clear(); cif << localFilter << clf; QList filtered = m_cm->contactIds(cif); QVERIFY(!filtered.contains(retrievalId(a))); QVERIFY(filtered.contains(retrievalId(b))); clf.setEventType(QContactChangeLogFilter::EventAdded); clf.setSince(betweenTime); // should not contain either a or b cif.clear(); cif << localFilter << clf; filtered = m_cm->contactIds(cif); QVERIFY(!filtered.contains(retrievalId(a))); QVERIFY(!filtered.contains(retrievalId(b))); clf.setEventType(QContactChangeLogFilter::EventChanged); clf.setSince(betweenTime); // should contain a (modified after betweenTime) but not b (modified before) cif.clear(); cif << localFilter << clf; filtered = m_cm->contactIds(cif); QVERIFY(filtered.contains(retrievalId(a))); QVERIFY(!filtered.contains(retrievalId(b))); clf.setEventType(QContactChangeLogFilter::EventChanged); clf.setSince(startTime); // should contain both a and b cif.clear(); cif << localFilter << clf; filtered = m_cm->contactIds(cif); QVERIFY(filtered.contains(retrievalId(a))); QVERIFY(filtered.contains(retrievalId(b))); // Filtering for removed contactIds is supported clf.setEventType(QContactChangeLogFilter::EventRemoved); clf.setSince(startTime); // should contain neither a nor b filtered = m_cm->contactIds(clf); QVERIFY(!filtered.contains(retrievalId(a))); QVERIFY(!filtered.contains(retrievalId(b))); // Filtering in combination with syncTarget filtering is also supported cif.clear(); cif << localFilter << clf; filtered = m_cm->contactIds(cif); QVERIFY(!filtered.contains(retrievalId(a))); QVERIFY(!filtered.contains(retrievalId(b))); // Either order of intersected filters is the same cif.clear(); cif << clf << localFilter; filtered = m_cm->contactIds(cif); QVERIFY(!filtered.contains(retrievalId(a))); QVERIFY(!filtered.contains(retrievalId(b))); QContactId idA(removalId(a)); QVERIFY(m_cm->removeContact(idA)); QTest::qWait(1); QDateTime postDeleteTime = QDateTime::currentDateTimeUtc(); QContactId idB(removalId(b)); QVERIFY(m_cm->removeContact(idB)); clf = QContactChangeLogFilter(); clf.setEventType(QContactChangeLogFilter::EventRemoved); clf.setSince(startTime); // should contain both a and b filtered = m_cm->contactIds(clf); QVERIFY(filtered.count() >= 2); QVERIFY(filtered.contains(idA)); QVERIFY(filtered.contains(idB)); // Check that syncTarget filtering is also applied cif.clear(); cif << localFilter << clf; filtered = m_cm->contactIds(cif); QVERIFY(filtered.count() >= 2); QVERIFY(filtered.contains(idA)); QVERIFY(filtered.contains(idB)); // And that aggregate contacts are not reported for EventRemoved filters // as aggregate contacts are deleted directly rather than marked with changeFlags. cif.clear(); cif << aggFilter << clf; filtered = m_cm->contactIds(cif); QCOMPARE(filtered.count(), 0); // Check that since values are applied clf = QContactChangeLogFilter(); clf.setEventType(QContactChangeLogFilter::EventRemoved); clf.setSince(postDeleteTime); // should contain only b filtered = m_cm->contactIds(clf); QVERIFY(filtered.count() >= 1); QVERIFY(filtered.contains(idB)); cif.clear(); cif << localFilter << clf; filtered = m_cm->contactIds(cif); QVERIFY(filtered.count() >= 1); QVERIFY(filtered.contains(idB)); // Check that since is not required clf = QContactChangeLogFilter(); clf.setEventType(QContactChangeLogFilter::EventRemoved); filtered = m_cm->contactIds(clf); QVERIFY(filtered.count() >= 2); QVERIFY(filtered.contains(idA)); QVERIFY(filtered.contains(idB)); } void tst_Aggregation::deactivationSingle() { QContactCollectionFilter allCollections; QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); // add a new contact (collectionId must be specified to deactivate) QContact syncAlice; syncAlice.setCollectionId(testAddressbook.id()); QContactName an; an.setFirstName("Alice"); an.setMiddleName("Through The"); an.setLastName("Looking-Glass"); syncAlice.saveDetail(&an); QVERIFY(m_cm->saveContact(&syncAlice)); QContact aggregateAlice; QList contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { syncAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation occurred QVERIFY(syncAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 1); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(syncAlice.id())); // Verify the presence of the contact IDs QList contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); contactIds = m_cm->contactIds(); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); QContactId syncAliceId = syncAlice.id(); // Now deactivate the test contact QContactDeactivated deactivated; syncAlice.saveDetail(&deactivated); QVERIFY(m_cm->saveContact(&syncAlice)); syncAlice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { syncAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // The deactivated contact is not found (although relationships remain) // The deactivated contact is not found and the aggregate is removed QVERIFY(syncAlice.id() == QContactId()); QVERIFY(aggregateAlice.id() == QContactId()); // Verify that test alice still exists syncAlice = m_cm->contact(syncAliceId); QVERIFY(syncAlice.id() == syncAliceId); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 0); // Verify the presence/absence of the contact IDs contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice)) == false); contactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeactivated, QContactFilter::MatchContains)); QVERIFY(contactIds.contains(syncAliceId)); // Reactivate deactivated = syncAlice.detail(); syncAlice.removeDetail(&deactivated, QContact::IgnoreAccessConstraints); QVERIFY(m_cm->saveContact(&syncAlice)); syncAlice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { syncAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation is restored QVERIFY(syncAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 1); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(syncAlice.id())); // Check that the reactivated contact retains the same ID QVERIFY(syncAlice.id() == syncAliceId); // Verify the presence of all contact IDs when queried contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); } void tst_Aggregation::deactivationMultiple() { QContactCollectionFilter allCollections; QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContactCollection trialAddressbook; trialAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("trial")); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/trial"); QVERIFY(m_cm->saveCollection(&trialAddressbook)); // add a new contact (collection must be specified to deactivate) QContact syncAlice; syncAlice.setCollectionId(testAddressbook.id()); QContactName an; an.setFirstName("Alice"); an.setMiddleName("Through The"); an.setLastName("Looking-Glass"); syncAlice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("34567"); syncAlice.saveDetail(&aph); QVERIFY(m_cm->saveContact(&syncAlice)); // now add the doppelganger from another sync source QContact otherAlice; otherAlice.setCollectionId(trialAddressbook.id()); QContactName san; san.setFirstName(an.firstName()); san.setMiddleName(an.middleName()); san.setLastName(an.lastName()); otherAlice.saveDetail(&san); QContactPhoneNumber saph; saph.setNumber("76543"); otherAlice.saveDetail(&saph); QVERIFY(m_cm->saveContact(&otherAlice)); QContact aggregateAlice; QList contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { syncAlice = curr; } else if (curr.collectionId() == trialAddressbook.id()) { otherAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation occurred QVERIFY(syncAlice.id() != QContactId()); QVERIFY(otherAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(otherAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(otherAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 2); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(syncAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(otherAlice.id())); QCOMPARE(syncAlice.details().count(), 1); QCOMPARE(otherAlice.details().count(), 1); QCOMPARE(aggregateAlice.details().count(), 2); // Verify the presence of the contact IDs QList contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice))); QVERIFY(contactIds.contains(ContactId::apiId(otherAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); contactIds = m_cm->contactIds(); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(otherAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); QContactId syncAliceId = syncAlice.id(); // Now deactivate the test contact QContactDeactivated deactivated; syncAlice.saveDetail(&deactivated); QVERIFY(m_cm->saveContact(&syncAlice)); syncAlice = otherAlice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { syncAlice = curr; } else if (curr.collectionId() == trialAddressbook.id()) { otherAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // The deactivated contact is not found (although relationships remain) QVERIFY(syncAlice.id() == QContactId()); QVERIFY(otherAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(otherAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(otherAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 2); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(syncAliceId)); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(otherAlice.id())); // Check that the aggregate does not contain the deactivated detail QCOMPARE(otherAlice.details().count(), 1); QCOMPARE(aggregateAlice.details().count(), 1); // Verify that test alice still exists syncAlice = m_cm->contact(syncAliceId); QVERIFY(syncAlice.id() == syncAliceId); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); // Verify the presence/absence of the contact IDs contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(otherAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); contactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeactivated, QContactFilter::MatchContains)); QVERIFY(contactIds.contains(syncAliceId)); QVERIFY(contactIds.contains(ContactId::apiId(otherAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice)) == false); // Reactivate deactivated = syncAlice.detail(); syncAlice.removeDetail(&deactivated); QVERIFY(m_cm->saveContact(&syncAlice)); syncAlice = otherAlice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { syncAlice = curr; } else if (curr.collectionId() == trialAddressbook.id()) { otherAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation remains intact QVERIFY(syncAlice.id() != QContactId()); QVERIFY(otherAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(syncAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(otherAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(otherAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 2); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(syncAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(otherAlice.id())); // Re-activated details are now aggregated QCOMPARE(syncAlice.details().count(), 1); QCOMPARE(otherAlice.details().count(), 1); QCOMPARE(aggregateAlice.details().count(), 2); // Check that the reactivated contact retains the same ID QVERIFY(syncAlice.id() == syncAliceId); // Verify the presence of all contact IDs when queried contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(syncAlice))); QVERIFY(contactIds.contains(ContactId::apiId(otherAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); } void tst_Aggregation::deletionSingle() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; QContactCollectionFilter allCollections; QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); // add a new contact QContact alice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("Through The"); an.setLastName("Looking-Glass"); alice.saveDetail(&an); QVERIFY(m_cm->saveContact(&alice)); QContact aggregateAlice; QList contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId().localId() == localAddressbookId()) { alice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation occurred QVERIFY(alice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(alice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(alice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 1); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(alice.id())); // Verify the presence of the contact IDs QList contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(alice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); contactIds = m_cm->contactIds(); QVERIFY(contactIds.contains(ContactId::apiId(alice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); QContactId aliceId = alice.id(); // Now delete the contact QVERIFY(m_cm->removeContact(alice.id())); alice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId().localId() == localAddressbookId()) { alice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // The deleted contact is not found and the aggregate is removed QVERIFY(alice.id() == QContactId()); QVERIFY(aggregateAlice.id() == QContactId()); alice = m_cm->contact(aliceId); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); QVERIFY(alice.id() == QContactId()); // not found. // Verify the presence/absence of the contact IDs with appropriate filter applied. contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(alice)) == false); contactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QVERIFY(contactIds.contains(aliceId)); // attempt to modify alice. should fail. QContactHobby ahobby; ahobby.setHobby("Snowboarding"); alice.saveDetail(&ahobby); alice.setId(aliceId); QVERIFY(!m_cm->saveContact(&alice)); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); // Undelete alice. alice.clearDetails(); QContactUndelete undelete; alice.saveDetail(&undelete, QContact::IgnoreAccessConstraints); alice.setId(aliceId); QVERIFY(m_cm->saveContact(&alice)); alice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId().localId() == localAddressbookId()) { alice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation is restored QVERIFY(alice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(alice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(alice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 1); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(alice.id())); // Check that the undeleted contact retains the same ID QVERIFY(alice.id() == aliceId); // Verify the presence of all contact IDs when queried contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(alice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); // Now both delete and purge the contact. QVERIFY(m_cm->removeContact(aliceId)); QVERIFY(cme->clearChangeFlags(QList() << aliceId, &err)); // Verify the absence of the contact IDs even with appropriate filter applied. contactIds = m_cm->contactIds(allCollections); QVERIFY(!contactIds.contains(aliceId)); contactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QVERIFY(!contactIds.contains(aliceId)); // Verify that we cannot undelete the purged contact. alice.clearDetails(); QContactUndelete undelete2; alice.saveDetail(&undelete2, QContact::IgnoreAccessConstraints); alice.setId(aliceId); QVERIFY(!m_cm->saveContact(&alice)); } void tst_Aggregation::deletionMultiple() { QContactCollectionFilter allCollections; QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContactCollection trialAddressbook; trialAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("trial")); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/trial"); QVERIFY(m_cm->saveCollection(&trialAddressbook)); // add a new contact QContact testAlice; testAlice.setCollectionId(testAddressbook.id()); QContactName an; an.setFirstName("Alice"); an.setMiddleName("Through The"); an.setLastName("Looking-Glass"); testAlice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("34567"); testAlice.saveDetail(&aph); QVERIFY(m_cm->saveContact(&testAlice)); // now add the doppelganger from another sync source QContact trialAlice; trialAlice.setCollectionId(trialAddressbook.id()); QContactName san; san.setFirstName(an.firstName()); san.setMiddleName(an.middleName()); san.setLastName(an.lastName()); trialAlice.saveDetail(&san); QContactPhoneNumber saph; saph.setNumber("76543"); trialAlice.saveDetail(&saph); QVERIFY(m_cm->saveContact(&trialAlice)); QContact aggregateAlice; QList contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { testAlice = curr; } else if (curr.collectionId() == trialAddressbook.id()) { trialAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation occurred QVERIFY(testAlice.id() != QContactId()); QVERIFY(trialAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(testAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(testAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(trialAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(trialAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 2); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(testAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(trialAlice.id())); QCOMPARE(testAlice.details().count(), 1); QCOMPARE(trialAlice.details().count(), 1); QCOMPARE(aggregateAlice.details().count(), 2); // Verify the presence of the contact IDs QList contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(testAlice))); QVERIFY(contactIds.contains(ContactId::apiId(trialAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); contactIds = m_cm->contactIds(); QVERIFY(contactIds.contains(ContactId::apiId(testAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(trialAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); QContactId testAliceId = testAlice.id(); // Now delete the test contact QVERIFY(m_cm->removeContact(testAlice.id())); testAlice = trialAlice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { testAlice = curr; } else if (curr.collectionId() == trialAddressbook.id()) { trialAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // The deleted contact is not found QVERIFY(testAlice.id() == QContactId()); QVERIFY(trialAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(trialAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(trialAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 1); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(trialAlice.id())); QVERIFY(!aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(testAliceId)); // Relationships to the deleted contact are not found QList aggRels = m_cm->relationships(aggregateAlice.id()); QList testRels = m_cm->relationships(testAliceId); QCOMPARE(aggRels.size(), 1); QCOMPARE(testRels.size(), 0); // Check that the aggregate does not contain the deleted details QCOMPARE(trialAlice.details().count(), 1); QCOMPARE(aggregateAlice.details().count(), 1); QCOMPARE(aggregateAlice.detail().number(), trialAlice.detail().number()); // Verify that test alice does not exist testAlice = m_cm->contact(testAliceId); QVERIFY(testAlice.id() == QContactId()); // Verify the presence/absence of the contact IDs contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(testAliceId) == false); QVERIFY(contactIds.contains(ContactId::apiId(trialAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); contactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QVERIFY(contactIds.contains(testAliceId)); QVERIFY(contactIds.contains(ContactId::apiId(trialAlice)) == false); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice)) == false); // Undelete testAlice.clearDetails(); QContactUndelete undelete; testAlice.saveDetail(&undelete); testAlice.setId(testAliceId); testAlice.setCollectionId(testAddressbook.id()); QVERIFY(m_cm->saveContact(&testAlice)); testAlice = trialAlice = aggregateAlice = QContact(); contacts = m_cm->contacts(allCollections); foreach (const QContact &curr, contacts) { QContactName currName = curr.detail(); if (currName.firstName() == QLatin1String("Alice") && currName.middleName() == QLatin1String("Through The") && currName.lastName() == QLatin1String("Looking-Glass")) { if (curr.collectionId() == testAddressbook.id()) { testAlice = curr; } else if (curr.collectionId() == trialAddressbook.id()) { trialAlice = curr; } else { QCOMPARE(curr.collectionId().localId(), aggregateAddressbookId()); aggregateAlice = curr; } } } // Check that aggregation remains intact QVERIFY(testAlice.id() != QContactId()); QVERIFY(trialAlice.id() != QContactId()); QVERIFY(aggregateAlice.id() != QContactId()); QVERIFY(testAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(testAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(trialAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).count() == 1); QVERIFY(trialAlice.relatedContacts(aggregatesRelationship, QContactRelationship::First).contains(aggregateAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).count() == 2); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(testAlice.id())); QVERIFY(aggregateAlice.relatedContacts(aggregatesRelationship, QContactRelationship::Second).contains(trialAlice.id())); // Re-activated details are now aggregated QCOMPARE(testAlice.details().count(), 1); QCOMPARE(trialAlice.details().count(), 1); QCOMPARE(aggregateAlice.details().count(), 2); // Check that the reactivated contact retains the same ID QVERIFY(testAlice.id() == testAliceId); // Verify the presence of all contact IDs when queried contactIds = m_cm->contactIds(allCollections); QVERIFY(contactIds.contains(ContactId::apiId(testAlice))); QVERIFY(contactIds.contains(ContactId::apiId(trialAlice))); QVERIFY(contactIds.contains(ContactId::apiId(aggregateAlice))); } void tst_Aggregation::deletionCollections() { QContactCollectionFilter allCollections; const int count = m_cm->contactIds(allCollections).size(); const int deletedCount = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)).size(); // create two test collections. one is aggregable, one is not. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, false); testAddressbook.setExtendedMetaData("customKey1", "customValue1"); QVERIFY(m_cm->saveCollection(&testAddressbook)); QContactCollectionId testAddressbookId = testAddressbook.id(); QContactCollection trialAddressbook; trialAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("trial")); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_aggregation"); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); trialAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/trial"); trialAddressbook.setExtendedMetaData("customKey2", "customValue2"); trialAddressbook.setExtendedMetaData("customKey1", "customValue3"); QVERIFY(m_cm->saveCollection(&trialAddressbook)); QContactCollectionId trialAddressbookId = trialAddressbook.id(); // add three contacts to each addressbook. QContact a, b, c, x, y, z; a.setCollectionId(testAddressbook.id()); b.setCollectionId(testAddressbook.id()); c.setCollectionId(testAddressbook.id()); x.setCollectionId(trialAddressbook.id()); y.setCollectionId(trialAddressbook.id()); z.setCollectionId(trialAddressbook.id()); QContactName an, bn, cn, xn, yn, zn; an.setFirstName("A"); an.setLastName("A"); bn.setFirstName("B"); bn.setLastName("B"); cn.setFirstName("C"); cn.setLastName("C"); xn.setFirstName("X"); xn.setLastName("X"); yn.setFirstName("Y"); yn.setLastName("Y"); zn.setFirstName("Z"); zn.setLastName("Z"); QVERIFY(m_cm->saveContact(&a)); QVERIFY(m_cm->saveContact(&b)); QVERIFY(m_cm->saveContact(&c)); QVERIFY(m_cm->saveContact(&x)); QVERIFY(m_cm->saveContact(&y)); QVERIFY(m_cm->saveContact(&z)); // ensure that we have the number of contacts we expect, including xyz aggregates. QList ids = m_cm->contactIds(allCollections); QList deletedIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(ids.size(), count + 9); // a,b,c,x,xa,y,ya,z,za QVERIFY(ids.contains(a.id())); QVERIFY(ids.contains(b.id())); QVERIFY(ids.contains(c.id())); QVERIFY(ids.contains(x.id())); QVERIFY(ids.contains(y.id())); QVERIFY(ids.contains(z.id())); QCOMPARE(deletedIds.size(), deletedCount + 0); // now mark b, x and z for deletion. QVERIFY(m_cm->removeContact(b.id())); QVERIFY(m_cm->removeContact(x.id())); QVERIFY(m_cm->removeContact(z.id())); // now modify a and y to set some modification change flags. QContactPhoneNumber ap, yp; ap.setNumber("12345"); yp.setNumber("54321"); QVERIFY(a.saveDetail(&ap)); QVERIFY(y.saveDetail(&yp)); QVERIFY(m_cm->saveContact(&a)); QVERIFY(m_cm->saveContact(&y)); // ensure that we have the number of contacts we expect, including y aggregate. // note that aggregates for deleted contacts don't exist, so xa,za won't be returned in deletedIds. ids = m_cm->contactIds(allCollections); deletedIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(ids.size(), count + 4); // a,c,y,ya QVERIFY(ids.contains(a.id())); QVERIFY(!ids.contains(b.id())); QVERIFY(ids.contains(c.id())); QVERIFY(!ids.contains(x.id())); QVERIFY(ids.contains(y.id())); QVERIFY(!ids.contains(z.id())); QCOMPARE(deletedIds.size(), deletedCount + 3); // b, x, z QVERIFY(!deletedIds.contains(a.id())); QVERIFY(deletedIds.contains(b.id())); QVERIFY(!deletedIds.contains(c.id())); QVERIFY(deletedIds.contains(x.id())); QVERIFY(!deletedIds.contains(y.id())); QVERIFY(deletedIds.contains(z.id())); // now clear the change flags for trialAddressbook. This should result in x+z being purged. QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; QVERIFY(cme->clearChangeFlags(trialAddressbook.id(), &err)); // should still be able to access the trialAddressbook itself. QContactCollection reloadedTrialAddressbook = m_cm->collection(trialAddressbookId); QCOMPARE(m_cm->error(), QContactManager::NoError); QCOMPARE(reloadedTrialAddressbook.metaData(QContactCollection::KeyName).toString(), QStringLiteral("trial")); QCOMPARE(reloadedTrialAddressbook.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(), QStringLiteral("tst_aggregation")); QCOMPARE(reloadedTrialAddressbook.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(), 6); QCOMPARE(reloadedTrialAddressbook.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString(), QStringLiteral("/addressbooks/trial")); QCOMPARE(reloadedTrialAddressbook.extendedMetaData("customKey2").toString(), QStringLiteral("customValue2")); QCOMPARE(reloadedTrialAddressbook.extendedMetaData("customKey1").toString(), QStringLiteral("customValue3")); // ensure x and z have been purged, leaving just a,b,c,y,ya accessible ids = m_cm->contactIds(allCollections); deletedIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(ids.size(), count + 4); // a,c,y,ya QVERIFY(ids.contains(a.id())); QVERIFY(!ids.contains(b.id())); QVERIFY(ids.contains(c.id())); QVERIFY(!ids.contains(x.id())); QVERIFY(ids.contains(y.id())); QVERIFY(!ids.contains(z.id())); QCOMPARE(deletedIds.size(), deletedCount + 1); // b QVERIFY(!deletedIds.contains(a.id())); QVERIFY(deletedIds.contains(b.id())); QVERIFY(!deletedIds.contains(c.id())); QVERIFY(!deletedIds.contains(x.id())); QVERIFY(!deletedIds.contains(y.id())); QVERIFY(!deletedIds.contains(z.id())); // now delete testAddressbook. this should also mark a and c as deleted. QVERIFY(m_cm->removeCollection(testAddressbookId)); ids = m_cm->contactIds(allCollections); deletedIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(ids.size(), count + 2); // y,ya QVERIFY(!ids.contains(a.id())); QVERIFY(!ids.contains(b.id())); QVERIFY(!ids.contains(c.id())); QVERIFY(!ids.contains(x.id())); QVERIFY(ids.contains(y.id())); QVERIFY(!ids.contains(z.id())); QCOMPARE(deletedIds.size(), deletedCount + 3); // a,b,c QVERIFY(deletedIds.contains(a.id())); QVERIFY(deletedIds.contains(b.id())); QVERIFY(deletedIds.contains(c.id())); QVERIFY(!deletedIds.contains(x.id())); QVERIFY(!deletedIds.contains(y.id())); QVERIFY(!deletedIds.contains(z.id())); // we should not be able to access testAddressbook any more via the normal accessor. QContactCollection deletedTestAddressbook = m_cm->collection(testAddressbookId); QVERIFY(deletedTestAddressbook.id().isNull()); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); // now clearChangeFlags for testAddressbook. this should purge that addressbook and a,b,c. QVERIFY(cme->clearChangeFlags(testAddressbookId, &err)); ids = m_cm->contactIds(allCollections); deletedIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(ids.size(), count + 2); // y,ya QVERIFY(!ids.contains(a.id())); QVERIFY(!ids.contains(b.id())); QVERIFY(!ids.contains(c.id())); QVERIFY(!ids.contains(x.id())); QVERIFY(ids.contains(y.id())); QVERIFY(!ids.contains(z.id())); QCOMPARE(deletedIds.size(), deletedCount + 0); // nothing QVERIFY(!deletedIds.contains(a.id())); QVERIFY(!deletedIds.contains(b.id())); QVERIFY(!deletedIds.contains(c.id())); QVERIFY(!deletedIds.contains(x.id())); QVERIFY(!deletedIds.contains(y.id())); QVERIFY(!deletedIds.contains(z.id())); // now delete trialAddressbook and purge it. QVERIFY(m_cm->removeCollection(trialAddressbookId)); QVERIFY(cme->clearChangeFlags(trialAddressbookId, &err)); ids = m_cm->contactIds(allCollections); deletedIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(ids.size(), count + 0); // nothing QVERIFY(!ids.contains(a.id())); QVERIFY(!ids.contains(b.id())); QVERIFY(!ids.contains(c.id())); QVERIFY(!ids.contains(x.id())); QVERIFY(!ids.contains(y.id())); QVERIFY(!ids.contains(z.id())); QCOMPARE(deletedIds.size(), deletedCount + 0); // nothing QVERIFY(!deletedIds.contains(a.id())); QVERIFY(!deletedIds.contains(b.id())); QVERIFY(!deletedIds.contains(c.id())); QVERIFY(!deletedIds.contains(x.id())); QVERIFY(!deletedIds.contains(y.id())); QVERIFY(!deletedIds.contains(z.id())); } void tst_Aggregation::testOOB() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); const QString &scope(QString::fromLatin1("tst_Aggregation")); // Test simple OOB fetches and stores QVariant data; QVERIFY(cme->fetchOOB(scope, "nonexistentData", &data)); QCOMPARE(data, QVariant()); QVERIFY(cme->fetchOOB(scope, "data", &data)); if (!data.isNull()) { QVERIFY(cme->removeOOB(scope, "data")); } QStringList keys; QVERIFY(cme->fetchOOBKeys(scope, &keys)); QCOMPARE(keys, QStringList()); QVERIFY(cme->storeOOB(scope, "data", QVariant::fromValue(0.123456789))); data = QVariant(); QVERIFY(cme->fetchOOB(scope, "data", &data)); QCOMPARE(data.toDouble(), 0.123456789); keys.clear(); QVERIFY(cme->fetchOOBKeys(scope, &keys)); QCOMPARE(keys, QStringList() << "data"); // Test overwrite QVERIFY(cme->storeOOB(scope, "data", QVariant::fromValue(QString::fromLatin1("Testing")))); data = QVariant(); QVERIFY(cme->fetchOOB(scope, "data", &data)); QCOMPARE(data.toString(), QString::fromLatin1("Testing")); // Test insertion of a long string QString lorem(QString::fromLatin1("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent consectetur elit ut semper porta. Aenean gravida risus ligula, sollicitudin pharetra magna varius quis. Donec mattis vehicula lobortis. In a pulvinar est. Donec consectetur sem eu metus blandit rhoncus. In volutpat lobortis porta. Aliquam ultrices nulla sit amet erat pharetra, in mollis elit condimentum. Sed auctor cursus viverra. Vestibulum at placerat ipsum." "Integer venenatis venenatis justo, vel tincidunt felis mattis sit amet. Aliquam tempus augue quis magna ultricies, id volutpat lorem ornare. Ut volutpat hendrerit tincidunt. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sagittis risus non ipsum adipiscing, in semper urna imperdiet. Vivamus lobortis euismod justo, id vestibulum purus posuere cursus. Sed fermentum non sem ac tempor. Vivamus enim velit, euismod nec rutrum et, pellentesque vitae enim. Praesent dignissim consectetur tellus, vel sagittis justo pulvinar eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse potenti. Curabitur condimentum dolor ac dictum condimentum. Nulla id libero hendrerit, facilisis velit at, porttitor erat." "Proin blandit a nisl quis laoreet. Pellentesque venenatis, sem non pulvinar blandit, leo est sodales tellus, sit amet semper orci neque non enim. Mauris tincidunt, quam sollicitudin fermentum dignissim, neque nunc consequat mauris, quis facilisis est massa ut purus. Sed et lacus lectus. Aenean laoreet lectus in suscipit pretium. Suspendisse at justo adipiscing, aliquam est ut, tristique tortor. Mauris tincidunt sem pharetra, volutpat erat non, cursus eros. In hac habitasse platea dictumst. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce porttitor ultrices tortor, vel tincidunt libero feugiat a. Etiam elementum, magna sed imperdiet ullamcorper, nisl dolor vehicula magna, vel facilisis quam mi eget tortor. Donec pellentesque odio a eros iaculis varius. Sed purus nisi, accumsan quis urna eget, tincidunt venenatis sapien. Suspendisse quis diam dui. Donec eu sollicitudin nibh." "Sed pretium urna at odio dictum convallis. Donec vel pulvinar purus. Duis et augue ac turpis porttitor hendrerit quis quis urna. Sed ac lectus odio. Sed volutpat placerat hendrerit. Mauris ac mollis nisl. Praesent ornare egestas elit, vitae ultricies quam imperdiet a. Nam accumsan nulla ut blandit scelerisque. Maecenas condimentum erat sit amet turpis feugiat, ac dictum sapien mattis. In sagittis nulla mi, ut facilisis urna lacinia et. Integer sed erat id massa vestibulum fringilla. Ut nec placerat lorem, quis semper ipsum. Aenean facilisis, odio vitae condimentum interdum, tortor tellus scelerisque purus, at pellentesque leo erat eu orci. Duis in feugiat quam. Mauris lorem dolor, pharetra quis blandit non, cursus et odio." "Nunc eu tristique dui. Donec sit amet velit id ipsum rhoncus facilisis. Integer quis ultrices metus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec ac velit lacus. Fusce pharetra lacus metus, nec adipiscing erat consequat consectetur. Proin ipsum massa, placerat eget dignissim in, interdum ut lorem. Aliquam erat volutpat. Duis sagittis nec est in suscipit. Mauris non auctor nibh. Suspendisse ultrices laoreet neque, a lacinia ante lacinia a. Praesent tempus luctus mauris eu ullamcorper. Praesent ultricies ac metus eget imperdiet. Sed massa lectus, tincidunt in dui non, faucibus mattis ante. Curabitur neque quam, congue non dapibus quis, fringilla ut orci.")); QVERIFY(cme->storeOOB(scope, "data", QVariant::fromValue(lorem))); QVERIFY(cme->fetchOOB(scope, "data", &data)); QCOMPARE(data.toString(), lorem); // Test insertion of a large byte arrays QList uniqueSequence; QList repeatingSequence; QList randomSequence; qsrand(0); for (int i = 0; i < 100; ++i) { for (int j = 0; j < 10; ++j) { uniqueSequence.append(i * 100 + j); repeatingSequence.append(j); randomSequence.append(qrand()); } } QByteArray buffer; QList extracted; { QDataStream os(&buffer, QIODevice::WriteOnly); os << uniqueSequence; } QVERIFY(cme->storeOOB(scope, "data", QVariant::fromValue(buffer))); QVERIFY(cme->fetchOOB(scope, "data", &data)); { buffer = data.value(); QDataStream is(buffer); is >> extracted; } QCOMPARE(extracted, uniqueSequence); { QDataStream os(&buffer, QIODevice::WriteOnly); os << repeatingSequence; } QVERIFY(cme->storeOOB(scope, "data", QVariant::fromValue(buffer))); QVERIFY(cme->fetchOOB(scope, "data", &data)); { buffer = data.value(); QDataStream is(buffer); is >> extracted; } QCOMPARE(extracted, repeatingSequence); { QDataStream os(&buffer, QIODevice::WriteOnly); os << randomSequence; } QVERIFY(cme->storeOOB(scope, "data", QVariant::fromValue(buffer))); QVERIFY(cme->fetchOOB(scope, "data", &data)); { buffer = data.value(); QDataStream is(buffer); is >> extracted; } QCOMPARE(extracted, randomSequence); keys.clear(); QVERIFY(cme->fetchOOBKeys(scope, &keys)); QCOMPARE(keys, QStringList() << "data"); // Test remove QVERIFY(cme->removeOOB(scope, "data")); QVERIFY(cme->fetchOOB(scope, "data", &data)); QCOMPARE(data, QVariant()); keys.clear(); QVERIFY(cme->fetchOOBKeys(scope, &keys)); QCOMPARE(keys, QStringList()); // Test multiple items QMap values; values.insert("data", 100); values.insert("other", 200); QVERIFY(cme->storeOOB(scope, values)); values.clear(); QVERIFY(cme->fetchOOB(scope, (QStringList() << "data" << "other" << "nonexistent"), &values)); QCOMPARE(values.count(), 2); QCOMPARE(values["data"].toInt(), 100); QCOMPARE(values["other"].toInt(), 200); keys.clear(); QVERIFY(cme->fetchOOBKeys(scope, &keys)); QCOMPARE(keys, QStringList() << "data" << "other"); // Test empty lists values.clear(); QVERIFY(cme->fetchOOB(scope, &values)); QCOMPARE(values.count(), 2); QCOMPARE(values["data"].toInt(), 100); QCOMPARE(values["other"].toInt(), 200); keys.clear(); QVERIFY(cme->fetchOOBKeys(scope, &keys)); QCOMPARE(keys, QStringList() << "data" << "other"); QVERIFY(cme->removeOOB(scope)); values.clear(); QVERIFY(cme->fetchOOB(scope, &values)); QCOMPARE(values.count(), 0); keys.clear(); QVERIFY(cme->fetchOOBKeys(scope, &keys)); QCOMPARE(keys, QStringList()); } QTEST_GUILESS_MAIN(tst_Aggregation) #include "tst_aggregation.moc" qtcontacts-sqlite-0.3.19/tests/auto/auto.pro000066400000000000000000000003421436373107600211030ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ qcontactmanager \ aggregation \ qcontactmanagerfiltering \ phonenumber \ memorytable \ database \ displaylabelgroups \ detailfetchrequest \ synctransactions qtcontacts-sqlite-0.3.19/tests/auto/database/000077500000000000000000000000001436373107600211565ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/database/database.pro000066400000000000000000000021771436373107600234530ustar00rootroot00000000000000TARGET = tst_database include(../../common.pri) QT += sql # copied from src/engine/engine.pro, modified for test db DEFINES += 'QTCONTACTS_SQLITE_PRIVILEGED_DIR=\'\"privileged\"\'' DEFINES += 'QTCONTACTS_SQLITE_DATABASE_DIR=\'\"Contacts/qtcontacts-sqlite\"\'' DEFINES += 'QTCONTACTS_SQLITE_DATABASE_NAME=\'\"contacts-test.db\"\'' # we build a path like: /home/nemo/.local/share/system/Contacts/qtcontacts-sqlite-test/contacts-test.db INCLUDEPATH += \ ../../../src/engine/ HEADERS += ../../../src/engine/contactsdatabase.h SOURCES += ../../../src/engine/contactsdatabase.cpp HEADERS += ../../../src/engine/semaphore_p.h SOURCES += ../../../src/engine/semaphore_p.cpp HEADERS += ../../../src/engine/contactstransientstore.h SOURCES += ../../../src/engine/contactstransientstore.cpp HEADERS += ../../../src/engine/memorytable.h SOURCES += ../../../src/engine/memorytable.cpp HEADERS += ../../../src/engine/conversion_p.h SOURCES += ../../../src/engine/conversion.cpp HEADERS += ../../../src/engine/defaultdlggenerator.h SOURCES += ../../../src/engine/defaultdlggenerator.cpp SOURCES += stub_contactsengine.cpp SOURCES += tst_database.cpp qtcontacts-sqlite-0.3.19/tests/auto/database/stub_contactsengine.cpp000066400000000000000000000034531436373107600257300ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * * 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." */ #define QT_STATICPLUGIN #include "../../../src/engine/contactsengine.h" QString ContactsEngine::normalizedPhoneNumber(QString const& number) { return number; } qtcontacts-sqlite-0.3.19/tests/auto/database/tst_database.cpp000066400000000000000000000123741436373107600243270ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * * 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." */ #define QT_STATICPLUGIN #include #include "../../../src/engine/contactsdatabase.h" class tst_Database : public QObject { Q_OBJECT protected slots: void init(); void cleanup(); private slots: void fromDateTimeString_data(); void fromDateTimeString(); void fromDateTimeString_speed(); void fromDateTimeString_tz_speed(); void fromDateTimeString_isodate_speed(); private: char *old_TZ; }; void tst_Database::init() { old_TZ = getenv("TZ"); unsetenv("TZ"); } void tst_Database::cleanup() { if (old_TZ) setenv("TZ", old_TZ, 1); else unsetenv("TZ"); } void tst_Database::fromDateTimeString_data() { QTest::addColumn("datetime"); QTest::addColumn("TZ"); QTest::addColumn("year"); QTest::addColumn("month"); QTest::addColumn("day"); QTest::addColumn("hour"); QTest::addColumn("minute"); QTest::addColumn("second"); QTest::addColumn("msec"); QTest::newRow("base case") << "2014-08-12T14:22:09.334" << "" << 2014 << 8 << 12 << 14 << 22 << 9 << 334; // Some older databases may contain values without fractional seconds QTest::newRow("no msec") << "2014-08-12T14:22:09" << "" << 2014 << 8 << 12 << 14 << 22 << 9 << 0; // TZ should have no effect on result because the string is in UTC QTest::newRow("with TZ") << "2014-08-12T14:22:09.334" << "Europe/Helsinki" << 2014 << 8 << 12 << 14 << 22 << 9 << 334; QTest::newRow("ancient") // timestamp long before epoch << "1890-02-04T11:07:12.123" << "" << 1890 << 2 << 4 << 11 << 7 << 12 << 123; QTest::newRow("future") // past year-2038 problem << "2050-02-04T11:07:12.123" << "" << 2050 << 2 << 4 << 11 << 7 << 12 << 123; // Try a timespec that does not exist in the given TZ // It should still be parsed correctly because the time is in UTC QTest::newRow("DST trap") << "2013-03-31T03:30:09.334" << "Europe/Helsinki" << 2013 << 3 << 31 << 3 << 30 << 9 << 334; } void tst_Database::fromDateTimeString() { QFETCH(QString, datetime); QFETCH(QString, TZ); if (!TZ.isEmpty()) setenv("TZ", qPrintable(TZ), 1); QDateTime actual = ContactsDatabase::fromDateTimeString(datetime); QVERIFY(actual.isValid()); QCOMPARE(actual.timeSpec(), Qt::UTC); QTEST(actual.date().year(), "year"); QTEST(actual.date().month(), "month"); QTEST(actual.date().day(), "day"); QTEST(actual.time().hour(), "hour"); QTEST(actual.time().minute(), "minute"); QTEST(actual.time().second(), "second"); QTEST(actual.time().msec(), "msec"); } void tst_Database::fromDateTimeString_speed() { QString datetime("2014-08-12T14:22:09.334"); QBENCHMARK { QDateTime actual = ContactsDatabase::fromDateTimeString(datetime); } } void tst_Database::fromDateTimeString_tz_speed() { QString datetime("2014-08-12T14:22:09.334"); setenv("TZ", ":/etc/localtime", 1); // this works around a weird glibc issue QBENCHMARK { QDateTime actual = ContactsDatabase::fromDateTimeString(datetime); } } // Compare with Qt upstream date parsing, to see if it's worth // simplifying back to calling that. void tst_Database::fromDateTimeString_isodate_speed() { QString datetime("2014-08-12T14:22:09.334"); QBENCHMARK { QDateTime rv = QDateTime::fromString(datetime, Qt::ISODate); rv.setTimeSpec(Qt::UTC); } } QTEST_GUILESS_MAIN(tst_Database) #include "tst_database.moc" qtcontacts-sqlite-0.3.19/tests/auto/detailfetchrequest/000077500000000000000000000000001436373107600232775ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/detailfetchrequest/detailfetchrequest.pro000066400000000000000000000005121436373107600277040ustar00rootroot00000000000000TARGET = tst_detailfetchrequest include (../../common.pri) # We need access to the ContactManagerEngine header and moc output INCLUDEPATH += ../../../src/extensions/ HEADERS += ../../../src/extensions/contactmanagerengine.h \ ../../../src/extensions/qcontactdetailfetchrequest.h SOURCES += tst_detailfetchrequest.cpp qtcontacts-sqlite-0.3.19/tests/auto/detailfetchrequest/tst_detailfetchrequest.cpp000066400000000000000000000164201436373107600305650ustar00rootroot00000000000000/* * Copyright (C) 2019 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include #include #include #include #include #include #include #include #include #include "qtcontacts-extensions.h" #include "qtcontacts-extensions_manager_impl.h" #include "qcontactdetailfetchrequest.h" #include "qcontactdetailfetchrequest_impl.h" QTCONTACTS_USE_NAMESPACE Q_DECLARE_METATYPE(QList) class tst_DetailFetchRequest : public QObject { Q_OBJECT public: tst_DetailFetchRequest(); ~tst_DetailFetchRequest(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); private slots: void testDetailFetchRequest(); private: QContactManager *m_cm; QSet m_createdIds; }; tst_DetailFetchRequest::tst_DetailFetchRequest() { qRegisterMetaType("QContactId"); qRegisterMetaType >("QList"); QMap parameters; parameters.insert(QString::fromLatin1("autoTest"), QString::fromLatin1("true")); parameters.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("true")); m_cm = new QContactManager(QString::fromLatin1("org.nemomobile.contacts.sqlite"), parameters); QTest::qWait(250); // creating self contact etc will cause some signals to be emitted. ignore them. connect(m_cm, &QContactManager::contactsAdded, [this] (const QList &ids) { for (const QContactId &id : ids) { this->m_createdIds.insert(id); } }); } tst_DetailFetchRequest::~tst_DetailFetchRequest() { QTest::qWait(250); // wait for signals. if (!m_createdIds.isEmpty()) { m_cm->removeContacts(m_createdIds.toList()); m_createdIds.clear(); } delete m_cm; } void tst_DetailFetchRequest::initTestCase() { } void tst_DetailFetchRequest::init() { } void tst_DetailFetchRequest::cleanupTestCase() { QTest::qWait(250); // wait for signals. if (!m_createdIds.isEmpty()) { m_cm->removeContacts(m_createdIds.toList()); m_createdIds.clear(); } } void tst_DetailFetchRequest::cleanup() { QTest::qWait(250); // wait for signals. if (!m_createdIds.isEmpty()) { m_cm->removeContacts(m_createdIds.toList()); m_createdIds.clear(); } } void tst_DetailFetchRequest::testDetailFetchRequest() { QContact c1, c2, c3; QContactName n1, n2, n3; QContactDisplayLabel d1, d2, d3; QContactPhoneNumber p1, p2, p3; QContactEmailAddress e1, e2, e3; QContactHobby h1, h2, h3; n1.setLastName("Angry"); n1.setFirstName("Aardvark"); d1.setLabel("Test A Contact"); p1.setNumber("11111111"); e1.setEmailAddress("angry@aardvark.tld"); h1.setHobby("Acting"); c1.saveDetail(&n1); c1.saveDetail(&d1); c1.saveDetail(&p1); c1.saveDetail(&e1); c1.saveDetail(&h1); n2.setLastName("Brigand"); n2.setFirstName("Bradley"); d2.setLabel("Test B Contact"); p2.setNumber("22222222"); e2.setEmailAddress("bradley@brigand.tld"); h2.setHobby("Bungee"); c2.saveDetail(&n2); c2.saveDetail(&d2); c2.saveDetail(&p2); c2.saveDetail(&e2); c2.saveDetail(&h2); n3.setLastName("Crispy"); n3.setFirstName("Chip"); d3.setLabel("Test C Contact"); p3.setNumber("33333333"); e3.setEmailAddress("chip@crispy.tld"); h3.setHobby("Cooking"); c3.saveDetail(&n3); c3.saveDetail(&d3); c3.saveDetail(&p3); c3.saveDetail(&e3); c3.saveDetail(&h3); // store the first two contacts to the database QVERIFY(m_cm->saveContact(&c1)); QVERIFY(m_cm->saveContact(&c2)); // perform a detail fetch request query and ensure we get the details we expect QContactSortOrder ascHobbySort; ascHobbySort.setDetailType(QContactHobby::Type, QContactHobby::FieldHobby); ascHobbySort.setDirection(Qt::AscendingOrder); QContactDetailFetchRequest *dfr = new QContactDetailFetchRequest; dfr->setManager(m_cm); dfr->setType(QContactHobby::Type); dfr->setSorting(QList() << ascHobbySort); dfr->start(); QVERIFY(dfr->waitForFinished(5000)); QList hobbies = dfr->details(); // ensure that the returned details includes just h1, h2 // and that they are returned in that (ascending) order. QCOMPARE(hobbies.size(), 2); QCOMPARE(hobbies[0].type(), QContactHobby::Type); QCOMPARE(hobbies[0].value(QContactHobby::FieldHobby).toString(), h1.hobby()); QCOMPARE(hobbies[1].type(), QContactHobby::Type); QCOMPARE(hobbies[1].value(QContactHobby::FieldHobby).toString(), h2.hobby()); // store the third contact to the database QVERIFY(m_cm->saveContact(&c3)); // perform another detail fetch request // this time with a different sort order QContactSortOrder dscHobbySort; dscHobbySort.setDetailType(QContactHobby::Type, QContactHobby::FieldHobby); dscHobbySort.setDirection(Qt::DescendingOrder); dfr->setSorting(QList() << dscHobbySort); dfr->start(); QVERIFY(dfr->waitForFinished(5000)); hobbies = dfr->details(); // ensure that the returned details includes h3, h2, h1. QCOMPARE(hobbies.size(), 3); QCOMPARE(hobbies[0].type(), QContactHobby::Type); QCOMPARE(hobbies[0].value(QContactHobby::FieldHobby).toString(), h3.hobby()); QCOMPARE(hobbies[1].type(), QContactHobby::Type); QCOMPARE(hobbies[1].value(QContactHobby::FieldHobby).toString(), h2.hobby()); QCOMPARE(hobbies[2].type(), QContactHobby::Type); QCOMPARE(hobbies[2].value(QContactHobby::FieldHobby).toString(), h1.hobby()); } QTEST_MAIN(tst_DetailFetchRequest) #include "tst_detailfetchrequest.moc" qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/000077500000000000000000000000001436373107600233175ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/displaylabelgroups.pro000066400000000000000000000000511436373107600277420ustar00rootroot00000000000000TEMPLATE=subdirs SUBDIRS=testplugin test qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/test/000077500000000000000000000000001436373107600242765ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/test/test.pro000066400000000000000000000006501436373107600260000ustar00rootroot00000000000000TARGET = tst_displaylabelgroups include (../../../common.pri) # We need access to the ContactManagerEngine header and moc output INCLUDEPATH += ../../../../src/extensions/ HEADERS += ../../../../src/extensions/contactmanagerengine.h SOURCES += tst_displaylabelgroups.cpp # Override the test command to setup the environment check.commands = "QTCONTACTS_SQLITE_PLUGIN_PATH=../testplugin/contacts_dlgg/ $${check.commands}" qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/test/tst_displaylabelgroups.cpp000066400000000000000000000311471436373107600316070ustar00rootroot00000000000000/* * Copyright (C) 2019 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include #include #include #include #include #include #include #include #include "contactmanagerengine.h" #include "qtcontacts-extensions.h" #include "qtcontacts-extensions_manager_impl.h" QTCONTACTS_USE_NAMESPACE Q_DECLARE_METATYPE(QList) class tst_DisplayLabelGroups : public QObject { Q_OBJECT public: tst_DisplayLabelGroups(); ~tst_DisplayLabelGroups(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); private slots: void testDisplayLabelGroups(); private: QContactManager *m_cm; QSet m_createdIds; }; tst_DisplayLabelGroups::tst_DisplayLabelGroups() { qRegisterMetaType("QContactId"); qRegisterMetaType >("QList"); QMap parameters; parameters.insert(QString::fromLatin1("autoTest"), QString::fromLatin1("true")); parameters.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("true")); m_cm = new QContactManager(QString::fromLatin1("org.nemomobile.contacts.sqlite"), parameters); QTest::qWait(250); // creating self contact etc will cause some signals to be emitted. ignore them. connect(m_cm, &QContactManager::contactsAdded, [this] (const QList &ids) { for (const QContactId &id : ids) { this->m_createdIds.insert(id); } }); } tst_DisplayLabelGroups::~tst_DisplayLabelGroups() { QTest::qWait(250); // wait for signals. if (!m_createdIds.isEmpty()) { m_cm->removeContacts(m_createdIds.toList()); m_createdIds.clear(); } delete m_cm; } void tst_DisplayLabelGroups::initTestCase() { } void tst_DisplayLabelGroups::init() { } void tst_DisplayLabelGroups::cleanupTestCase() { QTest::qWait(250); // wait for signals. if (!m_createdIds.isEmpty()) { m_cm->removeContacts(m_createdIds.toList()); m_createdIds.clear(); } } void tst_DisplayLabelGroups::cleanup() { QTest::qWait(250); // wait for signals. if (!m_createdIds.isEmpty()) { m_cm->removeContacts(m_createdIds.toList()); m_createdIds.clear(); } } #define DETERMINE_ACTUAL_ORDER_AND_GROUPS \ do { \ actualOrder.clear(); \ actualGroups.clear(); \ for (const QContact &c : sorted) { \ actualOrder += c.detail().number(); \ if (c.detail().number().isEmpty()) { \ actualOrder += c.detail().hobby(); \ } \ actualGroups += c.detail().value(QContactDisplayLabel__FieldLabelGroup).toString(); \ } \ } while (0) void tst_DisplayLabelGroups::testDisplayLabelGroups() { #ifndef HAS_MLITE QSKIP("Test has wrong expectations if MLITE is not available"); #endif // this test relies on the display label grouping // semantics provided by the testdlggplugin. // it also relies on the displayLabelGroupPreferredProperty() // being the value: QContactName::LastName. // `dconf read /org/nemomobile/contacts/group_property` // should return 'lastName' rather than 'firstName'. // set with: // `dconf write /org/nemomobile/contacts/group_property "'lastName'"` // create some contacts QContact c1, c2, c3, c4, c5, c6, c7, c8, c9; QContactName n1, n2, n3, n4, n5, n6, n7, n8, n9; QContactDisplayLabel d1, d2, d3, d4, d5, d6, d7, d8, d9; QContactPhoneNumber p1, p2, p3, p4, p5, p6, p7, p8, p9; n1.setLastName("A"); // length=1, so group='1' n1.setFirstName("Test"); d1.setLabel("Test A Contact"); p1.setNumber("1"); c1.saveDetail(&n1); c1.saveDetail(&d1); c1.saveDetail(&p1); n2.setLastName("BBBBB"); // length=5, so group='5' n2.setFirstName("Test"); d2.setLabel("Test B Contact"); p2.setNumber("2"); c2.saveDetail(&n2); c2.saveDetail(&d2); c2.saveDetail(&p2); n3.setLastName("CCCCCCCC"); // length=8, so group='E' n3.setFirstName("Test"); d3.setLabel("Test C Contact"); p3.setNumber("3"); c3.saveDetail(&n3); c3.saveDetail(&d3); c3.saveDetail(&p3); n4.setLastName("DDDDDDD"); // length=7, so group='O' n4.setFirstName("Test"); d4.setLabel("Test D Contact"); p4.setNumber("4"); c4.saveDetail(&n4); c4.saveDetail(&d4); c4.saveDetail(&p4); n5.setLastName("EEE"); // length=3, so group='3' n5.setFirstName("Test"); d5.setLabel("Test E Contact"); p5.setNumber("5"); c5.saveDetail(&n5); c5.saveDetail(&d5); c5.saveDetail(&p5); n6.setLastName(""); // length=0, so group='Z' n6.setFirstName(""); d6.setLabel(""); p6.setNumber(""); c6.saveDetail(&n6); c6.saveDetail(&d6); c6.saveDetail(&p6); // phone number can be used to generate a display label // so don't use that. but hobby will not! so use that. QContactHobby h6; h6.setHobby("6"); c6.saveDetail(&h6); n7.setLastName("GGGGGG"); // length=6, so group='E' n7.setFirstName("Aardvark"); // should first-name sort before c3 and c7. d7.setLabel("Test G Contact"); p7.setNumber("7"); c7.saveDetail(&n7); c7.saveDetail(&d7); c7.saveDetail(&p7); n8.setLastName("HHHH"); // length=4, so group='4' n8.setFirstName("Test"); d8.setLabel("Test H Contact"); p8.setNumber("8"); c8.saveDetail(&n8); c8.saveDetail(&d8); c8.saveDetail(&p8); n9.setLastName("CCCCCCCC"); // length = 8, so group='E'; same as c3. n9.setFirstName("Abel"); // should first-name sort before c3 but after c7. d9.setLabel("Test I Contact"); p9.setNumber("9"); c9.saveDetail(&n9); c9.saveDetail(&d9); c9.saveDetail(&p9); // store them to the database QVERIFY(m_cm->saveContact(&c1)); QVERIFY(m_cm->saveContact(&c2)); QVERIFY(m_cm->saveContact(&c3)); QVERIFY(m_cm->saveContact(&c4)); QVERIFY(m_cm->saveContact(&c5)); QVERIFY(m_cm->saveContact(&c6)); QVERIFY(m_cm->saveContact(&c7)); QVERIFY(m_cm->saveContact(&c8)); QVERIFY(m_cm->saveContact(&c9)); // Ensure that they sort as we expect the test plugin to sort them. // Note that because we only have a single sort order defined, // any contacts which have the same display label group // may be returned in any order by the backend. QContactSortOrder displayLabelGroupSort; displayLabelGroupSort.setDetailType(QContactDisplayLabel::Type, QContactDisplayLabel__FieldLabelGroup); QList sorted = m_cm->contacts(displayLabelGroupSort); QString actualOrder, actualGroups; DETERMINE_ACTUAL_ORDER_AND_GROUPS; // fixup for potential ambiguity in sort order. 3, 7 and 9 all sort equally. actualOrder.replace(QChar('7'), QChar('3')); actualOrder.replace(QChar('9'), QChar('3')); QCOMPARE(actualOrder, QStringLiteral("615824333")); QCOMPARE(actualGroups, QStringLiteral("Z1345OEEE")); // Now sort by display label group followed by last name. // We expect the same sorting as display-group-only sorting, // except that contact 9's last name causes it to be sorted before contact 7. // The ordering between 3 and 9 is not disambiguated by the sort order. QContactSortOrder lastNameSort; lastNameSort.setDetailType(QContactName::Type, QContactName::FieldLastName); sorted = m_cm->contacts(QList() << displayLabelGroupSort << lastNameSort); DETERMINE_ACTUAL_ORDER_AND_GROUPS; // fixup for potential ambiguity in sort order. 3 and 9 sort equally. actualOrder.replace(QChar('9'), QChar('3')); QCOMPARE(actualOrder, QStringLiteral("615824337")); QCOMPARE(actualGroups, QStringLiteral("Z1345OEEE")); // Now sort by display label group followed by first name. // We expect the same sorting as display-group-only sorting, // except that contact 7's first name causes it to be sorted before contact 3 and contact 9, // and contact 9's first name causes it to be sorted before contact 3. QContactSortOrder firstNameSort; firstNameSort.setDetailType(QContactName::Type, QContactName::FieldFirstName); sorted = m_cm->contacts(QList() << displayLabelGroupSort << firstNameSort); DETERMINE_ACTUAL_ORDER_AND_GROUPS; QCOMPARE(actualOrder, QStringLiteral("615824793")); QCOMPARE(actualGroups, QStringLiteral("Z1345OEEE")); // Now sort by display label group followed by last name followed by first name. // We expect the same sorting as display-group-only sorting, // except that contact 9's last name causes it to be sorted before contact 7, // and contact 9 should sort before contact 3 due to the first name. sorted = m_cm->contacts(QList() << displayLabelGroupSort << lastNameSort << firstNameSort); DETERMINE_ACTUAL_ORDER_AND_GROUPS; QCOMPARE(actualOrder, QStringLiteral("615824937")); QCOMPARE(actualGroups, QStringLiteral("Z1345OEEE")); // Now add a contact which has a special name such that the test // display label group generator plugin will generate a group // for it which was previously "unknown", i.e. dynamic group. // We expect that group to be added before '#' but after other groups. QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); const QStringList oldContactDisplayLabelGroups = cme->displayLabelGroups(); QSignalSpy dlgcSpy(cme, SIGNAL(displayLabelGroupsChanged(QStringList))); QContact c10, c11; QContactName n10, n11; QContactDisplayLabel d10, d11; QContactPhoneNumber p10, p11; n10.setLastName("10ten"); // first letter is digit, should be in #. n10.setFirstName("Ten"); d10.setLabel("Test J Contact"); p10.setNumber("J"); c10.saveDetail(&n10); c10.saveDetail(&d10); c10.saveDetail(&p10); n11.setLastName("tst_displaylabelgroups_unknown_dlg"); // special case, dynamic group &. n11.setFirstName("Eleven"); d11.setLabel("Test K Contact"); p11.setNumber("K"); c11.saveDetail(&n11); c11.saveDetail(&d11); c11.saveDetail(&p11); QVERIFY(m_cm->saveContact(&c10)); QVERIFY(m_cm->saveContact(&c11)); // ensure that the resultant sort order is expected sorted = m_cm->contacts(QList() << displayLabelGroupSort << lastNameSort << firstNameSort); DETERMINE_ACTUAL_ORDER_AND_GROUPS; QCOMPARE(actualOrder, QStringLiteral("615824937KJ")); QCOMPARE(actualGroups, QStringLiteral("Z1345OEEE&#")); // should have received signal that display label groups have changed. QTest::qWait(250); QCOMPARE(dlgcSpy.count(), 1); QStringList expected(oldContactDisplayLabelGroups); expected.insert(expected.indexOf(QStringLiteral("#")), QStringLiteral("&")); // & group should have been inserted before #. QList data = dlgcSpy.takeFirst(); QCOMPARE(data.first().value(), expected); } QTEST_MAIN(tst_DisplayLabelGroups) #include "tst_displaylabelgroups.moc" qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/testplugin/000077500000000000000000000000001436373107600255155ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/testplugin/testdlggplugin.cpp000066400000000000000000000100551436373107600312560ustar00rootroot00000000000000/* * Copyright (C) 2019 Jolla Ltd. * * 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 "testdlggplugin.h" /* This test plugin provides a display label group generator with the following semantics: 1) if the name or display label data is empty, it returns 'Z' (for "zero-length") as the group. 2) if the name or display label data is greater than zero but less than six characters in length, it returns that length as a group (i.e. '1', '2', '3', '4', or '5'). 3) otherwise, if the name or display label data has an even number of characters it returns 'E' as the group, else (odd) it returns 'O' as the group. */ TestDlgg::TestDlgg(QObject *parent) : QObject(parent) { } QString TestDlgg::name() const { return QStringLiteral("testdlgg"); } int TestDlgg::priority() const { return 1; // test plugin has slightly higher than the default/fallback. } bool TestDlgg::preferredForLocale(const QLocale &) const { return true; // this test plugin is always "preferred". } bool TestDlgg::validForLocale(const QLocale &) const { return true; // this test plugin is always "valid". } QStringList TestDlgg::displayLabelGroups() const { static QStringList allGroups { QStringLiteral("Z"), QStringLiteral("1"), QStringLiteral("2"), QStringLiteral("3"), QStringLiteral("4"), QStringLiteral("5"), QStringLiteral("O"), // sort O before E to test DisplayLabelGroupSortOrder semantics QStringLiteral("E"), QStringLiteral("#"), }; return allGroups; } QString TestDlgg::displayLabelGroup(const QString &data) const { if (data.size() && data.at(0).isDigit()) { return QStringLiteral("#"); // default # group for numeric names } if (data == QStringLiteral("tst_displaylabelgroups_unknown_dlg")) { // special case: return a group which is NOT included in the "all groups" above. // this allows us to test that dynamic group adding works as expected. return QStringLiteral("&"); // should be sorted before '#' but after every other group. } if (data.size() > 5) { return (data.size() % 2 == 0) ? QStringLiteral("E") : QStringLiteral("O"); } switch (data.size()) { case 1: return QStringLiteral("1"); case 2: return QStringLiteral("2"); case 3: return QStringLiteral("3"); case 4: return QStringLiteral("4"); case 5: return QStringLiteral("5"); default: return QStringLiteral("Z"); } } qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/testplugin/testdlggplugin.h000066400000000000000000000046721436373107600307330ustar00rootroot00000000000000/* * Copyright (C) 2019 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef TESTDLGGPLUGIN_H #define TESTDLGGPLUGIN_H #include #include class TestDlgg : public QObject, QtContactsSqliteExtensions::DisplayLabelGroupGenerator { Q_OBJECT Q_PLUGIN_METADATA(IID QtContactsSqliteExtensions_DisplayLabelGroupGeneratorInterface_iid) Q_INTERFACES(QtContactsSqliteExtensions::DisplayLabelGroupGenerator) public: TestDlgg(QObject *parent = Q_NULLPTR); QString name() const Q_DECL_OVERRIDE; int priority() const Q_DECL_OVERRIDE; bool preferredForLocale(const QLocale &locale) const Q_DECL_OVERRIDE; bool validForLocale(const QLocale &locale) const Q_DECL_OVERRIDE; QString displayLabelGroup(const QString &data) const Q_DECL_OVERRIDE; QStringList displayLabelGroups() const Q_DECL_OVERRIDE; }; #endif // TESTDLGGPLUGIN_H qtcontacts-sqlite-0.3.19/tests/auto/displaylabelgroups/testplugin/testplugin.pro000066400000000000000000000006641436373107600304430ustar00rootroot00000000000000TEMPLATE = lib CONFIG += plugin c++11 link_pkgconfig INCLUDEPATH += ../../../../src/extensions/ QT -= gui HEADERS = testdlggplugin.h SOURCES = testdlggplugin.cpp TARGET = $$qtLibraryTarget(testdlgg) PLUGIN_TYPE = contacts_dlgg DESTDIR = $${PLUGIN_TYPE} PKGCONFIG += Qt5Contacts target.path = $$[QT_INSTALL_LIBS]/qtcontacts-sqlite-qt5/ INSTALLS += target qtcontacts-sqlite-0.3.19/tests/auto/memorytable/000077500000000000000000000000001436373107600217325ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/memorytable/memorytable.pro000066400000000000000000000003131436373107600247710ustar00rootroot00000000000000TARGET = tst_memorytable include(../../common.pri) HEADERS += \ ../../util.h SOURCES += \ tst_memorytable.cpp \ ../../../src/engine/conversion.cpp \ ../../../src/engine/memorytable.cpp qtcontacts-sqlite-0.3.19/tests/auto/memorytable/tst_memorytable.cpp000066400000000000000000000534221436373107600256560ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Contact: Matt Vogt * * 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 "../../util.h" #include "../../../src/engine/memorytable_p.h" #include #include class tst_MemoryTable : public QObject { Q_OBJECT public: tst_MemoryTable(); virtual ~tst_MemoryTable(); public slots: void init(); void initTestCase(); void cleanup(); void cleanupTestCase(); private slots: void invalidInitialization(); void initialization(); void noninitialization(); void addressIndependence(); void basicOperation(); void reinsertion(); void repeatedReinsertion(); void orderedReinsertion(); void replacement(); void migration(); void iteration(); private: char *testBuffer(size_t length); }; tst_MemoryTable::tst_MemoryTable() { } tst_MemoryTable::~tst_MemoryTable() { } void tst_MemoryTable::init() { } void tst_MemoryTable::initTestCase() { } void tst_MemoryTable::cleanup() { } void tst_MemoryTable::cleanupTestCase() { } char *tst_MemoryTable::testBuffer(size_t length) { Q_ASSERT((length % 16) == 0); char* buf(new char[length]); // Fill the buffer with garbage qsrand(static_cast(QDateTime::currentDateTime().toMSecsSinceEpoch())); for (char *p = buf, *end = p + length; p != end; p += sizeof(uint)) { *(reinterpret_cast(p)) = qrand(); } return buf; } void tst_MemoryTable::invalidInitialization() { QScopedArrayPointer buf(testBuffer(128)); { MemoryTable mt(0, 128, true); QCOMPARE(mt.isValid(), false); } { MemoryTable mt(buf.data() + 1, 128, true); QCOMPARE(mt.isValid(), false); } { MemoryTable mt(buf.data() + 2, 128, true); QCOMPARE(mt.isValid(), false); } { MemoryTable mt(buf.data(), 0, true); QCOMPARE(mt.isValid(), false); } { MemoryTable mt(buf.data(), 8, true); QCOMPARE(mt.isValid(), false); } } void tst_MemoryTable::initialization() { QScopedArrayPointer buf(testBuffer(128)); MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(0), false); } void tst_MemoryTable::noninitialization() { QScopedArrayPointer buf(testBuffer(128)); { MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(0), false); // Add some data QCOMPARE(mt.insert(1, QByteArray("abc")), MemoryTable::NoError); QCOMPARE(mt.insert(2, QByteArray("def")), MemoryTable::NoError); QCOMPARE(mt.insert(3, QByteArray("efg")), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(3u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(1), QByteArray("abc")); QCOMPARE(mt.value(2), QByteArray("def")); QCOMPARE(mt.value(3), QByteArray("efg")); } { // Inspect the same data MemoryTable mt(buf.data(), 128, false); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(3u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(1), QByteArray("abc")); QCOMPARE(mt.value(2), QByteArray("def")); QCOMPARE(mt.value(3), QByteArray("efg")); } } void tst_MemoryTable::addressIndependence() { QScopedArrayPointer buf(testBuffer(128)); { MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(0), false); // Add some data QCOMPARE(mt.insert(1, QByteArray("abc")), MemoryTable::NoError); QCOMPARE(mt.insert(2, QByteArray("def")), MemoryTable::NoError); QCOMPARE(mt.insert(3, QByteArray("efg")), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(3u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(1), QByteArray("abc")); QCOMPARE(mt.value(2), QByteArray("def")); QCOMPARE(mt.value(3), QByteArray("efg")); } // Copy the data to a different address QScopedArrayPointer buf2(testBuffer(128)); std::memcpy(buf2.data(), buf.data(), 128); { // Inspect the copied data MemoryTable mt(buf2.data(), 128, false); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(3u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(1), QByteArray("abc")); QCOMPARE(mt.value(2), QByteArray("def")); QCOMPARE(mt.value(3), QByteArray("efg")); // Modify the data QCOMPARE(mt.remove(1), true); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(1), QByteArray()); QCOMPARE(mt.value(2), QByteArray("def")); QCOMPARE(mt.value(3), QByteArray("efg")); QCOMPARE(mt.remove(2), true); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.contains(2), false); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(1), QByteArray()); QCOMPARE(mt.value(2), QByteArray()); QCOMPARE(mt.value(3), QByteArray("efg")); QCOMPARE(mt.remove(3), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.contains(2), false); QCOMPARE(mt.contains(3), false); QCOMPARE(mt.value(1), QByteArray()); QCOMPARE(mt.value(2), QByteArray()); QCOMPARE(mt.value(3), QByteArray()); } } void tst_MemoryTable::basicOperation() { QScopedArrayPointer buf(testBuffer(128)); MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(0), false); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.insert(1, QByteArray()), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.contains(0), false); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), false); QCOMPARE(mt.value(0), QByteArray()); QCOMPARE(mt.value(1), QByteArray()); QByteArray ba("test byte array"); QCOMPARE(mt.insert(2, ba), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(0), false); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), false); QCOMPARE(mt.value(0), QByteArray()); QCOMPARE(mt.value(1), QByteArray()); QCOMPARE(mt.value(2), ba); QCOMPARE(mt.remove(0), false); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.remove(1), true); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.contains(0), false); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), false); QCOMPARE(mt.value(0), QByteArray()); QCOMPARE(mt.value(1), QByteArray()); QCOMPARE(mt.value(2), ba); QCOMPARE(mt.remove(1), false); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.remove(2), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(0), false); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.contains(2), false); QCOMPARE(mt.contains(3), false); QCOMPARE(mt.value(0), QByteArray()); QCOMPARE(mt.value(1), QByteArray()); QCOMPARE(mt.value(2), QByteArray()); QCOMPARE(mt.remove(2), false); QCOMPARE(mt.count(), static_cast(0u)); // An impossible allocation should not disrupt the table QCOMPARE(mt.insert(3, QByteArray(1, 'x')), MemoryTable::NoError); QCOMPARE(mt.insert(2, QByteArray(128, 'y')), MemoryTable::InsufficientSpace); QCOMPARE(mt.insert(1, QByteArray(1, 'z')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(0), false); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), false); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(0), QByteArray()); QCOMPARE(mt.value(1), QByteArray(1, 'z')); QCOMPARE(mt.value(2), QByteArray()); QCOMPARE(mt.value(3), QByteArray(1, 'x')); } void tst_MemoryTable::reinsertion() { QScopedArrayPointer buf(testBuffer(128)); MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(1), false); QByteArray ba(65, 'x'); QCOMPARE(mt.insert(1, ba), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.value(1), ba); QCOMPARE(mt.remove(1), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.value(1), QByteArray()); // Reinsert - if we haven't reclaimed the space, this will fail QCOMPARE(mt.insert(1, ba), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.value(1), ba); } void tst_MemoryTable::repeatedReinsertion() { QScopedArrayPointer buf(testBuffer(128)); MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(1), false); QByteArray ba(8, 'x'); // Insert items until no space is available quint32 count[3] = { 0 }; quint32 key = 0u; MemoryTable::Error e; while ((e = mt.insert(key, ba)) == MemoryTable::NoError) { ++key; ++count[0]; } QCOMPARE(e, MemoryTable::InsufficientSpace); QCOMPARE(mt.count(), static_cast(count[0])); // Remove all items key = 0u; while (count[1] != count[0]) { QCOMPARE(mt.contains(key), true); QCOMPARE(mt.value(key), ba); QCOMPARE(mt.remove(key), true); QCOMPARE(mt.contains(key), false); ++key; ++count[1]; } QCOMPARE(count[1], count[0]); QCOMPARE(mt.count(), static_cast(0u)); // Insert the items back key = 0u; while ((e = mt.insert(key, ba)) == MemoryTable::NoError) { QCOMPARE(mt.value(key), ba); ++key; ++count[2]; } QCOMPARE(e, MemoryTable::InsufficientSpace); QCOMPARE(mt.count(), static_cast(count[2])); // We should have been able to insert the same number of items QCOMPARE(count[2], count[0]); } void tst_MemoryTable::orderedReinsertion() { QScopedArrayPointer buf(testBuffer(256)); MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(1), false); // Insert items until no space is available quint32 count[5] = { 0 }; quint32 key = 0u; MemoryTable::Error e; while ((e = mt.insert(key, QByteArray(key * 8, 'x'))) == MemoryTable::NoError) { ++key; ++count[0]; } QCOMPARE(e, MemoryTable::InsufficientSpace); QCOMPARE(mt.count(), static_cast(count[0])); // Remove all items, in reverse insertion order --key; while (count[1] != count[0]) { QCOMPARE(mt.contains(key), true); QCOMPARE(mt.value(key), QByteArray(key * 8, 'x')); QCOMPARE(mt.remove(key), true); QCOMPARE(mt.contains(key), false); --key; ++count[1]; } QCOMPARE(count[1], count[0]); QCOMPARE(mt.count(), static_cast(0u)); // Insert the items back key = 0u; while ((e = mt.insert(key, QByteArray(key * 8, 'y'))) == MemoryTable::NoError) { QCOMPARE(mt.value(key), QByteArray(key * 8, 'y')); ++key; ++count[2]; } QCOMPARE(e, MemoryTable::InsufficientSpace); QCOMPARE(mt.count(), static_cast(count[2])); // We should have been able to insert the same number of items QCOMPARE(count[2], count[0]); // Remove all items, in original insertion order key = 0u; while (count[3] != count[0]) { QCOMPARE(mt.contains(key), true); QCOMPARE(mt.value(key), QByteArray(key * 8, 'y')); QCOMPARE(mt.remove(key), true); QCOMPARE(mt.contains(key), false); ++key; ++count[3]; } QCOMPARE(count[3], count[0]); QCOMPARE(mt.count(), static_cast(0u)); // Insert the items back key = 0u; while ((e = mt.insert(key, QByteArray(key * 8, 'z'))) == MemoryTable::NoError) { QCOMPARE(mt.value(key), QByteArray(key * 8, 'z')); ++key; ++count[4]; } QCOMPARE(e, MemoryTable::InsufficientSpace); QCOMPARE(mt.count(), static_cast(count[4])); // We should have been able to insert the same number of items QCOMPARE(count[4], count[0]); } void tst_MemoryTable::replacement() { QScopedArrayPointer buf(testBuffer(128)); MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.insert(1, QByteArray(1, 'x')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.value(1), QByteArray(1, 'x')); QCOMPARE(mt.insert(2, QByteArray(10, 'y')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.value(2), QByteArray(10, 'y')); // Replacement with a larger value requires a new allocation (68 bytes consumes all available space) QCOMPARE(mt.insert(2, QByteArray(68, 'y')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.value(2), QByteArray(68, 'y')); // Replacement with a smaller value uses the same allocation QCOMPARE(mt.insert(2, QByteArray(40, 'y')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.value(2), QByteArray(40, 'y')); QCOMPARE(mt.insert(2, QByteArray(60, 'y')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.value(2), QByteArray(60, 'y')); // Replacement with a larger value still fits QCOMPARE(mt.insert(2, QByteArray(68, 'y')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.value(2), QByteArray(68, 'y')); // Replacement may fail because we can't allocate more space QCOMPARE(mt.insert(2, QByteArray(69, 'y')), MemoryTable::InsufficientSpace); // Insertion may fail because we can't expand the index, even though we have a // large enough free block from the earlier replacement QCOMPARE(mt.insert(3, QByteArray(10, 'z')), MemoryTable::InsufficientSpace); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(1), true); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), false); // Remove another item to free up the index space QCOMPARE(mt.remove(1), true); QCOMPARE(mt.count(), static_cast(1u)); QCOMPARE(mt.contains(1), false); QCOMPARE(mt.contains(2), true); QCOMPARE(mt.contains(3), false); // Insertion now succeeds QCOMPARE(mt.insert(3, QByteArray(10, 'z')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); QCOMPARE(mt.contains(3), true); QCOMPARE(mt.value(3), QByteArray(10, 'z')); // Free up all the space QCOMPARE(mt.remove(2), true); QCOMPARE(mt.remove(3), true); QCOMPARE(mt.count(), static_cast(0u)); QCOMPARE(mt.contains(2), false); QCOMPARE(mt.contains(3), false); // Free list space remains fragmented after removing all items QCOMPARE(mt.insert(1, QByteArray(69, 'x')), MemoryTable::InsufficientSpace); } void tst_MemoryTable::migration() { QScopedArrayPointer buf(testBuffer(1024)); QScopedArrayPointer buf2(testBuffer(1024)); quint32 seed = static_cast(QDateTime::currentDateTime().toMSecsSinceEpoch()); qDebug() << "Randomized test - seed:" << seed; qsrand(seed); for (int i = 0; i < 10; ++i) { MemoryTable mt(buf.data(), 1024, true); // Populate the table quint32 key = 0u; MemoryTable::Error e; while ((e = mt.insert(key, QByteArray(qrand() % 64, 'x'))) == MemoryTable::NoError) { if ((qrand() % 2) == 0) { // Update this key, possibly causing re-allocation of the block ++key; } } QCOMPARE(e, MemoryTable::InsufficientSpace); // Migrate to the next table MemoryTable mt2(buf2.data(), 1024, true); QCOMPARE(mt.migrateTo(mt2), MemoryTable::NoError); do { --key; QCOMPARE(mt2.contains(key), true); QCOMPARE(mt2.value(key), mt.value(key)); } while (key != 0u); } } void tst_MemoryTable::iteration() { QScopedArrayPointer buf(testBuffer(128)); MemoryTable mt(buf.data(), 128, true); QCOMPARE(mt.isValid(), true); QCOMPARE(mt.count(), static_cast(0u)); MemoryTable::const_iterator it, end; it = mt.constBegin(); end = mt.constEnd(); QVERIFY(it == end); QCOMPARE(std::distance(it, end), static_cast(0)); QCOMPARE(mt.insert(1, QByteArray()), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(1u)); it = mt.constBegin(); end = mt.constEnd(); QVERIFY(it != end); QCOMPARE(std::distance(it, end), static_cast(1)); QCOMPARE(static_cast(it.key()), 1); QCOMPARE(it.value(), QByteArray()); ++it; QVERIFY(it == end); QCOMPARE(std::distance(it, end), static_cast(0)); QByteArray ba("test byte array"); QCOMPARE(mt.insert(2, ba), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); it = mt.constBegin(); end = mt.constEnd(); QVERIFY(it != end); QCOMPARE(std::distance(it, end), static_cast(2)); QCOMPARE(static_cast(it.key()), 1); QCOMPARE(it.value(), QByteArray()); ++it; QVERIFY(it != end); QCOMPARE(std::distance(it, end), static_cast(1)); QCOMPARE(static_cast(it.key()), 2); QCOMPARE(it.value(), ba); ++it; QVERIFY(it == end); QCOMPARE(std::distance(it, end), static_cast(0)); QCOMPARE(mt.remove(1), true); QCOMPARE(mt.count(), static_cast(1u)); it = mt.constBegin(); end = mt.constEnd(); QVERIFY(it != end); QCOMPARE(std::distance(it, end), static_cast(1)); QCOMPARE(static_cast(it.key()), 2); QCOMPARE(it.value(), ba); ++it; QVERIFY(it == end); QCOMPARE(std::distance(it, end), static_cast(0)); QCOMPARE(mt.remove(2), true); QCOMPARE(mt.count(), static_cast(0u)); it = mt.constBegin(); end = mt.constEnd(); QVERIFY(it == end); QCOMPARE(std::distance(it, end), static_cast(0)); // An impossible allocation should not disrupt the iteration QCOMPARE(mt.insert(3, QByteArray(1, 'x')), MemoryTable::NoError); QCOMPARE(mt.insert(2, QByteArray(128, 'y')), MemoryTable::InsufficientSpace); QCOMPARE(mt.insert(1, QByteArray(1, 'z')), MemoryTable::NoError); QCOMPARE(mt.count(), static_cast(2u)); it = mt.constBegin(); end = mt.constEnd(); QVERIFY(it != end); QCOMPARE(static_cast(it.key()), 1); QCOMPARE(it.value(), QByteArray(1, 'z')); QCOMPARE(std::distance(it, end), static_cast(2)); ++it; QVERIFY(it != end); QCOMPARE(std::distance(it, end), static_cast(1)); QCOMPARE(static_cast(it.key()), 3); QCOMPARE(it.value(), QByteArray(1, 'x')); ++it; QVERIFY(it == end); QCOMPARE(std::distance(it, end), static_cast(0)); } QTEST_GUILESS_MAIN(tst_MemoryTable) #include "tst_memorytable.moc" qtcontacts-sqlite-0.3.19/tests/auto/phonenumber/000077500000000000000000000000001436373107600217345ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/phonenumber/phonenumber.pro000066400000000000000000000001701436373107600247760ustar00rootroot00000000000000TARGET = tst_phonenumber include(../../common.pri) HEADERS += \ ../../util.h SOURCES += \ tst_phonenumber.cpp qtcontacts-sqlite-0.3.19/tests/auto/phonenumber/tst_phonenumber.cpp000066400000000000000000000262411436373107600256610ustar00rootroot00000000000000/* * Copyright (C) 2013 Jolla Ltd. * * 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." */ #define QT_STATICPLUGIN #include "../../util.h" #include "../../../src/extensions/qtcontacts-extensions.h" #include "../../../src/extensions/qtcontacts-extensions_impl.h" class tst_PhoneNumber : public QObject { Q_OBJECT public: tst_PhoneNumber(); virtual ~tst_PhoneNumber(); public slots: void init(); void initTestCase(); void cleanup(); void cleanupTestCase(); private slots: void normalization_data(); void normalization(); }; tst_PhoneNumber::tst_PhoneNumber() { } tst_PhoneNumber::~tst_PhoneNumber() { } void tst_PhoneNumber::init() { } void tst_PhoneNumber::initTestCase() { } void tst_PhoneNumber::cleanup() { } void tst_PhoneNumber::cleanupTestCase() { } void tst_PhoneNumber::normalization_data() { QTest::addColumn("number"); QTest::addColumn("keepAll"); QTest::addColumn("keepPunctuation"); QTest::addColumn("keepDialString"); QTest::addColumn("normalized"); QTest::addColumn("valid"); QTest::newRow("empty") << "" << "" << "" << "" << "" << false; QTest::newRow("simple") << "1234567890" << "1234567890" << "1234567890" << "1234567890" << "1234567890" << true; QTest::newRow("unicode digits") // Non-ASCII digits converted to ASCII equivalents << QStringLiteral("1234") + QChar(0xff15) + QStringLiteral("6789") + QChar(0xff10) << "1234567890" << "1234567890" << "1234567890" << "1234567890" << true; QTest::newRow("short") << "12345" << "12345" << "12345" << "12345" << "12345" << true; QTest::newRow("bad") << "abcdefg" << "" << "" << "" << "" << false; QTest::newRow("bad trailing characters") << "1234567abcdefg" << "1234567" << "1234567" << "1234567" << "1234567" << false; QTest::newRow("bad leading characters") << "abcdefg1234567" << "1234567" << "1234567" << "1234567" << "1234567" << false; QTest::newRow("bad internal characters") << "12abc34defg567" << "1234567" << "1234567" << "1234567" << "1234567" << false; QTest::newRow("spaces") << " 123 456 7890 " << "123 456 7890" // Internal spaces are kept as punctuation << "123 456 7890" << "1234567890" << "1234567890" << true; QTest::newRow("whitespace") << "\t 123 4567\r\n89\t0 " << "123 4567890" << "123 4567890" << "1234567890" << "1234567890" << false; // Other than space, whitespace chars are invalid QTest::newRow("parentheses") << "(12)34[567]890" // Note, we permit square brackets << "(12)34[567]890" << "(12)34[567]890" << "1234567890" << "1234567890" << true; QTest::newRow("invalid braces") << "12{34}[567]890" << "1234[567]890" << "1234[567]890" << "1234567890" << "1234567890" << false; QTest::newRow("punctuation") << "12-34.567.-890" << "12-34.567.-890" << "12-34.567.-890" << "1234567890" << "1234567890" << true; QTest::newRow("invalid punctuation") << "12_34.567.|890" << "1234.567.890" << "1234.567.890" << "1234567890" << "1234567890" << false; QTest::newRow("plus 1") << "+1234567890" << "+1234567890" << "+1234567890" << "+1234567890" << "+1234567890" << true; QTest::newRow("plus 2") << "(+12)34567890" << "(+12)34567890" << "(+12)34567890" << "+1234567890" << "+1234567890" << true; QTest::newRow("plus 3") << " +[1 2] 34567890" << "+[1 2] 34567890" << "+[1 2] 34567890" << "+1234567890" << "+1234567890" << true; QTest::newRow("plus 4") << "+1234567890*1" << "+1234567890*1" << "+1234567890" << "+1234567890*1" << "+1234567890*1" << true; QTest::newRow("invalid plus 1") << "12345+67890" << "1234567890" << "1234567890" << "1234567890" << "1234567890" << false; QTest::newRow("invalid plus 2") << "1234567890+" << "1234567890" << "1234567890" << "1234567890" << "1234567890" << false; QTest::newRow("DTMF 1") << "1234567890p1" << "1234567890p1" << "1234567890" << "1234567890p1" << "1234567890p1" << true; QTest::newRow("DTMF 2") << "1234567890P1" << "1234567890P1" << "1234567890" << "1234567890P1" << "1234567890P1" << true; QTest::newRow("DTMF 3") << "1234567890w1" << "1234567890w1" << "1234567890" << "1234567890w1" << "1234567890w1" << true; QTest::newRow("DTMF 4") << "1234567890W1" << "1234567890W1" << "1234567890" << "1234567890W1" << "1234567890W1" << true; QTest::newRow("DTMF 5") << "1234567890x1" << "1234567890p1" // 'x' is converted to 'p' << "1234567890" << "1234567890p1" << "1234567890p1" << true; QTest::newRow("DTMF 6") << "1234567890X1" << "1234567890p1" // 'X' is converted to 'p' << "1234567890" << "1234567890p1" << "1234567890p1" << true; QTest::newRow("DTMF 7") << "1234567890#1" << "1234567890#1" << "1234567890" << "1234567890#1" << "1234567890#1" << true; QTest::newRow("DTMF 8") << "1234567890*1" << "1234567890*1" << "1234567890" << "1234567890*1" << "1234567890*1" << true; QTest::newRow("DTMF 9") << "1234567890w1p2x3#4*5" << "1234567890w1p2p3#4*5" << "1234567890" << "1234567890w1p2p3#4*5" << "1234567890w1p2p3#4*5" << true; QTest::newRow("DTMF 10") << " 1234567890 w1 p2 (x3) #4*5 " << "1234567890 w1 p2 (p3) #4*5" << "1234567890" << "1234567890w1p2p3#4*5" << "1234567890w1p2p3#4*5" << true; QTest::newRow("DTMF 11") << "1234567890,1" << "1234567890p1" // ',' is converted to 'p' << "1234567890" << "1234567890p1" << "1234567890p1" << true; QTest::newRow("DTMF 12") << "1234567890;1" << "1234567890w1" // ';' is converted to 'w' << "1234567890" << "1234567890w1" << "1234567890w1" << true; QTest::newRow("invalid DTMF 1") << "w12345" << "12345" << "12345" << "12345" << "12345" << false; QTest::newRow("invalid DTMF 2") << "w1p2x3" << "1p2p3" << "1" << "1p2p3" << "1p2p3" << false; QTest::newRow("invalid DTMF 3") << "1W2Y3" << "1W23" << "1" << "1W23" << "1W23" << false; QTest::newRow("invalid DTMF 4") << "Cowpox" << "" << "" << "" << "" << false; QTest::newRow("questionable DTMF") // Currently permitted << "*123#456" << "*123#456" << "" << "*123#456" << "*123#456" << true; QTest::newRow("control code 1") << "123 #31# 456" << "123 #31# 456" << "123" << "123#31#456" << "123#31#456" << true; QTest::newRow("control code 2") << "123 *31# 456" << "123 *31# 456" << "123" << "123*31#456" << "123*31#456" << true; QTest::newRow("control code 3") << "123# 3 (1# [456" << "123# 3 (1# [456" << "123" << "123#31#456" << "123#31#456" << true; QTest::newRow("control code 4") << "#31#-123456" << "#31#-123456" << "" << "#31#123456" << "#31#123456" << true; QTest::newRow("control code 5") << "*31# +123456" << "*31# +123456" << "" << "*31#+123456" << "*31#+123456" << true; QTest::newRow("control code 6") << "*31#123456#31#789" << "*31#123456#31#789" << "" << "*31#123456#31#789" << "*31#123456#31#789" << true; } void tst_PhoneNumber::normalization() { QFETCH(QString, number); QFETCH(QString, keepAll); QFETCH(QString, keepPunctuation); QFETCH(QString, keepDialString); QFETCH(QString, normalized); QFETCH(bool, valid); using namespace QtContactsSqliteExtensions; QCOMPARE(normalizePhoneNumber(number, KeepPhoneNumberPunctuation | KeepPhoneNumberDialString), keepAll); QCOMPARE(normalizePhoneNumber(number, KeepPhoneNumberPunctuation), keepPunctuation); QCOMPARE(normalizePhoneNumber(number, KeepPhoneNumberDialString), keepDialString); QCOMPARE(normalizePhoneNumber(number, KeepPhoneNumberPunctuation | KeepPhoneNumberDialString | ValidatePhoneNumber).isEmpty(), !valid); for (int i = -1; i <= 1; ++i) { size_t maxLen(DefaultMaximumPhoneNumberCharacters + i); QVERIFY(normalized.endsWith(minimizePhoneNumber(number, maxLen))); } } QTEST_GUILESS_MAIN(tst_PhoneNumber) #include "tst_phonenumber.moc" qtcontacts-sqlite-0.3.19/tests/auto/qcontactmanager/000077500000000000000000000000001436373107600225615ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/qcontactmanager/qcontactmanager.pro000066400000000000000000000005301436373107600264500ustar00rootroot00000000000000TARGET = tst_qcontactmanager include(../../common.pri) INCLUDEPATH += \ ../../../src/engine/ HEADERS += \ ../../../src/engine/contactid_p.h \ ../../../src/extensions/contactmanagerengine.h \ ../../util.h \ ../../qcontactmanagerdataholder.h SOURCES += \ ../../../src/engine/contactid.cpp \ tst_qcontactmanager.cpp qtcontacts-sqlite-0.3.19/tests/auto/qcontactmanager/tst_qcontactmanager.cpp000066400000000000000000006417031436373107600273410ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). ** Copyright (C) 2020 Open Mobile Platform LLC. ** ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Mobility Components. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #define QT_STATICPLUGIN #if defined(USE_VERSIT_PLZ) // This makes it easier to create specific QContacts #include "qversitcontactimporter.h" #include "qversitdocument.h" #include "qversitreader.h" #endif #include "../../util.h" #include "../../qcontactmanagerdataholder.h" #define SQLITE_MANAGER "org.nemomobile.contacts.sqlite" //TESTED_COMPONENT=src/contacts //TESTED_CLASS= //TESTED_FILES= // to get QFETCH to work with the template expression... typedef QMap tst_QContactManager_QStringMap; Q_DECLARE_METATYPE(tst_QContactManager_QStringMap) /* A class that no backend can support */ class UnsupportedMetatype { int foo; }; Q_DECLARE_METATYPE(UnsupportedMetatype) Q_DECLARE_METATYPE(QContact) Q_DECLARE_METATYPE(QContactManager::Error) Q_DECLARE_METATYPE(Qt::CaseSensitivity) static bool variantEqual(const QVariant &lhs, const QVariant &rhs) { // 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 >()); } return (lhs == rhs); } static bool detailValuesEqual(const QContactDetail &lhs, const QContactDetail &rhs) { const DetailMap lhsValues(detailValues(lhs, false)); const DetailMap rhsValues(detailValues(rhs, false)); if (lhsValues.count() != rhsValues.count()) { return false; } DetailMap::const_iterator lit = lhsValues.constBegin(), lend = lhsValues.constEnd(); DetailMap::const_iterator rit = rhsValues.constBegin(); for ( ; lit != lend; ++lit, ++rit) { if (!variantEqual(*lit, *rit)) { return false; } } return true; } static bool detailsEquivalent(const QContactDetail &lhs, const QContactDetail &rhs) { // Same as operator== except ignores differences in accessConstraints values if (detailType(lhs) != detailType(rhs)) return false; return detailValuesEqual(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, false)); const DetailMap rhsValues(detailValues(rhs, false)); 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 bool detailsSuperset(const QContactDetail &lhs, const QContactDetail &rhs) { // True is lhs is a superset of rhs if (detailType(lhs) != detailType(rhs)) return false; return detailValuesSuperset(lhs, rhs); } class tst_QContactManager : public QObject { Q_OBJECT public: tst_QContactManager(); virtual ~tst_QContactManager(); private: void dumpContactDifferences(const QContact& a, const QContact& b); void dumpContact(const QContact &c, QContactManager *cm); void dumpContacts(QContactManager *cm); bool isSuperset(const QContact& ca, const QContact& cb); QList removeAllDefaultDetails(const QList& details); QContactManager *newContactManager(const QMap ¶ms = QMap()); void addManagers(); // add standard managers to the data #ifndef DETAIL_DEFINITION_SUPPORTED QContact createContact(QString firstName, QString lastName, QString phoneNumber); void saveContactName(QContact *contact, QContactName *contactName, const QString &name) const; #else QContact createContact(QContactDetailDefinition nameDef, QString firstName, QString lastName, QString phoneNumber); void saveContactName(QContact *contact, QContactDetailDefinition nameDef, QContactName *contactName, const QString &name) const; void validateDefinitions(const QMap& defs) const; #endif QScopedPointer managerDataHolder; public slots: void initTestCase(); void cleanup(); void cleanupTestCase(); private slots: void doDump(); void doDump_data() {addManagers();} #ifdef MUTABLE_SCHEMA_SUPPORTED void doDumpSchema(); void doDumpSchema_data() {addManagers();} #endif /* Special test with special data */ void uriParsing(); #ifdef COMPATIBLE_CONTACT_SUPPORTED void compatibleContact(); #endif /* Backend-specific tests */ /* Presence reporting specific to qtcontacts-sqlite */ void presenceReporting(); void presenceReporting_data(); /* Transient presence accumulation */ void presenceAccumulation(); void presenceAccumulation_data() {addManagers();} /* Nonprivileged DB variant */ void nonprivileged(); /* Tests that are run on all managers */ void metadata(); void nullIdOperations(); void add(); void update(); void remove(); void removeAsync(); void batch(); void observerDeletion(); void signalEmission(); #ifdef DETAIL_DEFINITION_SUPPORTED void detailDefinitions(); #endif #ifdef DISPLAY_LABEL_SUPPORTED void displayName(); #endif void actionPreferences(); void selfContactId(); void detailOrders(); void relationships(); void contactType(); void familyDetail(); void geoLocationDetail(); void lateDeletion(); void compareVariant(); void createCollection(); void modifyCollection(); void removeCollection(); void saveContactIntoCollections(); void constituentOfSelf(); void searchSensitivity(); #if defined(USE_VERSIT_PLZ) void partialSave(); void partialSave_data() {addManagers();} #endif void extendedDetail(); void extendedDetail_data() {addManagers();} void onlineAccountFields(); void onlineAccountFields_data() {addManagers();} /* Tests that take no data */ #ifdef MUTABLE_SCHEMA_SUPPORTED void contactValidation(); #endif void errorStayingPut(); void invalidManager(); void changeSet(); void fetchHint(); #ifdef MUTABLE_SCHEMA_SUPPORTED void engineDefaultSchema(); #endif /* Special test with special data */ void uriParsing_data(); void nameSynthesis_data(); #ifdef COMPATIBLE_CONTACT_SUPPORTED void compatibleContact_data(); #endif void compareVariant_data(); /* Tests that are run on all managers */ void metadata_data() {addManagers();} void nullIdOperations_data() {addManagers();} void add_data() {addManagers();} void update_data() {addManagers();} void remove_data() {addManagers();} void removeAsync_data() {addManagers();} void batch_data() {addManagers();} void signalEmission_data() {addManagers();} void detailDefinitions_data() {addManagers();} void displayName_data() {addManagers();} void actionPreferences_data() {addManagers();} void selfContactId_data() {addManagers();} void detailOrders_data() {addManagers();} void relationships_data() {addManagers();} void contactType_data() {addManagers();} void familyDetail_data() {addManagers();} void geoLocationDetail_data() {addManagers();} void lateDeletion_data() {addManagers();} void createCollection_data() {addManagers();} void modifyCollection_data() {addManagers();} void removeCollection_data() {addManagers();} void saveContactIntoCollections_data() {addManagers();} }; // Helper class that connects to a signal on ctor, and disconnects on dtor class QTestSignalSink : public QObject { Q_OBJECT public: // signal and object must remain valid for the lifetime QTestSignalSink(QObject *object, const char *signal) : mObject(object), mSignal(signal) { connect(object, signal, this, SLOT(ignored())); } ~QTestSignalSink() { disconnect(mObject, mSignal, this, SLOT(ignored())); } public slots: void ignored() {} private: QObject *mObject; const char * const mSignal; }; static bool managerSupportsFeature(const QContactManager &m, const char *feature) { Q_UNUSED(m) // No feature tests in qtpim if (feature == QString::fromLatin1("Relationships")) { return true; } else if (feature == QString::fromLatin1("ArbitraryRelationshipTypes")) { return true; } return false; } tst_QContactManager::tst_QContactManager() { } tst_QContactManager::~tst_QContactManager() { } void tst_QContactManager::initTestCase() { registerIdType(); managerDataHolder.reset(new QContactManagerDataHolder(false)); /* Make sure these other test plugins are NOT loaded by default */ // These are now removed from the list of managers in addManagers() //QVERIFY(!QContactManager::availableManagers().contains("testdummy")); //QVERIFY(!QContactManager::availableManagers().contains("teststaticdummy")); //QVERIFY(!QContactManager::availableManagers().contains("maliciousplugin")); } void tst_QContactManager::cleanup() { QMap params; params.insert("autoTest", "true"); QString mgrUri = QContactManager::buildUri(QLatin1String(SQLITE_MANAGER), params); // the following parameter doesn't affect the uri, // as it doesn't affect id namespace for the qtcontacts-sqlite backend. params.insert("mergePresenceChanges", "false"); QScopedPointer cm(QContactManager::fromUri(mgrUri)); if (cm) { QList contacts = cm->contacts(); for (const QContact &c : contacts) { if (c.id().localId() != QByteArrayLiteral("sql-1") && c.id().localId() != QByteArrayLiteral("sql-2")) { cm->removeContact(c.id()); } } QList collections = cm->collections(); for (const QContactCollection &c : collections) { if (c.id().localId() != QByteArrayLiteral("col-1") && c.id().localId() != QByteArrayLiteral("col-2")) { cm->removeCollection(c.id()); } } } } void tst_QContactManager::cleanupTestCase() { managerDataHolder.reset(0); } void tst_QContactManager::dumpContactDifferences(const QContact& ca, const QContact& cb) { // Try to narrow down the differences QContact a(ca); QContact b(cb); QContactName n1 = a.detail(); QContactName n2 = b.detail(); // Check the name components in more detail QCOMPARE(n1.firstName(), n2.firstName()); QCOMPARE(n1.middleName(), n2.middleName()); QCOMPARE(n1.lastName(), n2.lastName()); QCOMPARE(n1.prefix(), n2.prefix()); QCOMPARE(n1.suffix(), n2.suffix()); #ifdef CUSTOM_LABEL_SUPPORTED QCOMPARE(n1.customLabel(), n2.customLabel()); #elif defined(CUSTOM_LABEL_STORAGE_SUPPORTED) QCOMPARE(n1.value(QContactName::FieldCustomLabel), n2.value(QContactName::FieldCustomLabel)); #endif #ifdef DISPLAY_LABEL_SUPPORTED // Check the display label QCOMPARE(a.displayLabel(), b.displayLabel()); #endif // Now look at the rest QList aDetails = a.details(); QList bDetails = b.details(); // They can be in any order, so loop // First remove any matches foreach(QContactDetail d, aDetails) { foreach(QContactDetail d2, bDetails) { if(d == d2) { a.removeDetail(&d); b.removeDetail(&d2); break; } } } // Now dump the extra details that were unmatched in A (note that DisplayLabel and Type are always present). // We ignore timestamp since it can get autogenerated too aDetails = a.details(); bDetails = b.details(); foreach (const QContactDetail &d, aDetails) { if (detailType(d) != detailType() && detailType(d) != detailType() && detailType(d) != detailType()) qDebug() << "A contact had extra detail:" << detailTypeName(d) << detailValues(d); } // and same for B foreach (const QContactDetail &d, bDetails) { if (detailType(d) != detailType() && detailType(d) != detailType() && detailType(d) != detailType()) qDebug() << "B contact had extra detail:" << detailTypeName(d) << detailValues(d); } #ifdef DISPLAY_LABEL_SUPPORTED // now test specifically the display label and the type if (a.displayLabel() != b.displayLabel()) { qDebug() << "A contact display label =" << a.displayLabel(); qDebug() << "B contact display label =" << b.displayLabel(); } #endif if (a.type() != b.type()) { qDebug() << "A contact type =" << a.type(); qDebug() << "B contact type =" << b.type(); } } bool tst_QContactManager::isSuperset(const QContact& ca, const QContact& cb) { // returns true if contact ca is a superset of contact cb // we use this test instead of equality because dynamic information // such as presence/location, and synthesised information such as // display label and (possibly) type, may differ between a contact // in memory and the contact in the managed store. QContact a(ca); QContact b(cb); QList aDetails = a.details(); QList bDetails = b.details(); // They can be in any order, so loop // First remove any matches foreach(QContactDetail d, aDetails) { foreach(QContactDetail d2, bDetails) { if (detailsEquivalent(d, d2)) { a.removeDetail(&d); b.removeDetail(&d2); break; } } } // Second remove any superset matches (eg, backend adds a field) aDetails = a.details(); bDetails = b.details(); foreach (QContactDetail d, aDetails) { foreach (QContactDetail d2, bDetails) { if (detailType(d) == detailType(d2)) { bool canRemove = true; DetailMap d2map = detailValues(d2, false); foreach (DetailMap::key_type key, d2map.keys()) { if (d.value(key) != d2.value(key)) { // d can have _more_ keys than d2, // but not _less_; and it cannot // change the value. canRemove = false; } } if (canRemove) { // if we get to here, we can remove the details. a.removeDetail(&d); b.removeDetail(&d2); break; } } } } // check for contact type updates if (validContactType(a)) if (validContactType(b)) if (a.type() != b.type()) return false; // nonempty type is different. // Now check to see if b has any details remaining; if so, a is not a superset. // Note that the DisplayLabel and Type can never be removed. if (b.details().size() > 2 || (b.details().size() == 2 && (detailType(b.details().value(0)) != detailType() || detailType(b.details().value(1)) != detailType()))) return false; return true; } void tst_QContactManager::dumpContact(const QContact& contact, QContactManager *cm) { #ifndef DISPLAY_LABEL_SUPPORTED Q_UNUSED(cm) qDebug() << "Contact: " << ContactId::toString(contact); #else qDebug() << "Contact: " << ContactId::toString(contact) << "(" << cm->synthesizedContactDisplayLabel(contact) << ")"; #endif foreach (const QContactDetail &d, contact.details()) { qDebug() << " " << detailType(d) << ":"; qDebug() << " Vals:" << detailValues(d); } } void tst_QContactManager::dumpContacts(QContactManager *cm) { QList ids = cm->contactIds(); qDebug() << "There are" << ids.count() << "contacts in" << cm->managerUri(); foreach (const QContactId &id, ids) { QContact c = cm->contact(id); dumpContact(c, cm); } } void tst_QContactManager::uriParsing_data() { QTest::addColumn("uri"); QTest::addColumn("good"); // is this a good uri or not QTest::addColumn("manager"); QTest::addColumn >("parameters"); QMap inparameters; inparameters.insert("foo", "bar"); inparameters.insert("bazflag", QString()); inparameters.insert("bar", "glob"); QMap inparameters2; inparameters2.insert("this has spaces", QString()); inparameters2.insert("and& an", " &"); inparameters2.insert("and an ", "=quals"); QTest::newRow("built") << QContactManager::buildUri("manager", inparameters) << true << "manager" << inparameters; QTest::newRow("built with escaped parameters") << QContactManager::buildUri("manager", inparameters2) << true << "manager" << inparameters2; QTest::newRow("no scheme") << "this should not split" << false << QString() << tst_QContactManager_QStringMap(); QTest::newRow("wrong scheme") << "invalidscheme:foo bar" << false << QString() << tst_QContactManager_QStringMap(); QTest::newRow("right scheme, no colon") << "qtcontacts" << false << QString() << tst_QContactManager_QStringMap(); QTest::newRow("no manager, colon, no params") << "qtcontacts::" << false << "manager" << tst_QContactManager_QStringMap(); QTest::newRow("yes manager, no colon, no params") << "qtcontacts:manager" << true << "manager" << tst_QContactManager_QStringMap(); QTest::newRow("yes manager, yes colon, no params") << "qtcontacts:manager:" << true << "manager"<< tst_QContactManager_QStringMap(); QTest::newRow("yes params") << "qtcontacts:manager:foo=bar&bazflag=&bar=glob" << true << "manager" << inparameters; QTest::newRow("yes params but misformed") << "qtcontacts:manager:foo=bar&=gloo&bar=glob" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 2") << "qtcontacts:manager:=&=gloo&bar=glob" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 3") << "qtcontacts:manager:==" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 4") << "qtcontacts:manager:&&" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 5") << "qtcontacts:manager:&goo=bar" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 6") << "qtcontacts:manager:goo&bar" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 7") << "qtcontacts:manager:goo&bar&gob" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 8") << "qtcontacts:manager:==&&==&goo=bar" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 9") << "qtcontacts:manager:foo=bar=baz" << false << "manager" << inparameters; QTest::newRow("yes params but misformed 10") << "qtcontacts:manager:foo=bar=baz=glob" << false << "manager" << inparameters; QTest::newRow("no manager but yes params") << "qtcontacts::foo=bar&bazflag=&bar=glob" << false << QString() << inparameters; QTest::newRow("no manager or params") << "qtcontacts::" << false << QString() << inparameters; QTest::newRow("no manager or params or colon") << "qtcontacts:" << false << QString() << inparameters; } QContactManager *tst_QContactManager::newContactManager(const QMap ¶ms) { QMap parameters; parameters.insert("autoTest", "true"); parameters.insert("mergePresenceChanges", "false"); QMap::const_iterator it = params.constBegin(), end = params.constEnd(); for ( ; it != end; ++it) { parameters.insert(it.key(), it.value()); } return new QContactManager(SQLITE_MANAGER, parameters); } void tst_QContactManager::addManagers() { QTest::addColumn("uri"); QTest::addColumn("params"); // Only test the qtcontacts-sqlite engine QMap params; params.insert("autoTest", "true"); const QString uri = QContactManager::buildUri(SQLITE_MANAGER, params); // the following parameter doesn't affect the uri, // as it doesn't affect id namespace for the qtcontacts-sqlite backend. params.insert("mergePresenceChanges", "false"); QTest::newRow("mgr='" SQLITE_MANAGER "'") << uri << params; } /* * Helper method for creating a QContact instance with name and phone number * details. Name is generated according to the detail definition assuming that * either first and last name or custom label is supported. */ QContact tst_QContactManager::createContact( #ifdef DETAIL_DEFINITION_SUPPORTED QContactDetailDefinition nameDef, #endif QString firstName, QString lastName, QString phoneNumber) { QContact contact; if(!firstName.isEmpty() || !lastName.isEmpty()) { QContactName n; #ifndef DETAIL_DEFINITION_SUPPORTED n.setFirstName(firstName); n.setLastName(lastName); #else if(nameDef.fields().contains(QContactName::FieldFirstName) && nameDef.fields().contains(QContactName::FieldFirstName)) { n.setFirstName(firstName); n.setLastName(lastName); } else if(nameDef.fields().contains(QContactName::FieldCustomLabel)) { n.setCustomLabel(firstName + " " + lastName); } else { // assume that either first and last name or custom label is supported QTest::qWarn("Neither custom label nor first name/last name supported!"); return QContact(); } #endif contact.saveDetail(&n); } if (!phoneNumber.isEmpty()) { QContactPhoneNumber ph; ph.setNumber(phoneNumber); contact.saveDetail(&ph); } return contact; } #ifndef DETAIL_DEFINITION_SUPPORTED void tst_QContactManager::saveContactName(QContact *contact, QContactName *contactName, const QString &name) const #else void tst_QContactManager::saveContactName(QContact *contact, QContactDetailDefinition nameDef, QContactName *contactName, const QString &name) const #endif { #ifndef DETAIL_DEFINITION_SUPPORTED #ifdef CUSTOM_LABEL_SUPPORTED contactName->setCustomLabel(name); #elif defined(CUSTOM_LABEL_STORAGE_SUPPORTED) contactName->setValue(QContactName::FieldCustomLabel, name); #else contactName->setFirstName(name); #endif #else // check which name fields are supported in the following order: // 1. custom label, 2. first name, 3. last name if(nameDef.fields().contains(QContactName::FieldCustomLabel)) { contactName->setCustomLabel(name); } else if(nameDef.fields().contains(QContactName::FieldFirstName)) { contactName->setFirstName(name); } else if(nameDef.fields().contains(QContactName::FieldLastName)) { contactName->setLastName(name); } else { // Assume that at least one of the above name fields is supported by the backend QVERIFY(false); } #endif contact->saveDetail(contactName); } void tst_QContactManager::metadata() { // ensure that the backend is publishing its metadata (name / parameters / uri) correctly QFETCH(QString, uri); QFETCH(tst_QContactManager_QStringMap, params); QScopedPointer cm(newContactManager()); QCOMPARE(uri, cm->managerUri()); QCOMPARE(params, cm->managerParameters()); } void tst_QContactManager::nullIdOperations() { QFETCH(QString, uri); QScopedPointer cm(newContactManager()); QVERIFY(!cm->removeContact(QContactId())); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); QContact c = cm->contact(QContactId()); QVERIFY(c.id() == QContactId()); QVERIFY(c.isEmpty()); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); } void tst_QContactManager::uriParsing() { QFETCH(QString, uri); QFETCH(bool, good); QFETCH(QString, manager); QFETCH(tst_QContactManager_QStringMap, parameters); QString outmanager; QMap outparameters; if (good) { /* Good split */ /* Test splitting */ QVERIFY(QContactManager::parseUri(uri, 0, 0)); // no out parms // 1 out param QVERIFY(QContactManager::parseUri(uri, &outmanager, 0)); QCOMPARE(manager, outmanager); QVERIFY(QContactManager::parseUri(uri, 0, &outparameters)); QCONTACTMANAGER_REMOVE_VERSIONS_FROM_URI(outparameters); QCOMPARE(parameters, outparameters); outmanager.clear(); outparameters.clear(); QVERIFY(QContactManager::parseUri(uri, &outmanager, &outparameters)); QCONTACTMANAGER_REMOVE_VERSIONS_FROM_URI(outparameters); QCOMPARE(manager, outmanager); QCOMPARE(parameters, outparameters); } else { /* bad splitting */ outmanager.clear(); outparameters.clear(); QVERIFY(QContactManager::parseUri(uri, 0, 0) == false); QVERIFY(QContactManager::parseUri(uri, &outmanager, 0) == false); QVERIFY(outmanager.isEmpty()); QVERIFY(QContactManager::parseUri(uri, 0, &outparameters) == false); QCONTACTMANAGER_REMOVE_VERSIONS_FROM_URI(outparameters); QVERIFY(outparameters.isEmpty()); /* make sure the in parameters don't change with a bad split */ outmanager = manager; outparameters = parameters; QVERIFY(QContactManager::parseUri(uri, &outmanager, 0) == false); QCOMPARE(manager, outmanager); QVERIFY(QContactManager::parseUri(uri, 0, &outparameters) == false); QCONTACTMANAGER_REMOVE_VERSIONS_FROM_URI(outparameters); QCOMPARE(parameters, outparameters); } } void tst_QContactManager::doDump() { // Only do this if it has been explicitly selected if (QCoreApplication::arguments().contains("doDump")) { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); dumpContacts(cm.data()); } } Q_DECLARE_METATYPE(QVariant) #ifdef MUTABLE_SCHEMA_SUPPORTED void tst_QContactManager::doDumpSchema() { // Only do this if it has been explicitly selected if (QCoreApplication::arguments().contains("doDumpSchema")) { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); // Get the schema for each supported type foreach(QString type, cm->supportedContactTypes()) { QMap defs = cm->detailDefinitions(type); foreach(QContactDetailDefinition def, defs.values()) { if (def.isUnique()) qDebug() << QString("%2::%1 (Unique) {").arg(def.name()).arg(type).toAscii().constData(); else qDebug() << QString("%2::%1 {").arg(def.name()).arg(type).toAscii().constData(); QMap fields = def.fields(); foreach(QString fname, fields.keys()) { QContactDetailFieldDefinition field = fields.value(fname); if (field.allowableValues().count() > 0) { // Make some pretty output QStringList allowedList; foreach(QVariant var, field.allowableValues()) { QString allowed; if (var.type() == QVariant::String) allowed = QString("'%1'").arg(var.toString()); else if (var.type() == QVariant::StringList) allowed = QString("'%1'").arg(var.toStringList().join(",")); else { // use the textstream << QDebug dbg(&allowed); dbg << var; } allowedList.append(allowed); } qDebug() << QString(" %2 %1 {%3}").arg(fname).arg(QMetaType::typeName(field.dataType())).arg(allowedList.join(",")).toAscii().constData(); } else qDebug() << QString(" %2 %1").arg(fname).arg(QMetaType::typeName(field.dataType())).toAscii().constData(); } qDebug() << "}"; } } } } #endif void tst_QContactManager::add() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); #ifndef DETAIL_DEFINITION_SUPPORTED QContact alice = createContact("Alice", "inWonderland", "1234567"); #else QContactDetailDefinition nameDef = cm->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact); QContact alice = createContact(nameDef, "Alice", "inWonderland", "1234567"); #endif int currCount = cm->contactIds().count(); QVERIFY(cm->saveContact(&alice)); QVERIFY(cm->error() == QContactManager::NoError); QVERIFY(!alice.id().managerUri().isEmpty()); QVERIFY(ContactId::isValid(alice.id())); QCOMPARE(cm->contactIds().count(), currCount+1); // Test that the ID is roundtripped via string correctly QCOMPARE(QContactId::fromString(alice.id().toString()), alice.id()); QContact added = cm->contact(retrievalId(alice)); QVERIFY(added.id() == alice.id()); if (!isSuperset(added, alice)) { dumpContacts(cm.data()); dumpContactDifferences(added, alice); QCOMPARE(added, alice); } // Verify that the computed properties are correct QContactStatusFlags flags = added.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::HasPhoneNumber), true); QCOMPARE(flags.testFlag(QContactStatusFlags::HasEmailAddress), false); QCOMPARE(flags.testFlag(QContactStatusFlags::HasOnlineAccount), false); QCOMPARE(flags.testFlag(QContactStatusFlags::IsDeactivated), false); // now try adding a contact that does not exist in the database with non-zero id #ifndef DETAIL_DEFINITION_SUPPORTED QContact nonexistent = createContact("nonexistent", "contact", ""); #else QContact nonexistent = createContact(nameDef, "nonexistent", "contact", ""); #endif QVERIFY(cm->saveContact(&nonexistent)); // should work QVERIFY(cm->removeContact(removalId(nonexistent))); // now nonexistent has an id which does not exist QVERIFY(!cm->saveContact(&nonexistent)); // hence, should fail QCOMPARE(cm->error(), QContactManager::DoesNotExistError); nonexistent.setId(QContactId()); QVERIFY(cm->saveContact(&nonexistent)); // after setting id to zero, should save QVERIFY(cm->removeContact(removalId(nonexistent))); #ifdef DETAIL_DEFINITION_SUPPORTED // now try adding a "megacontact" // - get list of all definitions supported by the manager // - add one detail of each definition to a contact // - save the contact // - read it back // - ensure that it's the same. QContact megacontact; QMap defmap = cm->detailDefinitions(); QList defs = defmap.values(); foreach (const QContactDetailDefinition def, defs) { // Leave these warnings here - might need an API for this // XXX FIXME: access constraint reporting as moved to the detail itself //if (def.accessConstraint() == QContactDetailDefinition::ReadOnly) { // continue; //} // This is probably read-only if (def.name() == QContactTimestamp::DefinitionName) continue; // otherwise, create a new detail of the given type and save it to the contact QContactDetail det(def.name()); QMap fieldmap = def.fields(); QStringList fieldKeys = fieldmap.keys(); foreach (const QString& fieldKey, fieldKeys) { // get the field, and check to see that it's not constrained. QContactDetailFieldDefinition currentField = fieldmap.value(fieldKey); // Don't test detail uris as these are manager specific if (fieldKey == QContactDetail::FieldDetailUri) continue; // Special case: phone number. if (def.name() == QContactPhoneNumber::DefinitionName && fieldKey == QContactPhoneNumber::FieldNumber) { det.setValue(fieldKey, "+3581234567890"); continue; } // Attempt to create a worthy value if (!currentField.allowableValues().isEmpty()) { // we want to save a value that will be accepted. if (currentField.dataType() == QVariant::StringList) det.setValue(fieldKey, QStringList() << currentField.allowableValues().first().toString()); else if (currentField.dataType() == QVariant::List) det.setValue(fieldKey, QVariantList() << currentField.allowableValues().first()); else det.setValue(fieldKey, currentField.allowableValues().first()); } else { // any value of the correct type will be accepted bool savedSuccessfully = false; QVariant dummyValue = QVariant(fieldKey); // try to get some unique string data if (dummyValue.canConvert(currentField.dataType())) { savedSuccessfully = dummyValue.convert(currentField.dataType()); if (savedSuccessfully) { // we have successfully created a (supposedly) valid field for this detail. det.setValue(fieldKey, dummyValue); continue; } } // nope, couldn't save the string value (test); try a date. dummyValue = QVariant(QDate::currentDate()); if (dummyValue.canConvert(currentField.dataType())) { savedSuccessfully = dummyValue.convert(currentField.dataType()); if (savedSuccessfully) { // we have successfully created a (supposedly) valid field for this detail. det.setValue(fieldKey, dummyValue); continue; } } // nope, couldn't convert a string or a date - try the integer value (42) dummyValue = QVariant(42); if (dummyValue.canConvert(currentField.dataType())) { savedSuccessfully = dummyValue.convert(currentField.dataType()); if (savedSuccessfully) { // we have successfully created a (supposedly) valid field for this detail. det.setValue(fieldKey, dummyValue); continue; } } // if we get here, we don't know what sort of value can be saved... } } if (!det.isEmpty()) megacontact.saveDetail(&det); } QVERIFY(cm->saveContact(&megacontact)); // must be able to save since built from definitions. QContact retrievedMegacontact = cm->contact(retrievalId(megacontact)); if (!isSuperset(retrievedMegacontact, megacontact)) { dumpContactDifferences(retrievedMegacontact, megacontact); QEXPECT_FAIL("mgr='wince'", "Address Display Label mismatch", Continue); QCOMPARE(megacontact, retrievedMegacontact); } #endif // now a contact with many details of a particular definition // if the detail is not unique it should then support minimum of two of the same kind const int nrOfdetails = 2; #ifndef DETAIL_DEFINITION_SUPPORTED QContact veryContactable = createContact("Very", "Contactable", ""); #else QContact veryContactable = createContact(nameDef, "Very", "Contactable", ""); #endif for (int i = 0; i < nrOfdetails; i++) { QString phnStr = QString::number(i); QContactPhoneNumber vcphn; vcphn.setNumber(phnStr); QVERIFY(veryContactable.saveDetail(&vcphn)); } // check that all the numbers were added successfully QVERIFY(veryContactable.details().size() == nrOfdetails); #ifdef DETAIL_DEFINITION_SUPPORTED // check if it can be saved QContactDetailDefinition def = cm->detailDefinition(QContactPhoneNumber::DefinitionName); if (def.isUnique()) { QVERIFY(!cm->saveContact(&veryContactable)); } else { QVERIFY(cm->saveContact(&veryContactable)); // verify save QContact retrievedContactable = cm->contact(retrievalId(veryContactable)); if (!isSuperset(retrievedContactable, veryContactable)) { dumpContactDifferences(veryContactable, retrievedContactable); QEXPECT_FAIL("mgr='wince'", "Number of phones supported mismatch", Continue); QCOMPARE(veryContactable, retrievedContactable); } } #endif } void tst_QContactManager::update() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); /* Save a new contact first */ int contactCount = cm->contacts().size(); #ifndef DETAIL_DEFINITION_SUPPORTED QContact alice = createContact("AliceUpdate", "inWonderlandUpdate", "2345678"); #else QContactDetailDefinition nameDef = cm->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact); QContact alice = createContact(nameDef, "AliceUpdate", "inWonderlandUpdate", "2345678"); #endif QVERIFY(cm->saveContact(&alice)); QVERIFY(cm->error() == QContactManager::NoError); contactCount += 1; // added a new contact. QCOMPARE(cm->contacts().size(), contactCount); /* Update name */ QContactName name = alice.detail(); #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&alice, &name, "updated"); #else saveContactName(&alice, nameDef, &name, "updated"); #endif QVERIFY(cm->saveContact(&alice)); QVERIFY(cm->error() == QContactManager::NoError); #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&alice, &name, "updated2"); #else saveContactName(&alice, nameDef, &name, "updated2"); #endif QVERIFY(cm->saveContact(&alice)); QVERIFY(cm->error() == QContactManager::NoError); alice = cm->contact(retrievalId(alice)); // force reload of (persisted) alice QContact updated = cm->contact(retrievalId(alice)); QContactName updatedName = updated.detail(); QCOMPARE(updatedName, name); QCOMPARE(cm->contacts().size(), contactCount); // contact count should be the same, no new contacts QContactStatusFlags flags = updated.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::HasPhoneNumber), true); QCOMPARE(flags.testFlag(QContactStatusFlags::HasEmailAddress), false); QCOMPARE(flags.testFlag(QContactStatusFlags::HasOnlineAccount), false); QCOMPARE(flags.testFlag(QContactStatusFlags::IsDeactivated), false); /* Test that adding a new detail doesn't cause unwanted side effects */ int detailCount = alice.details().size(); QContactEmailAddress email; email.setEmailAddress("test@example.com"); alice.saveDetail(&email); QVERIFY(cm->saveContact(&alice)); QCOMPARE(cm->contacts().size(), contactCount); // contact count shoudl be the same, no new contacts // This test is imprecise, since backends can add timestamps etc... detailCount += 1; updated = cm->contact(retrievalId(alice)); QVERIFY(updated.details().size() >= detailCount); flags = updated.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::HasPhoneNumber), true); QCOMPARE(flags.testFlag(QContactStatusFlags::HasEmailAddress), true); QCOMPARE(flags.testFlag(QContactStatusFlags::HasOnlineAccount), false); QCOMPARE(flags.testFlag(QContactStatusFlags::IsDeactivated), false); /* Test that removal of fields in a detail works */ QContactPhoneNumber phn = alice.detail(); phn.setNumber("1234567"); phn.setContexts(QContactDetail::ContextHome); alice.saveDetail(&phn); QVERIFY(cm->saveContact(&alice)); alice = cm->contact(retrievalId(alice)); // force reload of (persisted) alice QVERIFY(alice.detail().contexts().contains(QContactDetail::ContextHome)); // check context saved. phn = alice.detail(); // reload the detail, since it's key could have changed phn.setContexts(QList()); // remove context field. alice.saveDetail(&phn); QVERIFY(cm->saveContact(&alice)); alice = cm->contact(retrievalId(alice)); // force reload of (persisted) alice QVERIFY(alice.detail().contexts().isEmpty()); // check context removed. QCOMPARE(cm->contacts().size(), contactCount); // removal of a field of a detail shouldn't affect the contact count // This test is dangerous, since backends can add timestamps etc... QCOMPARE(detailCount, alice.details().size()); // removing a field from a detail should affect the detail count /* Test that removal of details works */ phn = alice.detail(); // reload the detail, since it's key could have changed alice.removeDetail(&phn); QVERIFY(cm->saveContact(&alice)); alice = cm->contact(retrievalId(alice)); // force reload of (persisted) alice QVERIFY(alice.details().isEmpty()); // no such detail. QCOMPARE(cm->contacts().size(), contactCount); // removal of a detail shouldn't affect the contact count flags = alice.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::HasPhoneNumber), false); QCOMPARE(flags.testFlag(QContactStatusFlags::HasEmailAddress), true); QCOMPARE(flags.testFlag(QContactStatusFlags::HasOnlineAccount), false); QCOMPARE(flags.testFlag(QContactStatusFlags::IsDeactivated), false); // This test is dangerous, since backends can add timestamps etc... //detailCount -= 1; //QCOMPARE(detailCount, alice.details().size()); // removing a detail should cause the detail count to decrease by one. if (managerSupportsFeature(*cm, "Groups")) { // Try changing types - not allowed // from contact -> group alice.setType(QContactType::TypeGroup); QContactName na = alice.detail(); alice.removeDetail(&na); QVERIFY(!cm->saveContact(&alice)); QVERIFY(cm->error() == QContactManager::AlreadyExistsError); // from group -> contact #ifndef DETAIL_DEFINITION_SUPPORTED QContact jabberwock = createContact("", "", "1234567890"); #else QContact jabberwock = createContact(nameDef, "", "", "1234567890"); #endif jabberwock.setType(QContactType::TypeGroup); QVERIFY(cm->saveContact(&jabberwock)); jabberwock.setType(QContactType::TypeContact); QVERIFY(!cm->saveContact(&jabberwock)); QVERIFY(cm->error() == QContactManager::AlreadyExistsError); } } void tst_QContactManager::remove() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); const int contactCount = cm->contactIds().count(); QTest::qWait(500); // wait for signal coalescing. QSignalSpy addedSpy(cm.data(), contactsAddedSignal); QSignalSpy removedSpy(cm.data(), contactsRemovedSignal); /* Save a new contact first */ #ifndef DETAIL_DEFINITION_SUPPORTED QContact alice = createContact("AliceRemove", "inWonderlandRemove", "123456789"); #else QContactDetailDefinition nameDef = cm->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact); QContact alice = createContact(nameDef, "AliceRemove", "inWonderlandRemove", "123456789"); #endif QVERIFY(cm->saveContact(&alice)); QVERIFY(cm->error() == QContactManager::NoError); QVERIFY(alice.id() != QContactId()); QVERIFY(cm->contactIds().count() > contactCount); QTRY_VERIFY(addedSpy.count() > 0); addedSpy.clear(); QVERIFY(removedSpy.count() == 0); /* Remove the created contact */ QVERIFY(cm->removeContact(retrievalId(alice))); QCOMPARE(cm->contactIds().count(), contactCount); QVERIFY(cm->contact(retrievalId(alice)).isEmpty()); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); QTRY_VERIFY(removedSpy.count() > 0); removedSpy.clear(); QVERIFY(addedSpy.count() == 0); } void tst_QContactManager::removeAsync() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); const int contactCount = cm->contactIds().count(); QTest::qWait(500); // wait for signal coalescing. QSignalSpy addedSpy(cm.data(), contactsAddedSignal); QSignalSpy removedSpy(cm.data(), contactsRemovedSignal); /* Save a new contact first */ #ifndef DETAIL_DEFINITION_SUPPORTED QContact alice = createContact("AliceRemove", "inWonderlandRemove", "123456789"); #else QContactDetailDefinition nameDef = cm->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact); QContact alice = createContact(nameDef, "AliceRemove", "inWonderlandRemove", "123456789"); #endif /* Use an asynchronous save request */ QContactSaveRequest saveRequest; saveRequest.setContact(alice); saveRequest.setManager(cm.data()); saveRequest.start(); QVERIFY(saveRequest.waitForFinished()); QVERIFY(saveRequest.isFinished()); QMap errorMap(saveRequest.errorMap()); QVERIFY(errorMap.count() == 0 || (errorMap.count() == 1 && errorMap.first() == QContactManager::NoError)); alice = saveRequest.contacts().first(); QVERIFY(alice.id() != QContactId()); QVERIFY(cm->contactIds().count() > contactCount); QTRY_VERIFY(addedSpy.count() > 0); addedSpy.clear(); QVERIFY(removedSpy.count() == 0); /* Remove the created contact asynchronously */ QContactRemoveRequest removeRequest; removeRequest.setContactId(alice.id()); removeRequest.setManager(cm.data()); removeRequest.start(); QVERIFY(removeRequest.waitForFinished()); QVERIFY(removeRequest.isFinished()); errorMap = removeRequest.errorMap(); QVERIFY(errorMap.count() == 0 || (errorMap.count() == 1 && errorMap.first() == QContactManager::NoError)); QCOMPARE(cm->contactIds().count(), contactCount); QVERIFY(cm->contact(retrievalId(alice)).isEmpty()); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); QTRY_VERIFY(removedSpy.count() > 0); removedSpy.clear(); QVERIFY(addedSpy.count() == 0); } void tst_QContactManager::batch() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); /* First test null pointer operations */ QVERIFY(!cm->saveContacts(NULL, NULL)); QVERIFY(cm->error() == QContactManager::BadArgumentError); QVERIFY(!cm->removeContacts(QList(), NULL)); QVERIFY(cm->error() == QContactManager::BadArgumentError); // Get supported name field int nameField = QContactName::FieldFirstName; /* Now add 3 contacts, all valid */ QContact a; QContactName na; na.setValue(nameField, "XXXXXX Albert"); a.saveDetail(&na); QContact b; QContactName nb; nb.setValue(nameField, "XXXXXX Bob"); b.saveDetail(&nb); QContact c; QContactName nc; nc.setValue(nameField, "XXXXXX Carol"); c.saveDetail(&nc); QList contacts; contacts << a << b << c; QMap errorMap; // Add one dummy error to test if the errors are reset errorMap.insert(0, QContactManager::NoError); QVERIFY(cm->saveContacts(&contacts, &errorMap)); QCOMPARE(cm->error(), QContactManager::NoError); QCOMPARE(errorMap.count(), 0); /* Make sure our contacts got updated too */ QCOMPARE(contacts.count(), 3); QVERIFY(contacts.at(0).id() != QContactId()); QVERIFY(contacts.at(1).id() != QContactId()); QVERIFY(contacts.at(2).id() != QContactId()); QCOMPARE(contacts.at(0).detail().value(nameField), na.value(nameField)); QCOMPARE(contacts.at(1).detail().value(nameField), nb.value(nameField)); QCOMPARE(contacts.at(2).detail().value(nameField), nc.value(nameField)); /* Retrieve again */ a = cm->contact(retrievalId(contacts.at(0))); b = cm->contact(retrievalId(contacts.at(1))); c = cm->contact(retrievalId(contacts.at(2))); QCOMPARE(contacts.at(0).detail().value(nameField), na.value(nameField)); QCOMPARE(contacts.at(1).detail().value(nameField), nb.value(nameField)); QCOMPARE(contacts.at(2).detail().value(nameField), nc.value(nameField)); /* Save again, with a null error map */ QVERIFY(cm->saveContacts(&contacts, NULL)); QCOMPARE(cm->error(), QContactManager::NoError); /* Now make an update to them all */ QContactPhoneNumber number; number.setNumber("1234567"); QVERIFY(contacts[0].saveDetail(&number)); number.setNumber("234567"); QVERIFY(contacts[1].saveDetail(&number)); number.setNumber("34567"); QVERIFY(contacts[2].saveDetail(&number)); QVERIFY(cm->saveContacts(&contacts, &errorMap)); QCOMPARE(cm->error(), QContactManager::NoError); QCOMPARE(errorMap.count(), 0); /* Retrieve them and check them again */ a = cm->contact(retrievalId(contacts.at(0))); b = cm->contact(retrievalId(contacts.at(1))); c = cm->contact(retrievalId(contacts.at(2))); QCOMPARE(contacts.at(0).detail().value(nameField), na.value(nameField)); QCOMPARE(contacts.at(1).detail().value(nameField), nb.value(nameField)); QCOMPARE(contacts.at(2).detail().value(nameField), nc.value(nameField)); QCOMPARE(a.details().count(), 1); QCOMPARE(b.details().count(), 1); QCOMPARE(c.details().count(), 1); QCOMPARE(a.details().at(0).number(), QString::fromLatin1("1234567")); QCOMPARE(b.details().at(0).number(), QString::fromLatin1("234567")); QCOMPARE(c.details().at(0).number(), QString::fromLatin1("34567")); /* Retrieve them with the batch ID fetch API */ QList batchIds; batchIds << ContactId::apiId(a) << ContactId::apiId(b) << ContactId::apiId(c); // Null error map first (doesn't crash) QMap map; QList batchFetch = cm->contacts(batchIds, QContactFetchHint(), &map); QCOMPARE(cm->error(), QContactManager::NoError); QCOMPARE(batchFetch.count(), 3); QCOMPARE(batchFetch.at(0).detail().value(nameField), na.value(nameField)); QCOMPARE(batchFetch.at(1).detail().value(nameField), nb.value(nameField)); QCOMPARE(batchFetch.at(2).detail().value(nameField), nc.value(nameField)); // With error map batchFetch = cm->contacts(batchIds, QContactFetchHint(), &errorMap); QCOMPARE(cm->error(), QContactManager::NoError); QCOMPARE(errorMap.count(), 0); QCOMPARE(batchFetch.count(), 3); QCOMPARE(batchFetch.at(0).detail().value(nameField), na.value(nameField)); QCOMPARE(batchFetch.at(1).detail().value(nameField), nb.value(nameField)); QCOMPARE(batchFetch.at(2).detail().value(nameField), nc.value(nameField)); QCOMPARE(batchFetch.at(0).details().count(), 1); QCOMPARE(batchFetch.at(1).details().count(), 1); QCOMPARE(batchFetch.at(2).details().count(), 1); /* Now an empty id */ batchIds.clear(); batchIds << QContactId() << ContactId::apiId(a) << ContactId::apiId(b) << ContactId::apiId(c); batchFetch = cm->contacts(batchIds, QContactFetchHint(), 0); QVERIFY(cm->error() != QContactManager::NoError); QCOMPARE(batchFetch.count(), 4); QCOMPARE(batchFetch.at(0).detail(), QContactName()); QCOMPARE(batchFetch.at(1).detail().value(nameField), na.value(nameField)); QCOMPARE(batchFetch.at(2).detail().value(nameField), nb.value(nameField)); QCOMPARE(batchFetch.at(3).detail().value(nameField), nc.value(nameField)); batchFetch = cm->contacts(batchIds, QContactFetchHint(), &errorMap); QVERIFY(cm->error() != QContactManager::NoError); QCOMPARE(batchFetch.count(), 4); if (errorMap.size()) QCOMPARE(errorMap[0], QContactManager::DoesNotExistError); QCOMPARE(batchFetch.at(0).detail(), QContactName()); QCOMPARE(batchFetch.at(1).detail().value(nameField), na.value(nameField)); QCOMPARE(batchFetch.at(2).detail().value(nameField), nb.value(nameField)); QCOMPARE(batchFetch.at(3).detail().value(nameField), nc.value(nameField)); /* Now multiple of the same contact */ batchIds.clear(); batchIds << ContactId::apiId(c) << ContactId::apiId(b) << ContactId::apiId(c) << ContactId::apiId(a) << ContactId::apiId(a) << ContactId::apiId(b); batchFetch = cm->contacts(batchIds, QContactFetchHint(), &errorMap); QVERIFY(cm->error() == QContactManager::NoError); QCOMPARE(batchFetch.count(), 6); QCOMPARE(errorMap.count(), 0); QCOMPARE(batchFetch.at(0).detail().value(nameField), nc.value(nameField)); QCOMPARE(batchFetch.at(1).detail().value(nameField), nb.value(nameField)); QCOMPARE(batchFetch.at(2).detail().value(nameField), nc.value(nameField)); QCOMPARE(batchFetch.at(3).detail().value(nameField), na.value(nameField)); QCOMPARE(batchFetch.at(4).detail().value(nameField), na.value(nameField)); QCOMPARE(batchFetch.at(5).detail().value(nameField), nb.value(nameField)); QCOMPARE(batchFetch.at(0).details().count(), 1); QCOMPARE(batchFetch.at(1).details().count(), 1); QCOMPARE(batchFetch.at(2).details().count(), 1); QCOMPARE(batchFetch.at(3).details().count(), 1); QCOMPARE(batchFetch.at(4).details().count(), 1); QCOMPARE(batchFetch.at(5).details().count(), 1); QCOMPARE(batchFetch.at(0).details().at(0).number(), QString::fromLatin1("34567")); QCOMPARE(batchFetch.at(1).details().at(0).number(), QString::fromLatin1("234567")); QCOMPARE(batchFetch.at(2).details().at(0).number(), QString::fromLatin1("34567")); QCOMPARE(batchFetch.at(3).details().at(0).number(), QString::fromLatin1("1234567")); QCOMPARE(batchFetch.at(4).details().at(0).number(), QString::fromLatin1("1234567")); QCOMPARE(batchFetch.at(5).details().at(0).number(), QString::fromLatin1("234567")); /* Now delete them all */ QList ids; ids << ContactId::apiId(a) << ContactId::apiId(b) << ContactId::apiId(c); QVERIFY(cm->removeContacts(ids, &errorMap)); QCOMPARE(errorMap.count(), 0); QCOMPARE(cm->error(), QContactManager::NoError); /* Make sure the contacts really don't exist any more */ QCOMPARE(cm->contact(retrievalId(a)).id(), QContactId()); QVERIFY(cm->contact(retrievalId(a)).isEmpty()); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); QCOMPARE(cm->contact(retrievalId(b)).id(), QContactId()); QVERIFY(cm->contact(retrievalId(b)).isEmpty()); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); QCOMPARE(cm->contact(retrievalId(c)).id(), QContactId()); QVERIFY(cm->contact(retrievalId(c)).isEmpty()); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); /* Now try removing with all invalid ids (e.g. the ones we just removed) */ ids.clear(); ids << ContactId::apiId(a) << ContactId::apiId(b) << ContactId::apiId(c); QVERIFY(!cm->removeContacts(ids, &errorMap)); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); QCOMPARE(errorMap.count(), 3); QCOMPARE(errorMap.values().at(0), QContactManager::DoesNotExistError); QCOMPARE(errorMap.values().at(1), QContactManager::DoesNotExistError); QCOMPARE(errorMap.values().at(2), QContactManager::DoesNotExistError); /* And again with a null error map */ QVERIFY(!cm->removeContacts(ids, NULL)); QCOMPARE(cm->error(), QContactManager::DoesNotExistError); /* Try adding some new ones again, this time one with an error */ contacts.clear(); a.setId(QContactId()); b.setId(QContactId()); c.setId(QContactId()); /* Make B the bad guy */ QContactDetail bad(static_cast(QContactDetail::TypeVersion + 0x100)); bad.setValue(100, "Very bad"); b.saveDetail(&bad); contacts << a << b << c; QVERIFY(!cm->saveContacts(&contacts, &errorMap)); /* We can't really say what the error will be.. maybe bad argument, maybe invalid detail */ QVERIFY(cm->error() != QContactManager::NoError); /* It's permissible to fail all the adds, or to add the successful ones */ QVERIFY(errorMap.count() > 0); QVERIFY(errorMap.count() <= 3); // A might have gone through if (errorMap.keys().contains(0)) { QVERIFY(errorMap.value(0) != QContactManager::NoError); QCOMPARE(contacts.at(0).id(), QContactId()); } else { QVERIFY(contacts.at(0).id() != QContactId()); } // B should have failed QCOMPARE(errorMap.value(1), QContactManager::InvalidDetailError); QCOMPARE(contacts.at(1).id(), QContactId()); // C might have gone through if (errorMap.keys().contains(2)) { QVERIFY(errorMap.value(2) != QContactManager::NoError); QCOMPARE(contacts.at(2).id(), QContactId()); } else { QVERIFY(contacts.at(2).id() != QContactId()); } /* Fix up B and re save it */ QVERIFY(contacts[1].removeDetail(&bad)); QVERIFY(cm->saveContacts(&contacts, &errorMap)); QCOMPARE(errorMap.count(), 0); QCOMPARE(cm->error(), QContactManager::NoError); // Save and remove a fourth contact. Store the id. a.setId(QContactId()); QVERIFY(cm->saveContact(&a)); QContactId removedId = ContactId::apiId(a); QVERIFY(cm->removeContact(removedId)); /* Now delete 3 items, but with one bad argument */ ids.clear(); ids << ContactId::apiId(contacts.at(0)); ids << removedId; ids << ContactId::apiId(contacts.at(2)); QVERIFY(!cm->removeContacts(ids, &errorMap)); QVERIFY(cm->error() != QContactManager::NoError); /* Again, the backend has the choice of either removing the successful ones, or not */ QVERIFY(errorMap.count() > 0); QVERIFY(errorMap.count() <= 3); // A might have gone through if (errorMap.keys().contains(0)) { QVERIFY(errorMap.value(0) != QContactManager::NoError); QCOMPARE(contacts.at(0).id(), QContactId()); } else { QVERIFY(contacts.at(0).id() != QContactId()); } /* B should definitely have failed */ QCOMPARE(errorMap.value(1), QContactManager::DoesNotExistError); QCOMPARE(ids.at(1), removedId); // A might have gone through if (errorMap.keys().contains(2)) { QVERIFY(errorMap.value(2) != QContactManager::NoError); QCOMPARE(contacts.at(2).id(), QContactId()); } else { QVERIFY(contacts.at(2).id() != QContactId()); } } void tst_QContactManager::invalidManager() { /* Create an invalid manager */ QContactManager manager("this should never work"); QVERIFY(manager.managerName() == "invalid"); QVERIFY(manager.managerVersion() == 0); /* also, test the other ctor behaviour is sane also */ QContactManager anotherManager("this should never work", 15); QVERIFY(anotherManager.managerName() == "invalid"); QVERIFY(anotherManager.managerVersion() == 0); /* Now test that all the operations fail */ QVERIFY(manager.contactIds().count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError); QContact foo; QContactName nf; nf.setLastName("Lastname"); foo.saveDetail(&nf); #ifdef DISPLAY_LABEL_SUPPORTED QVERIFY(manager.synthesizedContactDisplayLabel(foo).isEmpty()); QVERIFY(manager.error() == QContactManager::NotSupportedError); #endif QVERIFY(manager.saveContact(&foo) == false); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(foo.id() == QContactId()); QVERIFY(manager.contactIds().count() == 0); QVERIFY(manager.contact(retrievalId(foo)).id() == QContactId()); QVERIFY(manager.contact(retrievalId(foo)).isEmpty()); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(manager.removeContact(removalId(foo)) == false); QVERIFY(manager.error() == QContactManager::NotSupportedError); QMap errorMap; errorMap.insert(0, QContactManager::NoError); QVERIFY(!manager.saveContacts(0, &errorMap)); QVERIFY(manager.errorMap().count() == 0); QVERIFY(errorMap.count() == 0); QVERIFY(manager.error() == QContactManager::BadArgumentError); /* filters */ QContactFilter f; // matches everything QContactDetailFilter df; setFilterDetail(df, QContactDisplayLabel::FieldLabel); QVERIFY(manager.contactIds(QContactFilter()).count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(manager.contactIds(df).count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(manager.contactIds(f | f).count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(manager.contactIds(df | df).count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(manager.isFilterSupported(f) == false); QVERIFY(manager.isFilterSupported(df) == false); QList list; list << foo; QVERIFY(!manager.saveContacts(&list, &errorMap)); QVERIFY(errorMap.count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(!manager.removeContacts(QList(), &errorMap)); QVERIFY(errorMap.count() == 0); QVERIFY(manager.error() == QContactManager::BadArgumentError); QList idlist; idlist << ContactId::apiId(foo); QVERIFY(!manager.removeContacts(idlist, &errorMap)); QVERIFY(errorMap.count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError); #ifdef DETAIL_DEFINITION_SUPPORTED /* Detail definitions */ QVERIFY(manager.detailDefinitions().count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); QContactDetailDefinition def; def.setUnique(true); def.setName("new field"); QMap fields; QContactDetailFieldDefinition currField; currField.setDataType(QVariant::String); fields.insert("value", currField); def.setFields(fields); QVERIFY(manager.saveDetailDefinition(def, QContactType::TypeContact) == false); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); QVERIFY(manager.saveDetailDefinition(def) == false); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); QVERIFY(manager.detailDefinitions().count(QContactType::TypeContact) == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); QVERIFY(manager.detailDefinitions().count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); QVERIFY(manager.detailDefinition("new field").name() == QString()); QVERIFY(manager.removeDetailDefinition(def.name(), QContactType::TypeContact) == false); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); QVERIFY(manager.removeDetailDefinition(def.name()) == false); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); QVERIFY(manager.detailDefinitions().count() == 0); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::InvalidContactTypeError); #endif /* Self contact id */ QVERIFY(!manager.setSelfContactId(ContactId::apiId(12, manager.managerUri()))); QVERIFY(manager.error() == QContactManager::NotSupportedError); QVERIFY(manager.selfContactId() == QContactId()); QVERIFY(manager.error() == QContactManager::NotSupportedError || manager.error() == QContactManager::DoesNotExistError); /* Capabilities */ QVERIFY(manager.supportedDataTypes().count() == 0); QVERIFY(!managerSupportsFeature(manager, "ActionPreferences")); QVERIFY(!managerSupportsFeature(manager, "MutableDefinitions")); } void tst_QContactManager::presenceReporting() { QFETCH(QString, uri); QFETCH(bool, mergePresenceChanges); QScopedPointer cm(QContactManager::fromUri(uri)); QSignalSpy addedSpy(cm.data(), contactsAddedSignal); QSignalSpy changedSpy(cm.data(), contactsChangedSignal); QSignalSpy removedSpy(cm.data(), contactsRemovedSignal); // The contactsPresenceChanged signal is not exported by QContactManager, so we // need to find it from the manager's engine object QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*cm.data()); QSignalSpy presenceChangedSpy(cme, contactsPresenceChangedSignal); QContact a; QContactName n; n.setFirstName("A"); n.setMiddleName("Test"); n.setLastName("PresenceUpdate"); a.saveDetail(&n); QDateTime ts(QDateTime::currentDateTime()); QContactPresence p; p.setPresenceState(QContactPresence::PresenceBusy); p.setTimestamp(ts); QVERIFY(a.saveDetail(&p)); QContactOnlineAccount oa; oa.setAccountUri("FakeImAccount"); oa.setValue(QContactOnlineAccount__FieldEnabled, false); QVERIFY(a.saveDetail(&oa)); QContactOriginMetadata om; om.setId("TestContact"); om.setGroupId("TestGroup"); om.setEnabled(false); QVERIFY(a.saveDetail(&om)); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QContact b; QContactName n2; n2.setFirstName("B"); n2.setLastName("PresenceUnchanged"); b.saveDetail(&n2); QContactPresence p2; p2.setPresenceState(QContactPresence::PresenceAway); p2.setTimestamp(ts); QVERIFY(b.saveDetail(&p2)); QVERIFY(cm->saveContact(&b)); b = cm->contact(retrievalId(b)); QDateTime saveTimestamp(QDateTime::currentDateTime()); QTest::qWait(500); // wait for signal coalescing. QTRY_VERIFY(addedSpy.count() > 0); addedSpy.clear(); QCOMPARE(presenceChangedSpy.count(), 0); changedSpy.clear(); removedSpy.clear(); // Sort on presence QContactIdFilter idFilter; idFilter.setIds(QList() << a.id() << b.id()); QList sortOrders; QContactSortOrder byPhoneNumber; setSortDetail(byPhoneNumber, QContactPhoneNumber::FieldNumber); sortOrders.append(byPhoneNumber); QContactSortOrder presenceOrder; setSortDetail(presenceOrder, QContactGlobalPresence::FieldPresenceState); QList sortedIds(cm->contactIds(idFilter, QList() << presenceOrder)); QCOMPARE(sortedIds.count(), 2); QCOMPARE(sortedIds.at(0), b.id()); QCOMPARE(sortedIds.at(1), a.id()); QContactChangeLogFilter clFilter(QContactChangeLogFilter::EventChanged); clFilter.setSince(saveTimestamp); sortedIds = cm->contactIds(idFilter & clFilter); QCOMPARE(sortedIds.count(), 0); // Test a presence-only update (can include presence/origin-metadata/online-account changes) p = a.detail(); p.setPresenceState(QContactPresence::PresenceAvailable); QVERIFY(a.saveDetail(&p)); oa = a.detail(); oa.setValue(QContactOnlineAccount__FieldEnabled, true); QVERIFY(a.saveDetail(&oa)); om = a.detail(); om.setEnabled(true); QVERIFY(a.saveDetail(&om)); // We need to use a detail definition mask to enable the presence-only update QList contacts; contacts.append(a); QVERIFY(cm->saveContacts(&contacts, DetailList() << detailType() << detailType() << detailType())); a = cm->contact(retrievalId(a)); QCOMPARE(a.detail().presenceState(), QContactPresence::PresenceAvailable); QCOMPARE(a.detail().timestamp(), ts); QTest::qWait(500); // wait for signal coalescing. if (!mergePresenceChanges) { QTRY_VERIFY(presenceChangedSpy.count() > 0); presenceChangedSpy.clear(); QCOMPARE(changedSpy.count(), 0); } else { QTRY_VERIFY(changedSpy.count() > 0); changedSpy.clear(); QCOMPARE(presenceChangedSpy.count(), 0); } QCOMPARE(addedSpy.count(), 0); QCOMPARE(removedSpy.count(), 0); // Check that the sort now includes the update state sortedIds = cm->contactIds(idFilter, QList() << presenceOrder); QCOMPARE(sortedIds.count(), 2); QCOMPARE(sortedIds.at(0), a.id()); QCOMPARE(sortedIds.at(1), b.id()); // Perform queries that require access to the presence for filtering QContactDetailFilter availableFilter; setFilterDetail(availableFilter, QContactGlobalPresence::FieldPresenceState); setFilterValue(availableFilter, QContactPresence::PresenceAvailable); sortedIds = cm->contactIds(idFilter & availableFilter); QEXPECT_FAIL("", "fails due to invalid SQL result", Continue); QCOMPARE(sortedIds.count(), 1); //QCOMPARE(sortedIds.at(0), a.id()); // Check that the modified timestamp is updated sortedIds = cm->contactIds(idFilter & clFilter); QCOMPARE(sortedIds.count(), 1); QCOMPARE(sortedIds.at(0), a.id()); // Test an update including non-presence changes p = a.detail(); p.setPresenceState(QContactPresence::PresenceBusy); QVERIFY(a.saveDetail(&p)); n = a.detail(); n.setMiddleName("Dummy"); QVERIFY(a.saveDetail(&n)); contacts.clear(); contacts.append(a); QVERIFY(cm->saveContacts(&contacts, DetailList() << detailType() << detailType())); a = cm->contact(retrievalId(a)); QTest::qWait(500); // wait for signal coalescing. QTRY_VERIFY(changedSpy.count() > 0); changedSpy.clear(); QCOMPARE(addedSpy.count(), 0); QCOMPARE(presenceChangedSpy.count(), 0); QCOMPARE(removedSpy.count(), 0); QVERIFY(cm->removeContact(retrievalId(a))); QTest::qWait(500); QTRY_VERIFY(removedSpy.count() > 0); removedSpy.clear(); QCOMPARE(addedSpy.count(), 0); QCOMPARE(changedSpy.count(), 0); QCOMPARE(presenceChangedSpy.count(), 0); } void tst_QContactManager::presenceReporting_data() { QTest::addColumn("mergePresenceChanges"); QTest::addColumn("uri"); const QString managerName(QString::fromLatin1(SQLITE_MANAGER)); QMap params; params.insert("autoTest", "true"); params.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("true")); QTest::newRow("mergePresenceChanges=true") << true << QContactManager::buildUri(managerName, params); params.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("false")); QTest::newRow("mergePresenceChanges=false") << false << QContactManager::buildUri(managerName, params); } void tst_QContactManager::presenceAccumulation() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); QContact a; QContactName n; n.setFirstName("A"); n.setMiddleName("Test"); n.setLastName("Presence-Accumulation"); a.saveDetail(&n); QDateTime ts(QDateTime::currentDateTime()); QContactPresence p; p.setPresenceState(QContactPresence::PresenceAway); p.setTimestamp(ts); QVERIFY(a.saveDetail(&p)); QContactOnlineAccount oa; oa.setAccountUri("FakeImAccount"); oa.setValue(QContactOnlineAccount__FieldEnabled, false); QVERIFY(a.saveDetail(&oa)); QContactOriginMetadata om; om.setId("TestContact"); om.setGroupId("TestGroup"); om.setEnabled(false); QVERIFY(a.saveDetail(&om)); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QCOMPARE(a.detail().presenceState(), QContactPresence::PresenceAway); QCOMPARE(a.detail().timestamp(), ts); QCOMPARE(a.detail().presenceState(), QContactPresence::PresenceAway); QCOMPARE(a.detail().timestamp(), ts); for (int i = 0; i < 50; ++i) { // Test a presence-only update (increase the size each time) p = a.detail(); p.setPresenceState(QContactPresence::PresenceAvailable); const QString newCustomMessage(p.customMessage() + QString::fromLatin1(QByteArray(10, 'a'))); p.setCustomMessage(newCustomMessage); QVERIFY(a.saveDetail(&p)); oa = a.detail(); oa.setValue(QContactOnlineAccount__FieldEnabled, !oa.value(QContactOnlineAccount__FieldEnabled).toBool()); QVERIFY(a.saveDetail(&oa)); om = a.detail(); om.setEnabled(true); QVERIFY(a.saveDetail(&om)); // We need to use a detail definition mask to enable the presence-only update QList contacts; contacts.append(a); QVERIFY(cm->saveContacts(&contacts, DetailList() << detailType() << detailType() << detailType())); a = cm->contact(retrievalId(a)); QCOMPARE(a.detail().presenceState(), QContactPresence::PresenceAvailable); QCOMPARE(a.detail().customMessage(), newCustomMessage); QCOMPARE(a.detail().timestamp(), ts); QCOMPARE(a.detail().presenceState(), QContactPresence::PresenceAvailable); QCOMPARE(a.detail().customMessage(), newCustomMessage); QCOMPARE(a.detail().timestamp(), ts); QCOMPARE(a.detail().value(QContactOnlineAccount__FieldEnabled), oa.value(QContactOnlineAccount__FieldEnabled)); QCOMPARE(a.detail().enabled(), om.enabled()); } // Test an update including non-presence changes p = a.detail(); p.setPresenceState(QContactPresence::PresenceBusy); QVERIFY(a.saveDetail(&p)); n = a.detail(); n.setMiddleName("Dummy"); QVERIFY(a.saveDetail(&n)); QList contacts; contacts.append(a); QVERIFY(cm->saveContacts(&contacts, DetailList() << detailType() << detailType())); a = cm->contact(retrievalId(a)); QCOMPARE(a.detail().presenceState(), QContactPresence::PresenceBusy); QCOMPARE(a.detail().customMessage(), p.customMessage()); QCOMPARE(a.detail().timestamp(), p.timestamp()); QCOMPARE(a.detail().presenceState(), QContactPresence::PresenceBusy); QCOMPARE(a.detail().customMessage(), p.customMessage()); QCOMPARE(a.detail().timestamp(), p.timestamp()); QVERIFY(cm->removeContact(retrievalId(a))); } void tst_QContactManager::nonprivileged() { const QString managerName(QString::fromLatin1(SQLITE_MANAGER)); QMap params; params.insert("autoTest", "true"); QScopedPointer privilegedCm(QContactManager::fromUri(QContactManager::buildUri(managerName, params))); QVERIFY(privilegedCm); QVERIFY(!privilegedCm->managerUri().isEmpty()); params.insert(QString::fromLatin1("nonprivileged"), QString::fromLatin1("true")); QScopedPointer nonprivilegedCm(QContactManager::fromUri(QContactManager::buildUri(managerName, params))); QVERIFY(nonprivilegedCm); QVERIFY(!nonprivilegedCm->managerUri().isEmpty()); QVERIFY(nonprivilegedCm->managerUri() != privilegedCm->managerUri()); QSignalSpy privilegedAddedSpy(privilegedCm.data(), contactsAddedSignal); QSignalSpy nonprivilegedAddedSpy(nonprivilegedCm.data(), contactsAddedSignal); QContact a; QContactName n; n.setFirstName("A"); n.setMiddleName("Test"); n.setLastName("Nonprivileged"); a.saveDetail(&n); QVERIFY(privilegedCm->saveContact(&a)); a = privilegedCm->contact(retrievalId(a)); QVERIFY(a.id() != QContactId()); QCOMPARE(a.detail().firstName(), n.firstName()); QCOMPARE(a.detail().middleName(), n.middleName()); QCOMPARE(a.detail().lastName(), n.lastName()); QTest::qWait(500); // wait for signal coalescing. QTRY_VERIFY(privilegedAddedSpy.count() > 0); privilegedAddedSpy.clear(); QTRY_VERIFY(nonprivilegedAddedSpy.count() == 0); // The contact should not be present in the other DB (or should be a different contact) QContact b = nonprivilegedCm->contact(retrievalId(a)); QVERIFY(b.id() == QContactId() || b.detail().firstName() != n.firstName() || b.detail().middleName() != n.middleName() || b.detail().lastName() != n.lastName()); // Add a contact to the nonprivileged DB n.setFirstName("B"); b.saveDetail(&n); QVERIFY(nonprivilegedCm->saveContact(&b)); b = nonprivilegedCm->contact(retrievalId(b)); QVERIFY(b.id() != QContactId()); QTest::qWait(500); QTRY_VERIFY(nonprivilegedAddedSpy.count() > 0); nonprivilegedAddedSpy.clear(); QTRY_VERIFY(privilegedAddedSpy.count() == 0); // This contact should not be present in the privileged DB QContact c = privilegedCm->contact(retrievalId(b)); QVERIFY(c.id() == QContactId() || c.detail().firstName() != n.firstName() || c.detail().middleName() != n.middleName() || c.detail().lastName() != n.lastName()); } void tst_QContactManager::nameSynthesis_data() { QTest::addColumn("expected"); QTest::addColumn("addname"); QTest::addColumn("prefix"); QTest::addColumn("first"); QTest::addColumn("middle"); QTest::addColumn("last"); QTest::addColumn("suffix"); QTest::addColumn("addcompany"); QTest::addColumn("company"); QTest::addColumn("addname2"); QTest::addColumn("secondprefix"); QTest::addColumn("secondfirst"); QTest::addColumn("secondmiddle"); QTest::addColumn("secondlast"); QTest::addColumn("secondsuffix"); QTest::addColumn("addcompany2"); QTest::addColumn("secondcompany"); QString e; // empty string.. gets a work out /* Various empty ones */ QTest::newRow("empty contact") << e << false << e << e << e << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("empty name") << e << true << e << e << e << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("empty names") << e << true << e << e << e << e << e << false << e << true << e << e << e << e << e << false << e; QTest::newRow("empty org") << e << false << e << e << e << e << e << true << e << false << e << e << e << e << e << true << e; QTest::newRow("empty orgs") << e << false << e << e << e << e << e << true << e << false << e << e << e << e << e << true << e; QTest::newRow("empty orgs and names") << e << true << e << e << e << e << e << true << e << true << e << e << e << e << e << true << e; /* Single values */ QTest::newRow("prefix") << "Prefix" << true << "Prefix" << e << e << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("first") << "First" << true << e << "First" << e << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("middle") << "Middle" << true << e << e << "Middle" << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("last") << "Last" << true << e << e << e << "Last" << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("suffix") << "Suffix" << true << e << e << e << e << "Suffix" << false << e << false << e << e << e << e << e << false << e; /* Single values in the second name */ QTest::newRow("prefix in second") << "Prefix" << false << "Prefix" << e << e << e << e << false << e << true << "Prefix" << e << e << e << e << false << e; QTest::newRow("first in second") << "First" << false << e << "First" << e << e << e << false << e << true << e << "First" << e << e << e << false << e; QTest::newRow("middle in second") << "Middle" << false << e << e << "Middle" << e << e << false << e << true << e << e << "Middle" << e << e << false << e; QTest::newRow("last in second") << "Last" << false << e << e << e << "Last" << e << false << e << true << e << e << e << "Last" << e << false << e; QTest::newRow("suffix in second") << "Suffix" << false << e << e << e << e << "Suffix" << false << e << true << e << e << e << e << "Suffix" << false << e; /* Multiple name values */ QTest::newRow("prefix first") << "Prefix First" << true << "Prefix" << "First" << e << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("prefix middle") << "Prefix Middle" << true << "Prefix" << e << "Middle" << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("prefix last") << "Prefix Last" << true << "Prefix" << e << e << "Last" << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("prefix suffix") << "Prefix Suffix" << true << "Prefix" << e << e << e << "Suffix" << false << e << false << e << e << e << e << e << false << e; QTest::newRow("first middle") << "First Middle" << true << e << "First" << "Middle" << e << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("first last") << "First Last" << true << e << "First" << e << "Last" << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("first suffix") << "First Suffix" << true << e << "First" << e << e << "Suffix" << false << e << false << e << e << e << e << e << false << e; QTest::newRow("middle last") << "Middle Last" << true << e << e << "Middle" << "Last" << e << false << e << false << e << e << e << e << e << false << e; QTest::newRow("middle suffix") << "Middle Suffix" << true << e << e << "Middle" << e << "Suffix" << false << e << false << e << e << e << e << e << false << e; QTest::newRow("last suffix") << "Last Suffix" << true << e << e << e << "Last" << "Suffix" << false << e << false << e << e << e << e << e << false << e; /* Everything.. */ QTest::newRow("all name") << "Prefix First Middle Last Suffix" << true << "Prefix" << "First" << "Middle" << "Last" << "Suffix" << false << e << false << e << e << e << e << e << false << e; QTest::newRow("all name second") << "Prefix First Middle Last Suffix" << false << "Prefix" << "First" << "Middle" << "Last" << "Suffix" << false << e << true << "Prefix" << "First" << "Middle" << "Last" << "Suffix" << false << e; /* Org */ QTest::newRow("org") << "Company" << false << e << e << e << e << e << true << "Company" << false << e << e << e << e << e << false << e; QTest::newRow("second org") << "Company" << false << e << e << e << e << e << false << e << false << e << e << e << e << e << true << "Company"; /* Mix */ QTest::newRow("org and empty name") << "Company" << true << e << e << e << e << e << true << "Company" << false << e << e << e << e << e << false << e; QTest::newRow("name and empty org") << "Prefix First Middle Last Suffix" << true << "Prefix" << "First" << "Middle" << "Last" << "Suffix" << false << e << false << e << e << e << e << e << false << e; /* names are preferred to orgs */ QTest::newRow("name and org") << "Prefix First Middle Last Suffix" << true << "Prefix" << "First" << "Middle" << "Last" << "Suffix" << true << "Company" << false << e << e << e << e << e << false << e; } #ifdef COMPATIBLE_CONTACT_SUPPORTED void tst_QContactManager::compatibleContact_data() { QTest::addColumn("input"); QTest::addColumn("expected"); QTest::addColumn("error"); QContact baseContact; QContactName name; name.setFirstName(QLatin1String("First")); baseContact.saveDetail(&name); { QTest::newRow("already compatible") << baseContact << baseContact << QContactManager::NoError; } { QContact contact(baseContact); QContactDetail detail(static_cast(QContactDetail::TypeVersion + 0x100)); detail.setValue(100, QLatin1String("Value")); contact.saveDetail(&detail); QTest::newRow("unknown detail") << contact << baseContact << QContactManager::NoError; } { QContact contact(baseContact); QContactType type1; type1.setType(QContactType::TypeContact); contact.saveDetail(&type1); QContactType type2; type2.setType(QContactType::TypeGroup); contact.saveDetail(&type2); QContact expected(baseContact); expected.saveDetail(&type1); QTest::newRow("duplicate unique field") << contact << expected << QContactManager::NoError; } { QContact contact(baseContact); QContactPhoneNumber phoneNumber; phoneNumber.setValue("UnknownKey", "Value"); contact.saveDetail(&phoneNumber); QTest::newRow("unknown field") << contact << baseContact << QContactManager::NoError; } #ifdef DISPLAY_LABEL_SUPPORTED { QContact contact(baseContact); QContactDisplayLabel displayLabel; displayLabel.setValue(QContactDisplayLabel::FieldLabel, QStringList("Value")); contact.saveDetail(&displayLabel); QTest::newRow("wrong type") << contact << baseContact << QContactManager::NoError; } #endif { QContact contact(baseContact); QContactPhoneNumber phoneNumber1; phoneNumber1.setNumber(QLatin1String("1234")); phoneNumber1.setSubTypes(QStringList() << QContactPhoneNumber::SubTypeMobile << QContactPhoneNumber::SubTypeVoice << QLatin1String("InvalidSubtype")); contact.saveDetail(&phoneNumber1); QContact expected(baseContact); QContactPhoneNumber phoneNumber2; phoneNumber2.setNumber(QLatin1String("1234")); phoneNumber2.setSubTypes(QStringList() << QContactPhoneNumber::SubTypeMobile << QContactPhoneNumber::SubTypeVoice); expected.saveDetail(&phoneNumber2); QTest::newRow("bad value (list)") << contact << expected << QContactManager::NoError; } { QContact contact(baseContact); QContactPhoneNumber phoneNumber1; phoneNumber1.setNumber(QLatin1String("1234")); phoneNumber1.setSubTypes(QStringList(QLatin1String("InvalidSubtype"))); contact.saveDetail(&phoneNumber1); QContact expected(baseContact); QContactPhoneNumber phoneNumber2; phoneNumber2.setNumber(QLatin1String("1234")); expected.saveDetail(&phoneNumber2); QTest::newRow("all bad value (list)") << contact << expected << QContactManager::NoError; } { QContact contact(baseContact); QContactGender gender; gender.setGender(QLatin1String("UnknownGender")); contact.saveDetail(&gender); QTest::newRow("bad value (string)") << contact << baseContact << QContactManager::NoError; } { QContact contact; QContactGender gender; gender.setGender(QLatin1String("UnknownGender")); contact.saveDetail(&gender); QTest::newRow("bad value (string)") << contact << QContact() << QContactManager::DoesNotExistError; } } void tst_QContactManager::compatibleContact() { QScopedPointer cm(newContactManager()); QFETCH(QContact, input); QFETCH(QContact, expected); QFETCH(QContactManager::Error, error); QEXPECT_FAIL("duplicate unique field", "Inexplicably broken", Abort); QCOMPARE(cm->compatibleContact(input), expected); QCOMPARE(cm->error(), error); } #endif #ifdef MUTABLE_SCHEMA_SUPPORTED void tst_QContactManager::contactValidation() { /* Use the default engine as a reference (validation is not engine specific) */ QScopedPointer cm(newContactManager()); QContact c; /* * Add some definitions for testing: * * 1) a unique detail * 2) a detail with restricted values * 3) a create only detail * 4) a unique create only detail */ QContactDetailDefinition uniqueDef; QMap fields; QContactDetailFieldDefinition field; field.setDataType(QVariant::String); fields.insert("value", field); uniqueDef.setName("UniqueDetail"); uniqueDef.setFields(fields); uniqueDef.setUnique(true); QVERIFY(cm->saveDetailDefinition(uniqueDef)); QContactDetailDefinition restrictedDef; restrictedDef.setName("RestrictedDetail"); fields.clear(); field.setAllowableValues(QVariantList() << "One" << "Two" << "Three"); fields.insert("value", field); restrictedDef.setFields(fields); QVERIFY(cm->saveDetailDefinition(restrictedDef)); // first, test an invalid definition QContactDetail d1 = QContactDetail("UnknownDefinition"); d1.setValue("test", "test"); c.saveDetail(&d1); QVERIFY(!cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::InvalidDetailError); c.removeDetail(&d1); // second, test an invalid uniqueness constraint QContactDetail d2 = QContactDetail("UniqueDetail"); d2.setValue("value", "test"); c.saveDetail(&d2); // One unique should be ok QVERIFY(cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::NoError); // Two uniques should not be ok QContactDetail d3 = QContactDetail("UniqueDetail"); d3.setValue("value", "test2"); c.saveDetail(&d3); QVERIFY(!cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::AlreadyExistsError); c.removeDetail(&d3); c.removeDetail(&d2); // third, test an invalid field name QContactDetail d4 = QContactDetail(QContactPhoneNumber::DefinitionName); d4.setValue("test", "test"); c.saveDetail(&d4); QVERIFY(!cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::InvalidDetailError); c.removeDetail(&d4); // fourth, test an invalid field data type QContactDetail d5 = QContactDetail(QContactPhoneNumber::DefinitionName); d5.setValue(QContactPhoneNumber::FieldNumber, QDateTime::currentDateTime()); c.saveDetail(&d5); QVERIFY(!cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::InvalidDetailError); c.removeDetail(&d5); // fifth, test an invalid field value (not in the allowed list) QContactDetail d6 = QContactDetail("RestrictedDetail"); d6.setValue("value", "Seven"); // not in One, Two or Three c.saveDetail(&d6); QVERIFY(!cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::InvalidDetailError); c.removeDetail(&d6); /* Now a valid value */ d6.setValue("value", "Two"); c.saveDetail(&d6); QVERIFY(cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::NoError); c.removeDetail(&d6); // Test a completely valid one. QContactDetail d7 = QContactDetail(QContactPhoneNumber::DefinitionName); d7.setValue(QContactPhoneNumber::FieldNumber, "0123456"); c.saveDetail(&d7); QVERIFY(cm->saveContact(&c)); QCOMPARE(cm->error(), QContactManager::NoError); c.removeDetail(&d7); } #endif void tst_QContactManager::observerDeletion() { QContactManager *manager = newContactManager(); QContact c; QVERIFY(manager->saveContact(&c)); QContactId id = ContactId::apiId(c); QContactObserver *observer = new QContactObserver(manager, id); Q_UNUSED(observer) delete manager; delete observer; // Test for bug MOBILITY-2566 - that QContactObserver doesn't crash when it is // destroyed after the associated QContactManager } void tst_QContactManager::signalEmission() { QTest::qWait(500); // clear the signal queue QFETCH(QString, uri); QScopedPointer m1(QContactManager::fromUri(uri)); QSignalSpy spyCA(m1.data(), contactsAddedSignal); QSignalSpy spyCM(m1.data(), contactsChangedSignal); QSignalSpy spyCR(m1.data(), contactsRemovedSignal); QTestSignalSink casink(m1.data(), contactsAddedSignal); QTestSignalSink cmsink(m1.data(), contactsChangedSignal); QTestSignalSink crsink(m1.data(), contactsRemovedSignal); QList args; QList arg; QContact c; QList batchAdd; QList batchRemove; QList sigids; int addSigCount = 0; // the expected signal counts. int modSigCount = 0; int remSigCount = 0; #ifdef DETAIL_DEFINITION_SUPPORTED QContactDetailDefinition nameDef = m1->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact); #endif // verify add emits signal added QContactName nc; #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c, &nc, "John Sigem"); #else saveContactName(&c, nameDef, &nc, "John Sigem"); #endif QVERIFY(m1->saveContact(&c)); QContactId cid = ContactId::apiId(c); addSigCount += 1; QTest::qWait(500); // wait for signal coalescing. QTRY_VERIFY(spyCA.count() >= addSigCount); addSigCount = spyCA.count(); QScopedPointer c1Observer(new QContactObserver(m1.data(), cid)); QScopedPointer spyCOM1(new QSignalSpy(c1Observer.data(), SIGNAL(contactChanged(QList)))); QScopedPointer spyCOR1(new QSignalSpy(c1Observer.data(), SIGNAL(contactRemoved()))); // verify save modified emits signal changed spyCM.clear(); #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c, &nc, "Citizen Sigem"); #else saveContactName(&c, nameDef, &nc, "Citizen Sigem"); #endif QVERIFY(m1->saveContact(&c)); modSigCount = 1; QTRY_VERIFY(spyCM.count() >= modSigCount); modSigCount = spyCM.count(); QTRY_COMPARE(spyCOM1->count(), 1); args = spyCM.takeFirst(); modSigCount -= 1; arg = args.first().value >(); while (spyCM.count()) { arg.append(spyCM.takeFirst().first().value >()); } modSigCount = spyCM.count(); QVERIFY(arg.contains(cid)); // verify remove emits signal removed m1->removeContact(removalId(c)); remSigCount += 1; QTRY_COMPARE(spyCR.count(), remSigCount); QTRY_COMPARE(spyCOR1->count(), 1); args = spyCR.takeFirst(); remSigCount -= 1; arg = args.first().value >(); while (spyCR.count()) { arg.append(spyCM.takeFirst().first().value >()); } remSigCount = spyCR.count(); QVERIFY(arg.contains(cid)); // verify multiple adds works as advertised QContact c2, c3; QContactName nc2, nc3; #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c2, &nc2, "Mark"); saveContactName(&c3, &nc3, "Garry"); #else saveContactName(&c2, nameDef, &nc2, "Mark"); saveContactName(&c3, nameDef, &nc3, "Garry"); #endif QVERIFY(m1->saveContact(&c2)); addSigCount += 1; QVERIFY(m1->saveContact(&c3)); addSigCount += 1; QTRY_VERIFY(spyCA.count() >= (addSigCount-1)); QVERIFY(spyCA.count() == (addSigCount-1) // if all of the signals are coalesced || spyCA.count() == (addSigCount) // if all but one of the signals are coalesced || spyCA.count() == (addSigCount+1) // if only two of the signals are coalesced || spyCA.count() == (addSigCount+2)); // if no signals were coalesced. spyCOM1->clear(); spyCOR1->clear(); QScopedPointer c2Observer(new QContactObserver(m1.data(), ContactId::apiId(c2))); QScopedPointer c3Observer(new QContactObserver(m1.data(), ContactId::apiId(c3))); QScopedPointer spyCOM2(new QSignalSpy(c2Observer.data(), SIGNAL(contactChanged(QList)))); QScopedPointer spyCOM3(new QSignalSpy(c3Observer.data(), SIGNAL(contactChanged(QList)))); QScopedPointer spyCOR2(new QSignalSpy(c2Observer.data(), SIGNAL(contactRemoved()))); QScopedPointer spyCOR3(new QSignalSpy(c3Observer.data(), SIGNAL(contactRemoved()))); // verify multiple modifies works as advertised #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c2, &nc2, "M."); #else saveContactName(&c2, nameDef, &nc2, "M."); #endif QVERIFY(m1->saveContact(&c2)); modSigCount += 1; if (uri.contains(QLatin1String("sqlite"))) { // backend coalesces signals for performance reasons, so wait a little QTest::qWait(1000); } #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c2, &nc2, "Mark"); saveContactName(&c3, &nc3, "G."); #else saveContactName(&c2, nameDef, &nc2, "Mark"); saveContactName(&c3, nameDef, &nc3, "G."); #endif QVERIFY(m1->saveContact(&c2)); modSigCount += 1; QVERIFY(m1->saveContact(&c3)); modSigCount += 1; QTRY_VERIFY(spyCM.count() >= (modSigCount - 2)); // it may coalesce signals, and it may update aggregates. QTRY_COMPARE(spyCOM2->count(), 2); QTRY_COMPARE(spyCOM3->count(), 1); QCOMPARE(spyCOM1->count(), 0); // verify multiple removes works as advertised m1->removeContact(removalId(c3)); remSigCount += 1; m1->removeContact(removalId(c2)); remSigCount += 1; QTRY_VERIFY(spyCM.count() >= (remSigCount - 2)); // it may coalesce signals, and it may remove aggregates. QTRY_COMPARE(spyCOR2->count(), 1); QTRY_COMPARE(spyCOR3->count(), 1); QCOMPARE(spyCOR1->count(), 0); spyCR.clear(); if(! uri.contains(QLatin1String("tracker"))) { // The tracker backend does not support checking for existance of a contact. QVERIFY(!m1->removeContact(removalId(c))); // not saved. } QTest::qWait(2000); // the above removeContact() might cause a delayed/coalesced signal emission. /* Now test the batch equivalents */ spyCA.clear(); spyCM.clear(); spyCR.clear(); /* Batch adds - set ids to zero so add succeeds. */ c.setId(QContactId()); c2.setId(QContactId()); c3.setId(QContactId()); batchAdd << c << c2 << c3; QMap errorMap; QVERIFY(m1->saveContacts(&batchAdd, &errorMap)); QVERIFY(batchAdd.count() == 3); c = batchAdd.at(0); c2 = batchAdd.at(1); c3 = batchAdd.at(2); /* We basically loop, processing events, until we've seen an Add signal for each contact */ sigids.clear(); QTRY_WAIT( while(spyCA.size() > 0) {sigids += spyCA.takeFirst().at(0).value >(); }, sigids.contains(ContactId::apiId(c)) && sigids.contains(ContactId::apiId(c2)) && sigids.contains(ContactId::apiId(c3))); // if we perform aggregation, aggregates might get updated; this cannot be verified: //QTRY_COMPARE(spyCM.count(), 0); c1Observer.reset(new QContactObserver(m1.data(), ContactId::apiId(c))); c2Observer.reset(new QContactObserver(m1.data(), ContactId::apiId(c2))); c3Observer.reset(new QContactObserver(m1.data(), ContactId::apiId(c3))); spyCOM1.reset(new QSignalSpy(c1Observer.data(), SIGNAL(contactChanged(QList)))); spyCOM2.reset(new QSignalSpy(c2Observer.data(), SIGNAL(contactChanged(QList)))); spyCOM3.reset(new QSignalSpy(c3Observer.data(), SIGNAL(contactChanged(QList)))); spyCOR1.reset(new QSignalSpy(c1Observer.data(), SIGNAL(contactRemoved()))); spyCOR2.reset(new QSignalSpy(c2Observer.data(), SIGNAL(contactRemoved()))); spyCOR3.reset(new QSignalSpy(c3Observer.data(), SIGNAL(contactRemoved()))); QTRY_COMPARE(spyCR.count(), 0); /* Batch modifies */ #ifndef DETAIL_DEFINITION_SUPPORTED QContactName modifiedName = c.detail(); saveContactName(&c, &modifiedName, "Modified number 1"); modifiedName = c2.detail(); saveContactName(&c2, &modifiedName, "Modified number 2"); modifiedName = c3.detail(); saveContactName(&c3, &modifiedName, "Modified number 3"); #else QContactName modifiedName = c.detail(); saveContactName(&c, nameDef, &modifiedName, "Modified number 1"); modifiedName = c2.detail(); saveContactName(&c2, nameDef, &modifiedName, "Modified number 2"); modifiedName = c3.detail(); saveContactName(&c3, nameDef, &modifiedName, "Modified number 3"); #endif batchAdd.clear(); batchAdd << c << c2 << c3; QVERIFY(m1->saveContacts(&batchAdd, &errorMap)); sigids.clear(); QTRY_WAIT( while(spyCM.size() > 0) {sigids += spyCM.takeFirst().at(0).value >(); }, sigids.contains(ContactId::apiId(c)) && sigids.contains(ContactId::apiId(c2)) && sigids.contains(ContactId::apiId(c3))); QTRY_COMPARE(spyCOM1->count(), 1); QTRY_COMPARE(spyCOM2->count(), 1); QTRY_COMPARE(spyCOM3->count(), 1); /* Batch removes */ batchRemove << ContactId::apiId(c) << ContactId::apiId(c2) << ContactId::apiId(c3); QVERIFY(m1->removeContacts(batchRemove, &errorMap)); sigids.clear(); QTRY_WAIT( while(spyCR.size() > 0) {sigids += spyCR.takeFirst().at(0).value >(); }, sigids.contains(ContactId::apiId(c)) && sigids.contains(ContactId::apiId(c2)) && sigids.contains(ContactId::apiId(c3))); QTRY_COMPARE(spyCOR1->count(), 1); QTRY_COMPARE(spyCOR2->count(), 1); QTRY_COMPARE(spyCOR3->count(), 1); QTRY_COMPARE(spyCA.count(), 0); // if we perform aggregation, removes can cause regenerates of aggregates; this cannot be verified: //QTRY_COMPARE(spyCM.count(), 0); QScopedPointer m2(QContactManager::fromUri(uri)); QCOMPARE(managerSupportsFeature(*m1, "Anonymous"), managerSupportsFeature(*m2, "Anonymous")); /* Now some cross manager testing */ if (!managerSupportsFeature(*m1, "Anonymous")) { // verify that signals are emitted for modifications made to other managers (same id). QContactName ncs = c.detail(); #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c, &ncs, "Test"); #else saveContactName(&c, nameDef, &ncs, "Test"); #endif c.setId(QContactId()); // reset id so save can succeed. QVERIFY(m2->saveContact(&c)); #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c, &ncs, "Test2"); #else saveContactName(&c, nameDef, &ncs, "Test2"); #endif QVERIFY(m2->saveContact(&c)); QTRY_VERIFY(spyCA.count() >= 1); // check that we received the update signals. QTRY_VERIFY(spyCM.count() >= 1); // check that we received the update signals. m2->removeContact(removalId(c)); QTRY_VERIFY(spyCR.count() >= 1); // check that we received the remove signal. } } void tst_QContactManager::errorStayingPut() { /* Make sure that when we clone a manager, we don't clone the error */ QMap params; params.insert("id", "error isolation test"); QScopedPointer m1(newContactManager(params)); QVERIFY(m1->error() == QContactManager::NoError); /* Remove an invalid contact to get an error */ QVERIFY(m1->removeContact(ContactId::apiId(0, m1->managerUri())) == false); QVERIFY(m1->error() == QContactManager::DoesNotExistError); /* Create a new manager with hopefully the same backend */ QScopedPointer m2(newContactManager(params)); QVERIFY(m1->error() == QContactManager::DoesNotExistError); QVERIFY(m2->error() == QContactManager::NoError); /* Cause an error on the other ones and check the first is not affected */ m2->saveContacts(0, 0); QVERIFY(m1->error() == QContactManager::DoesNotExistError); QVERIFY(m2->error() == QContactManager::BadArgumentError); QContact c; QContactDetail d(static_cast(QContactDetail::TypeVersion + 0x100)); d.setValue(100, "Value that also doesn't exist"); c.saveDetail(&d); QVERIFY(m1->saveContact(&c) == false); QVERIFY(m1->error() == QContactManager::InvalidDetailError); QVERIFY(m2->error() == QContactManager::BadArgumentError); } #ifdef DETAIL_DEFINITION_SUPPORTED void tst_QContactManager::validateDefinitions(const QMap& defs) const { // Do some sanity checking on the definitions first if (defs.keys().count() != defs.uniqueKeys().count()) { qDebug() << "ERROR - duplicate definitions with the same name:"; QList defkeys = defs.keys(); foreach(QString uniq, defs.uniqueKeys()) { if (defkeys.count(uniq) > 1) { qDebug() << QString(" %1").arg(uniq).toAscii().constData(); defkeys.removeAll(uniq); } } QVERIFY(defs.keys().count() == defs.uniqueKeys().count()); } foreach(QContactDetailDefinition def, defs.values()) { QMap fields = def.fields(); // Again some sanity checking if (fields.keys().count() != fields.uniqueKeys().count()) { qDebug() << "ERROR - duplicate fields with the same name:"; QList defkeys = fields.keys(); foreach(QString uniq, fields.uniqueKeys()) { if (defkeys.count(uniq) > 1) { qDebug() << QString(" %2::%1").arg(uniq).arg(def.name()).toAscii().constData(); defkeys.removeAll(uniq); } } QVERIFY(fields.keys().count() == fields.uniqueKeys().count()); } foreach(QContactDetailFieldDefinition field, def.fields().values()) { // Sanity check the allowed values if (field.allowableValues().count() > 0) { if (field.dataType() == QVariant::StringList) { // We accept QString or QStringList allowed values foreach(QVariant var, field.allowableValues()) { if (var.type() != QVariant::String && var.type() != QVariant::StringList) { QString foo; QDebug dbg(&foo); dbg.nospace() << var; qDebug().nospace() << "Field " << QString("%1::%2").arg(def.name()).arg(def.fields().key(field)).toAscii().constData() << " allowable value '" << foo.simplified().toAscii().constData() << "' not supported for field type " << QMetaType::typeName(field.dataType()); } QVERIFY(var.type() == QVariant::String || var.type() == QVariant::StringList); } } else if (field.dataType() == QVariant::List || field.dataType() == QVariant::Map || field.dataType() == (QVariant::Type) qMetaTypeId()) { // Well, anything goes } else { // The type of each allowed value must match the data type foreach(QVariant var, field.allowableValues()) { if (var.type() != field.dataType()) { QString foo; QDebug dbg(&foo); dbg.nospace() << var; qDebug().nospace() << "Field " << QString("%1::%2").arg(def.name()).arg(def.fields().key(field)).toAscii().constData() << " allowable value '" << foo.simplified().toAscii().constData() << "' not supported for field type " << QMetaType::typeName(field.dataType()); } QVERIFY(var.type() == field.dataType()); } } } } } } #endif #ifdef MUTABLE_SCHEMA_SUPPORTED void tst_QContactManager::engineDefaultSchema() { /* Test the default schemas - mostly just that they are valid, and v2 has certain changes */ QMap > v1defaultSchemas = QContactManagerEngine::schemaDefinitions(); QMap > v1Schemas = QContactManagerEngine::schemaDefinitions(1); QMap > v2Schemas = QContactManagerEngine::schemaDefinitions(2); QVERIFY(v1Schemas == v1defaultSchemas); QVERIFY(v1Schemas != v2Schemas); QCOMPARE(v1Schemas.keys().count(), v1Schemas.uniqueKeys().count()); QCOMPARE(v2Schemas.keys().count(), v2Schemas.uniqueKeys().count()); foreach(const QString& type, v1Schemas.keys()) { validateDefinitions(v1Schemas.value(type)); } foreach(const QString& type, v2Schemas.keys()) { validateDefinitions(v2Schemas.value(type)); } /* Make sure that birthdays do not have calendar ids in v1, but do in v2*/ QVERIFY(!v1Schemas.value(QContactType::TypeContact).value(QContactBirthday::DefinitionName).fields().contains(QContactBirthday::FieldCalendarId)); QVERIFY(!v1Schemas.value(QContactType::TypeGroup).value(QContactBirthday::DefinitionName).fields().contains(QContactBirthday::FieldCalendarId)); QVERIFY(v2Schemas.value(QContactType::TypeContact).value(QContactBirthday::DefinitionName).fields().contains(QContactBirthday::FieldCalendarId)); QVERIFY(v2Schemas.value(QContactType::TypeGroup).value(QContactBirthday::DefinitionName).fields().contains(QContactBirthday::FieldCalendarId)); /* Urls can be blogs in v2 but not b1 */ QVERIFY(!v1Schemas.value(QContactType::TypeContact).value(QContactUrl::DefinitionName).fields().value(QContactUrl::FieldSubType).allowableValues().contains(QString(QLatin1String(QContactUrl::SubTypeBlog)))); QVERIFY(!v1Schemas.value(QContactType::TypeGroup).value(QContactUrl::DefinitionName).fields().value(QContactUrl::FieldSubType).allowableValues().contains(QString(QLatin1String(QContactUrl::SubTypeBlog)))); QVERIFY(v2Schemas.value(QContactType::TypeContact).value(QContactUrl::DefinitionName).fields().value(QContactUrl::FieldSubType).allowableValues().contains(QString(QLatin1String(QContactUrl::SubTypeBlog)))); QVERIFY(v2Schemas.value(QContactType::TypeGroup).value(QContactUrl::DefinitionName).fields().value(QContactUrl::FieldSubType).allowableValues().contains(QString(QLatin1String(QContactUrl::SubTypeBlog)))); /* Make sure family, favorite and hobby are not in v1, but are in v2 */ QVERIFY(!v1Schemas.value(QContactType::TypeContact).contains(QContactFamily::DefinitionName)); QVERIFY(!v1Schemas.value(QContactType::TypeGroup).contains(QContactFamily::DefinitionName)); QVERIFY(v2Schemas.value(QContactType::TypeContact).contains(QContactFamily::DefinitionName)); QVERIFY(v2Schemas.value(QContactType::TypeGroup).contains(QContactFamily::DefinitionName)); QVERIFY(!v1Schemas.value(QContactType::TypeContact).contains(QContactFavorite::DefinitionName)); QVERIFY(!v1Schemas.value(QContactType::TypeGroup).contains(QContactFavorite::DefinitionName)); QVERIFY(v2Schemas.value(QContactType::TypeContact).contains(QContactFavorite::DefinitionName)); QVERIFY(v2Schemas.value(QContactType::TypeGroup).contains(QContactFavorite::DefinitionName)); QVERIFY(!v1Schemas.value(QContactType::TypeContact).contains(QContactHobby::DefinitionName)); QVERIFY(!v1Schemas.value(QContactType::TypeGroup).contains(QContactHobby::DefinitionName)); QVERIFY(v2Schemas.value(QContactType::TypeContact).contains(QContactHobby::DefinitionName)); QVERIFY(v2Schemas.value(QContactType::TypeGroup).contains(QContactHobby::DefinitionName)); } #endif #ifdef DETAIL_DEFINITION_SUPPORTED void tst_QContactManager::detailDefinitions() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); QMap defs = cm->detailDefinitions(); /* Validate the existing definitions */ foreach(const QString& contactType, cm->supportedContactTypes()) { validateDefinitions(cm->detailDefinitions(contactType)); } /* Try to make a credible definition */ QContactDetailDefinition newDef; QContactDetailFieldDefinition field; QMap fields; field.setDataType(cm->supportedDataTypes().value(0)); fields.insert("New Value", field); newDef.setName("New Definition"); newDef.setFields(fields); /* Updated version of an existing definition */ QContactDetailDefinition updatedDef = defs.begin().value(); // XXX TODO Fixme fields = updatedDef.fields(); fields.insert("New Value", field); updatedDef.setFields(fields); /* A detail definition with valid allowed values (or really just one) */ QContactDetailDefinition allowedDef = newDef; field.setAllowableValues(field.allowableValues() << (QVariant(field.dataType()))); fields.clear(); fields.insert("Restricted value", field); allowedDef.setFields(fields); /* Many invalid definitions */ QContactDetailDefinition noIdDef; noIdDef.setFields(fields); QContactDetailDefinition noFieldsDef; noFieldsDef.setName("No fields"); QContactDetailDefinition invalidFieldKeyDef; invalidFieldKeyDef.setName("Invalid field key"); QMap badfields; badfields.insert(QString(), field); invalidFieldKeyDef.setFields(badfields); QContactDetailDefinition invalidFieldTypeDef; invalidFieldTypeDef.setName("Invalid field type"); badfields.clear(); QContactDetailFieldDefinition badfield; badfield.setDataType((QVariant::Type) qMetaTypeId()); badfields.insert("Bad type", badfield); invalidFieldTypeDef.setFields(badfields); QContactDetailDefinition invalidAllowedValuesDef; invalidAllowedValuesDef.setName("Invalid field allowed values"); badfields.clear(); badfield.setDataType(field.dataType()); // use a supported type badfield.setAllowableValues(QList() << "String" << 5); // but unsupported value badfields.insert("Bad allowed", badfield); invalidAllowedValuesDef.setFields(badfields); /* XXX Multiply defined fields.. depends on semantichangeSet. */ if (managerSupportsFeature(*cm, "MutableDefinitions")) { /* First do some negative testing */ /* Bad add class */ QVERIFY(cm->saveDetailDefinition(QContactDetailDefinition()) == false); QVERIFY(cm->error() == QContactManager::BadArgumentError); /* Bad remove string */ QVERIFY(cm->removeDetailDefinition(QString()) == false); QVERIFY(cm->error() == QContactManager::BadArgumentError); QVERIFY(cm->saveDetailDefinition(noIdDef) == false); QVERIFY(cm->error() == QContactManager::BadArgumentError); QVERIFY(cm->saveDetailDefinition(noFieldsDef) == false); QVERIFY(cm->error() == QContactManager::BadArgumentError); QVERIFY(cm->saveDetailDefinition(invalidFieldKeyDef) == false); QVERIFY(cm->error() == QContactManager::BadArgumentError); QVERIFY(cm->saveDetailDefinition(invalidFieldTypeDef) == false); QVERIFY(cm->error() == QContactManager::BadArgumentError); QVERIFY(cm->saveDetailDefinition(invalidAllowedValuesDef) == false); QVERIFY(cm->error() == QContactManager::BadArgumentError); /* Check that our new definition doesn't already exist */ QVERIFY(cm->detailDefinition(newDef.name()).isEmpty()); QVERIFY(cm->error() == QContactManager::DoesNotExistError); QVERIFY(cm->removeDetailDefinition(newDef.name()) == false); QVERIFY(cm->error() == QContactManager::DoesNotExistError); /* Add a new definition */ QVERIFY(cm->saveDetailDefinition(newDef) == true); QVERIFY(cm->error() == QContactManager::NoError); /* Now retrieve it */ QContactDetailDefinition def = cm->detailDefinition(newDef.name()); QVERIFY(def == newDef); /* Update it */ QMap newFields = def.fields(); newFields.insert("Another new value", field); newDef.setFields(newFields); QVERIFY(cm->saveDetailDefinition(newDef) == true); QVERIFY(cm->error() == QContactManager::NoError); QVERIFY(cm->detailDefinition(newDef.name()) == newDef); /* Remove it */ QVERIFY(cm->removeDetailDefinition(newDef.name()) == true); QVERIFY(cm->error() == QContactManager::NoError); /* and make sure it does not exist any more */ QVERIFY(cm->detailDefinition(newDef.name()) == QContactDetailDefinition()); QVERIFY(cm->error() == QContactManager::DoesNotExistError); /* Add the other good one */ QVERIFY(cm->saveDetailDefinition(allowedDef) == true); QVERIFY(cm->error() == QContactManager::NoError); QVERIFY(allowedDef == cm->detailDefinition(allowedDef.name())); /* and remove it */ QVERIFY(cm->removeDetailDefinition(allowedDef.name()) == true); QVERIFY(cm->detailDefinition(allowedDef.name()) == QContactDetailDefinition()); QVERIFY(cm->error() == QContactManager::DoesNotExistError); } else { /* Bad add class */ QVERIFY(cm->saveDetailDefinition(QContactDetailDefinition()) == false); QVERIFY(cm->error() == QContactManager::NotSupportedError); /* Make sure we can't add/remove/modify detail definitions */ QVERIFY(cm->removeDetailDefinition(QString()) == false); QVERIFY(cm->error() == QContactManager::NotSupportedError); /* Try updating an existing definition */ QVERIFY(cm->saveDetailDefinition(updatedDef) == false); QVERIFY(cm->error() == QContactManager::NotSupportedError); /* Try removing an existing definition */ QVERIFY(cm->removeDetailDefinition(updatedDef.name()) == false); QVERIFY(cm->error() == QContactManager::NotSupportedError); } } #endif #ifdef DISPLAY_LABEL_SUPPORTED void tst_QContactManager::displayName() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); /* * Very similar to the tst_QContact functions, except we test * saving and retrieving contacts updates the display label */ /* Try to make this a bit more consistent by using a single name */ QContact d; QContactName name; saveContactName(&d, cm->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact), &name, "Wesley"); QVERIFY(d.displayLabel().isEmpty()); QString synth = cm->synthesizedContactDisplayLabel(d); // Make sure this doesn't crash cm->synthesizeContactDisplayLabel(0); // Make sure this gives the same results cm->synthesizeContactDisplayLabel(&d); QCOMPARE(d.displayLabel(), synth); /* * The display label is not updated until you save the contact or call synthCDL */ QVERIFY(cm->saveContact(&d)); d = cm->contact(retrievalId(d)); QVERIFY(!d.isEmpty()); QCOMPARE(d.displayLabel(), synth); /* Remove the detail via removeDetail */ QContactDisplayLabel old; int count = d.details().count(); QVERIFY(!d.removeDetail(&old)); // should fail. QVERIFY(d.isEmpty() == false); QVERIFY(d.details().count() == count); // it should not be removed! /* Save the contact again */ QVERIFY(cm->saveContact(&d)); d = cm->contact(retrievalId(d)); QVERIFY(!d.isEmpty()); /* Make sure the label is still the synth version */ QCOMPARE(d.displayLabel(), synth); /* And delete the contact */ QVERIFY(cm->removeContact(removalId(d))); } #endif void tst_QContactManager::actionPreferences() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); // early out if the manager doesn't support action preference saving. if (!managerSupportsFeature(*cm, "ActionPreferences")) { QSKIP("Manager does not support action preferences"); } // create a sample contact QContactAvatar a; a.setImageUrl(QUrl("test.png")); QContactPhoneNumber p1; p1.setNumber("12345"); QContactPhoneNumber p2; p2.setNumber("34567"); QContactPhoneNumber p3; p3.setNumber("56789"); QContactUrl u; u.setUrl("http://test.example.com"); QContactName n; QContact c; #ifndef DETAIL_DEFINITION_SUPPORTED saveContactName(&c, &n, "TestContact"); #else saveContactName(&c, cm->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact), &n, "TestContact"); #endif c.saveDetail(&a); c.saveDetail(&p1); c.saveDetail(&p2); c.saveDetail(&p3); c.saveDetail(&u); // set a preference for dialing a particular saved phonenumber. c.setPreferredDetail("Dial", p2); QVERIFY(cm->saveContact(&c)); // save the contact QContact loaded = cm->contact(retrievalId(c)); // reload the contact // test that the preference was saved correctly. QContactDetail pref = loaded.preferredDetail("Dial"); QCOMPARE(pref, static_cast(p2)); cm->removeContact(removalId(c)); } void tst_QContactManager::changeSet() { QContactId id(ContactId::apiId(1, QStringLiteral("tst_QContactManager::changeSet"))); QContactChangeSet changeSet; QVERIFY(changeSet.addedContacts().isEmpty()); QVERIFY(changeSet.changedContacts().isEmpty()); QVERIFY(changeSet.removedContacts().isEmpty()); changeSet.insertAddedContact(id); QVERIFY(!changeSet.addedContacts().isEmpty()); QVERIFY(changeSet.changedContacts().isEmpty()); QVERIFY(changeSet.removedContacts().isEmpty()); QVERIFY(changeSet.addedContacts().contains(id)); changeSet.insertChangedContact(id, QList()); changeSet.insertChangedContacts(QList() << id, QList()); QCOMPARE(changeSet.changedContacts().size(), 1); // set, should only be added once. QCOMPARE(changeSet.changedContacts().first().second.size(), 1); // only one changed contact ID QVERIFY(!changeSet.addedContacts().isEmpty()); QVERIFY(!changeSet.changedContacts().isEmpty()); QVERIFY(changeSet.removedContacts().isEmpty()); changeSet.clearChangedContacts(); changeSet.insertChangedContacts(QList() << id, QList() << QContactName::Type); changeSet.insertChangedContacts(QList() << id, QList() << QContactBirthday::Type); QCOMPARE(changeSet.changedContacts().size(), 2); // should be added twice with differing change types QVERIFY(!changeSet.addedContacts().isEmpty()); QVERIFY(!changeSet.changedContacts().isEmpty()); QVERIFY(changeSet.removedContacts().isEmpty()); QSet changedIds; QSet changedTypes; foreach (const QContactChangeSet::ContactChangeList &changes, changeSet.changedContacts()) { changedIds |= changes.second.toSet(); if (changes.second.contains(id)) { changedTypes |= changes.first.toSet(); } } QCOMPARE(changedIds, (QList() << id).toSet()); QCOMPARE(changedTypes, (QList() << QContactName::Type << QContactBirthday::Type).toSet()); changeSet.clearChangedContacts(); QVERIFY(changeSet.changedContacts().isEmpty()); QList l1, l2; foreach (int n, QList() << 1 << 1 << 1 << 2 << 2 << 3 << 3 << 4 << 4 << 4 << 5 << 10 << 9 << 8 << 8 << 8 << 7 << 7 << 6) { ((qrand() % 2) ? l1 : l2).append(ContactId::apiId(n, QStringLiteral("tst_QContactManager::changeSet"))); } changeSet.clearChangedContacts(); changeSet.insertChangedContacts(l1, QList() << QContactName::Type << QContactBirthday::Type); changeSet.insertChangedContacts(l2, QList() << QContactBirthday::Type << QContactName::Type << QContactBirthday::Type); QCOMPARE(changeSet.changedContacts().size(), 1); QList expected((l1.toSet() | l2.toSet()).toList()); std::sort(expected.begin(), expected.end()); QCOMPARE(changeSet.changedContacts().first().second, expected); changeSet.insertRemovedContacts(QList() << id); QVERIFY(changeSet.removedContacts().contains(id)); changeSet.clearRemovedContacts(); QVERIFY(changeSet.removedContacts().isEmpty()); QVERIFY(changeSet.dataChanged() == false); QContactChangeSet changeSet2; changeSet2 = changeSet; QVERIFY(changeSet.addedContacts() == changeSet2.addedContacts()); changeSet.emitSignals(0); changeSet2.clearAddedContacts(); QVERIFY(changeSet2.addedContacts().isEmpty()); changeSet2.insertAddedContacts(changeSet.addedContacts().toList()); QVERIFY(changeSet.addedContacts() == changeSet2.addedContacts()); changeSet2.clearAll(); QVERIFY(changeSet.addedContacts() != changeSet2.addedContacts()); QContactChangeSet changeSet3(changeSet2); QVERIFY(changeSet.addedContacts() != changeSet3.addedContacts()); QVERIFY(changeSet2.addedContacts() == changeSet3.addedContacts()); changeSet.setDataChanged(true); QVERIFY(changeSet.dataChanged() == true); QVERIFY(changeSet.dataChanged() != changeSet2.dataChanged()); QVERIFY(changeSet.dataChanged() != changeSet3.dataChanged()); changeSet.emitSignals(0); changeSet.addedRelationshipsContacts().insert(id); changeSet.insertAddedRelationshipsContacts(QList() << id); QVERIFY(changeSet.addedRelationshipsContacts().contains(id)); changeSet.clearAddedRelationshipsContacts(); QVERIFY(changeSet.addedRelationshipsContacts().isEmpty()); changeSet.insertRemovedRelationshipsContacts(QList() << id); QVERIFY(changeSet.removedRelationshipsContacts().contains(id)); changeSet.clearRemovedRelationshipsContacts(); QVERIFY(changeSet.removedRelationshipsContacts().isEmpty()); changeSet.emitSignals(0); changeSet.removedRelationshipsContacts().insert(id); changeSet.emitSignals(0); changeSet.setOldAndNewSelfContactId(QPair(QContactId(), id)); changeSet2 = changeSet; QVERIFY(changeSet2.addedRelationshipsContacts() == changeSet.addedRelationshipsContacts()); QVERIFY(changeSet2.removedRelationshipsContacts() == changeSet.removedRelationshipsContacts()); QVERIFY(changeSet2.oldAndNewSelfContactId() == changeSet.oldAndNewSelfContactId()); changeSet.emitSignals(0); changeSet.setOldAndNewSelfContactId(QPair(id, QContactId())); QVERIFY(changeSet2.oldAndNewSelfContactId() != changeSet.oldAndNewSelfContactId()); changeSet.setDataChanged(true); changeSet.emitSignals(0); } void tst_QContactManager::fetchHint() { // This just tests the accessors and mutators (API). // See tst_qcontactmanagerfiltering for the "backend support" test. QContactFetchHint hint; hint.setOptimizationHints(QContactFetchHint::NoBinaryBlobs); QCOMPARE(hint.optimizationHints(), QContactFetchHint::NoBinaryBlobs); QStringList rels; rels << relationshipString(QContactRelationship::HasMember); hint.setRelationshipTypesHint(rels); QCOMPARE(hint.relationshipTypesHint(), rels); QList defs; defs << QContactName::Type << QContactPhoneNumber::Type; hint.setDetailTypesHint(defs); QCOMPARE(hint.detailTypesHint(), defs); QSize prefImageSize(33, 33); hint.setPreferredImageSize(prefImageSize); QCOMPARE(hint.preferredImageSize(), prefImageSize); int limit = 15; hint.setMaxCountHint(limit); QCOMPARE(hint.maxCountHint(), limit); } void tst_QContactManager::selfContactId() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); QContactId selfContact = cm->selfContactId(); QCOMPARE(cm->error(), QContactManager::NoError); QVERIFY(!cm->setSelfContactId(ContactId::apiId(123, cm->managerUri()))); QCOMPARE(cm->error(), QContactManager::NotSupportedError); // attempt to set a valid, saved contact as the self contact. QContact self; QContactPhoneNumber selfPhn; selfPhn.setNumber("12345"); self.saveDetail(&selfPhn); if (!cm->saveContact(&self)) { QSKIP("Unable to save the generated self contact"); } QContactId newSelfContact = ContactId::apiId(self); // Setup signal spy QSignalSpy spy(cm.data(), selfContactIdChangedSignal); QTestSignalSink sink(cm.data(), selfContactIdChangedSignal); // Set new self contact. qtcontacts-sqlite doesn't support this. QVERIFY(!cm->setSelfContactId(newSelfContact)); QCOMPARE(cm->error(), QContactManager::NotSupportedError); QTest::qWait(250); QCOMPARE(spy.count(), 0); // qtcontacts sqlite doesn't support changing self contact. if (spy.count()) { QCOMPARE(spy.at(0).count(), 2); QCOMPARE(spy.at(0).at(0), QVariant::fromValue(selfContact)); QCOMPARE(spy.at(0).at(1), QVariant::fromValue(newSelfContact)); QCOMPARE(cm->selfContactId(), newSelfContact); // changed. } else { QCOMPARE(cm->selfContactId(), selfContact); // unchanged. } // Remove self contact if (cm->removeContact(selfContact)) { QTRY_VERIFY(spy.count() == 2); QCOMPARE(spy.at(1).count(), 2); QCOMPARE(spy.at(1).at(0), QVariant::fromValue(newSelfContact)); QCOMPARE(spy.at(1).at(1), QVariant::fromValue(QContactId())); QCOMPARE(cm->selfContactId(), QContactId()); // ensure reset after removed. // reset to original state. cm->setSelfContactId(selfContact); } } QList tst_QContactManager::removeAllDefaultDetails(const QList& details) { QList newlist; foreach (const QContactDetail d, details) { if (detailType(d) != detailType() && detailType(d) != detailType() && detailType(d) != detailType()) { newlist << d; } } return newlist; } void tst_QContactManager::detailOrders() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); if (!managerSupportsFeature(*cm, "DetailOrdering")) QSKIP("Skipping: This manager does not support detail ordering!"); QContact a; QContactDetail::DetailContext contextOther(QContactDetail::ContextOther); //phone numbers { #ifdef DETAIL_DEFINITION_SUPPORTED d = cm->detailDefinition(QContactPhoneNumber::DefinitionName, QContactType::TypeContact); supportedContexts = d.fields().value(QContactDetail::FieldContext); contextOther = QString(QLatin1String(QContactDetail::ContextOther)); if (!supportedContexts.allowableValues().contains(contextOther)) { contextOther = QString(); } #endif QContactPhoneNumber number1, number2, number3; number1.setNumber("11111111"); number1.setContexts(QContactPhoneNumber::ContextHome); number2.setNumber("22222222"); number2.setContexts(QContactPhoneNumber::ContextWork); number3.setNumber("33333333"); number3.setContexts(QContactPhoneNumber::ContextOther); number3.setContexts(contextOther); a.saveDetail(&number1); a.saveDetail(&number2); a.saveDetail(&number3); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QList details = a.details(); QVERIFY(details.count() == 3); QVERIFY(details.at(0).value(QContactPhoneNumber::FieldContext) == QContactPhoneNumber::ContextHome); QVERIFY(details.at(1).value(QContactPhoneNumber::FieldContext) == QContactPhoneNumber::ContextWork); QVERIFY(details.at(2).value(QContactPhoneNumber::FieldContext) == contextOther); QVERIFY(a.removeDetail(&number2)); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); details = a.details(); QVERIFY(details.count() == 2); QVERIFY(details.at(0).value(QContactPhoneNumber::FieldContext) == QContactPhoneNumber::ContextHome); QVERIFY(details.at(1).value(QContactPhoneNumber::FieldContext) == contextOther); a.saveDetail(&number2); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); details = a.details(); QVERIFY(details.count() == 3); QVERIFY(details.at(0).value(QContactPhoneNumber::FieldContext) == QContactPhoneNumber::ContextHome); QVERIFY(details.at(1).value(QContactPhoneNumber::FieldContext) == contextOther); QVERIFY(details.at(2).value(QContactPhoneNumber::FieldContext) == QContactPhoneNumber::ContextWork); } //addresses { #ifdef DETAIL_DEFINITION_SUPPORTED d = cm->detailDefinition(QContactAddress::DefinitionName, QContactType::TypeContact); supportedContexts = d.fields().value(QContactDetail::FieldContext); contextOther = QString(QLatin1String(QContactDetail::ContextOther)); if (!supportedContexts.allowableValues().contains(contextOther)) { contextOther = QString(); } #endif QContactAddress address1, address2, address3; address1.setStreet("Brandl St"); address1.setRegion("Brisbane"); address3 = address2 = address1; address1.setContexts(QContactAddress::ContextHome); address2.setContexts(QContactAddress::ContextWork); address3.setContexts(contextOther); a.saveDetail(&address1); a.saveDetail(&address2); a.saveDetail(&address3); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QList details = a.details(); QVERIFY(details.count() == 3); QVERIFY(details.at(0).value(QContactAddress::FieldContext) == QContactAddress::ContextHome); QVERIFY(details.at(1).value(QContactAddress::FieldContext) == QContactAddress::ContextWork); QVERIFY(details.at(2).value(QContactAddress::FieldContext) == contextOther); QVERIFY(a.removeDetail(&address2)); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); details = a.details(); QVERIFY(details.count() == 2); QVERIFY(details.at(0).value(QContactAddress::FieldContext) == QContactAddress::ContextHome); QVERIFY(details.at(1).value(QContactAddress::FieldContext) == contextOther); a.saveDetail(&address2); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); details = a.details(); QVERIFY(details.count() == 3); QVERIFY(details.at(0).value(QContactAddress::FieldContext) == QContactAddress::ContextHome); QVERIFY(details.at(1).value(QContactAddress::FieldContext) == contextOther); QVERIFY(details.at(2).value(QContactAddress::FieldContext) == QContactAddress::ContextWork); } //emails { #ifdef DETAIL_DEFINITION_SUPPORTED d = cm->detailDefinition(QContactEmailAddress::DefinitionName, QContactType::TypeContact); supportedContexts = d.fields().value(QContactDetail::FieldContext); contextOther = QString(QLatin1String(QContactDetail::ContextOther)); if (!supportedContexts.allowableValues().contains(contextOther)) { contextOther = QString(); } #endif QContactEmailAddress email1, email2, email3; email1.setEmailAddress("aaron@example.com"); email3 = email2 = email1; email1.setContexts(QContactEmailAddress::ContextHome); email2.setContexts(QContactEmailAddress::ContextWork); email3.setContexts(contextOther); a.saveDetail(&email1); a.saveDetail(&email2); a.saveDetail(&email3); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QList details = a.details(); QVERIFY(details.count() == 3); QVERIFY(details.at(0).value(QContactEmailAddress::FieldContext) == QContactEmailAddress::ContextHome); QVERIFY(details.at(1).value(QContactEmailAddress::FieldContext) == QContactEmailAddress::ContextWork); QVERIFY(details.at(2).value(QContactEmailAddress::FieldContext) == contextOther); QVERIFY(a.removeDetail(&email2)); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); details = a.details(); QVERIFY(details.count() == 2); QVERIFY(details.at(0).value(QContactEmailAddress::FieldContext) == QContactEmailAddress::ContextHome); QVERIFY(details.at(1).value(QContactEmailAddress::FieldContext) == contextOther); a.saveDetail(&email2); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); details = a.details(); QVERIFY(details.count() == 3); QVERIFY(details.at(0).value(QContactEmailAddress::FieldContext) == QContactEmailAddress::ContextHome); QVERIFY(details.at(1).value(QContactEmailAddress::FieldContext) == contextOther); QVERIFY(details.at(2).value(QContactEmailAddress::FieldContext) == QContactEmailAddress::ContextWork); QVERIFY(cm->removeContact(removalId(a))); QVERIFY(cm->error() == QContactManager::NoError); } } void tst_QContactManager::relationships() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); // save some contacts QContact source; QContact dest1, dest2, dest3, dest4; QContactPhoneNumber n1, n2, n3, n4; n1.setNumber("1"); n2.setNumber("2"); n3.setNumber("3"); n4.setNumber("4"); dest1.saveDetail(&n1); dest2.saveDetail(&n2); dest3.saveDetail(&n3); dest4.saveDetail(&n3); cm->saveContact(&source); cm->saveContact(&dest1); cm->saveContact(&dest2); cm->saveContact(&dest3); cm->saveContact(&dest4); // check if manager supports relationships if (!managerSupportsFeature(*cm, "Relationships")) { // ensure that the operations all fail as required. QContactRelationship r1, r2, r3; r1 = makeRelationship(QContactRelationship::HasManager, source.id(), dest1.id()); r2 = makeRelationship(QContactRelationship::HasManager, source.id(), dest2.id()); r3 = makeRelationship(QContactRelationship::HasManager, source.id(), dest3.id()); QList batchList; batchList << r2 << r3; // test save and remove QVERIFY(!cm->saveRelationship(&r1)); QVERIFY(cm->error() == QContactManager::NotSupportedError); QVERIFY(!cm->removeRelationship(r1)); QVERIFY(cm->error() == QContactManager::NotSupportedError); cm->saveRelationships(&batchList, NULL); QVERIFY(cm->error() == QContactManager::NotSupportedError); // test retrieval QList retrieveList; retrieveList = cm->relationships(relatedContactId(source), QContactRelationship::First); QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); retrieveList = cm->relationships(relatedContactId(source), QContactRelationship::Second); QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); retrieveList = cm->relationships(relatedContactId(source), QContactRelationship::Either); // Either QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); retrieveList = cm->relationships(relationshipString(QContactRelationship::HasManager), relatedContactId(source), QContactRelationship::First); QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); retrieveList = cm->relationships(relationshipString(QContactRelationship::HasManager), relatedContactId(source), QContactRelationship::Second); QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); retrieveList = cm->relationships(relationshipString(QContactRelationship::HasManager), relatedContactId(source), QContactRelationship::Either); QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); retrieveList = cm->relationships(relationshipString(QContactRelationship::HasManager), relatedContactId(source)); QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); retrieveList = cm->relationships(relationshipString(QContactRelationship::HasManager)); QVERIFY(retrieveList.isEmpty()); QVERIFY(cm->error() == QContactManager::NotSupportedError); return; } // Get supported relationship types QStringList availableRelationshipTypes; if (cm->isRelationshipTypeSupported(relationshipString(QContactRelationship::HasMember))) availableRelationshipTypes << relationshipString(QContactRelationship::HasMember); if (cm->isRelationshipTypeSupported(relationshipString(QContactRelationship::HasAssistant))) availableRelationshipTypes << relationshipString(QContactRelationship::HasAssistant); if (cm->isRelationshipTypeSupported(relationshipString(QContactRelationship::HasManager))) availableRelationshipTypes << relationshipString(QContactRelationship::HasManager); if (cm->isRelationshipTypeSupported(relationshipString(QContactRelationship::HasSpouse))) availableRelationshipTypes << relationshipString(QContactRelationship::HasSpouse); if (cm->isRelationshipTypeSupported(relationshipString(QContactRelationship::IsSameAs))) availableRelationshipTypes << relationshipString(QContactRelationship::IsSameAs); // Check arbitrary relationship support if (managerSupportsFeature(*cm, "ArbitraryRelationshipTypes")) { // add some arbitrary type for testing if (availableRelationshipTypes.count()) availableRelationshipTypes.insert(0, "test-arbitrary-relationship-type"); else { availableRelationshipTypes.append("test-arbitrary-relationship-type"); availableRelationshipTypes.append(relationshipString(QContactRelationship::HasMember)); availableRelationshipTypes.append(relationshipString(QContactRelationship::HasAssistant)); } } // Verify that we have relationship types. If there are none then the manager // is saying it supports relationships but does not actually implement any // relationship type. QVERIFY(!availableRelationshipTypes.isEmpty()); // Some backends (eg. symbian) require that when type is "HasMember" // then "first" contact must be a group. if (availableRelationshipTypes.at(0) == relationshipString(QContactRelationship::HasMember)) { cm->removeContact(removalId(source)); source.setId(QContactId()); source.setType(QContactType::TypeGroup); cm->saveContact(&source); } // Create some common contact id's for testing QContactId dest1Uri = dest1.id(); QContactId dest2Uri = dest2.id(); QContactId dest3Uri = dest3.id(); QContactId dest1EmptyUri = dest1.id(); QContactId dest3EmptyUri = dest3.id(); // build our relationship - source is the manager all of the dest contacts. QContactRelationship customRelationshipOne; customRelationshipOne = makeRelationship(availableRelationshipTypes.at(0), source.id(), dest1EmptyUri); QVERIFY(customRelationshipOne.relationshipType() == availableRelationshipTypes.at(0)); // save the relationship int managerRelationshipsCount = cm->relationships(availableRelationshipTypes.at(0)).count(); QVERIFY(cm->saveRelationship(&customRelationshipOne)); // test our accessors. QCOMPARE(cm->relationships(availableRelationshipTypes.at(0)).count(), (managerRelationshipsCount + 1)); QVERIFY(cm->relationships(availableRelationshipTypes.at(0), relatedContactId(source)).count() == 1); // remove the dest1 contact, relationship should be removed. QContact copy1(dest1); cm->removeContact(removalId(dest1)); QCOMPARE(cm->relationships(availableRelationshipTypes.at(0), relatedContactId(copy1), QContactRelationship::Second).count(), 0); // modify and save the relationship customRelationshipOne = makeRelationship(availableRelationshipTypes.at(0), source.id(), dest2Uri); QVERIFY(cm->saveRelationship(&customRelationshipOne)); // attempt to save the relationship again. XXX TODO: what should the result be? currently succeeds (overwrites) int relationshipsCount = cm->relationships().count(); QVERIFY(cm->saveRelationship(&customRelationshipOne)); // succeeds, but just overwrites QCOMPARE(relationshipsCount, cm->relationships().count()); // shouldn't change; save should have overwritten. // removing the source contact should result in removal of the relationship. QVERIFY(cm->removeContact(removalId(source))); QCOMPARE(cm->relationships().count(), relationshipsCount - 2); // the relationship should have been removed, as well as an Aggregates. // now ensure that qcontact relationship caching works as required - perhaps this should be in tst_QContact? source.setId(QContactId()); // reset id so we can resave QVERIFY(cm->saveContact(&source)); // save source again. customRelationshipOne = makeRelationship(availableRelationshipTypes.at(0), source.id(), dest2.id()); QVERIFY(cm->saveRelationship(&customRelationshipOne)); // Add a second relationship QContactRelationship customRelationshipTwo; int reltype = availableRelationshipTypes.count() > 1 ? 1 : 0; customRelationshipTwo = makeRelationship(availableRelationshipTypes.at(reltype), source.id(), dest3.id()); QVERIFY(cm->saveRelationship(&customRelationshipTwo)); // currently, the contacts are "stale" - no cached relationships QVERIFY(dest3.relatedContacts().isEmpty()); QVERIFY(dest3.relationships().isEmpty()); QVERIFY(dest2.relatedContacts().isEmpty()); QVERIFY(dest2.relationships().isEmpty()); // now refresh the contacts dest3 = cm->contact(retrievalId(dest3)); dest2 = cm->contact(retrievalId(dest2)); source = cm->contact(retrievalId(source)); // and test again. // when we aggregate, the source will be the second in an Aggregated relationship QVERIFY(!source.relatedContacts(QString(), QContactRelationship::First).contains(dest1.id())); QVERIFY(!source.relatedContacts(QString(), QContactRelationship::First).contains(dest2.id())); QVERIFY(!source.relatedContacts(QString(), QContactRelationship::First).contains(dest3.id())); QVERIFY(!source.relatedContacts(QString(), QContactRelationship::First).contains(dest4.id())); QVERIFY(source.relatedContacts(QString(), QContactRelationship::Second).contains(dest2.id())); QVERIFY(source.relatedContacts(QString(), QContactRelationship::Either).contains(dest2.id())); QVERIFY(source.relatedContacts(QString(), QContactRelationship::Second).contains(dest3.id())); QVERIFY(source.relatedContacts(QString(), QContactRelationship::Either).contains(dest3.id())); QVERIFY(source.relatedContacts(availableRelationshipTypes.at(0), QContactRelationship::Second).contains(dest2.id())); QVERIFY(source.relatedContacts(availableRelationshipTypes.at(0), QContactRelationship::First).isEmpty()); QVERIFY(dest2.relatedContacts().contains(source.id())); QVERIFY(dest2.relationships().contains(customRelationshipOne)); QVERIFY(!dest2.relationships().contains(customRelationshipTwo)); QVERIFY(dest2.relationships(availableRelationshipTypes.at(0)).contains(customRelationshipOne)); QVERIFY(!dest2.relationships(availableRelationshipTypes.at(0)).contains(customRelationshipTwo)); QVERIFY(dest2.relatedContacts(availableRelationshipTypes.at(0)).contains(source.id())); QVERIFY(dest2.relatedContacts(availableRelationshipTypes.at(0), QContactRelationship::First).contains(source.id())); QVERIFY(dest2.relatedContacts(availableRelationshipTypes.at(0), QContactRelationship::Second).isEmpty()); QVERIFY(!dest2.relatedContacts(availableRelationshipTypes.at(0), QContactRelationship::Second).contains(source.id())); QVERIFY(dest3.relatedContacts().contains(source.id())); QVERIFY(!dest3.relationships().contains(customRelationshipOne)); QVERIFY(dest3.relationships().contains(customRelationshipTwo)); QVERIFY(!dest3.relationships(availableRelationshipTypes.at(0)).contains(customRelationshipOne)); // Test iteration QList relats = source.relationships(); QList::iterator it = relats.begin(); while (it != relats.end()) { if (it->first() != relatedContactId(source)) { // assume it's the aggregate and ignore it... it++; continue; } QVERIFY(it->second() == dest2.id() || it->second() == dest3.id()); it++; } if (availableRelationshipTypes.count() > 1) { QVERIFY(source.relatedContacts(availableRelationshipTypes.at(1), QContactRelationship::Second).contains(dest3.id())); QVERIFY(source.relatedContacts(availableRelationshipTypes.at(1), QContactRelationship::First).isEmpty()); QVERIFY(dest2.relationships(availableRelationshipTypes.at(1)).isEmpty()); QVERIFY(!dest3.relationships(availableRelationshipTypes.at(0)).contains(customRelationshipTwo)); QVERIFY(dest3.relationships(availableRelationshipTypes.at(1)).contains(customRelationshipTwo)); QVERIFY(!dest3.relationships(availableRelationshipTypes.at(1)).contains(customRelationshipOne)); QVERIFY(dest3.relatedContacts(availableRelationshipTypes.at(1)).contains(source.id())); QVERIFY(!dest3.relatedContacts(availableRelationshipTypes.at(0)).contains(source.id())); QVERIFY(dest3.relatedContacts(availableRelationshipTypes.at(1)).contains(source.id())); // role = either QVERIFY(!dest3.relatedContacts(availableRelationshipTypes.at(1), QContactRelationship::Second).contains(source.id())); QVERIFY(dest3.relatedContacts(availableRelationshipTypes.at(1), QContactRelationship::First).contains(source.id())); QVERIFY(dest2.relatedContacts(availableRelationshipTypes.at(1)).isEmpty()); } else { QVERIFY(source.relatedContacts(availableRelationshipTypes.at(0), QContactRelationship::Second).contains(dest3.id())); } // Cleanup a bit QMap errorMap; QList moreRels; moreRels << customRelationshipOne << customRelationshipTwo; errorMap.insert(5, QContactManager::BadArgumentError); QVERIFY(cm->removeRelationships(moreRels, &errorMap)); QVERIFY(errorMap.count() == 0); // test batch API and ordering in contacts QList currentRelationships = cm->relationships(relatedContactId(source), QContactRelationship::First); QList batchList; QContactRelationship br1, br2, br3; br1 = makeRelationship(availableRelationshipTypes.at(0), source.id(), dest2.id()); br2 = makeRelationship(availableRelationshipTypes.at(0), source.id(), dest3.id()); if (availableRelationshipTypes.count() > 1) { br3 = makeRelationship(availableRelationshipTypes.at(1), source.id(), dest3.id()); } else { br3 = makeRelationship(availableRelationshipTypes.at(0), source.id(), dest4.id()); } batchList << br1 << br2 << br3; // ensure that the batch save works properly cm->saveRelationships(&batchList, NULL); QCOMPARE(cm->error(), QContactManager::NoError); QList batchRetrieve = cm->relationships(relatedContactId(source), QContactRelationship::First); QVERIFY(batchRetrieve.contains(br1)); QVERIFY(batchRetrieve.contains(br2)); QVERIFY(batchRetrieve.contains(br3)); // remove a single relationship QVERIFY(cm->removeRelationship(br3)); batchRetrieve = cm->relationships(relatedContactId(source), QContactRelationship::First); QVERIFY(batchRetrieve.contains(br1)); QVERIFY(batchRetrieve.contains(br2)); QVERIFY(!batchRetrieve.contains(br3)); // has already been removed. // now ensure that the batch remove works and we get returned to the original state. batchList.removeOne(br3); cm->removeRelationships(batchList, NULL); QVERIFY(cm->error() == QContactManager::NoError); QCOMPARE(cm->relationships(relatedContactId(source), QContactRelationship::First), currentRelationships); // attempt to save relationships between an existing source but non-existent destination quint32 idSeed = 0x5544; QContactId nonexistentId = ContactId::apiId(idSeed, cm->managerUri()); while (true) { QContact r = cm->contact(retrievalId(nonexistentId)); if (!ContactId::isValid(r)) { // found a "spare" local id (no contact with that id) break; } // keep looking... idSeed += 1; QVERIFY(idSeed != 0); // integer overflow check. nonexistentId = ContactId::apiId(idSeed, cm->managerUri()); } QContactRelationship maliciousRel; maliciousRel = makeRelationship(QString::fromLatin1("test-invalid-relationship-type"), source.id(), nonexistentId); QVERIFY(!cm->saveRelationship(&maliciousRel)); // attempt to save a circular relationship - should fail! maliciousRel = makeRelationship(availableRelationshipTypes.at(0), source.id(), source.id()); QVERIFY(!cm->saveRelationship(&maliciousRel)); // remove the nonexistent relationship relationshipsCount = cm->relationships().count(); QVERIFY(!cm->removeRelationship(maliciousRel)); // does not exist; fail remove. QVERIFY(cm->error() == QContactManager::DoesNotExistError || cm->error() == QContactManager::InvalidRelationshipError); QCOMPARE(cm->relationships().count(), relationshipsCount); // should be unchanged. // now we want to ensure that a relationship is removed if one of the contacts is removed. customRelationshipOne = makeRelationship(availableRelationshipTypes.at(0), source.id(), dest2.id()); // Test batch save with an error map moreRels.clear(); moreRels << customRelationshipOne; errorMap.insert(0, QContactManager::BadArgumentError); QVERIFY(cm->saveRelationships(&moreRels, &errorMap)); QVERIFY(cm->error() == QContactManager::NoError); QVERIFY(errorMap.count() == 0); // should be reset source = cm->contact(retrievalId(source)); dest2 = cm->contact(retrievalId(dest2)); QVERIFY(cm->removeContact(removalId(dest2))); // remove dest2, the relationship should be removed QVERIFY(cm->relationships(availableRelationshipTypes.at(0), relatedContactId(dest2), QContactRelationship::Second).isEmpty()); source = cm->contact(retrievalId(source)); QVERIFY(!source.relatedContacts().contains(dest2.id())); // and it shouldn't appear in cache. // now clean up and remove our dests. QVERIFY(cm->removeContact(removalId(source))); QVERIFY(cm->removeContact(removalId(dest3))); // attempt to save relationships with nonexistent contacts QVERIFY(!cm->saveRelationship(&br1)); QVERIFY(cm->error() == QContactManager::InvalidRelationshipError); cm->saveRelationships(&batchList, NULL); QVERIFY(cm->error() == QContactManager::InvalidRelationshipError); QVERIFY(!cm->removeRelationship(br1)); QVERIFY(cm->error() == QContactManager::DoesNotExistError || cm->error() == QContactManager::InvalidRelationshipError); cm->removeRelationships(batchList, NULL); QVERIFY(cm->error() == QContactManager::DoesNotExistError || cm->error() == QContactManager::InvalidRelationshipError); } void tst_QContactManager::contactType() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); QContact g1, g2, c; g1.setType(QContactType::TypeGroup); g2.setType(QContactType::TypeGroup); QContactPhoneNumber g1p, g2p, cp; g1p.setNumber("22222"); g2p.setNumber("11111"); cp.setNumber("33333"); g1.saveDetail(&g1p); g2.saveDetail(&g2p); c.saveDetail(&cp); QVERIFY(cm->saveContact(&c)); // test that the accessing by type works properly for contacts QContactCollectionFilter allCollectionsFilter; QContactDetailFilter typeFilter; setFilterDetail(typeFilter, QContactType::FieldType); setFilterValue(typeFilter, QContactType::TypeContact); QVERIFY(cm->contactIds(typeFilter & allCollectionsFilter).contains(ContactId::apiId(c))); if (!managerSupportsFeature(*cm, "Groups")) QSKIP("Skipping: This manager does not support group contacts!"); QVERIFY(cm->saveContact(&g1)); QVERIFY(cm->saveContact(&g2)); // test that the accessing by type works properly for groups QContactDetailFilter groupFilter; setFilterDetail(groupFilter, QContactType::FieldType); setFilterValue(groupFilter, QContactType::TypeGroup); QVERIFY(cm->contactIds(groupFilter).contains(ContactId::apiId(g1))); QVERIFY(cm->contactIds(groupFilter).contains(ContactId::apiId(g2))); QVERIFY(!cm->contactIds(groupFilter).contains(ContactId::apiId(c))); QList sortOrders; QContactSortOrder byPhoneNumber; setSortDetail(byPhoneNumber, QContactPhoneNumber::FieldNumber); sortOrders.append(byPhoneNumber); // and ensure that sorting works properly with typed contacts also QList sortedIds = cm->contactIds(groupFilter, sortOrders); QVERIFY(sortedIds.indexOf(ContactId::apiId(g2)) < sortedIds.indexOf(ContactId::apiId(g1))); cm->removeContact(removalId(g1)); cm->removeContact(removalId(g2)); cm->removeContact(removalId(c)); } void tst_QContactManager::familyDetail() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); QContact a; QContactName n; n.setFirstName("Adam"); a.saveDetail(&n); QContactFamily f; f.setSpouse("Eve"); f.setChildren(QStringList() << "Cain" << "Abel"); a.saveDetail(&f); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QCOMPARE(a.details().count(), 1); n = a.details().at(0); QCOMPARE(n.firstName(), QLatin1String("Adam")); QCOMPARE(a.details().count(), 1); f = a.details().at(0); QCOMPARE(f.spouse(), QLatin1String("Eve")); QCOMPARE(f.children().toSet(), QSet() << "Cain" << "Abel"); QCOMPARE(a.relatedContacts(QContactRelationship::Aggregates(), QContactRelationship::First).count(), 1); QContactId aa(a.relatedContacts(QContactRelationship::Aggregates(), QContactRelationship::First).first()); QVERIFY(!aa.isNull()); QContactDetailFilter familyFilter; setFilterDetail(familyFilter, -1); QVERIFY(cm->contactIds(familyFilter).contains(aa)); QContactDetailFilter spouseFilter; setFilterDetail(spouseFilter, QContactFamily::FieldSpouse); setFilterValue(spouseFilter, "Eve"); QVERIFY(cm->contactIds(spouseFilter).contains(aa)); QContactDetailFilter childrenFilter; setFilterDetail(childrenFilter, QContactFamily::FieldChildren); setFilterValue(childrenFilter, "Abel"); QVERIFY(cm->contactIds(childrenFilter).contains(aa)); setFilterValue(childrenFilter, "Mabel"); QCOMPARE(cm->contactIds(childrenFilter).contains(aa), false); } void tst_QContactManager::geoLocationDetail() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); QContact a; QContactName n; n.setFirstName("Cristo Redentor"); a.saveDetail(&n); const QDateTime ts(QDateTime::currentDateTime()); QContactGeoLocation l; l.setLabel("Position"); l.setLatitude(-22.951994); l.setLongitude(-43.210492); l.setAccuracy(0.000001); l.setTimestamp(ts); a.saveDetail(&l); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QCOMPARE(a.details().count(), 1); n = a.details().at(0); QCOMPARE(n.firstName(), QLatin1String("Cristo Redentor")); QCOMPARE(a.details().count(), 1); l = a.details().at(0); QCOMPARE(l.label(), QLatin1String("Position")); QCOMPARE(l.latitude(), -22.951994); QCOMPARE(l.longitude(), -43.210492); QCOMPARE(l.accuracy(), 0.000001); QCOMPARE(l.timestamp(), ts); QCOMPARE(a.relatedContacts(QContactRelationship::Aggregates(), QContactRelationship::First).count(), 1); QContactId aa(a.relatedContacts(QContactRelationship::Aggregates(), QContactRelationship::First).first()); QVERIFY(!aa.isNull()); QContactDetailFilter geoLocationFilter; setFilterDetail(geoLocationFilter, -1); QVERIFY(cm->contactIds(geoLocationFilter).contains(aa)); QContactDetailFilter labelFilter; setFilterDetail(labelFilter, QContactGeoLocation::FieldLabel); setFilterValue(labelFilter, "Position"); QVERIFY(cm->contactIds(labelFilter).contains(aa)); QContactDetailFilter latitudeFilter; setFilterDetail(latitudeFilter, QContactGeoLocation::FieldLatitude); setFilterValue(latitudeFilter, -22.951994); QVERIFY(cm->contactIds(latitudeFilter).contains(aa)); QContactDetailFilter altitudeFilter; setFilterDetail(altitudeFilter, QContactGeoLocation::FieldAltitude); setFilterValue(altitudeFilter, 1.0); QCOMPARE(cm->contactIds(altitudeFilter).contains(aa), false); } #if defined(USE_VERSIT_PLZ) void tst_QContactManager::partialSave() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); const bool isAllowingDetailsNotInSchema = false; QVersitContactImporter imp; QVersitReader reader(QByteArray( "BEGIN:VCARD\r\nFN:Alice\r\nN:Alice\r\nTEL:12345\r\nEND:VCARD\r\n" "BEGIN:VCARD\r\nFN:Bob\r\nN:Bob\r\nTEL:5678\r\nEND:VCARD\r\n" "BEGIN:VCARD\r\nFN:Carol\r\nN:Carol\r\nEMAIL:carol@example.com\r\nEND:VCARD\r\n" "BEGIN:VCARD\r\nFN:David\r\nN:David\r\nORG:DavidCorp\r\nEND:VCARD\r\n")); reader.startReading(); reader.waitForFinished(); QCOMPARE(reader.error(), QVersitReader::NoError); QCOMPARE(reader.results().count(), 4); QVERIFY(imp.importDocuments(reader.results())); QCOMPARE(imp.contacts().count(), 4); QVERIFY(imp.contacts()[0].displayLabel() == QLatin1String("Alice")); QVERIFY(imp.contacts()[1].displayLabel() == QLatin1String("Bob")); QVERIFY(imp.contacts()[2].displayLabel() == QLatin1String("Carol")); QVERIFY(imp.contacts()[3].displayLabel() == QLatin1String("David")); QList contacts = imp.contacts(); QMap errorMap; // First save these contacts QVERIFY(cm->saveContacts(&contacts, &errorMap)); QList originalContacts = contacts; // Now try some partial save operations // 0) empty mask == full save // 1) Ignore an added phonenumber // 2) Only save a modified phonenumber, not a modified email // 3) Remove an email address & phone, mask out phone // 4) new contact, no details in the mask // 5) new contact, some details in the mask // 6) Have a bad manager uri in the middle // 7) Have a non existing contact in the middle // 8) A list entirely of new contacts QContactPhoneNumber pn; pn.setNumber("111111"); contacts[0].saveDetail(&pn); // 0) empty mask QVERIFY(cm->saveContacts(&contacts, QStringList(), &errorMap)); // That should have updated everything QContact a = cm->contact(retrievalId(originalContacts[0])); QVERIFY(a.details().count() == 2); // 1) Add a phone number to b, mask it out contacts[1].saveDetail(&pn); QVERIFY(cm->saveContacts(&contacts, QStringList(QContactEmailAddress::DefinitionName), &errorMap)); QVERIFY(errorMap.isEmpty()); QContact b = cm->contact(retrievalId(originalContacts[1])); QVERIFY(b.details().count() == 1); // 2) save a modified detail in the mask QContactEmailAddress e; e.setEmailAddress("example@example.com"); contacts[1].saveDetail(&e); // contacts[1] should have both phone and email QVERIFY(cm->saveContacts(&contacts, QStringList(QContactEmailAddress::DefinitionName), &errorMap)); QVERIFY(errorMap.isEmpty()); b = cm->contact(retrievalId(originalContacts[1])); QVERIFY(b.details().count() == 1); QVERIFY(b.details().count() == 1); // 3) Remove an email address and a phone number QVERIFY(contacts[1].removeDetail(&e)); QVERIFY(contacts[1].removeDetail(&pn)); QVERIFY(contacts[1].details().count() == 0); QVERIFY(contacts[1].details().count() == 1); QVERIFY(cm->saveContacts(&contacts, QStringList(QContactEmailAddress::DefinitionName), &errorMap)); QVERIFY(errorMap.isEmpty()); b = cm->contact(retrievalId(originalContacts[1])); QVERIFY(b.details().count() == 1); QVERIFY(b.details().count() == 0); // 4 - New contact, no details in the mask QContact newContact = originalContacts[3]; newContact.setId(QContactId()); contacts.append(newContact); QVERIFY(cm->saveContacts(&contacts, QStringList(QContactEmailAddress::DefinitionName), &errorMap)); QVERIFY(errorMap.isEmpty()); QVERIFY(ContactId::isValid(contacts[4])); // Saved b = cm->contact(retrievalId(contacts[4])); QVERIFY(b.details().count() == 0); // not saved QVERIFY(b.details().count() == 0); // not saved // 5 - New contact, some details in the mask newContact = originalContacts[2]; newContact.setId(QContactId()); contacts.append(newContact); QVERIFY(cm->saveContacts(&contacts, QStringList(QContactEmailAddress::DefinitionName), &errorMap)); QVERIFY(errorMap.isEmpty()); QVERIFY(ContactId::isValid(contacts[5])); // Saved b = cm->contact(retrievalId(contacts[5])); QVERIFY(b.details().count() == 1); QVERIFY(b.details().count() == 0); // not saved // 6) Have a bad manager uri in the middle followed by a save error QContactId id4(contacts[4].id()); QContactId badId(id4); badId.setManagerUri(QString()); contacts[4].setId(badId); QContactDetail badDetail("BadDetail"); badDetail.setValue("BadField", "BadValue"); contacts[5].saveDetail(&badDetail); QVERIFY(!cm->saveContacts(&contacts, QStringList("BadDetail"), &errorMap)); QCOMPARE(errorMap.count(), isAllowingDetailsNotInSchema ? 1 : 2); QCOMPARE(errorMap[4], QContactManager::DoesNotExistError); QCOMPARE(errorMap[5], isAllowingDetailsNotInSchema ? QContactManager::NoError : QContactManager::InvalidDetailError); // 7) Have a non existing contact in the middle followed by a save error badId = id4; badId.setLocalId(987234); // something nonexistent contacts[4].setId(badId); QVERIFY(!cm->saveContacts(&contacts, QStringList("BadDetail"), &errorMap)); QCOMPARE(errorMap.count(), isAllowingDetailsNotInSchema ? 1 : 2); QCOMPARE(errorMap[4], QContactManager::DoesNotExistError); QCOMPARE(errorMap[5], isAllowingDetailsNotInSchema ? QContactManager::NoError : QContactManager::InvalidDetailError); // 8 - New contact, no details in the mask newContact = originalContacts[3]; QCOMPARE(newContact.details().count(), 1); QCOMPARE(newContact.details().count(), 1); newContact.setId(QContactId()); QList contacts2; contacts2.append(newContact); QVERIFY(cm->saveContacts(&contacts2, QStringList(QContactEmailAddress::DefinitionName), &errorMap)); QVERIFY(errorMap.isEmpty()); QVERIFY(ContactId::isValid(contacts2[0])); // Saved b = cm->contact(retrievalId(contacts2[0])); QVERIFY(b.details().count() == 0); // not saved QVERIFY(b.details().count() == 0); // not saved // 9 - A list with only a new contact, with some details in the mask newContact = originalContacts[2]; newContact.setId(QContactId()); contacts2.clear(); contacts2.append(newContact); QVERIFY(cm->saveContacts(&contacts2, QStringList(QContactEmailAddress::DefinitionName), &errorMap)); QVERIFY(errorMap.isEmpty()); QVERIFY(ContactId::isValid(contacts2[0])); // Saved b = cm->contact(retrievalId(contacts2[0])); QVERIFY(b.details().count() == 1); QVERIFY(b.details().count() == 0); // not saved // 10 - A list with new a contact for the wrong manager, followed by a new contact with an // invalid detail newContact = originalContacts[2]; newContact.setId(QContactId()); contacts2.clear(); contacts2.append(newContact); contacts2.append(newContact); contacts2[0].setId(badId); contacts2[1].saveDetail(&badDetail); QVERIFY(!cm->saveContacts(&contacts2, QStringList("BadDetail"), &errorMap)); QCOMPARE(errorMap.count(), isAllowingDetailsNotInSchema ? 1 : 2); QCOMPARE(errorMap[0], QContactManager::DoesNotExistError); QCOMPARE(errorMap[1], isAllowingDetailsNotInSchema ? QContactManager::NoError : QContactManager::InvalidDetailError); } #endif void tst_QContactManager::extendedDetail() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); // Verify that QContactExtendedDetail is supported QContact a; QContactName n; n.setFirstName("A"); n.setMiddleName("Test"); n.setLastName("Person"); a.saveDetail(&n); QContactExtendedDetail ed; ed.setName(QString::fromLatin1("Testing")); a.saveDetail(&ed); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QCOMPARE(a.details().count(), 1); QCOMPARE(a.details().at(0).name(), QString::fromLatin1("Testing")); QCOMPARE(a.details().at(0).data(), QVariant()); QByteArray d1(QString::fromLatin1("1-2-3").toUtf8()); ed = a.details().at(0); ed.setData(d1); a.saveDetail(&ed); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QCOMPARE(a.details().count(), 1); QCOMPARE(a.details().at(0).name(), QString::fromLatin1("Testing")); QCOMPARE(a.details().at(0).data().toByteArray(), d1); QByteArray d2; { QDataStream ds(&d2, QIODevice::WriteOnly); for (int i = 0; i < 10; ++i) { int x = qrand(); int y = qrand(); const double q = x / (y ? y : 1); ds << q; } } QCOMPARE(static_cast(d2.size()), 10 * sizeof(double)); ed = QContactExtendedDetail(); ed.setName(QString::fromLatin1("Second")); ed.setData(d2); a.saveDetail(&ed); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QCOMPARE(a.details().count(), 2); if (a.details().at(0).name() == QString::fromLatin1("Testing")) { QCOMPARE(a.details().at(0).data().toByteArray(), d1); QCOMPARE(a.details().at(1).name(), QString::fromLatin1("Second")); QCOMPARE(a.details().at(1).data().toByteArray(), d2); } else { QCOMPARE(a.details().at(0).name(), QString::fromLatin1("Second")); QCOMPARE(a.details().at(0).data().toByteArray(), d2); QCOMPARE(a.details().at(1).name(), QString::fromLatin1("Testing")); QCOMPARE(a.details().at(1).data().toByteArray(), d1); } QContactDetailFilter filter; filter.setDetailType(QContactExtendedDetail::Type, QContactExtendedDetail::FieldName); filter.setValue(QString::fromLatin1("Second")); QList contacts(cm->contacts(filter)); QCOMPARE(contacts.count(), 1); } void tst_QContactManager::onlineAccountFields() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); // Verify that the extended fields of QContactOnlineAccount are correctly implemented QContact a; QContactName n; n.setFirstName("A"); n.setMiddleName("Test"); n.setLastName("Person"); a.saveDetail(&n); const QString accountUri(QString::fromLatin1("test@example.org")); const QString serviceProvider(QString::fromLatin1("example-im")); const QContactOnlineAccount::Protocol protocol(QContactOnlineAccount::ProtocolJabber); const QStringList capabilities(QStringList() << QString::fromLatin1("text") << QString::fromLatin1("voice")); const QList subTypes(QList() << QContactOnlineAccount::SubTypeSip << QContactOnlineAccount::SubTypeImpp); const QString accountPath(QString::fromLatin1("/example/jabber/0")); const QString accountIconPath(QString::fromLatin1("icons/example.png")); const bool enabled(true); const QString accountDisplayName(QString::fromLatin1("My Account")); const QString serviceProviderDisplayName(QString::fromLatin1("Example")); QContactOnlineAccount oa; oa.setAccountUri(accountUri); oa.setServiceProvider(serviceProvider); oa.setProtocol(protocol); oa.setCapabilities(capabilities); oa.setSubTypes(subTypes); oa.setValue(QContactOnlineAccount__FieldAccountPath, accountPath); oa.setValue(QContactOnlineAccount__FieldAccountIconPath, accountIconPath); oa.setValue(QContactOnlineAccount__FieldEnabled, enabled); oa.setValue(QContactOnlineAccount__FieldAccountDisplayName, accountDisplayName); oa.setValue(QContactOnlineAccount__FieldServiceProviderDisplayName, serviceProviderDisplayName); a.saveDetail(&oa); QVERIFY(cm->saveContact(&a)); a = cm->contact(retrievalId(a)); QCOMPARE(a.details().count(), 1); oa = a.detail(); QCOMPARE(oa.accountUri(), accountUri); QCOMPARE(oa.serviceProvider(), serviceProvider); QCOMPARE(oa.protocol(), protocol); QCOMPARE(oa.capabilities(), capabilities); QCOMPARE(oa.subTypes(), subTypes); QCOMPARE(oa.value(QContactOnlineAccount__FieldAccountPath).value(), accountPath); QCOMPARE(oa.value(QContactOnlineAccount__FieldAccountIconPath).value(), accountIconPath); QCOMPARE(oa.value(QContactOnlineAccount__FieldEnabled).value(), enabled); QCOMPARE(oa.value(QContactOnlineAccount__FieldAccountDisplayName).value(), accountDisplayName); QCOMPARE(oa.value(QContactOnlineAccount__FieldServiceProviderDisplayName).value(), serviceProviderDisplayName); } void tst_QContactManager::lateDeletion() { // Create some engines, but make them get deleted at shutdown QFETCH(QString, uri); QContactManager* cm = QContactManager::fromUri(uri); cm->setParent(qApp); // now do nothing } void tst_QContactManager::compareVariant() { // Exercise this function a bit QFETCH(QVariant, a); QFETCH(QVariant, b); QFETCH(Qt::CaseSensitivity, cs); QFETCH(int, expected); int comparison = QContactManagerEngine::compareVariant(a, b, cs); // Since compareVariant is a little imprecise (just sign matters) // convert that here. if (comparison < 0) comparison = -1; else if (comparison > 0) comparison = 1; QCOMPARE(comparison, expected); comparison = QContactManagerEngine::compareVariant(b, a, cs); if (comparison < 0) comparison = -1; else if (comparison > 0) comparison = 1; // The sign should be flipped now QVERIFY((comparison + expected) == 0); } void tst_QContactManager::compareVariant_data() { QTest::addColumn("a"); QTest::addColumn("b"); QTest::addColumn("cs"); QTest::addColumn("expected"); // bool QTest::newRow("bool <") << QVariant(false) << QVariant(true) << Qt::CaseInsensitive << -1; QTest::newRow("bool =") << QVariant(false) << QVariant(false) << Qt::CaseInsensitive << -0; QTest::newRow("bool >") << QVariant(true) << QVariant(false) << Qt::CaseInsensitive << 1; // char (who uses these??) QTest::newRow("char <") << QVariant(QChar('a')) << QVariant(QChar('b')) << Qt::CaseInsensitive << -1; QTest::newRow("char < ci") << QVariant(QChar('A')) << QVariant(QChar('b')) << Qt::CaseInsensitive << -1; QTest::newRow("char < ci 2") << QVariant(QChar('a')) << QVariant(QChar('B')) << Qt::CaseInsensitive << -1; QTest::newRow("char < cs") << QVariant(QChar('a')) << QVariant(QChar('b')) << Qt::CaseSensitive << -1; QTest::newRow("char < cs") << QVariant(QChar('A')) << QVariant(QChar('b')) << Qt::CaseSensitive << -1; QTest::newRow("char = ci") << QVariant(QChar('a')) << QVariant(QChar('a')) << Qt::CaseInsensitive << 0; QTest::newRow("char = ci 2") << QVariant(QChar('a')) << QVariant(QChar('A')) << Qt::CaseInsensitive << 0; QTest::newRow("char = ci 3") << QVariant(QChar('A')) << QVariant(QChar('a')) << Qt::CaseInsensitive << 0; QTest::newRow("char = ci 4") << QVariant(QChar('A')) << QVariant(QChar('A')) << Qt::CaseInsensitive << 0; QTest::newRow("char = cs") << QVariant(QChar('a')) << QVariant(QChar('a')) << Qt::CaseSensitive << 0; QTest::newRow("char = cs 2") << QVariant(QChar('A')) << QVariant(QChar('A')) << Qt::CaseSensitive << 0; QTest::newRow("char >") << QVariant(QChar('b')) << QVariant(QChar('a')) << Qt::CaseInsensitive << 1; QTest::newRow("char > ci") << QVariant(QChar('b')) << QVariant(QChar('A')) << Qt::CaseInsensitive << 1; QTest::newRow("char > ci 2") << QVariant(QChar('B')) << QVariant(QChar('a')) << Qt::CaseInsensitive << 1; QTest::newRow("char > cs") << QVariant(QChar('b')) << QVariant(QChar('a')) << Qt::CaseSensitive << 1; QTest::newRow("char > cs") << QVariant(QChar('b')) << QVariant(QChar('A')) << Qt::CaseSensitive << 1; // Some numeric types // uint QTest::newRow("uint < boundary") << QVariant(uint(1)) << QVariant(uint(-1)) << Qt::CaseInsensitive << -1; QTest::newRow("uint <") << QVariant(uint(1)) << QVariant(uint(2)) << Qt::CaseInsensitive << -1; QTest::newRow("uint =") << QVariant(uint(2)) << QVariant(uint(2)) << Qt::CaseInsensitive << 0; QTest::newRow("uint = 0") << QVariant(uint(0)) << QVariant(uint(0)) << Qt::CaseInsensitive << 0; QTest::newRow("uint = boundary") << QVariant(uint(-1)) << QVariant(uint(-1)) << Qt::CaseInsensitive << 0; QTest::newRow("uint >") << QVariant(uint(5)) << QVariant(uint(2)) << Qt::CaseInsensitive << 1; QTest::newRow("uint > boundary") << QVariant(uint(-1)) << QVariant(uint(2)) << Qt::CaseInsensitive << 1; // boundary // int (hmm, signed 32 bit assumed) QTest::newRow("int < boundary") << QVariant(int(0x80000000)) << QVariant(int(0x7fffffff)) << Qt::CaseInsensitive << -1; QTest::newRow("int <") << QVariant(int(1)) << QVariant(int(2)) << Qt::CaseInsensitive << -1; QTest::newRow("int =") << QVariant(int(2)) << QVariant(int(2)) << Qt::CaseInsensitive << 0; QTest::newRow("int = 0") << QVariant(int(0)) << QVariant(int(0)) << Qt::CaseInsensitive << 0; QTest::newRow("int = boundary") << QVariant(int(0x80000000)) << QVariant(int(0x80000000)) << Qt::CaseInsensitive << 0; QTest::newRow("int >") << QVariant(int(5)) << QVariant(int(2)) << Qt::CaseInsensitive << 1; QTest::newRow("int > boundary") << QVariant(int(0x7fffffff)) << QVariant(int(0x80000000)) << Qt::CaseInsensitive << 1; // boundary // ulonglong QTest::newRow("ulonglong < boundary") << QVariant(qulonglong(1)) << QVariant(qulonglong(-1)) << Qt::CaseInsensitive << -1; QTest::newRow("ulonglong <") << QVariant(qulonglong(1)) << QVariant(qulonglong(2)) << Qt::CaseInsensitive << -1; QTest::newRow("ulonglong =") << QVariant(qulonglong(2)) << QVariant(qulonglong(2)) << Qt::CaseInsensitive << 0; QTest::newRow("ulonglong = 0") << QVariant(qulonglong(0)) << QVariant(qulonglong(0)) << Qt::CaseInsensitive << 0; QTest::newRow("ulonglong = boundary") << QVariant(qulonglong(-1)) << QVariant(qulonglong(-1)) << Qt::CaseInsensitive << 0; QTest::newRow("ulonglong >") << QVariant(qulonglong(5)) << QVariant(qulonglong(2)) << Qt::CaseInsensitive << 1; QTest::newRow("ulonglong > boundary") << QVariant(qulonglong(-1)) << QVariant(qulonglong(2)) << Qt::CaseInsensitive << 1; // boundary // longlong QTest::newRow("longlong < boundary") << QVariant(qlonglong(0x8000000000000000LL)) << QVariant(qlonglong(0x7fffffffffffffffLL)) << Qt::CaseInsensitive << -1; QTest::newRow("longlong <") << QVariant(qlonglong(1)) << QVariant(qlonglong(2)) << Qt::CaseInsensitive << -1; QTest::newRow("longlong =") << QVariant(qlonglong(2)) << QVariant(qlonglong(2)) << Qt::CaseInsensitive << 0; QTest::newRow("longlong = 0") << QVariant(qlonglong(0)) << QVariant(qlonglong(0)) << Qt::CaseInsensitive << 0; QTest::newRow("longlong = boundary") << QVariant(qlonglong(0x8000000000000000LL)) << QVariant(qlonglong(0x8000000000000000LL)) << Qt::CaseInsensitive << 0; QTest::newRow("longlong >") << QVariant(qlonglong(5)) << QVariant(qlonglong(2)) << Qt::CaseInsensitive << 1; QTest::newRow("longlong > boundary") << QVariant(qlonglong(0x7fffffffffffffffLL)) << QVariant(qlonglong(0x8000000000000000LL)) << Qt::CaseInsensitive << 1; // boundary // double (hmm, skips NaNs etc) QTest::newRow("double < inf 2") << QVariant(-qInf()) << QVariant(qInf()) << Qt::CaseInsensitive << -1; QTest::newRow("double < inf") << QVariant(1.0) << QVariant(qInf()) << Qt::CaseInsensitive << -1; QTest::newRow("double <") << QVariant(1.0) << QVariant(2.0) << Qt::CaseInsensitive << -1; QTest::newRow("double =") << QVariant(2.0) << QVariant(2.0) << Qt::CaseInsensitive << 0; QTest::newRow("double = 0") << QVariant(0.0) << QVariant(0.0) << Qt::CaseInsensitive << 0; QTest::newRow("double = inf") << QVariant(qInf()) << QVariant(qInf()) << Qt::CaseInsensitive << 0; QTest::newRow("double >") << QVariant(5.0) << QVariant(2.0) << Qt::CaseInsensitive << 1; QTest::newRow("double > inf") << QVariant(qInf()) << QVariant(5.0) << Qt::CaseInsensitive << 1; QTest::newRow("double > inf 2") << QVariant(0.0) << QVariant(-qInf()) << Qt::CaseInsensitive << 1; QTest::newRow("double > inf 3") << QVariant(qInf()) << QVariant(-qInf()) << Qt::CaseInsensitive << 1; // strings QTest::newRow("string <") << QVariant(QString("a")) << QVariant(QString("b")) << Qt::CaseInsensitive << -1; QTest::newRow("string <") << QVariant(QString("a")) << QVariant(QString("B")) << Qt::CaseInsensitive << -1; QTest::newRow("string <") << QVariant(QString("A")) << QVariant(QString("b")) << Qt::CaseInsensitive << -1; QTest::newRow("string <") << QVariant(QString("A")) << QVariant(QString("B")) << Qt::CaseInsensitive << -1; QTest::newRow("string < cs") << QVariant(QString("a")) << QVariant(QString("b")) << Qt::CaseSensitive << -1; QTest::newRow("string < cs 2") << QVariant(QString("A")) << QVariant(QString("b")) << Qt::CaseSensitive << -1; QTest::newRow("string < length") << QVariant(QString("a")) << QVariant(QString("aa")) << Qt::CaseInsensitive << -1; QTest::newRow("string < length cs") << QVariant(QString("a")) << QVariant(QString("aa")) << Qt::CaseSensitive << -1; QTest::newRow("string < length 2") << QVariant(QString("a")) << QVariant(QString("ba")) << Qt::CaseInsensitive << -1; QTest::newRow("string < length cs 2") << QVariant(QString("a")) << QVariant(QString("ba")) << Qt::CaseSensitive << -1; QTest::newRow("string aa < b") << QVariant(QString("aa")) << QVariant(QString("b")) << Qt::CaseInsensitive << -1; QTest::newRow("string aa < b cs") << QVariant(QString("aa")) << QVariant(QString("b")) << Qt::CaseSensitive << -1; QTest::newRow("string '' < a") << QVariant(QString("")) << QVariant(QString("aa")) << Qt::CaseInsensitive << -1; QTest::newRow("string '' < aa cs") << QVariant(QString("")) << QVariant(QString("aa")) << Qt::CaseSensitive << -1; QTest::newRow("string 0 < a") << QVariant(QString()) << QVariant(QString("aa")) << Qt::CaseInsensitive << -1; QTest::newRow("string 0 < aa cs") << QVariant(QString()) << QVariant(QString("aa")) << Qt::CaseSensitive << -1; QTest::newRow("string '' = ''") << QVariant(QString("")) << QVariant(QString("")) << Qt::CaseInsensitive << 0; QTest::newRow("string '' = '' cs") << QVariant(QString("")) << QVariant(QString("")) << Qt::CaseSensitive << 0; QTest::newRow("string 0 = 0") << QVariant(QString()) << QVariant(QString()) << Qt::CaseInsensitive << 0; QTest::newRow("string 0 = 0 cs") << QVariant(QString()) << QVariant(QString()) << Qt::CaseSensitive << 0; QTest::newRow("string a = a") << QVariant(QString("a")) << QVariant(QString("a")) << Qt::CaseInsensitive << 0; QTest::newRow("string a = a cs") << QVariant(QString("a")) << QVariant(QString("a")) << Qt::CaseSensitive << 0; // Stringlists // {} < {"a"} < {"aa"} < {"aa","bb"} < {"aa", "cc"} < {"bb"} QStringList empty; QStringList listA("a"); QStringList listAA("aa"); QStringList listAABB; listAABB << "aa" << "bb"; QStringList listAACC; listAACC << "aa" << "cc"; QStringList listBB; listBB << "bb"; QStringList listCCAA; listCCAA << "cc" << "aa"; QStringList listA2("A"); QStringList listAA2("AA"); QTest::newRow("stringlist {} < {a}") << QVariant(empty) << QVariant(listA) << Qt::CaseInsensitive << -1; QTest::newRow("stringlist {} < {a} cs") << QVariant(empty) << QVariant(listA) << Qt::CaseSensitive << -1; QTest::newRow("stringlist {} < {A}") << QVariant(empty) << QVariant(listA2) << Qt::CaseInsensitive << -1; QTest::newRow("stringlist {} < {A} cs") << QVariant(empty) << QVariant(listA2) << Qt::CaseSensitive << -1; QTest::newRow("stringlist {a} < {aa}") << QVariant(listA) << QVariant(listAA) << Qt::CaseInsensitive << -1; QTest::newRow("stringlist {a} < {aa} cs") << QVariant(listA) << QVariant(listAA) << Qt::CaseSensitive << -1; QTest::newRow("stringlist {a} < {AA}") << QVariant(listA) << QVariant(listAA2) << Qt::CaseInsensitive << -1; // The results of this test are variable - ignore for now... //QTest::newRow("stringlist {a} < {AA} cs") << QVariant(listA) << QVariant(listAA2) << Qt::CaseSensitive << -1; QTest::newRow("stringlist {A} < {aa,bb}") << QVariant(listA2) << QVariant(listAABB) << Qt::CaseInsensitive << -1; QTest::newRow("stringlist {A} < {aa,bb} cs") << QVariant(listA2) << QVariant(listAABB) << Qt::CaseSensitive << -1; QTest::newRow("stringlist {aa} < {aa,bb}") << QVariant(listAA) << QVariant(listAABB) << Qt::CaseInsensitive << -1; QTest::newRow("stringlist {aa} < {aa,bb} cs") << QVariant(listAA) << QVariant(listAABB) << Qt::CaseSensitive << -1; QTest::newRow("stringlist {aa,bb} < {aa,cc}") << QVariant(listAABB) << QVariant(listAACC) << Qt::CaseInsensitive << -1; QTest::newRow("stringlist {aa,bb} < {aa,cc} cs") << QVariant(listAABB) << QVariant(listAACC) << Qt::CaseSensitive << -1; QTest::newRow("stringlist {aa,cc} < {bb}") << QVariant(listAACC) << QVariant(listBB) << Qt::CaseInsensitive << -1; QTest::newRow("stringlist {aa,cc} < {bb} cs") << QVariant(listAACC) << QVariant(listBB) << Qt::CaseSensitive << -1; // equality QTest::newRow("stringlist {} = {}") << QVariant(empty) << QVariant(empty) << Qt::CaseInsensitive << 0; QTest::newRow("stringlist {} = {} cs") << QVariant(empty) << QVariant(empty) << Qt::CaseSensitive << 0; QTest::newRow("stringlist {aa} = {aa}") << QVariant(listAA) << QVariant(listAA) << Qt::CaseInsensitive << 0; QTest::newRow("stringlist {aa} = {AA}") << QVariant(listAA) << QVariant(listAA2) << Qt::CaseInsensitive << 0; QTest::newRow("stringlist {aa} = {aa} cs") << QVariant(listAA) << QVariant(listAA) << Qt::CaseSensitive << 0; // Times QTime t0; QTime t1(0,0,0,0); QTime t2(0,59,0,0); QTime t3(1,0,0,0); QTime t4(23,59,59,999); QTest::newRow("times t0 < t1") << QVariant(t0) << QVariant(t1) << Qt::CaseInsensitive << -1; QTest::newRow("times t1 < t2") << QVariant(t1) << QVariant(t2) << Qt::CaseInsensitive << -1; QTest::newRow("times t2 < t3") << QVariant(t2) << QVariant(t3) << Qt::CaseInsensitive << -1; QTest::newRow("times t3 < t4") << QVariant(t3) << QVariant(t4) << Qt::CaseInsensitive << -1; QTest::newRow("times t0 = t0") << QVariant(t0) << QVariant(t0) << Qt::CaseInsensitive << 0; QTest::newRow("times t4 = t4") << QVariant(t4) << QVariant(t4) << Qt::CaseInsensitive << 0; // Dates QDate d0; QDate d1 = QDate::fromJulianDay(1); QDate d2(1,1,1); QDate d3(2011,6,9); QDate d4 = QDate::fromJulianDay(0x7fffffff); QDate d5 = QDate::fromJulianDay(0x80000000); QDate d6 = QDate::fromJulianDay(0xffffffff); QTest::newRow("dates d0 < d1") << QVariant(d0) << QVariant(d1) << Qt::CaseInsensitive << -1; QTest::newRow("dates d1 < d2") << QVariant(d1) << QVariant(d2) << Qt::CaseInsensitive << -1; QTest::newRow("dates d2 < d3") << QVariant(d2) << QVariant(d3) << Qt::CaseInsensitive << -1; QTest::newRow("dates d3 < d4") << QVariant(d3) << QVariant(d4) << Qt::CaseInsensitive << -1; QTest::newRow("dates d4 < d5") << QVariant(d4) << QVariant(d5) << Qt::CaseInsensitive << -1; QTest::newRow("dates d5 < d6") << QVariant(d5) << QVariant(d6) << Qt::CaseInsensitive << -1; QTest::newRow("dates d0 < d6") << QVariant(d0) << QVariant(d6) << Qt::CaseInsensitive << -1; QTest::newRow("dates d1 < d6") << QVariant(d1) << QVariant(d6) << Qt::CaseInsensitive << -1; QTest::newRow("dates d0 = d0") << QVariant(d0) << QVariant(d0) << Qt::CaseInsensitive << 0; QTest::newRow("dates d1 = d1") << QVariant(d1) << QVariant(d1) << Qt::CaseInsensitive << 0; QTest::newRow("dates d2 = d2") << QVariant(d2) << QVariant(d2) << Qt::CaseInsensitive << 0; QTest::newRow("dates d3 = d3") << QVariant(d3) << QVariant(d3) << Qt::CaseInsensitive << 0; QTest::newRow("dates d4 = d4") << QVariant(d4) << QVariant(d4) << Qt::CaseInsensitive << 0; QTest::newRow("dates d5 = d5") << QVariant(d5) << QVariant(d5) << Qt::CaseInsensitive << 0; QTest::newRow("dates d6 = d6") << QVariant(d6) << QVariant(d6) << Qt::CaseInsensitive << 0; // DateTimes // Somewhat limited testing here QDateTime dt0; QDateTime dt1(d1, t1); QDateTime dt2(d1, t2); QDateTime dt3(d4, t4); QDateTime dt4(d5, t1); QDateTime dt5(d6, t4); // end of the universe QTest::newRow("datetimes dt1 < dt2") << QVariant(dt1) << QVariant(dt2) << Qt::CaseInsensitive << -1; QTest::newRow("datetimes dt2 < dt3") << QVariant(dt2) << QVariant(dt3) << Qt::CaseInsensitive << -1; QTest::newRow("datetimes dt3 < dt4") << QVariant(dt3) << QVariant(dt4) << Qt::CaseInsensitive << -1; QTest::newRow("datetimes dt4 < dt5") << QVariant(dt4) << QVariant(dt5) << Qt::CaseInsensitive << -1; QTest::newRow("datetimes dt0 < dt5") << QVariant(dt0) << QVariant(dt5) << Qt::CaseInsensitive << -1; QTest::newRow("datetimes dt1 < dt5") << QVariant(dt1) << QVariant(dt5) << Qt::CaseInsensitive << -1; QTest::newRow("datetimes dt0 = dt0") << QVariant(dt0) << QVariant(dt0) << Qt::CaseInsensitive << 0; QTest::newRow("datetimes dt1 = dt1") << QVariant(dt1) << QVariant(dt1) << Qt::CaseInsensitive << 0; QTest::newRow("datetimes dt2 = dt2") << QVariant(dt2) << QVariant(dt2) << Qt::CaseInsensitive << 0; QTest::newRow("datetimes dt3 = dt3") << QVariant(dt3) << QVariant(dt3) << Qt::CaseInsensitive << 0; QTest::newRow("datetimes dt4 = dt4") << QVariant(dt4) << QVariant(dt4) << Qt::CaseInsensitive << 0; QTest::newRow("datetimes dt5 = dt5") << QVariant(dt5) << QVariant(dt5) << Qt::CaseInsensitive << 0; // Uninitialized datetime now compares as the epoch date QTest::newRow("datetimes dt0 > dt1") << QVariant(dt0) << QVariant(dt1) << Qt::CaseInsensitive << 1; } void tst_QContactManager::createCollection() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); qRegisterMetaType >("QList"); QSignalSpy collectionsAddedSpy(cm.data(), SIGNAL(collectionsAdded(QList))); const QByteArray collectionName = QUuid::createUuid().toByteArray(); QContactCollectionId newCollectionId; // create collection { QContactCollection col; col.setMetaData(QContactCollection::KeyName, collectionName); QVERIFY(cm->saveCollection(&col)); newCollectionId = col.id(); } // check "collectionsAdded" signal QTRY_COMPARE(collectionsAddedSpy.count(), 1); QList ids = collectionsAddedSpy.takeFirst().at(0).value >(); QCOMPARE(ids.count(), 1); // query for new collection { QContactCollection col = cm->collection(ids.at(0)); QVERIFY(!col.id().isNull()); QCOMPARE(col.id().toString(), ids.at(0).toString()); QCOMPARE(col.metaData(QContactCollection::KeyName).toByteArray(), collectionName); } } void tst_QContactManager::modifyCollection() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); qRegisterMetaType >("QList"); QSignalSpy collectionsAddedSpy(cm.data(), SIGNAL(collectionsAdded(QList))); QSignalSpy collectionsChangedSpy(cm.data(), SIGNAL(collectionsChanged(QList))); QContactCollectionId colId; QByteArray collectionName = QUuid::createUuid().toByteArray(); // save a new collection { QContactCollection col; col.setMetaData(QContactCollection::KeyName, collectionName); QVERIFY(cm->saveCollection(&col)); QTRY_COMPARE(collectionsAddedSpy.count(), 1); colId = col.id(); QVERIFY(!colId.isNull()); } // edit collection { QCOMPARE(collectionsChangedSpy.count(), 0); QContactCollection col = cm->collection(colId); QByteArray newCollectionName = QUuid::createUuid().toByteArray(); col.setMetaData(QContactCollection::KeyName, newCollectionName); QVERIFY(cm->saveCollection(&col)); // check signal "collectionsChanged" fired contains the collection id QTRY_COMPARE(collectionsChangedSpy.count(), 1); QList ids = collectionsChangedSpy.takeFirst().at(0).value >(); QCOMPARE(ids.at(0).toString(), colId.toString()); // check if the collection name was updated QContactCollection col2 = cm->collection(colId); QCOMPARE(col2.metaData(QContactCollection::KeyName).toByteArray(), newCollectionName); } } void tst_QContactManager::removeCollection() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); qRegisterMetaType >("QList"); QSignalSpy collectionsAddedSpy(cm.data(), SIGNAL(collectionsAdded(QList))); QSignalSpy collectionsRemovedSpy(cm.data(), SIGNAL(collectionsRemoved(QList))); QContactCollectionId colId; // save a new collection { QContactCollection col; QByteArray collectionName = QUuid::createUuid().toByteArray(); col.setMetaData(QContactCollection::KeyName, collectionName); QVERIFY(cm->saveCollection(&col)); QTRY_COMPARE(collectionsAddedSpy.count(), 1); colId = col.id(); } QList collections = cm->collections(); // remove collection cm->removeCollection(colId); // check "collectionsRemoved" signal QTRY_COMPARE(collectionsRemovedSpy.count(), 1); QList ids = collectionsRemovedSpy.takeFirst().at(0).value >(); QCOMPARE(ids.at(0).toString(), colId.toString()); // check if the correct collection was removed QList collectionsAfterRemoval = cm->collections(); QCOMPARE(collections.count() - 1, collectionsAfterRemoval.count()); Q_FOREACH (const QContactCollection &col, collectionsAfterRemoval) { collections.removeAll(col); } QCOMPARE(collections.count(), 1); QCOMPARE(collections.at(0).id().toString(), colId.toString()); } void tst_QContactManager::saveContactIntoCollections() { QFETCH(QString, uri); QScopedPointer cm(QContactManager::fromUri(uri)); qRegisterMetaType >("QList"); QSignalSpy collectionsAddedSpy(cm.data(), SIGNAL(collectionsAdded(QList))); QByteArray collectionName = QUuid::createUuid().toByteArray(); QContactCollectionId colId; QContactId cId; // create collection { QContactCollection col; col.setMetaData(QContactCollection::KeyName, collectionName); QVERIFY(cm->saveCollection(&col)); QTRY_COMPARE(collectionsAddedSpy.count(), 1); colId = col.id(); } // create contact { QContact c = createContact("Alice", "Last", "12345"); c.setCollectionId(colId); cm->saveContact(&c); cId = c.id(); } // query new contact and check for collection { QContact c = cm->contact(cId); QCOMPARE(c.collectionId(), colId); QCOMPARE(c.collectionId().toString(), colId.toString()); } } void tst_QContactManager::constituentOfSelf() { QScopedPointer m(newContactManager()); QContactId selfId(m->selfContactId()); // Create a contact which is aggregated by the self contact QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_QContactManager::constituentOfSelf"); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QVERIFY(m->saveCollection(&testAddressbook)); QContact constituent; constituent.setCollectionId(testAddressbook.id()); QVERIFY(m->saveContact(&constituent)); QVERIFY(m->error() == QContactManager::NoError); // Find the aggregate contact created by saving QContactRelationshipFilter relationshipFilter; setFilterType(relationshipFilter, QContactRelationship::Aggregates); setFilterContactId(relationshipFilter, constituent.id()); relationshipFilter.setRelatedContactRole(QContactRelationship::Second); // Now connect our contact to the real self contact QContactRelationship relationship(makeRelationship(QContactRelationship::Aggregates, selfId, constituent.id())); QVERIFY(m->saveRelationship(&relationship)); foreach (const QContact &aggregator, m->contacts(relationshipFilter)) { if (aggregator.id() != selfId) { // Remove the relationship between these contacts QContactRelationship relationship; relationship = makeRelationship(QContactRelationship::Aggregates, aggregator.id(), constituent.id()); QVERIFY(m->removeRelationship(relationship)); // The aggregator should have been removed QContact nonexistent = m->contact(retrievalId(aggregator)); QVERIFY(m->error() == QContactManager::DoesNotExistError); QCOMPARE(nonexistent.id(), QContactId()); } } // Update the constituent QContactNickname nn; nn.setNickname("nickname"); constituent = m->contact(retrievalId(constituent)); QVERIFY(constituent.saveDetail(&nn)); QVERIFY(m->saveContact(&constituent)); QVERIFY(m->error() == QContactManager::NoError); constituent = m->contact(retrievalId(constituent)); QVERIFY(detailsSuperset(constituent.detail(), nn)); // Change should be reflected in the self contact QContact self = m->contact(m->selfContactId()); QVERIFY(detailsSuperset(self.detail(), nn)); // Check that no new aggregate has been generated foreach (const QContact &aggregator, m->contacts(relationshipFilter)) QCOMPARE(aggregator.id(), selfId); QContactStatusFlags flags = self.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::IsOnline), false); // Do a presence update QContactPresence presence; presence.setPresenceState(QContactPresence::PresenceAway); constituent = m->contact(retrievalId(constituent)); QVERIFY(constituent.saveDetail(&presence)); QVERIFY(m->saveContact(&constituent)); QVERIFY(m->error() == QContactManager::NoError); constituent = m->contact(retrievalId(constituent)); QVERIFY(detailsSuperset(constituent.detail(), presence)); QCOMPARE(constituent.detail().presenceState(), presence.presenceState()); // Update should be relected in the self contact self = m->contact(m->selfContactId()); QCOMPARE(self.detail().presenceState(), presence.presenceState()); flags = self.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::IsOnline), true); // Update again presence = constituent.detail(); presence.setPresenceState(QContactPresence::PresenceBusy); QVERIFY(constituent.saveDetail(&presence)); QVERIFY(m->saveContact(&constituent)); QVERIFY(m->error() == QContactManager::NoError); constituent = m->contact(retrievalId(constituent)); QVERIFY(detailsSuperset(constituent.detail(), presence)); QCOMPARE(constituent.detail().presenceState(), presence.presenceState()); // the aggregate should have its presence state updated accordingly. self = m->contact(m->selfContactId()); QCOMPARE(self.detail().presenceState(), presence.presenceState()); // Check that no new aggregate has been generated foreach (const QContact &aggregator, m->contacts(relationshipFilter)) QCOMPARE(aggregator.id(), selfId); flags = self.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::IsOnline), true); // Offline status makes the contact no longer offline presence = constituent.detail(); presence.setPresenceState(QContactPresence::PresenceOffline); QVERIFY(constituent.saveDetail(&presence)); QVERIFY(m->saveContact(&constituent)); QVERIFY(m->error() == QContactManager::NoError); self = m->contact(m->selfContactId()); QCOMPARE(self.detail().presenceState(), presence.presenceState()); flags = self.detail(); QCOMPARE(flags.testFlag(QContactStatusFlags::IsOnline), false); // Add a name to the self contact. QContact localSelf = m->contact(QContactId(self.id().managerUri(), QByteArrayLiteral("sql-1"))); QContactName n = localSelf.detail(); n.setFirstName("firstname"); n.setLastName("lastname"); localSelf.saveDetail(&n); QVERIFY(m->saveContact(&localSelf)); QVERIFY(m->error() == QContactManager::NoError); // Create a new contact with a matching name QContact newContact; n = QContactName(); n.setFirstName("firstname"); n.setLastName("lastname"); newContact.saveDetail(&n); QVERIFY(m->saveContact(&newContact)); QVERIFY(m->error() == QContactManager::NoError); // Verify that the new contact was not aggregated into the self contact. // Contacts will only be aggregated into self contact if specified manually via relationship. newContact = m->contact(retrievalId(newContact)); QVERIFY(!newContact.relatedContacts().contains(m->selfContactId())); // Cleanup QVERIFY(m->removeContact(constituent.id())); } void tst_QContactManager::searchSensitivity() { QScopedPointer m(newContactManager()); QContactDetailFilter exactMatch; setFilterDetail(exactMatch, QContactName::FieldFirstName); exactMatch.setMatchFlags(QContactFilter::MatchExactly); exactMatch.setValue("Ada"); QContactDetailFilter exactMismatch; setFilterDetail(exactMismatch, QContactName::FieldFirstName); exactMismatch.setMatchFlags(QContactFilter::MatchExactly); exactMismatch.setValue("adA"); QContactDetailFilter insensitiveMatch; setFilterDetail(insensitiveMatch, QContactName::FieldFirstName); insensitiveMatch.setMatchFlags(QContactFilter::MatchFixedString); insensitiveMatch.setValue("Ada"); QContactDetailFilter insensitiveMismatch; setFilterDetail(insensitiveMismatch, QContactName::FieldFirstName); insensitiveMismatch.setMatchFlags(QContactFilter::MatchFixedString); insensitiveMismatch.setValue("adA"); QContactDetailFilter sensitiveMatch; setFilterDetail(sensitiveMatch, QContactName::FieldFirstName); sensitiveMatch.setMatchFlags(QContactFilter::MatchFixedString | QContactFilter::MatchCaseSensitive); sensitiveMatch.setValue("Ada"); QContactDetailFilter sensitiveMismatch; setFilterDetail(sensitiveMismatch, QContactName::FieldFirstName); sensitiveMismatch.setMatchFlags(QContactFilter::MatchFixedString | QContactFilter::MatchCaseSensitive); sensitiveMismatch.setValue("adA"); int originalCount[6]; originalCount[0] = m->contactIds(exactMatch).count(); originalCount[1] = m->contactIds(exactMismatch).count(); originalCount[2] = m->contactIds(insensitiveMatch).count(); originalCount[3] = m->contactIds(insensitiveMismatch).count(); originalCount[4] = m->contactIds(sensitiveMatch).count(); originalCount[5] = m->contactIds(sensitiveMismatch).count(); #ifndef DETAIL_DEFINITION_SUPPORTED QContact ada = createContact("Ada", "Lovelace", "9876543"); #else QContactDetailDefinition nameDef = m->detailDefinition(QContactName::DefinitionName, QContactType::TypeContact); QContact ada = createContact(nameDef, "Ada", "Lovelace", "9876543"); #endif int currCount = m->contactIds().count(); QVERIFY(m->saveContact(&ada)); QVERIFY(m->error() == QContactManager::NoError); QVERIFY(!ada.id().managerUri().isEmpty()); QVERIFY(ContactId::isValid(ada)); QCOMPARE(m->contactIds().count(), currCount+1); QCOMPARE(m->contactIds(exactMatch).count(), originalCount[0] + 1); QCOMPARE(m->contactIds(exactMismatch).count(), originalCount[1]); QCOMPARE(m->contactIds(insensitiveMatch).count(), originalCount[2] + 1); QCOMPARE(m->contactIds(insensitiveMismatch).count(), originalCount[3] + 1); QCOMPARE(m->contactIds(sensitiveMatch).count(), originalCount[4] + 1); QCOMPARE(m->contactIds(sensitiveMismatch).count(), originalCount[5]); } QTEST_GUILESS_MAIN(tst_QContactManager) #include "tst_qcontactmanager.moc" qtcontacts-sqlite-0.3.19/tests/auto/qcontactmanagerfiltering/000077500000000000000000000000001436373107600244655ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/qcontactmanagerfiltering/qcontactmanagerfiltering.pro000066400000000000000000000005521436373107600322640ustar00rootroot00000000000000TARGET = tst_qcontactmanagerfiltering include(../../common.pri) INCLUDEPATH += \ ../../../src/engine/ HEADERS += \ ../../../src/engine/contactid_p.h \ ../../../src/extensions/contactmanagerengine.h \ ../../util.h \ ../../qcontactmanagerdataholder.h SOURCES += \ ../../../src/engine/contactid.cpp \ tst_qcontactmanagerfiltering.cpp qtcontacts-sqlite-0.3.19/tests/auto/qcontactmanagerfiltering/tst_qcontactmanagerfiltering.cpp000066400000000000000000005732101436373107600331460ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Mobility Components. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #define QT_STATICPLUGIN #include "../../util.h" #include "../../qcontactmanagerdataholder.h" #include "../../../src/extensions/qcontactdeactivated.h" #include "../../../src/extensions/qcontactstatusflags.h" #include //TESTED_COMPONENT=src/contacts //TESTED_CLASS= //TESTED_FILES= // Q_ASSERT replacement, since we often run in release builds #define Q_FATAL_VERIFY(statement) \ do { \ if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \ qFatal("severe failure encountered, test cannot continue"); \ } while (0) #define Q_ASSERT_VERIFY(statement) Q_FATAL_VERIFY(statement) /* * This test is mostly just for testing sorting and filtering - * having it in tst_QContactManager makes maintenance more * difficult! * * Note: this test maintains the semantics of qtpim filtering, and * thus works correctly only in nonprivileged mode (in privileged * mode aggregation is used, which complicates filtering results). * The test enforces access to the nonprivileged database. * * When changing the results of filtering, ensure that the privileged * semantics are tested by the aggregation autotest. */ Q_DECLARE_METATYPE(QVariant) Q_DECLARE_METATYPE(QContactManager*) Q_DECLARE_METATYPE(QContactDetail::DetailType) static int detailField(int field) { return field; } static bool validDetailField(int field) { return (field != -1); } static bool validDetailInfo(const QPair &info) { return validDetailType(info.first) && validDetailField(info.second); } /* * Global variables: * These are the definition and field names used by the actions for their matching. */ QMap > defAndFieldNamesForTypeForActions; /* * We use this code to compare the output and expected lists of filtering * where no sort order is implied. * TODO: use this instead of QCOMPARE in the various filtering tests! */ #define QCOMPARE_UNSORTED(output, expected) if (output.size() != expected.size()) { \ QCOMPARE(output, expected); \ } else { \ for (int i = 0; i < output.size(); i++) { \ if (!expected.contains(output.at(i))) { \ QCOMPARE(output, expected); \ } \ } \ } class tst_QContactManagerFiltering : public QObject { Q_OBJECT public: tst_QContactManagerFiltering(); virtual ~tst_QContactManagerFiltering(); private: void dumpContactDifferences(const QContact& a, const QContact& b); void dumpContact(const QContact &c); void dumpContacts(); bool isSuperset(const QContact& ca, const QContact& cb); #ifdef DETAIL_DEFINITION_SUPPORTED QPair definitionAndField(QContactManager *cm, QVariant::Type type, bool *nativelyFilterable); #endif QList prepareModel(QContactManager* cm); // add the standard contacts QString convertIds(QList allIds, QList ids, QChar minimumContact = 'a', QChar maximumContact = 'z'); // convert back to "abcd" QContact createContact(QContactManager* cm, QContactType::TypeValues type, QString name); typedef QContactDetail::DetailType TypeIdentifier; typedef int FieldIdentifier; typedef QPair FieldSelector; QMap > defAndFieldNamesForTypePerManager; QMultiMap contactsAddedToManagers; QMultiMap transientContacts; QMultiMap detailDefinitionsAddedToManagers; QList managers; QScopedPointer managerDataHolder; QTestData& newMRow(const char *tag, QContactManager *cm); private slots: void initTestCase(); void cleanupTestCase(); void cleanup(); void rangeFiltering(); // XXX should take all managers void rangeFiltering_data(); void detailStringFiltering(); // XXX should take all managers void detailStringFiltering_data(); void detailPhoneNumberFiltering(); void detailPhoneNumberFiltering_data(); void statusFlagsFiltering(); void statusFlagsFiltering_data(); void deactivation(); void deactivation_data(); void detailVariantFiltering(); void detailVariantFiltering_data(); void intersectionFiltering(); void intersectionFiltering_data(); void unionFiltering(); void unionFiltering_data(); void relationshipFiltering(); void relationshipFiltering_data(); #if 0 // These tests should be supported... void changelogFiltering(); void changelogFiltering_data(); #endif void idListFiltering(); void idListFiltering_data(); void convenienceFiltering(); void convenienceFiltering_data(); void sorting(); // XXX should take all managers void sorting_data(); void multiSorting(); void multiSorting_data(); void invalidFiltering_data(); void invalidFiltering(); void allFiltering_data(); void allFiltering(); void fetchHint_data(); void fetchHint(); }; tst_QContactManagerFiltering::tst_QContactManagerFiltering() { // In order to make our tests reliable, set the C locale QLocale::setDefault(QLocale::c()); } tst_QContactManagerFiltering::~tst_QContactManagerFiltering() { } void tst_QContactManagerFiltering::initTestCase() { managerDataHolder.reset(new QContactManagerDataHolder(true)); // firstly, build a list of the managers we wish to test. QStringList managerNames; managerNames << QLatin1String("org.nemomobile.contacts.sqlite"); // only test nemo sqlite backend foreach (const QString &mgr, managerNames) { QMap params; params.insert(QStringLiteral("autoTest"), QStringLiteral("true")); params.insert(QStringLiteral("nonprivileged"), QStringLiteral("true")); QString mgrUri = QContactManager::buildUri(mgr, params); QContactManager* cm = QContactManager::fromUri(mgrUri); cm->setObjectName(mgr); managers.append(cm); if (mgr == "memory") { params.insert("id", "tst_QContactManager"); mgrUri = QContactManager::buildUri(mgr, params); cm = QContactManager::fromUri(mgrUri); cm->setObjectName("memory[params]"); managers.append(cm); } } // for each manager that we wish to test, prepare the model. foreach (QContactManager* cm, managers) { QList addedContacts = prepareModel(cm); if (addedContacts != contactsAddedToManagers.values(cm)) { qDebug() << "prepareModel returned:" << addedContacts; qDebug() << "contactsAdded are: " << contactsAddedToManagers.values(cm); qFatal("returned list different from saved contacts list!"); } } } void tst_QContactManagerFiltering::cleanupTestCase() { // first, remove any contacts that we've added to any managers. foreach (QContactManager* manager, managers) { QList contactIds = contactsAddedToManagers.values(manager); manager->removeContacts(contactIds, 0); if (manager->managerUri().contains(QLatin1String("org.nemomobile.contacts.sqlite"))) { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*manager); QContactManager::Error err = QContactManager::NoError; cme->clearChangeFlags(QContactCollectionId(manager->managerUri(), QByteArrayLiteral("col-2")), &err); // local addressbook } } contactsAddedToManagers.clear(); #ifdef MUTABLE_SCHEMA_SUPPORTED // then, remove any detail definitions that we've added. foreach (QContactManager* manager, managers) { QStringList definitionNames = detailDefinitionsAddedToManagers.values(manager); foreach (const QString& definitionName, definitionNames) { manager->removeDetailDefinition(definitionName); } } detailDefinitionsAddedToManagers.clear(); #endif // finally, we can delete all of our manager instances qDeleteAll(managers); managers.clear(); defAndFieldNamesForTypePerManager.clear(); // And restore old contacts managerDataHolder.reset(0); } void tst_QContactManagerFiltering::cleanup() { foreach (QContactManager* manager, managers) { QList contactIds = transientContacts.values(manager); manager->removeContacts(contactIds, 0); } transientContacts.clear(); } QString tst_QContactManagerFiltering::convertIds(QList allIds, QList ids, QChar minimumContact, QChar maximumContact) { QString ret; /* Expected is of the form "abcd".. it's possible that there are some extra contacts */ for (int i = 0; i < ids.size(); i++) { if (allIds.indexOf(ids.at(i)) >= 0) { QChar curr = ('a' + allIds.indexOf(ids.at(i))); if (curr >= minimumContact && curr <= maximumContact) { ret += curr; } } } return ret; } QTestData& tst_QContactManagerFiltering::newMRow(const char *tag, QContactManager *cm) { // allocate a tag QString foo = QString("%1[%2]").arg(tag).arg(cm->objectName()); return QTest::newRow(foo.toLatin1().constData()); } void tst_QContactManagerFiltering::detailStringFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("defname"); QTest::addColumn("fieldname"); QTest::addColumn("value"); QTest::addColumn("matchflags"); QTest::addColumn("expected"); QVariant ev; // empty variant QString es; // empty string TypeIdentifier name = detailType(); FieldIdentifier firstname = QContactName::FieldFirstName; FieldIdentifier lastname = QContactName::FieldLastName; FieldIdentifier middlename = QContactName::FieldMiddleName; FieldIdentifier prefixname = QContactName::FieldPrefix; FieldIdentifier suffixname = QContactName::FieldSuffix; TypeIdentifier nickname = detailType(); FieldIdentifier nicknameField = QContactNickname::FieldNickname; TypeIdentifier emailaddr = detailType(); FieldIdentifier emailfield = QContactEmailAddress::FieldEmailAddress; TypeIdentifier phonenumber = detailType(); FieldIdentifier number = QContactPhoneNumber::FieldNumber; TypeIdentifier noType(QContactDetail::TypeUndefined); FieldIdentifier noField(-1); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); newMRow("Name == Aaro", manager) << manager << name << firstname << QVariant("Aaro") << 0 << es; newMRow("Name == Aaron", manager) << manager << name << firstname << QVariant("Aaron") << 0 << "a"; newMRow("Name == aaron", manager) << manager << name << firstname << QVariant("aaron") << 0 << "a"; newMRow("Name == Aaron, case sensitive", manager) << manager << name << firstname << QVariant("Aaron") << (int)(QContactFilter::MatchCaseSensitive) << "a"; newMRow("Name == aaron, case sensitive", manager) << manager << name << firstname << QVariant("aaron") << (int)(QContactFilter::MatchCaseSensitive) << es; newMRow("Name is empty", manager) << manager << name << firstname << QVariant("") << 0 << es; newMRow("Last name is empty", manager) << manager << name << lastname << QVariant("") << 0 << "hijk"; newMRow("Name == A, begins", manager) << manager << name << firstname << QVariant("A") << (int)(QContactFilter::MatchStartsWith) << "a"; newMRow("Name == Aaron, begins", manager) << manager << name << firstname << QVariant("Aaron") << (int)(QContactFilter::MatchStartsWith) << "a"; newMRow("Name == aaron, begins", manager) << manager << name << firstname << QVariant("aaron") << (int)(QContactFilter::MatchStartsWith) << "a"; newMRow("Name == Aaron, begins, case sensitive", manager) << manager << name << firstname << QVariant("Aaron") << (int)(QContactFilter::MatchStartsWith | QContactFilter::MatchCaseSensitive) << "a"; newMRow("Name == aaron, begins, case sensitive", manager) << manager << name << firstname << QVariant("aaron") << (int)(QContactFilter::MatchStartsWith | QContactFilter::MatchCaseSensitive) << es; newMRow("Name == Aaron1, begins", manager) << manager << name << firstname << QVariant("Aaron1") << (int)(QContactFilter::MatchStartsWith) << es; newMRow("Last name == A, begins", manager) << manager << name << lastname << QVariant("A") << (int)(QContactFilter::MatchStartsWith) << "abc"; newMRow("Last name == Aaronson, begins", manager) << manager << name << lastname << QVariant("Aaronson") << (int)(QContactFilter::MatchStartsWith) << "a"; newMRow("Last Name == Aaronson1, begins", manager) << manager << name << lastname << QVariant("Aaronson1") << (int)(QContactFilter::MatchStartsWith) << es; newMRow("Name == Aar, begins", manager) << manager << name << firstname << QVariant("Aar") << (int)(QContactFilter::MatchStartsWith) << "a"; newMRow("Name == aar, begins", manager) << manager << name << firstname << QVariant("aar") << (int)(QContactFilter::MatchStartsWith) << "a"; newMRow("Name == Aar, begins, case sensitive", manager) << manager << name << firstname << QVariant("Aar") << (int)(QContactFilter::MatchStartsWith | QContactFilter::MatchCaseSensitive) << "a"; newMRow("Name == aar, begins, case sensitive", manager) << manager << name << firstname << QVariant("aar") << (int)(QContactFilter::MatchStartsWith | QContactFilter::MatchCaseSensitive) << es; newMRow("Name == aro, contains", manager) << manager << name << firstname << QVariant("aro") << (int)(QContactFilter::MatchContains) << "a"; newMRow("Name == ARO, contains", manager) << manager << name << firstname << QVariant("ARO") << (int)(QContactFilter::MatchContains) << "a"; newMRow("Name == aro, contains, case sensitive", manager) << manager << name << firstname << QVariant("aro") << (int)(QContactFilter::MatchContains | QContactFilter::MatchCaseSensitive) << "a"; newMRow("Name == ARO, contains, case sensitive", manager) << manager << name << firstname << QVariant("ARO") << (int)(QContactFilter::MatchContains | QContactFilter::MatchCaseSensitive) << es; newMRow("Name == ron, ends", manager) << manager << name << firstname << QVariant("ron") << (int)(QContactFilter::MatchEndsWith) << "a"; newMRow("Name == ARON, ends", manager) << manager << name << firstname << QVariant("ARON") << (int)(QContactFilter::MatchEndsWith) << "a"; newMRow("Name == aron, ends, case sensitive", manager) << manager << name << firstname << QVariant("aron") << (int)(QContactFilter::MatchEndsWith | QContactFilter::MatchCaseSensitive) << "a"; newMRow("Name == ARON, ends, case sensitive", manager) << manager << name << firstname << QVariant("ARON") << (int)(QContactFilter::MatchEndsWith | QContactFilter::MatchCaseSensitive) << es; newMRow("Last name == n, ends", manager) << manager << name << lastname << QVariant("n") << (int)(QContactFilter::MatchEndsWith) << "abc"; newMRow("Name == Aaron, fixed", manager) << manager << name << firstname << QVariant("Aaron") << (int)(QContactFilter::MatchFixedString) << "a"; newMRow("Name == aaron, fixed", manager) << manager << name << firstname << QVariant("aaron") << (int)(QContactFilter::MatchFixedString) << "a"; newMRow("Name == Aaron, fixed, case sensitive", manager) << manager << name << firstname << QVariant("Aaron") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchCaseSensitive) << "a"; newMRow("Name == aaron, fixed, case sensitive", manager) << manager << name << firstname << QVariant("aaron") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchCaseSensitive) << es; // middle name #ifdef DETAIL_DEFINITION_SUPPORTED if (manager->detailDefinitions().value(QContactName::DefinitionName).fields().contains(QContactName::FieldMiddleName)) #endif newMRow("MName == Arne", manager) << manager << name << middlename << QVariant("Arne") << (int)(QContactFilter::MatchContains) << "a"; // prefix #ifdef DETAIL_DEFINITION_SUPPORTED if (manager->detailDefinitions().value(QContactName::DefinitionName).fields().contains(QContactName::FieldPrefix)) #endif newMRow("Prefix == Sir", manager) << manager << name << prefixname << QVariant("Sir") << (int)(QContactFilter::MatchContains) << "a"; // suffix #ifdef DETAIL_DEFINITION_SUPPORTED if (manager->detailDefinitions().value(QContactName::DefinitionName).fields().contains(QContactName::FieldSuffix)) #endif newMRow("Suffix == Dr.", manager) << manager << name << suffixname << QVariant("Dr.") << (int)(QContactFilter::MatchContains) << "a"; // nickname #ifdef DETAIL_DEFINITION_SUPPORTED if (manager->detailDefinitions().contains(QContactNickname::DefinitionName)) { #endif newMRow("Nickname detail exists", manager) << manager << nickname << noField << QVariant() << 0 << "ab"; newMRow("Nickname == Aaron, contains", manager) << manager << nickname << nicknameField << QVariant("Aaron") << (int)(QContactFilter::MatchContains) << "a"; #ifdef DETAIL_DEFINITION_SUPPORTED } #endif // email #ifdef DETAIL_DEFINITION_SUPPORTED if (manager->detailDefinitions().contains(QContactEmailAddress::DefinitionName)) { #endif newMRow("Email == Aaron@Aaronson.com", manager) << manager << emailaddr << emailfield << QVariant("Aaron@Aaronson.com") << 0 << "a"; newMRow("Email == Aaron@Aaronsen.com", manager) << manager << emailaddr << emailfield << QVariant("Aaron@Aaronsen.com") << 0 << es; #ifdef DETAIL_DEFINITION_SUPPORTED } #endif // phone number #ifdef DETAIL_DEFINITION_SUPPORTED if (manager->detailDefinitions().contains(QContactPhoneNumber::DefinitionName)) { #endif newMRow("Phone number detail exists", manager) << manager << phonenumber << noField << QVariant("") << 0 << "ab"; newMRow("Phone number = 5551212", manager) << manager << phonenumber << number << QVariant("5551212") << (int) QContactFilter::MatchExactly << "a"; newMRow("Phone number = 555, contains", manager) << manager << phonenumber << number << QVariant("555") << (int) QContactFilter::MatchContains << "ab"; newMRow("Phone number = 555, starts with", manager) << manager << phonenumber << number << QVariant("555") << (int) QContactFilter::MatchStartsWith << "a"; newMRow("Phone number = 1212, ends with", manager) << manager << phonenumber << number << QVariant("1212") << (int) QContactFilter::MatchEndsWith << "a"; newMRow("Phone number = 555-1212, match phone number", manager) << manager << phonenumber << number << QVariant("555-1212") << (int) QContactFilter::MatchPhoneNumber << "a"; // hyphens will be ignored by the match algorithm #ifdef DETAIL_DEFINITION_SUPPORTED } #endif /* Converting other types to strings */ FieldSelector defAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("Integer"); if (validDetailInfo(defAndFieldNames)) { QTest::newRow("integer == 20") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("20") << 0 << es; QTest::newRow("integer == 20, as string") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("20") << (int)(QContactFilter::MatchFixedString) << "b"; QTest::newRow("integer == 20, begins with, string") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("20") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchStartsWith) << "b"; QTest::newRow("integer == 2, begins with, string") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("2") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchStartsWith) << "b"; QTest::newRow("integer == 20, ends with, string") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("20") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchEndsWith) << "bc"; QTest::newRow("integer == 0, ends with, string") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("0") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchEndsWith) << "bc"; QTest::newRow("integer == 20, contains, string") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("20") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchContains) << "bc"; QTest::newRow("integer == 0, contains, string") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant("0") << (int)(QContactFilter::MatchFixedString | QContactFilter::MatchContains) << "bc"; } /* Detail filter semantics: empty definition or field */ newMRow("Empty Definition Name", manager) << manager << noType << lastname << QVariant("A") << (int)(QContactFilter::MatchStartsWith) << es; // empty definition name means filter matches nothing newMRow("Empty Def And Field Name", manager) << manager << noType << noField << QVariant("A") << (int)(QContactFilter::MatchStartsWith) << es; // as above newMRow("Empty Field Name", manager) << manager << name << noField << QVariant("A") << (int)(QContactFilter::MatchStartsWith) << "abcdefghijk"; // empty field name matches any with a name detail } } void tst_QContactManagerFiltering::detailStringFiltering() { QFETCH(QContactManager*, cm); QFETCH(TypeIdentifier, defname); QFETCH(FieldIdentifier, fieldname); QFETCH(QVariant, value); QFETCH(QString, expected); QFETCH(int, matchflags); QList contacts = contactsAddedToManagers.values(cm); QList ids; QContactDetailFilter df; setFilterDetail(df, defname, fieldname); df.setValue(value); if ((matchflags & QContactFilter::MatchCaseSensitive) == 0) { // Case insensitivity only applies to MatchFixedString matchflags |= QContactFilter::MatchFixedString; } df.setMatchFlags(QContactFilter::MatchFlags(matchflags)); if (cm->managerName() == "memory") { /* At this point, since we're using memory, assume the filter isn't really supported */ QVERIFY(cm->isFilterSupported(df) == false); } ids = cm->contactIds(df); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QEXPECT_FAIL("integer == 20", "Not sure if this should pass or fail", Continue); QCOMPARE_UNSORTED(output, expected); } void tst_QContactManagerFiltering::detailPhoneNumberFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("defname"); QTest::addColumn("fieldname"); QTest::addColumn("value"); QTest::addColumn("matchflags"); QTest::addColumn("expected"); // ITU-T standard keypad collation: // 2 = abc, 3 = def, 4 = ghi, 5 = jkl, 6 = mno, 7 = pqrs, 8 = tuv, 9 = wxyz, 0 = space TypeIdentifier phoneDef = detailType(); FieldIdentifier phoneField = QContactPhoneNumber::FieldNumber; const int mpn = (int)QContactFilter::MatchPhoneNumber; const int msw = (int)QContactFilter::MatchStartsWith; // purely to test phone number filtering. for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); // now do phone number matching - first, aaron's phone number QTest::newRow("a phone hyphen") << manager << phoneDef << phoneField << QVariant(QString("555-1212")) << mpn << "a"; // An initial-plus variant is not the same number, so this test is not valid: //QTest::newRow("a phone plus") << manager << phoneDef << phoneField << QVariant(QString("+5551212")) << mpn << "a"; QTest::newRow("a phone brackets") << manager << phoneDef << phoneField << QVariant(QString("(555)1212")) << mpn << "a"; QTest::newRow("a phone nospaces") << manager << phoneDef << phoneField << QVariant(QString("5551212")) << mpn << "a"; QTest::newRow("a phone single space") << manager << phoneDef << phoneField << QVariant(QString("555 1212")) << mpn << "a"; QTest::newRow("a phone random spaces") << manager << phoneDef << phoneField << QVariant(QString("55 512 12")) << mpn << "a"; QTest::newRow("a phone every space") << manager << phoneDef << phoneField << QVariant(QString("5 5 5 1 2 1 2")) << mpn << "a"; QTest::newRow("a phone hyphen") << manager << phoneDef << phoneField << QVariant(QString("555-1212")) << mpn << "a"; QTest::newRow("a phone brackets") << manager << phoneDef << phoneField << QVariant(QString("5(55)1212")) << mpn << "a"; QTest::newRow("a phone brackets hyphen") << manager << phoneDef << phoneField << QVariant(QString("5(55)1-212")) << mpn << "a"; QTest::newRow("a phone brackets hyphen spaces") << manager << phoneDef << phoneField << QVariant(QString("5 (55) 1-212")) << mpn << "a"; // XXX TODO: should we test for character to number conversions (eg, dial 1800-PESTCONTROL) etc ? //QTest::newRow("a phone characters") << manager << phoneDef << phoneField << QVariant(QString("jjj1a1a")) << mpn << "a"; // 5551212 //QTest::newRow("a phone characters") << manager << phoneDef << phoneField << QVariant(QString("jkl1b1a")) << mpn << "a"; // 5551212 // then matches bob's phone number (which has the initial-plus) QTest::newRow("b phone hyphen") << manager << phoneDef << phoneField << QVariant(QString("5555-3456")) << mpn << "b"; QTest::newRow("b phone plus") << manager << phoneDef << phoneField << QVariant(QString("+55553456")) << mpn << "b"; QTest::newRow("b phone brackets") << manager << phoneDef << phoneField << QVariant(QString("(5555)3456")) << mpn << "b"; QTest::newRow("b phone nospaces") << manager << phoneDef << phoneField << QVariant(QString("55553456")) << mpn << "b"; QTest::newRow("b phone single space") << manager << phoneDef << phoneField << QVariant(QString("5555 3456")) << mpn << "b"; QTest::newRow("b phone random spaces") << manager << phoneDef << phoneField << QVariant(QString("555 534 56")) << mpn << "b"; QTest::newRow("b phone every space") << manager << phoneDef << phoneField << QVariant(QString("5 5 5 5 3 4 5 6")) << mpn << "b"; QTest::newRow("b phone plus hyphen") << manager << phoneDef << phoneField << QVariant(QString("+5555-3456")) << mpn << "b"; QTest::newRow("b phone plus brackets") << manager << phoneDef << phoneField << QVariant(QString("+5(555)3456")) << mpn << "b"; QTest::newRow("b phone plus brackets hyphen") << manager << phoneDef << phoneField << QVariant(QString("+5(555)3-456")) << mpn << "b"; QTest::newRow("b phone plus brackets hyphen spaces") << manager << phoneDef << phoneField << QVariant(QString("+55 (55) 3-456")) << mpn << "b"; // then match no phone numbers (negative testing) -- 555-9999 matches nobody in our test set. QTest::newRow("no phone hyphen") << manager << phoneDef << phoneField << QVariant(QString("555-9999")) << mpn << ""; QTest::newRow("no phone plus") << manager << phoneDef << phoneField << QVariant(QString("+5559999")) << mpn << ""; QTest::newRow("no phone brackets") << manager << phoneDef << phoneField << QVariant(QString("(555)9999")) << mpn << ""; QTest::newRow("no phone nospaces") << manager << phoneDef << phoneField << QVariant(QString("5559999")) << mpn << ""; QTest::newRow("no phone single space") << manager << phoneDef << phoneField << QVariant(QString("555 9999")) << mpn << ""; QTest::newRow("no phone random spaces") << manager << phoneDef << phoneField << QVariant(QString("55 599 99")) << mpn << ""; QTest::newRow("no phone every space") << manager << phoneDef << phoneField << QVariant(QString("5 5 5 9 9 9 9")) << mpn << ""; QTest::newRow("no phone plus hyphen") << manager << phoneDef << phoneField << QVariant(QString("+555-9999")) << mpn << ""; QTest::newRow("no phone plus brackets") << manager << phoneDef << phoneField << QVariant(QString("+5(55)9999")) << mpn << ""; QTest::newRow("no phone plus brackets hyphen") << manager << phoneDef << phoneField << QVariant(QString("+5(55)9-999")) << mpn << ""; QTest::newRow("no phone plus brackets hyphen spaces") << manager << phoneDef << phoneField << QVariant(QString("+5 (55) 9-999")) << mpn << ""; // then match both aaron and bob via starts with QTest::newRow("ab phone starts nospace") << manager << phoneDef << phoneField << QVariant(QString("555")) << (mpn | msw) << "ab"; QTest::newRow("ab phone starts hyphen") << manager << phoneDef << phoneField << QVariant(QString("555-")) << (mpn | msw) << "ab"; QTest::newRow("ab phone starts space") << manager << phoneDef << phoneField << QVariant(QString("55 5")) << (mpn | msw) << "ab"; QTest::newRow("ab phone starts brackets") << manager << phoneDef << phoneField << QVariant(QString("(555)")) << (mpn | msw) << "ab"; QTest::newRow("ab phone starts plus") << manager << phoneDef << phoneField << QVariant(QString("+555")) << (mpn | msw) << "ab"; QTest::newRow("ab phone starts hyphen space") << manager << phoneDef << phoneField << QVariant(QString("5 55-")) << (mpn | msw) << "ab"; QTest::newRow("ab phone starts hyphen space brackets") << manager << phoneDef << phoneField << QVariant(QString("5 (55)-")) << (mpn | msw) << "ab"; QTest::newRow("ab phone starts hyphen space brackets plus") << manager << phoneDef << phoneField << QVariant(QString("+5 (55)-")) << (mpn | msw) << "ab"; } } void tst_QContactManagerFiltering::detailPhoneNumberFiltering() { QFETCH(QContactManager*, cm); QFETCH(TypeIdentifier, defname); QFETCH(FieldIdentifier, fieldname); QFETCH(QVariant, value); QFETCH(int, matchflags); QFETCH(QString, expected); // note: this test is exactly the same as string filtering, but uses different fields and specific matchflags. QList contacts = contactsAddedToManagers.values(cm); QList ids; QContactDetailFilter df; setFilterDetail(df, defname, fieldname); df.setValue(value); df.setMatchFlags(QContactFilter::MatchFlags(matchflags)); if (cm->managerName() == "memory") { /* At this point, since we're using memory, assume the filter isn't really supported */ QVERIFY(cm->isFilterSupported(df) == false); } ids = cm->contactIds(df); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts //QSKIP("TODO: fix default implementation of phone number matching!"); QCOMPARE_UNSORTED(output, expected); } void tst_QContactManagerFiltering::statusFlagsFiltering_data() { QTest::addColumn("cm"); for (int i = 0; i < managers.size(); i++) { QContactManager *cm = managers.at(i); QTest::newRow(qPrintable(cm->objectName())) << cm; } } void tst_QContactManagerFiltering::statusFlagsFiltering() { QFETCH(QContactManager*, cm); // Test for correct matching of all contact properties QSet phoneNumberIds = cm->contactIds(QContactStatusFlags::matchFlag(QContactStatusFlags::HasPhoneNumber, QContactFilter::MatchContains)).toSet(); QSet emailAddressIds = cm->contactIds(QContactStatusFlags::matchFlag(QContactStatusFlags::HasEmailAddress, QContactFilter::MatchContains)).toSet(); QSet onlineAccountIds = cm->contactIds(QContactStatusFlags::matchFlag(QContactStatusFlags::HasOnlineAccount, QContactFilter::MatchContains)).toSet(); QSet onlineIds = cm->contactIds(QContactStatusFlags::matchFlag(QContactStatusFlags::IsOnline, QContactFilter::MatchContains)).toSet(); QSet deactivatedIds = cm->contactIds(QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeactivated, QContactFilter::MatchContains)).toSet(); // Also test for combination tests QContactFilter filter(QContactStatusFlags::matchFlags(QContactStatusFlags::HasPhoneNumber | QContactStatusFlags::HasEmailAddress, QContactFilter::MatchContains)); QSet phoneAndEmailIds = cm->contactIds(filter).toSet(); // Doing MatchExactly on any status flag is likely to return no results, as the IsAdded or IsModified flag will generally // be set whenever the contact is saved, in addition to any HasPhoneNumber/HasEmailAddress etc flag. //filter = QContactStatusFlags::matchFlags(QContactStatusFlags::HasPhoneNumber, QContactFilter::MatchExactly); //QSet phoneOnlyIds = cm->contactIds(filter).toSet(); QList contacts = contactsAddedToManagers.values(cm); foreach (const QContact &contact, cm->contacts(contacts)) { QContactId contactId(contact.id()); const bool hasPhoneNumber = !contact.details().isEmpty(); const bool hasEmailAddress = !contact.details().isEmpty(); const bool hasOnlineAccount = !contact.details().isEmpty(); QContactGlobalPresence presence = contact.detail(); QContactPresence::PresenceState presenceState = presence.presenceState(); const bool isOnline = (presenceState > QContactPresence::PresenceUnknown) && (presenceState < QContactPresence::PresenceOffline); const bool isDeactivated = (contact.details().count() > 0); QCOMPARE(phoneNumberIds.contains(contactId), hasPhoneNumber); QCOMPARE(emailAddressIds.contains(contactId), hasEmailAddress); QCOMPARE(onlineAccountIds.contains(contactId), hasOnlineAccount); QCOMPARE(onlineIds.contains(contactId), isOnline); QCOMPARE(deactivatedIds.contains(contactId), isDeactivated); QCOMPARE(phoneAndEmailIds.contains(contactId), (hasPhoneNumber && hasEmailAddress)); //QCOMPARE(phoneOnlyIds.contains(contactId), (hasPhoneNumber && !hasEmailAddress && !hasOnlineAccount && !isOnline)); } } void tst_QContactManagerFiltering::deactivation_data() { QTest::addColumn("cm"); for (int i = 0; i < managers.size(); i++) { QContactManager *cm = managers.at(i); QTest::newRow(qPrintable(cm->objectName())) << cm; } } void tst_QContactManagerFiltering::deactivation() { QFETCH(QContactManager*, cm); QContact alice; QContact bob; QContactName an, bn; an.setFirstName("Alice"); an.setMiddleName("Through The"); an.setLastName("Looking-Glass"); alice.saveDetail(&an); bn.setFirstName("Bob"); bn.setMiddleName("The"); bn.setLastName("Demolisher"); bob.saveDetail(&bn); // Alice must have a sync-target to be deactivated QContactSyncTarget st; st.setSyncTarget("test"); alice.saveDetail(&st); QVERIFY(cm->saveContact(&alice)); QVERIFY(cm->saveContact(&bob)); alice = cm->contact(alice.id()); bob = cm->contact(bob.id()); transientContacts.insert(cm, alice.id()); transientContacts.insert(cm, bob.id()); QList ids(cm->contactIds()); QVERIFY(ids.contains(alice.id())); QVERIFY(ids.contains(bob.id())); // Verify that both contacts are non-deactivated QCOMPARE(alice.details().count(), 0); QCOMPARE(alice.detail().testFlag(QContactStatusFlags::IsDeactivated), false); QCOMPARE(bob.details().count(), 0); QCOMPARE(bob.detail().testFlag(QContactStatusFlags::IsDeactivated), false); // Deactivate alice QContactDeactivated deactivated; alice.saveDetail(&deactivated); QVERIFY(cm->saveContact(&alice)); alice = cm->contact(alice.id()); // Verify that alice is now deactivated QCOMPARE(alice.details().count(), 1); QCOMPARE(alice.detail().testFlag(QContactStatusFlags::IsDeactivated), true); // Alice is no longer returned in the contact set ids = cm->contactIds(); QVERIFY(ids.contains(alice.id()) == false); QVERIFY(ids.contains(bob.id())); // Alice is returned by a deactivated query ids = cm->contactIds(QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeactivated, QContactFilter::MatchContains)); QVERIFY(ids.contains(alice.id())); QVERIFY(ids.contains(bob.id()) == false); // Re-activate alice deactivated = alice.detail(); alice.removeDetail(&deactivated); QVERIFY(cm->saveContact(&alice)); alice = cm->contact(alice.id()); // Verify that alice is no longer deactivated QCOMPARE(alice.details().count(), 0); QCOMPARE(alice.detail().testFlag(QContactStatusFlags::IsDeactivated), false); // Alice is now returned in the contact set ids = cm->contactIds(); QVERIFY(ids.contains(alice.id())); QVERIFY(ids.contains(bob.id())); // Alice is not retuned by a deactivated query ids = cm->contactIds(QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeactivated, QContactFilter::MatchContains)); QVERIFY(ids.contains(alice.id()) == false); QVERIFY(ids.contains(bob.id()) == false); } void tst_QContactManagerFiltering::detailVariantFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("defname"); QTest::addColumn("fieldname"); QTest::addColumn("setValue"); QTest::addColumn("value"); QTest::addColumn("expected"); QVariant ev; // empty variant QString es; // empty string QContactDetail::DetailType noType(QContactDetail::TypeUndefined); int noField(-1); int invalidField(0x666); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); /* Nothings */ newMRow("no name", manager) << manager << noType << noField << false << ev << es; newMRow("no def name", manager) << manager << noType << detailField(QContactName::FieldFirstName) << false << ev << es; /* Strings (name) */ newMRow("first name presence", manager) << manager << detailType() << detailField(QContactName::FieldFirstName) << false << ev << "abcdefghijk"; newMRow("first name == Aaron", manager) << manager << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant("Aaron") << "a"; /* * Doubles * B has double(4.0) * C has double(4.0) * D has double(-128.0) */ FieldSelector defAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("Double"); if (validDetailInfo(defAndFieldNames)) { newMRow("double presence", manager) << manager << defAndFieldNames.first << noField << false << ev << "bcd"; QTest::newRow("double presence (inc field)") << manager << defAndFieldNames.first << defAndFieldNames.second << false << ev << "bcd"; QTest::newRow("double presence (wrong field)") << manager << defAndFieldNames.first << invalidField << false << ev << es; QTest::newRow("double value (no match)") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(3.5) << es; QTest::newRow("double value (wrong type)") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(QDateTime()) << es; QTest::newRow("double value (wrong field, no match)") << manager << defAndFieldNames.first << invalidField << true << QVariant(3.5) << es; newMRow("double value", manager) << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(4.0) << "bc"; QTest::newRow("double value (wrong field)") << manager << defAndFieldNames.first << invalidField << true << QVariant(4.0) << es; QTest::newRow("double value 2") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(-128.0) << "d"; QTest::newRow("double value 2 (wrong field)") << manager << defAndFieldNames.first << invalidField << true << QVariant(-128.0) << es; } /* * Integers * A has 3 * B has 20 * C has -20 */ defAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("Integer"); if (validDetailInfo(defAndFieldNames)) { newMRow("integer presence", manager) << manager << defAndFieldNames.first << noField << false << ev << "abc"; QTest::newRow("integer presence (inc field)") << manager << defAndFieldNames.first << defAndFieldNames.second << false << ev << "abc"; QTest::newRow("integer presence (wrong field)") << manager << defAndFieldNames.first << invalidField << false << ev << es; QTest::newRow("integer value (no match)") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(50) << es; QTest::newRow("integer value (wrong type)") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(3.5) << es; QTest::newRow("integer value (wrong field, no match)") << manager << defAndFieldNames.first << invalidField << true << QVariant(50) << es; newMRow("integer value", manager) << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(3) << "a"; QTest::newRow("integer value (wrong field)") << manager << defAndFieldNames.first << invalidField << true << QVariant(3) << es; QTest::newRow("integer value 2") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(-20) << "c"; QTest::newRow("integer value 2 (wrong field)") << manager << defAndFieldNames.first << invalidField << true << QVariant(-20) << es; } /* * Date times * A has QDateTime(QDate(2009, 06, 29), QTime(16, 52, 23, 0)) * C has QDateTime(QDate(2009, 06, 29), QTime(16, 54, 17, 0)) * NOTE: value presence filtering can fail due to automatic timestamp insertion by qtcontacts-sqlite backend */ const QDateTime adt(QDate(2009, 06, 29), QTime(16, 52, 23, 0)); const QDateTime cdt(QDate(2009, 06, 29), QTime(16, 54, 17, 0)); defAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("DateTime"); if (validDetailInfo(defAndFieldNames)) { newMRow("datetime presence", manager) << manager << defAndFieldNames.first << noField << false << ev << "abcdefghijk"; // all contacts have a Timestamp detail QTest::newRow("datetime presence (inc field)") << manager << defAndFieldNames.first << defAndFieldNames.second << false << ev << "abcdefghijk"; QTest::newRow("datetime presence (wrong field)") << manager << defAndFieldNames.first << invalidField << false << ev << es; QTest::newRow("datetime value (no match)") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(QDateTime(QDate(2100,5,13), QTime(5,5,5))) << es; QTest::newRow("datetime value (wrong type)") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(3.5) << es; QTest::newRow("datetime value (wrong field, no match)") << manager << defAndFieldNames.first << invalidField << true << QVariant(QDateTime(QDate(2100,5,13), QTime(5,5,5))) << es; newMRow("datetime value", manager) << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(adt) << "a"; QTest::newRow("datetime value (wrong field)") << manager << defAndFieldNames.first << invalidField << true << QVariant(adt) << es; QTest::newRow("datetime value 2") << manager << defAndFieldNames.first << defAndFieldNames.second << true << QVariant(cdt)<< "c"; QTest::newRow("datetime value 2 (wrong field)") << manager << defAndFieldNames.first << invalidField << true << QVariant(cdt) << es; } /* * Dates * A has QDate(1988, 1, 26) * B has QDate(2492, 5, 5) * D has QDate(2770, 10, 1) */ const QDate ad(1988, 1, 26); const QDate bd(2492, 5, 5); const QDate dd(2770, 10, 1); defAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("Date"); if (validDetailInfo(defAndFieldNames)) { newMRow("date presence", manager) << manager << defAndFieldNames.first << noField << false << ev << "abd"; QTest::newRow("date presence (inc field)") << manager << defAndFieldNames.first << defAndFieldNames.second << false << ev << "abd"; QTest::newRow("date presence (wrong field)") << manager << defAndFieldNames.first << invalidField << false << ev << es; QTest::newRow("date value (no match)") << manager << defAndFieldNames.first < contacts = contactsAddedToManagers.values(cm); QList ids; QContactDetailFilter df; setFilterDetail(df, defname, fieldname); if (setValue) df.setValue(value); if (cm->managerName() == "memory") { /* At this point, since we're using memory, assume the filter isn't really supported */ QVERIFY(cm->isFilterSupported(df) == false); } ids = cm->contactIds(df); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE_UNSORTED(output, expected); } void tst_QContactManagerFiltering::rangeFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("defname"); QTest::addColumn("fieldname"); QTest::addColumn("minrange"); QTest::addColumn("maxrange"); QTest::addColumn("setrfs"); QTest::addColumn("rangeflagsi"); QTest::addColumn("setmfs"); QTest::addColumn("matchflagsi"); QTest::addColumn("expected"); QVariant ev; // empty variant QString es; // empty string TypeIdentifier namedef = detailType(); FieldIdentifier firstname = QContactName::FieldFirstName; TypeIdentifier phonedef = detailType(); FieldIdentifier phonenum = QContactPhoneNumber::FieldNumber; TypeIdentifier noType(QContactDetail::TypeUndefined); FieldIdentifier noField(-1); TypeIdentifier invalidType = static_cast(0x666); FieldIdentifier invalidField = 0x666; int csflag = (int)QContactFilter::MatchCaseSensitive; for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); /* First, cover the "empty defname / fieldname / ranges" cases */ newMRow("invalid defname", manager) << manager << noType << firstname << QVariant("A") << QVariant("Bob") << false << 0 << true << 0 << es; newMRow("defn presence test", manager) << manager << namedef << noField << QVariant("A") << QVariant("Bob") << false << 0 << true << 0 << "abcdefghijk"; newMRow("field presence test", manager) << manager << phonedef << phonenum << QVariant() << QVariant() << false << 0 << true << 0 << "ab"; newMRow("good def, bad field", manager) << manager << namedef << invalidField << QVariant("A") << QVariant("Bob") << false << 0 << true << 0 << es; newMRow("bad def", manager) << manager << invalidType << invalidField << QVariant("A") << QVariant("Bob") << false << 0 << true << 0 << es; /* Presence for fields that aren't there */ newMRow("defn presence test negative", manager) << manager << invalidType << noField << ev << ev << false << 0 << false << 0 << es; newMRow("field presence test negative", manager) << manager << invalidType << invalidField << ev << ev << false << 0 << false << 0 << es; newMRow("defn yes, field no presence test negative", manager) << manager << namedef << invalidField << ev << ev << false << 0 << false << 0 << es; newMRow("no max, all results", manager) << manager << namedef << firstname << QVariant("a") << QVariant() << false << 0 << true << 0 << "abcdefghijk"; newMRow("no max, some results", manager) << manager << namedef << firstname << QVariant("bob") << QVariant() << false << 0 << true << 0 << "bcdefghijk"; newMRow("no max, no results", manager) << manager << namedef << firstname << QVariant("ZamBeZI") << QVariant() << false << 0 << true << 0 << es; newMRow("no min, all results", manager) << manager << namedef << firstname << QVariant() << QVariant("zambezi") << false << 0 << true << 0 << "abcdefghijk"; newMRow("no min, some results", manager) << manager << namedef << firstname << QVariant() << QVariant("bOb") << false << 0 << true << 0 << "a"; newMRow("no min, no results", manager) << manager << namedef << firstname << QVariant() << QVariant("aardvark") << false << 0 << true << 0 << es; /* now case sensitive */ newMRow("no max, cs, all results", manager) << manager << namedef << firstname << QVariant("A") << QVariant() << false << 0 << true << csflag << "abcdefghijk"; newMRow("no max, cs, some results", manager) << manager << namedef << firstname << QVariant("Bob") << QVariant() << false << 0 << true << csflag << "bcdefghijk"; newMRow("no max, cs, no results", manager) << manager << namedef << firstname << QVariant("Xambezi") << QVariant() << false << 0 << true << csflag << "hijk"; newMRow("no min, cs, most results", manager) << manager << namedef << firstname << QVariant() << QVariant("Xambezi") << false << 0 << true << csflag << "abcdefg"; newMRow("no min, cs, some results", manager) << manager << namedef << firstname << QVariant() << QVariant("Bob") << false << 0 << true << csflag << "a"; newMRow("no min, cs, no results", manager) << manager << namedef << firstname << QVariant() << QVariant("Aardvark") << false << 0 << true << csflag << es; newMRow("no max, cs, badcase, all results", manager) << manager << namedef << firstname << QVariant("A") << QVariant() << false << 0 << true << csflag << "abcdefghijk"; #if 0 qWarning() << "Test case \"no max, cs, badcase, some results\" will fail on some platforms because of QString::localeAwareCompare is not actually locale aware"; newMRow("no max, cs, badcase, some results", manager) << manager << namedef << firstname << QVariant("BOB") << QVariant() << false << 0 << true << csflag << "cdefghijk"; #endif newMRow("no max, cs, badcase, no results", manager) << manager << namedef << firstname << QVariant("XAMBEZI") << QVariant() << false << 0 << true << csflag << "hijk"; newMRow("no min, cs, badcase, all results", manager) << manager << namedef << firstname << QVariant() << QVariant("XAMBEZI") << false << 0 << true << csflag << "abcdefg"; #if 0 qWarning() << "Test case \"no min, cs, badcase, some results\" will fail on some platforms because of QString::localeAwareCompare is not actually locale aware"; newMRow("no min, cs, badcase, some results", manager) << manager << namedef << firstname << QVariant() << QVariant("BOB") << false << 0 << true << csflag << "ab"; #endif newMRow("no min, cs, badcase, no results", manager) << manager << namedef << firstname << QVariant() << QVariant("AARDVARK") << false << 0 << true << csflag << es; /* 'a' has phone number ("5551212") */ QTest::newRow("range1") << manager << phonedef << phonenum << QVariant("5551200") << QVariant("5551220") << false << 0 << false << 0 << "a"; /* A(Aaron Aaronson), B(Bob Aaronsen), C(Boris Aaronsun), D(Dennis FitzMacyntire) */ // string range matching - no matchflags set. QTest::newRow("string range - no matchflags - 1") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << false << 0 << true << 0 << "a"; QTest::newRow("string range - no matchflags - 2") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << 0 << "a"; QTest::newRow("string range - no matchflags - 3") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << 0 << "a"; QTest::newRow("string range - no matchflags - 4") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << true << 0 << "ab"; QTest::newRow("string range - no matchflags - 5") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::IncludeUpper) << true << 0 << "ab"; QTest::newRow("string range - no matchflags - 6") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::IncludeUpper) << true << 0 << "c"; QTest::newRow("string range - no matchflags - 7") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << 0 << "bc"; QTest::newRow("string range - no matchflags - 8") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << true << 0 << "bc"; QTest::newRow("string range - no matchflags - 9") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << 0 << "c"; QTest::newRow("string range - no matchflags - 10") << manager << namedef << firstname << QVariant("Barry") << QVariant("C") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << 0 << "bc"; // string range matching - QContactFilter::MatchStartsWith should produce the same results as without matchflags set. QTest::newRow("string range - startswith - 1") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "a"; QTest::newRow("string range - startswith - 2") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "a"; QTest::newRow("string range - startswith - 3") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "ab"; QTest::newRow("string range - startswith - 4") << manager << namedef << firstname << QVariant("A") << QVariant("Bob") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::IncludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "ab"; QTest::newRow("string range - startswith - 5") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::IncludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "c"; QTest::newRow("string range - startswith - 6") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "bc"; QTest::newRow("string range - startswith - 7") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "bc"; QTest::newRow("string range - startswith - 8") << manager << namedef << firstname << QVariant("Bob") << QVariant("C") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "c"; QTest::newRow("string range - startswith - 9") << manager << namedef << firstname << QVariant("Barry") << QVariant("C") << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::ExcludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "bc"; // Open ended starts with QTest::newRow("string range - startswith open top - 1") << manager << namedef << firstname << QVariant("A") << ev << true << (int)(QContactDetailRangeFilter::IncludeLower) << true << (int)(QContactFilter::MatchStartsWith) << "abcdefghijk"; QTest::newRow("string range - startswith open top - 2") << manager << namedef << firstname << QVariant("A") << ev << true << (int)(QContactDetailRangeFilter::ExcludeLower) << true << (int)(QContactFilter::MatchStartsWith) << "abcdefghijk"; QTest::newRow("string range - startswith open top - 3") << manager << namedef << firstname << QVariant("Aaron") << ev << true << (int)(QContactDetailRangeFilter::IncludeLower) << true << (int)(QContactFilter::MatchStartsWith) << "abcdefghijk"; QTest::newRow("string range - startswith open top - 4") << manager << namedef << firstname << QVariant("Aaron") << ev << true << (int)(QContactDetailRangeFilter::ExcludeLower) << true << (int)(QContactFilter::MatchStartsWith) << "bcdefghijk"; QTest::newRow("string range - startswith open bottom - 1") << manager << namedef << firstname << ev << QVariant("Borit") << true << (int)(QContactDetailRangeFilter::IncludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "abc"; QTest::newRow("string range - startswith open bottom - 2") << manager << namedef << firstname << ev << QVariant("Borit") << true << (int)(QContactDetailRangeFilter::ExcludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "abc"; QTest::newRow("string range - startswith open bottom - 3") << manager << namedef << firstname << ev << QVariant("Boris") << true << (int)(QContactDetailRangeFilter::IncludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "abc"; QTest::newRow("string range - startswith open bottom - 4") << manager << namedef << firstname << ev << QVariant("Boris") << true << (int)(QContactDetailRangeFilter::ExcludeUpper) << true << (int)(QContactFilter::MatchStartsWith) << "ab"; /* A(10), B(20), C(-20) */ // Now integer range testing FieldSelector defAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("Integer"); if (validDetailInfo(defAndFieldNames)) { QTest::newRow("int range - no rangeflags - 1") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(2) << QVariant(2) << false << 0 << false << 0 << es; QTest::newRow("int range - no rangeflags - 2") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(2) << QVariant(3) << false << 0 << false << 0 << es; QTest::newRow("int range - no rangeflags - 3") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(2) << QVariant(4) << false << 0 << false << 0 << "a"; QTest::newRow("int range - no rangeflags - 4") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(3) << QVariant(3) << false << 0 << false << 0 << es; QTest::newRow("int range - rangeflags - 1") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(3) << QVariant(3) << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::ExcludeUpper) << false << 0 << es; QTest::newRow("int range - rangeflags - 2") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(3) << QVariant(3) << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::ExcludeUpper) << false << 0 << es; QTest::newRow("int range - rangeflags - 3") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(3) << QVariant(3) << true << (int)(QContactDetailRangeFilter::ExcludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << es; QTest::newRow("int range - rangeflags - 4") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(3) << QVariant(3) << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << "a"; QTest::newRow("int range - rangeflags - 5") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(3) << QVariant(4) << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << "a"; QTest::newRow("int range - rangeflags - 6") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(4) << QVariant(4) << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << es; QTest::newRow("int range - rangeflags - 7") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(-30) << QVariant(-19) << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << "c"; QTest::newRow("int range - rangeflags - 8") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(-20) << QVariant(-30) << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << es; QTest::newRow("int range - rangeflags - variant - 1") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant(2) << QVariant() << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << "ab"; QTest::newRow("int range - rangeflags - variant - 2") << manager << defAndFieldNames.first << defAndFieldNames.second << QVariant() << QVariant(4) << true << (int)(QContactDetailRangeFilter::IncludeLower | QContactDetailRangeFilter::IncludeUpper) << false << 0 << "ac"; } } } void tst_QContactManagerFiltering::rangeFiltering() { QFETCH(QContactManager*, cm); QFETCH(TypeIdentifier, defname); QFETCH(FieldIdentifier, fieldname); QFETCH(QVariant, minrange); QFETCH(QVariant, maxrange); QFETCH(bool, setrfs); QFETCH(int, rangeflagsi); QFETCH(bool, setmfs); QFETCH(int, matchflagsi); QFETCH(QString, expected); QContactDetailRangeFilter::RangeFlags rangeflags = (QContactDetailRangeFilter::RangeFlags)rangeflagsi; QContactFilter::MatchFlags matchflags = (QContactFilter::MatchFlags) matchflagsi; if ((matchflags & QContactFilter::MatchCaseSensitive) == 0) { // Case insensitivity only applies to MatchFixedString matchflags |= QContactFilter::MatchFixedString; } QList contacts = contactsAddedToManagers.values(cm); QList ids; /* Build the range filter */ QContactDetailRangeFilter drf; setFilterDetail(drf, defname, fieldname); if (setrfs) drf.setRange(minrange, maxrange, rangeflags); else drf.setRange(minrange, maxrange); if (setmfs) drf.setMatchFlags(matchflags); if (cm->managerName() == "memory") { /* At this point, since we're using memory, assume the filter isn't really supported */ QVERIFY(cm->isFilterSupported(drf) == false); } ids = cm->contactIds(drf); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE_UNSORTED(output, expected); } void tst_QContactManagerFiltering::intersectionFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("firstfilter"); QTest::addColumn("fftype"); // 1 = detail, 2 = detailrange, 3 = groupmembership, 4 = union, 5 = intersection QTest::addColumn("ffdefname"); QTest::addColumn("fffieldname"); QTest::addColumn("ffsetvalue"); QTest::addColumn("ffvalue"); QTest::addColumn("ffminrange"); QTest::addColumn("ffmaxrange"); QTest::addColumn("secondfilter"); QTest::addColumn("sftype"); QTest::addColumn("sfdefname"); QTest::addColumn("sffieldname"); QTest::addColumn("sfsetvalue"); QTest::addColumn("sfvalue"); QTest::addColumn("sfminrange"); QTest::addColumn("sfmaxrange"); QTest::addColumn("order"); QTest::addColumn("expected"); QString es; // empty string. for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); // for the following tests, terminology: // X will be an (empty) intersection filter created in the test // Y will be the first filter defined here // Z will be the second filter defined here // note: have contacts: A(Aaron Aaronson), B(Bob Aaronsen), C(Boris Aaronsun), // D(Dennis FitzMacintyre), E(John Smithee), F(John Smithey), G(John Smithy) // WITH Y AND Z AS DETAIL FILTERS (with no overlap between Y and Z results) QTest::newRow("A1") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("Bob")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronson")) << QVariant() << QVariant() << "YZ" << es; // WITH Y AND Z AS DETAIL FILTERS (with 1 overlap between Y(B) and Z(B) results) QTest::newRow("A2") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("Bob")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronsen")) << QVariant() << QVariant() << "YZ" << "b"; // WITH Y AND Z AS DETAIL FILTERS (with 1 overlap between Y(EFG) and Z(E) results) QTest::newRow("A3") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("John")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Smithee")) << QVariant() << QVariant() << "YZ" << "e"; // WITH Y AND Z AS DETAIL FILTERS (with 1 overlap between Y(EFG) and Z(E) results) but intersecting X AND Y // Where X is the empty intersection filter. This should match nothing (anything intersected with empty intersection = nothing) QTest::newRow("A4") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("John")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Smithee")) << QVariant() << QVariant() << "XY" << es; // WITH Y AS DETAIL RANGE FILTER AND Z AS DETAIL FILTER (with no overlap between Y(BC+) and Z(A) results) QTest::newRow("B1") << manager << true << 2 << detailType() << detailField(QContactName::FieldFirstName) << false << QVariant() << QVariant(QString::fromLatin1("Bob")) << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronson")) << QVariant() << QVariant() << "YZ" << es; // WITH Y AS DETAIL RANGE FILTER AND Z AS DETAIL FILTER (with 1 overlap between Y(BC+) and Z(B) results) QTest::newRow("B2") << manager << true << 2 << detailType() << detailField(QContactName::FieldFirstName) << false << QVariant() << QVariant(QString::fromLatin1("Bob")) << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronsen")) << QVariant() << QVariant() << "YZ" << "b"; // WITH Y AND Z AS DETAIL RANGE FILTERS (with no overlap between Y(E+) and Z(ABC) results) QTest::newRow("C1") << manager << true << 2 << detailType() << detailField(QContactName::FieldFirstName) << false << QVariant() << QVariant(QString::fromLatin1("John")) << QVariant() << true << 2 << detailType() << detailField(QContactName::FieldLastName) << false << QVariant() << QVariant(QString::fromLatin1("Aaronaaa")) << QVariant(QLatin1String("Aaronzzz")) << "YZ" << es; // WITH Y AND Z AS DETAIL RANGE FILTERS (with 2 overlap between Y(BC) and Z(ABC) results) QTest::newRow("C2") << manager << true << 2 << detailType() << detailField(QContactName::FieldFirstName) << false << QVariant() << QVariant(QString::fromLatin1("Boa")) << QVariant() << true << 2 << detailType() << detailField(QContactName::FieldLastName) << false << QVariant() << QVariant(QString::fromLatin1("Aaronaaa")) << QVariant(QLatin1String("Aaronzzz")) << "YZ" << "bc"; } } void tst_QContactManagerFiltering::intersectionFiltering() { QFETCH(QContactManager*, cm); QFETCH(bool, firstfilter); QFETCH(int, fftype); // 1 = detail, 2 = detailrange, 3 = groupmembership, 4 = union, 5 = intersection QFETCH(TypeIdentifier, ffdefname); QFETCH(FieldIdentifier, fffieldname); QFETCH(bool, ffsetvalue); QFETCH(QVariant, ffvalue); QFETCH(QVariant, ffminrange); QFETCH(QVariant, ffmaxrange); QFETCH(bool, secondfilter); QFETCH(int, sftype); QFETCH(TypeIdentifier, sfdefname); QFETCH(FieldIdentifier, sffieldname); QFETCH(bool, sfsetvalue); QFETCH(QVariant, sfvalue); QFETCH(QVariant, sfminrange); QFETCH(QVariant, sfmaxrange); QFETCH(QString, order); QFETCH(QString, expected); QContactFilter *x = new QContactIntersectionFilter(); QContactFilter *y = 0, *z = 0; if (firstfilter) { switch (fftype) { case 1: // detail filter y = new QContactDetailFilter(); setFilterDetail(*static_cast(y), ffdefname, fffieldname); if (ffsetvalue) static_cast(y)->setValue(ffvalue); break; case 2: // range filter y = new QContactDetailRangeFilter(); setFilterDetail(*static_cast(y), ffdefname, fffieldname); static_cast(y)->setRange(ffminrange, ffmaxrange); break; case 3: // group membership filter case 4: // union filter case 5: // intersection filter break; default: QVERIFY(false); // force fail. break; } } if (secondfilter) { switch (sftype) { case 1: // detail filter z = new QContactDetailFilter(); setFilterDetail(*static_cast(z), sfdefname, sffieldname); if (sfsetvalue) static_cast(z)->setValue(sfvalue); break; case 2: // range filter z = new QContactDetailRangeFilter(); setFilterDetail(*static_cast(z), sfdefname, sffieldname); static_cast(z)->setRange(sfminrange, sfmaxrange); break; case 3: // group membership filter case 4: // union filter case 5: // intersection filter break; default: QVERIFY(false); // force fail. break; } } // control variables - order: starts, ends, mids bool sX = false; bool sY = false; bool sZ = false; bool eX = false; bool eY = false; bool eZ = false; bool mX = false; bool mY = false; bool mZ = false; if (order.startsWith("X")) sX = true; if (order.startsWith("Y")) sY = true; if (order.startsWith("Z")) sZ = true; if (order.endsWith("X")) eX = true; if (order.endsWith("Y")) eY = true; if (order.endsWith("Z")) eZ = true; if (order.size() > 2) { if (order.at(1) == 'X') mX = true; if (order.at(1) == 'Y') mY = true; if (order.at(1) == 'Z') mZ = true; } // now perform the filtering. QContactIntersectionFilter resultFilter; if (sX) { if (mY && eZ) resultFilter = *x & *y & *z; else if (mZ && eY) resultFilter = *x & *z & *y; else if (eY) resultFilter = *x & *y; else if (eZ) resultFilter = *x & *z; } else if (sY) { if (mX && eZ) resultFilter = *y & *x & *z; else if (mZ && eX) resultFilter = *y & *z & *x; else if (eX) resultFilter = *y & *x; else if (eZ) resultFilter = *y & *z; } else if (sZ) { if (mX && eY) resultFilter = *z & *x & *y; else if (mY && eX) resultFilter = *z & *y & *x; else if (eX) resultFilter = *z & *x; else if (eY) resultFilter = *z & *y; } QList contacts = contactsAddedToManagers.values(cm); QList ids; ids = cm->contactIds(resultFilter); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE_UNSORTED(output, expected); delete x; if (y) delete y; if (z) delete z; } void tst_QContactManagerFiltering::unionFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("firstfilter"); QTest::addColumn("fftype"); // 1 = detail, 2 = detailrange, 3 = groupmembership, 4 = union, 5 = intersection QTest::addColumn("ffdefname"); QTest::addColumn("fffieldname"); QTest::addColumn("ffsetvalue"); QTest::addColumn("ffvalue"); QTest::addColumn("ffminrange"); QTest::addColumn("ffmaxrange"); QTest::addColumn("secondfilter"); QTest::addColumn("sftype"); QTest::addColumn("sfdefname"); QTest::addColumn("sffieldname"); QTest::addColumn("sfsetvalue"); QTest::addColumn("sfvalue"); QTest::addColumn("sfminrange"); QTest::addColumn("sfmaxrange"); QTest::addColumn("order"); QTest::addColumn("expected"); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); // for the following tests, terminology: // X will be an (empty) union filter created in the test // Y will be the first filter defined here // Z will be the second filter defined here // WITH Y AND Z AS DETAIL FILTERS (with no overlap between Y(1) and Z(1) results) QTest::newRow("A1") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("Bob")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronson")) << QVariant() << QVariant() << "YZ" << "ab"; // WITH Y AND Z AS DETAIL FILTERS (with 1 overlap between Y(B) and Z(B) results) QTest::newRow("A2") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("Bob")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronsen")) << QVariant() << QVariant() << "YZ" << "b"; // WITH Y AND Z AS DETAIL FILTERS (with 1 overlap between Y(EFG) and Z(E) results) QTest::newRow("A3") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("John")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Smithee")) << QVariant() << QVariant() << "YZ" << "efg"; // WITH Y AND Z AS DETAIL FILTERS (with 1 overlap between Y(EFG) and Z(E) results) but intersecting X AND Y // Where X is the empty union filter. This should match all of Y matches. QTest::newRow("A4") << manager << true << 1 << detailType() << detailField(QContactName::FieldFirstName) << true << QVariant(QString::fromLatin1("John")) << QVariant() << QVariant() << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Smithee")) << QVariant() << QVariant() << "XY" << "efg"; // WITH Y AS DETAIL RANGE FILTER AND Z AS DETAIL FILTER (with no overlap between Y(AB) and Z(C) results) QTest::newRow("B1") << manager << true << 2 << detailType() << detailField(QContactName::FieldFirstName) << false << QVariant() << QVariant() << QVariant(QString::fromLatin1("Boz")) << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronsun")) << QVariant() << QVariant() << "YZ" << "abc"; // WITH Y AS DETAIL RANGE FILTER AND Z AS DETAIL FILTER (with 1 overlap between Y(AB) and Z(A) results) QTest::newRow("B2") << manager << true << 2 << detailType() << detailField(QContactName::FieldFirstName) << false << QVariant() << QVariant(QString::fromLatin1("Aaro")) << QVariant(QString::fromLatin1("Bod")) << true << 1 << detailType() << detailField(QContactName::FieldLastName) << true << QVariant(QString::fromLatin1("Aaronson")) << QVariant() << QVariant() << "YZ" << "ab"; // WITH Y AND Z AS DETAIL RANGE FILTERS (with 2 overlap between Y(AB) and Z(ABC) results) QTest::newRow("C1") << manager << true << 2 << detailType() << detailField(QContactName::FieldFirstName) << false << QVariant() << QVariant(QString::fromLatin1("Aaro")) << QVariant(QString::fromLatin1("Bod")) << true << 2 << detailType() << detailField(QContactName::FieldLastName) << false << QVariant() << QVariant(QString::fromLatin1("Aaronaaa")) << QVariant(QLatin1String("Aaronzzz")) << "YZ" << "abc"; } } void tst_QContactManagerFiltering::unionFiltering() { QFETCH(QContactManager*, cm); QFETCH(bool, firstfilter); QFETCH(int, fftype); // 1 = detail, 2 = detailrange, 3 = groupmembership, 4 = union, 5 = intersection QFETCH(TypeIdentifier, ffdefname); QFETCH(FieldIdentifier, fffieldname); QFETCH(bool, ffsetvalue); QFETCH(QVariant, ffvalue); QFETCH(QVariant, ffminrange); QFETCH(QVariant, ffmaxrange); QFETCH(bool, secondfilter); QFETCH(int, sftype); QFETCH(TypeIdentifier, sfdefname); QFETCH(FieldIdentifier, sffieldname); QFETCH(bool, sfsetvalue); QFETCH(QVariant, sfvalue); QFETCH(QVariant, sfminrange); QFETCH(QVariant, sfmaxrange); QFETCH(QString, order); QFETCH(QString, expected); QContactFilter *x = new QContactUnionFilter(); QContactFilter *y = 0, *z = 0; if (firstfilter) { switch (fftype) { case 1: // detail filter y = new QContactDetailFilter(); setFilterDetail(*static_cast(y), ffdefname, fffieldname); if (ffsetvalue) static_cast(y)->setValue(ffvalue); break; case 2: // range filter y = new QContactDetailRangeFilter(); setFilterDetail(*static_cast(y), ffdefname, fffieldname); static_cast(y)->setRange(ffminrange, ffmaxrange); break; case 3: // group membership filter case 4: // union filter case 5: // intersection filter break; default: QVERIFY(false); // force fail. break; } } if (secondfilter) { switch (sftype) { case 1: // detail filter z = new QContactDetailFilter(); setFilterDetail(*static_cast(z), sfdefname, sffieldname); if (sfsetvalue) static_cast(z)->setValue(sfvalue); break; case 2: // range filter z = new QContactDetailRangeFilter(); setFilterDetail(*static_cast(z), sfdefname, sffieldname); static_cast(z)->setRange(sfminrange, sfmaxrange); break; case 3: // group membership filter case 4: // union filter case 5: // intersection filter break; default: QVERIFY(false); // force fail. break; } } // control variables - order: starts, ends, mids bool sX = false; bool sY = false; bool sZ = false; bool eX = false; bool eY = false; bool eZ = false; bool mX = false; bool mY = false; bool mZ = false; if (order.startsWith("X")) sX = true; if (order.startsWith("Y")) sY = true; if (order.startsWith("Z")) sZ = true; if (order.endsWith("X")) eX = true; if (order.endsWith("Y")) eY = true; if (order.endsWith("Z")) eZ = true; if (order.size() > 2) { if (order.at(1) == 'X') mX = true; if (order.at(1) == 'Y') mY = true; if (order.at(1) == 'Z') mZ = true; } // now perform the filtering. QContactUnionFilter resultFilter; if (sX) { if (mY && eZ) resultFilter = *x | *y | *z; else if (mZ && eY) resultFilter = *x | *z | *y; else if (eY) resultFilter = *x | *y; else if (eZ) resultFilter = *x | *z; } else if (sY) { if (mX && eZ) resultFilter = *y | *x | *z; else if (mZ && eX) resultFilter = *y | *z | *x; else if (eX) resultFilter = *y | *x; else if (eZ) resultFilter = *y | *z; } else if (sZ) { if (mX && eY) resultFilter = *z | *x | *y; else if (mY && eX) resultFilter = *z | *y | *x; else if (eX) resultFilter = *z | *x; else if (eY) resultFilter = *z | *y; } QList contacts = contactsAddedToManagers.values(cm); QList ids; ids = cm->contactIds(resultFilter); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE_UNSORTED(output, expected); delete x; if (y) delete y; if (z) delete z; } void tst_QContactManagerFiltering::relationshipFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("relatedContactRole"); QTest::addColumn("relationshipType"); QTest::addColumn("relatedContactLocalId"); QTest::addColumn("otherManagerUri"); QTest::addColumn("expected"); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); // HasMember QTest::newRow("RF-1") << manager << static_cast(QContactRelationship::Second) << relationshipString(QContactRelationship::HasMember) << static_cast(0) << QString() << "a"; QTest::newRow("RF-2") << manager << static_cast(QContactRelationship::First) << relationshipString(QContactRelationship::HasMember) << static_cast(0) << QString() << "b"; QTest::newRow("RF-3") << manager << static_cast(QContactRelationship::Either) << relationshipString(QContactRelationship::HasMember) << static_cast(0) << QString() << "ab"; // match any contact that has an assistant QTest::newRow("RF-4") << manager << static_cast(QContactRelationship::Second) << relationshipString(QContactRelationship::HasAssistant) << static_cast(0) << QString() << "a"; // match any contact that is an assistant QTest::newRow("RF-5") << manager << static_cast(QContactRelationship::First) << relationshipString(QContactRelationship::HasAssistant) << static_cast(0) << QString() << "b"; // match any contact that has an assistant or is an assistant QTest::newRow("RF-6") << manager << static_cast(QContactRelationship::Either) << relationshipString(QContactRelationship::HasAssistant) << static_cast(0) << QString() << "ab"; // IsSameAs QTest::newRow("RF-7") << manager << static_cast(QContactRelationship::Second) << relationshipString(QContactRelationship::IsSameAs) << static_cast(0) << QString() << "a"; QTest::newRow("RF-8") << manager << static_cast(QContactRelationship::First) << relationshipString(QContactRelationship::IsSameAs) << static_cast(0) << QString() << "b"; QTest::newRow("RF-9") << manager << static_cast(QContactRelationship::Either) << relationshipString(QContactRelationship::IsSameAs) << static_cast(0) << QString() << "ab"; // Aggregates QTest::newRow("RF-10") << manager << static_cast(QContactRelationship::Second) << relationshipString(QContactRelationship::Aggregates) << static_cast(0) << QString() << "a"; QTest::newRow("RF-11") << manager << static_cast(QContactRelationship::First) << relationshipString(QContactRelationship::Aggregates) << static_cast(0) << QString() << "b"; QTest::newRow("RF-12") << manager << static_cast(QContactRelationship::Either) << relationshipString(QContactRelationship::Aggregates) << static_cast(0) << QString() << "ab"; // HasManager QTest::newRow("RF-13") << manager << static_cast(QContactRelationship::Second) << relationshipString(QContactRelationship::HasManager) << static_cast(0) << QString() << "a"; QTest::newRow("RF-14") << manager << static_cast(QContactRelationship::First) << relationshipString(QContactRelationship::HasManager) << static_cast(0) << QString() << "b"; QTest::newRow("RF-15") << manager << static_cast(QContactRelationship::Either) << relationshipString(QContactRelationship::HasManager) << static_cast(0) << QString() << "ab"; // HasSpouse QTest::newRow("RF-16") << manager << static_cast(QContactRelationship::Second) << relationshipString(QContactRelationship::HasSpouse) << static_cast(0) << QString() << "a"; QTest::newRow("RF-17") << manager << static_cast(QContactRelationship::First) << relationshipString(QContactRelationship::HasSpouse) << static_cast(0) << QString() << "b"; QTest::newRow("RF-18") << manager << static_cast(QContactRelationship::Either) << relationshipString(QContactRelationship::HasSpouse) << static_cast(0) << QString() << "ab"; const bool aribtraryRelationshipsFeatureSupported = true; // Unknown relationship if (aribtraryRelationshipsFeatureSupported) { QTest::newRow("RF-19") << manager << static_cast(QContactRelationship::Second) << QString::fromLatin1("UnknownRelationship") << static_cast(0) << QString() << "a"; QTest::newRow("RF-20") << manager << static_cast(QContactRelationship::First) << QString::fromLatin1("UnknownRelationship") << static_cast(0) << QString() << "b"; QTest::newRow("RF-21") << manager << static_cast(QContactRelationship::Either) << QString::fromLatin1("UnknownRelationship") << static_cast(0) << QString() << "ab"; } else { QTest::newRow("RF-19") << manager << static_cast(QContactRelationship::Second) << QString::fromLatin1("UnknownRelationship") << static_cast(0) << QString() << ""; QTest::newRow("RF-20") << manager << static_cast(QContactRelationship::First) << QString::fromLatin1("UnknownRelationship") << static_cast(0) << QString() << ""; QTest::newRow("RF-21") << manager << static_cast(QContactRelationship::Either) << QString::fromLatin1("UnknownRelationship") << static_cast(0) << QString() << ""; } // match any contact that is the related contact in a relationship with contact-A //QTest::newRow("RF-19") << manager << static_cast(QContactRelationship::Second) << QString() << static_cast(contactAId.value(manager).localId()) << contactAId.value(manager).managerUri() << "h"; // match any contact has contact-A as the related contact //QTest::newRow("RF-20") << manager << static_cast(QContactRelationship::First) << QString() << static_cast(contactAId.value(manager).localId()) << contactAId.value(manager).managerUri() << "i"; // match any contact that has any relationship with contact-A //QTest::newRow("RF-21") << manager << static_cast(QContactRelationship::Either) << QString() << static_cast(contactAId.value(manager).localId()) << contactAId.value(manager).managerUri() << "hi"; } } QContact tst_QContactManagerFiltering::createContact(QContactManager* cm, QContactType::TypeValues type, QString name) { QContact contact; contact.setType(type); QContactName contactName; #ifndef DETAIL_DEFINITION_SUPPORTED contactName.setFirstName(name); contactName.setLastName(name); contactName.setMiddleName(name); contactName.setPrefix(name); contactName.setSuffix(name); #else QContactDetailDefinition detailDefinition = cm->detailDefinition(QContactName::DefinitionName, type); detailDefinition.removeField(QContactDetail::FieldContext); foreach (const QString &fieldKey, detailDefinition.fields().keys()) { contactName.setValue(fieldKey, name); } #endif contact.saveDetail(&contactName); cm->saveContact(&contact); return contact; } void tst_QContactManagerFiltering::relationshipFiltering() { QFETCH(QContactManager*, cm); QFETCH(int, relatedContactRole); QFETCH(QString, relationshipType); QFETCH(quint32, relatedContactLocalId); QFETCH(QString, otherManagerUri); QFETCH(QString, expected); // TODO: A little re-factoring could be used to make the test case more readable // 1. Create contacts to be used in relationship testing QContact contactA; if (relationshipType == relationshipString(QContactRelationship::HasMember)) { if (!cm->supportedContactTypes().contains(QContactType::TypeGroup)) QSKIP("Manager does not support groups; skipping relationship filtering"); // Change contact type to group as this is required at least by symbian backend // TODO: should it be possible to query this constraint from the backend? contactA = createContact(cm, QContactType::TypeGroup, "ContactA"); } else { contactA = createContact(cm, QContactType::TypeContact, "ContactA"); } QContact contactB = createContact(cm, QContactType::TypeContact, "ContactB"); // 2. Create the relationship between the contacts QContactId firstId(contactA.id()); QContactId secondId(contactB.id()); QContactRelationship h2i; h2i = makeRelationship(relationshipType, firstId, secondId); // save and check error code bool succeeded = false; const bool relationshipsFeatureSupported = true; const bool aribtraryRelationshipsFeatureSupported = true; if((relationshipsFeatureSupported && cm->isRelationshipTypeSupported(relationshipType, contactA.type()) && cm->isRelationshipTypeSupported(relationshipType, contactB.type())) || aribtraryRelationshipsFeatureSupported) { succeeded = true; QVERIFY(cm->saveRelationship(&h2i)); QCOMPARE(cm->error(), QContactManager::NoError); } else { QVERIFY(!cm->saveRelationship(&h2i)); QCOMPARE(cm->error(), QContactManager::NotSupportedError); } // 3. Construct the filter QContactId relatedContactId; relatedContactId = ContactId::apiId(relatedContactLocalId, cm->managerUri()); Q_UNUSED(otherManagerUri) QContactRelationshipFilter crf; crf.setRelatedContactRole(static_cast(relatedContactRole)); crf.setRelationshipType(relationshipType); crf.setRelatedContactId(relatedContactId); // 4. Grab the filtering results QList contacts; contacts.append(contactA.id()); contacts.append(contactB.id()); QList ids = cm->contactIds(crf); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts // 5. Remove the created relationship and contacts if(succeeded) { // Check that an existing relationship can be removed QVERIFY(cm->removeRelationship(h2i)); QCOMPARE(cm->error(), QContactManager::NoError); } else { // Check that non-existing relationship cannot be removed QVERIFY(!cm->removeRelationship(h2i)); //TODO: what is the expected error code? //QCOMPARE(cm->error(), QContactManager::DoesNotExistError); } foreach (const QContactId& cid, contacts) { cm->removeContact(cid); } // 6. Verify the filtering result if (!relationshipsFeatureSupported) { QSKIP("Manager does not support relationships; skipping relationship filtering"); } else if(relationshipType.isEmpty() || (cm->isRelationshipTypeSupported(relationshipType, contactA.type()) && cm->isRelationshipTypeSupported(relationshipType, contactB.type()))) { // check that the relationship type is supported for both contacts. QCOMPARE_UNSORTED(output, expected); } else { QString msg = "Manager does not support relationship type " + relationshipType + " between " + contactA.type() + " and " + contactB.type() + " type contacts."; QSKIP(msg.toLatin1()); } } void tst_QContactManagerFiltering::sorting_data() { QTest::addColumn("cm"); QTest::addColumn("defname"); QTest::addColumn("fieldname"); QTest::addColumn("directioni"); QTest::addColumn("setbp"); QTest::addColumn("blankpolicyi"); QTest::addColumn("casesensitivityi"); QTest::addColumn("expected"); QTest::addColumn("unstable"); FieldIdentifier firstname = QContactName::FieldFirstName; FieldIdentifier lastname = QContactName::FieldLastName; TypeIdentifier namedef = detailType(); TypeIdentifier dldef = detailType(); FieldIdentifier dlfld = QContactDisplayLabel::FieldLabel; int asc = Qt::AscendingOrder; int desc = Qt::DescendingOrder; int cs = Qt::CaseSensitive; int ci = Qt::CaseInsensitive; int bll = QContactSortOrder::BlanksLast; int blf = QContactSortOrder::BlanksFirst; for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); #if 0 // the nemo sqlite backend has different sorting semantics to what is expected FieldSelector integerDefAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("Integer"); FieldSelector stringDefAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("String"); newMRow("first ascending", manager) << manager << namedef << firstname << asc << false << 0 << cs << "abcdefghjik" << "efg"; // efg have the same first name newMRow("first descending", manager) << manager << namedef << firstname << desc << false << 0 << cs << "kijhefgdcba" << "efg";// efg have the same first name newMRow("last ascending", manager) << manager << namedef << lastname << asc << false << 0 << cs << "bacdefghijk" << "hijk"; // all have a well defined, sortable last name except hijk newMRow("last descending", manager) << manager << namedef << lastname << desc << false << 0 << cs << "gfedcabhijk" << "hijk"; // all have a well defined, sortable last name except hijk if (validDetailInfo(integerDefAndFieldNames)) { newMRow("integer ascending, blanks last", manager) << manager << integerDefAndFieldNames.first << integerDefAndFieldNames.second << asc << true << bll << cs << "cabgfedhijk" << "gfedhijk"; // gfedhijk have no integer newMRow("integer descending, blanks last", manager) << manager << integerDefAndFieldNames.first << integerDefAndFieldNames.second << desc << true << bll << cs << "bacgfedhijk" << "gfedhijk"; // gfedhijk have no integer newMRow("integer ascending, blanks first", manager) << manager << integerDefAndFieldNames.first << integerDefAndFieldNames.second << asc << true << blf << cs << "hijkdefgcab" << "gfedhijk"; // gfedhijk have no integer newMRow("integer descending, blanks first", manager) << manager << integerDefAndFieldNames.first << integerDefAndFieldNames.second << desc << true << blf << cs << "hijkdefgbac" << "gfedhijk"; // gfedhijk have no integer } if (validDetailInfo(stringDefAndFieldNames)) { QTest::newRow("string ascending (null value), blanks first") << manager << stringDefAndFieldNames.first << stringDefAndFieldNames.second << asc << true << blf << cs << "feabcdg" << "fehijk"; // f and e have blank string QTest::newRow("string ascending (null value), blanks last") << manager << stringDefAndFieldNames.first << stringDefAndFieldNames.second << asc << true << bll << cs << "abcdgef" << "efhijk"; // f and e have blank string } newMRow("display label insensitive", manager) << manager << dldef << dlfld << asc << false << 0 << ci << "abcdefghjik" << "efghji"; newMRow("display label sensitive", manager) << manager << dldef << dlfld << asc << false << 0 << cs << "abcdefghjik" << "efg"; #else Q_UNUSED(ci) Q_UNUSED(bll) Q_UNUSED(blf) Q_UNUSED(dldef) Q_UNUSED(dlfld) #endif // nemo sqlite collation - instead we ensure the correctness of our ordering code with the following tests: newMRow("first ascending, cs, binary collation", manager) << manager << namedef << firstname << asc << false << 0 << cs << "abcdefgikjh" << "efg"; newMRow("first descending, cs, binary collation", manager) << manager << namedef << firstname << desc << false << 0 << cs << "hjkiefgdcba" << "efg"; newMRow("last ascending, cs, binary collation", manager) << manager << namedef << lastname << asc << false << 0 << cs << "bacdefgikjh" << "hijk"; newMRow("last descending, cs, binary collation", manager) << manager << namedef << lastname << desc << false << 0 << cs << "gfedcabhijk" << "hijk"; // Note - the current display label algorithm follows that of nemo-qml-plugin-contacts, and does not include prefix //newMRow("display label insensitive, binary collation", manager) << manager << dldef << dlfld << asc << false << 0 << ci << "bcdefgaijhk" << "efg"; // the display label is synthesized so that A has "Sir" at the start of it (instead of "Aaron"). //newMRow("display label sensitive, binary collation", manager) << manager << dldef << dlfld << asc << false << 0 << cs << "bcdefgaikjh" << "efg"; } } void tst_QContactManagerFiltering::sorting() { QFETCH(QContactManager*, cm); QFETCH(TypeIdentifier, defname); QFETCH(FieldIdentifier, fieldname); QFETCH(int, directioni); QFETCH(bool, setbp); QFETCH(int, blankpolicyi); QFETCH(int, casesensitivityi); QFETCH(QString, expected); QFETCH(QString, unstable); Qt::SortOrder direction = (Qt::SortOrder)directioni; QContactSortOrder::BlankPolicy blankpolicy = (QContactSortOrder::BlankPolicy) blankpolicyi; Qt::CaseSensitivity casesensitivity = (Qt::CaseSensitivity) casesensitivityi; QList contacts = contactsAddedToManagers.values(cm); QList ids; /* Build the sort order */ QContactSortOrder s; setSortDetail(s, defname, fieldname); s.setDirection(direction); if (setbp) s.setBlankPolicy(blankpolicy); s.setCaseSensitivity(casesensitivity); ids = cm->contactIds(s); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts // It's possible to get some contacts back in an arbitrary order (since we single sort) if (unstable.length() > 1) { // ensure that the maximum distance between unstable elements in the output is the size of the unstable string. int firstIndex = -1; int lastIndex = -1; for (int i = 0; i < output.size(); i++) { if (unstable.contains(output.at(i))) { firstIndex = i; break; } } for (int i = output.size() - 1; i >= 0; i--) { if (unstable.contains(output.at(i))) { lastIndex = i; break; } } if (firstIndex == -1 || lastIndex == -1) { bool containsAllUnstableElements = false; QVERIFY(containsAllUnstableElements); } bool unstableElementsAreGrouped = ((lastIndex - firstIndex) == (unstable.length() - 1)); QVERIFY(unstableElementsAreGrouped); // now remove all unstable elements from the output for (int i = 1; i < unstable.length(); i++) { output.remove(unstable.at(i)); expected.remove(unstable.at(i)); } } QCOMPARE(output, expected); /* Now do a check with a filter involved; the filter should not affect the sort order */ QContactDetailFilter presenceName; setFilterDetail(presenceName); ids = cm->contactIds(presenceName, s); output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts // It's possible to get some contacts back in an arbitrary order (since we single sort) if (unstable.length() > 1) { // ensure that the maximum distance between unstable elements in the output is the size of the unstable string. int firstIndex = -1; int lastIndex = -1; for (int i = 0; i < output.size(); i++) { if (unstable.contains(output.at(i))) { firstIndex = i; break; } } for (int i = output.size() - 1; i >= 0; i--) { if (unstable.contains(output.at(i))) { lastIndex = i; break; } } if (firstIndex == -1 || lastIndex == -1) { bool containsAllUnstableElements = false; QVERIFY(containsAllUnstableElements); } bool unstableElementsAreGrouped = ((lastIndex - firstIndex) == (unstable.length() - 1)); QVERIFY(unstableElementsAreGrouped); // now remove all unstable elements from the output for (int i = 1; i < unstable.length(); i++) { output.remove(unstable.at(i)); expected.remove(unstable.at(i)); } } QCOMPARE(output, expected); } void tst_QContactManagerFiltering::multiSorting_data() { QTest::addColumn("cm"); QTest::addColumn("firstsort"); QTest::addColumn("fsdefname"); QTest::addColumn("fsfieldname"); QTest::addColumn("fsdirectioni"); QTest::addColumn("secondsort"); QTest::addColumn("ssdefname"); QTest::addColumn("ssfieldname"); QTest::addColumn("ssdirectioni"); QTest::addColumn("expected"); QTest::addColumn("efunstable"); QTest::addColumn("efgunstable"); QString es; FieldIdentifier firstname = QContactName::FieldFirstName; FieldIdentifier lastname = QContactName::FieldLastName; TypeIdentifier namedef = detailType(); TypeIdentifier phonedef = detailType(); FieldIdentifier numberfield = QContactPhoneNumber::FieldNumber; TypeIdentifier noType(QContactDetail::TypeUndefined); FieldIdentifier noField(-1); int asc = Qt::AscendingOrder; int desc = Qt::DescendingOrder; for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); FieldSelector stringDefAndFieldNames = defAndFieldNamesForTypePerManager.value(manager).value("String"); QTest::newRow("1") << manager << true << namedef << firstname << asc << true << namedef << lastname << asc << "abcdefg" << false << false; QTest::newRow("2") << manager << true << namedef << firstname << asc << true << namedef << lastname << desc << "abcdgfe" << false << false; QTest::newRow("3") << manager << true << namedef << firstname << desc << true << namedef << lastname << asc << "efgdcba" << false << false; QTest::newRow("4") << manager << true << namedef << firstname << desc << true << namedef << lastname << desc << "gfedcba" << false << false; QTest::newRow("5") << manager << true << namedef << firstname << asc << false << namedef << lastname << asc << "abcdefg" << true << true; QTest::newRow("5b") << manager << true << namedef << firstname << asc << true << noType << noField << asc << "abcdefg" << true << true; QTest::newRow("6") << manager << false << namedef << firstname << asc << true << namedef << lastname << asc << "bacdefg" << false << false; // This test is completely unstable; no sort criteria means dependent upon internal sort order of manager. //QTest::newRow("7") << manager // << false << namedef << firstname << asc // << false << namedef << lastname << asc // << "abcdefg" << false; // XXX Isn't this totally unstable? if (validDetailInfo(stringDefAndFieldNames)) { QTest::newRow("8") << manager << true << stringDefAndFieldNames.first << stringDefAndFieldNames.second << asc << false << stringDefAndFieldNames.first << stringDefAndFieldNames.second << desc << "abcdgef" << true << false; // default policy = blanks last, and ef have no value (e is empty, f is null) QTest::newRow("8b") << manager << true << stringDefAndFieldNames.first << stringDefAndFieldNames.second << asc << false << noType << noField << desc << "abcdgef" << true << false; // default policy = blanks last, and ef have no value (e is empty, f is null) } #if 0 QTest::newRow("9") << manager << true << phonedef << numberfield << asc << true << namedef << lastname << desc << "abgfedc" << false; #else // We can't sort by phone number since there may be multiple instances per contact Q_UNUSED(phonedef) Q_UNUSED(numberfield) #endif QTest::newRow("10") << manager << true << namedef << firstname << asc << true << namedef << firstname << desc << "abcdefg" << true << true; } } void tst_QContactManagerFiltering::multiSorting() { QFETCH(QContactManager*, cm); QFETCH(bool, firstsort); QFETCH(TypeIdentifier, fsdefname); QFETCH(FieldIdentifier, fsfieldname); QFETCH(int, fsdirectioni); QFETCH(bool, secondsort); QFETCH(TypeIdentifier, ssdefname); QFETCH(FieldIdentifier, ssfieldname); QFETCH(int, ssdirectioni); QFETCH(QString, expected); QFETCH(bool, efunstable); QFETCH(bool, efgunstable); Qt::SortOrder fsdirection = (Qt::SortOrder)fsdirectioni; Qt::SortOrder ssdirection = (Qt::SortOrder)ssdirectioni; QList contacts = contactsAddedToManagers.values(cm); /* Build the sort orders */ QContactSortOrder fs; setSortDetail(fs, fsdefname, fsfieldname); fs.setDirection(fsdirection); QContactSortOrder ss; setSortDetail(ss, ssdefname, ssfieldname); ss.setDirection(ssdirection); QList sortOrders; if (firstsort) sortOrders.append(fs); if (secondsort) sortOrders.append(ss); QList ids = cm->contactIds(sortOrders); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts // Remove the display label tests output.remove('h'); output.remove('i'); output.remove('j'); output.remove('k'); // Just like the single sort test, we might get some contacts back in indeterminate order // (but their relative position with other contacts should not change) if (efunstable || efgunstable) { QVERIFY(output.count('e') == 1); QVERIFY(output.count('f') == 1); if (efgunstable) { QVERIFY(output.count('g') == 1); } output.remove('f'); expected.remove('f'); if (efgunstable) { output.remove('g'); expected.remove('g'); } } QCOMPARE(output, expected); } void tst_QContactManagerFiltering::idListFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("input"); QTest::addColumn("expected"); QString es; for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); newMRow("empty", manager) << manager << es << es; newMRow("a", manager) << manager << "a" << "a"; newMRow("ab", manager) << manager << "ab" << "ab"; newMRow("aa", manager) << manager << "aa" << "a"; newMRow("ba", manager) << manager << "ba" << "ab"; newMRow("abcd", manager) << manager << "abcd" << "abcd"; newMRow("abcdefg", manager) << manager << "abcdefg" << "abcd"; } } void tst_QContactManagerFiltering::idListFiltering() { QFETCH(QContactManager*, cm); QFETCH(QString, input); QFETCH(QString, expected); QList contacts = contactsAddedToManagers.values(cm); QList ids; // 3 extra ids that (hopefully) won't exist QContactId e = ContactId::apiId(0x54555657, cm->managerUri()); QContactId f = ContactId::apiId(0x96969696, cm->managerUri()); QContactId g = ContactId::apiId(0x44335566, cm->managerUri()); /* Convert the input to a list of ids */ foreach (const QChar &c, input) { if (c == 'a') ids << contacts.at(0); else if (c == 'b') ids << contacts.at(1); else if (c == 'c') ids << contacts.at(2); else if (c == 'd') ids << contacts.at(3); else if (c == 'e') ids << e; else if (c == 'f') ids << f; else if (c == 'g') ids << g; } /* And do the search */ QContactIdFilter idf; idf.setIds(ids); /* Retrieve contacts matching the filter, and compare (unsorted) output */ ids = cm->contactIds(idf); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE_UNSORTED(output, expected); } void tst_QContactManagerFiltering::convenienceFiltering_data() { QTest::addColumn("cm"); QTest::addColumn("addressSubString"); QTest::addColumn("addressEnabled"); QTest::addColumn("emailAddressSubString"); QTest::addColumn("emailEnabled"); QTest::addColumn("phoneSubString"); QTest::addColumn("phoneEnabled"); QTest::addColumn("displayLabelSubString"); QTest::addColumn("displayLabelEnabled"); QTest::addColumn("nameSubString"); QTest::addColumn("nameEnabled"); QTest::addColumn("favoriteEnabled"); QTest::addColumn("tagSubString"); QTest::addColumn("tagEnabled"); QTest::addColumn("expected"); QString es; // empty string QSet allDefs; allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); if (allDefs.contains(detailType())) { newMRow("address matching only", manager) << manager << "streetstring" << true << es << false << es << false << es << false << es << false << false // Favorite has no substring associated. << es << false << "l"; } if (allDefs.contains(detailType())) { newMRow("emailAddress matching only", manager) << manager << es << false << "@test.com" << true << es << false << es << false << es << false << false << es << false << "m"; } if (allDefs.contains(detailType())) { newMRow("phone matching only", manager) << manager << es << false << es << false << "12345" << true << es << false << es << false << false << es << false << "n"; } if (allDefs.contains(detailType())) { newMRow("displayLabel matching only", manager) << manager << es << false << es << false << es << false << "Freddy" << true << es << false << false << es << false << "o"; } if (allDefs.contains(detailType())) { newMRow("name matching only", manager) << manager << es << false << es << false << es << false << es << false << "Frederic" << true << false << es << false << "p"; } if (allDefs.contains(detailType())) { newMRow("favorite matching only", manager) << manager << es << false << es << false << es << false << es << false << es << false << true << es << false << "q"; } if (allDefs.contains(detailType())) { newMRow("tag matching only", manager) << manager << es << false << es << false << es << false << es << false << es << false << false << "Football" << true << "r"; } if (allDefs.contains(detailType()) && allDefs.contains(detailType())) { newMRow("address or phone matching", manager) << manager << "streetstring" << true << es << false << "12345" << true << es << false << es << false << false << es << false << "ln"; } if (allDefs.contains(detailType()) && allDefs.contains(detailType())) { newMRow("favorite or tag matching", manager) << manager << es << false << es << false << es << false << es << false << es << false << true << "Football" << true << "qr"; } } } void tst_QContactManagerFiltering::convenienceFiltering() { QFETCH(QContactManager*, cm); QFETCH(QString, addressSubString); QFETCH(bool, addressEnabled); QFETCH(QString, emailAddressSubString); QFETCH(bool, emailEnabled); QFETCH(QString, phoneSubString); QFETCH(bool, phoneEnabled); QFETCH(QString, displayLabelSubString); QFETCH(bool, displayLabelEnabled); QFETCH(QString, nameSubString); QFETCH(bool, nameEnabled); QFETCH(bool, favoriteEnabled); QFETCH(QString, tagSubString); QFETCH(bool, tagEnabled); QFETCH(QString, expected); QContactFilter af = QContactAddress::match(addressSubString); QContactFilter ef = QContactEmailAddress::match(emailAddressSubString); QContactFilter pf = QContactPhoneNumber::match(phoneSubString); QContactFilter df = QContactDisplayLabel::match(displayLabelSubString); QContactFilter nf = QContactName::match(nameSubString); QContactFilter ff = QContactFavorite::match(); QContactFilter tf = QContactTag::match(tagSubString); QList convenienceFilters; if (addressEnabled) convenienceFilters << af; if (emailEnabled) convenienceFilters << ef; if (phoneEnabled) convenienceFilters << pf; if (displayLabelEnabled) convenienceFilters << df; if (nameEnabled) convenienceFilters << nf; if (favoriteEnabled) convenienceFilters << ff; if (tagEnabled) convenienceFilters << tf; QContactFilter finalFilter; finalFilter = convenienceFilters.at(0); if (convenienceFilters.size() > 1) { for (int i = 1; i < convenienceFilters.size(); ++i) { // if more than one filter, we union them. finalFilter = (finalFilter | convenienceFilters.at(i)); } } /* Retrieve contacts matching the filter, and ensure that the results are expected */ QList ids = cm->contactIds(finalFilter); // build a string containing letters corresponding to the ids we retrieved. QList contacts = contactsAddedToManagers.values(cm); QString resultString = convertIds(contacts, ids, 'l', 'r'); // just the convenience filtering contacts (L->R) QCOMPARE(resultString, expected); } void tst_QContactManagerFiltering::invalidFiltering_data() { QTest::addColumn("cm"); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); QTest::newRow(manager->managerName().toLatin1().constData()) << manager; } } void tst_QContactManagerFiltering::invalidFiltering() { QFETCH(QContactManager*, cm); QList contacts = contactsAddedToManagers.values(cm); QContactInvalidFilter f; // invalid QList ids = cm->contactIds(f); QVERIFY(ids.count() == 0); // Try unions/intersections of invalids too ids = cm->contactIds(f | f); QVERIFY(ids.count() == 0); ids = cm->contactIds(f & f); QVERIFY(ids.count() == 0); } void tst_QContactManagerFiltering::allFiltering_data() { QTest::addColumn("cm"); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); QTest::newRow(manager->managerName().toLatin1().constData()) << manager; } } void tst_QContactManagerFiltering::allFiltering() { QFETCH(QContactManager*, cm); QList contacts = contactsAddedToManagers.values(cm); QContactFilter f; // default = permissive QList ids = cm->contactIds(f); // Remove the "self contact" (aggregate and local) if required if (cm->managerUri().contains(QStringLiteral("org.nemomobile.contacts.sqlite"))) { for (int i = ids.size() - 1; i >= 0; --i) { if (ids[i].localId() == QByteArrayLiteral("sql-1") || ids[i].localId() == QByteArrayLiteral("sql-2")) { ids.removeAt(i); } } } QCOMPARE(ids.count(), contacts.size()); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QString expected = convertIds(contacts, contacts, 'a', 'k'); // :) QCOMPARE_UNSORTED(output, expected); // Try unions/intersections of defaults ids = cm->contactIds(f | f); output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE_UNSORTED(output, expected); ids = cm->contactIds(f & f); output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE_UNSORTED(output, expected); } void tst_QContactManagerFiltering::fetchHint_data() { QTest::addColumn("cm"); for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); QTest::newRow(manager->managerName().toLatin1().constData()) << manager; } } void tst_QContactManagerFiltering::fetchHint() { QFETCH(QContactManager*, cm); // if no fetch hint is provided, the manager should return all data // if a fetch hint is provided, it should have a clearly defined effect, // unless it is ignored by the manager, in which case the result should // be equivalent to not providing a fetch hint. // we use a defined sort order for the retrieval of contacts to make comparison simple. // We sort on name, because we include name details in the fetch hint. QList nameSort; QContactSortOrder firstNameSort, middleNameSort, lastNameSort; firstNameSort.setDetailType(detailType(), QContactName::FieldFirstName); middleNameSort.setDetailType(detailType(), QContactName::FieldMiddleName); lastNameSort.setDetailType(detailType(), QContactName::FieldLastName); nameSort << lastNameSort << middleNameSort << firstNameSort; // fetch all contacts from the manager. QList allContacts = cm->contacts(nameSort); // define some maximum count limit, and some list of detail definitions to retrieve. int countLimit = (allContacts.size() / 2) + 1; QList defs; defs << detailType() << detailType(); // test that the manager doesn't incorrectly implement fetch hints. // we test max count limit, and detail definition limits. // XXX TODO: other hints! QContactFetchHint mclh; // max count limited hint QContactFetchHint ddh; // detail definitions hint mclh.setMaxCountHint(countLimit); ddh.setDetailTypesHint(defs); // the next part of the test requires some contacts to be saved in the manager. if (allContacts.size() == 0) { QSKIP("No contacts in manager; skipping fetch hint limit test."); } // test with a hint which sets a maximum count limit for retrieved contacts. QList mclhContacts = cm->contacts(nameSort, mclh); QVERIFY(allContacts.size() >= mclhContacts.size()); if (allContacts.size() > mclh.maxCountHint()) { // shouldn't return an arbitrarily smaller amount of contacts. QVERIFY(mclhContacts.size() == mclh.maxCountHint() || mclhContacts.size() == allContacts.size()); } for (int i = 0; i < mclhContacts.size(); ++i) { // the sort order should still be defined. QVERIFY(mclhContacts.at(i) == allContacts.at(i)); } // now test with a hint which describes which details the client is interested in. QList ddhContacts = cm->contacts(nameSort, ddh); QCOMPARE(ddhContacts.size(), allContacts.size()); for (int i = 0; i < allContacts.size(); ++i) { QContact a = allContacts.at(i); QContact b = ddhContacts.at(i); // since we're sorting on a detail which should exist (since it was included in the hint) // the order of the contacts returned shouldn't have changed. QVERIFY(a.id() == b.id()); // check that the hint didn't remove names or phones. QCOMPARE(a.details().size(), b.details().size()); QCOMPARE(a.details().size(), b.details().size()); // other details are not necessarily returned. QVERIFY(a.details().size() >= b.details().size()); } } #if 0 void tst_QContactManagerFiltering::changelogFiltering_data() { QTest::addColumn("cm"); QTest::addColumn >("contacts"); QTest::addColumn("eventType"); QTest::addColumn("since"); QTest::addColumn("expected"); int added = (int)QContactChangeLogFilter::EventAdded; int changed = (int)QContactChangeLogFilter::EventChanged; int removed = (int)QContactChangeLogFilter::EventRemoved; for (int i = 0; i < managers.size(); i++) { QContactManager *manager = managers.at(i); if (manager->hasFeature(QContactManager::ChangeLogs)) { QList contacts = contactsAddedToManagers.values(manager); QContact a,b,c,d; a = manager->contact(contacts.at(0)); b = manager->contact(contacts.at(1)); c = manager->contact(contacts.at(2)); d = manager->contact(contacts.at(3)); QDateTime ac = a.detail().created(); QDateTime bc = b.detail().created(); QDateTime cc = c.detail().created(); QDateTime dc = d.detail().created(); QDateTime am = a.detail().lastModified(); QDateTime bm = b.detail().lastModified(); QDateTime cm = c.detail().lastModified(); QDateTime dm = d.detail().lastModified(); newMRow("Added since before start", manager) << manager << contacts << added << ac.addSecs(-1) << "abcdefg"; newMRow("Added since first", manager) << manager << contacts << added << ac << "abcdefg"; newMRow("Added since second", manager) << manager << contacts << added << bc << "bcdefg"; newMRow("Added since third", manager) << manager << contacts << added << cc << "cdefg"; newMRow("Added since fourth", manager) << manager << contacts << added << dc << "defg"; newMRow("Added since after fourth", manager) << manager << contacts << added << dc.addSecs(1) << "efg"; newMRow("Added since first changed", manager) << manager << contacts << added << am << ""; newMRow("Added since second changed", manager) << manager << contacts << added << bm << ""; newMRow("Added since third changed", manager) << manager << contacts << added << cm << ""; newMRow("Added since fourth changed", manager) << manager << contacts << added << cm << ""; newMRow("Changed since before start", manager) << manager << contacts << changed << ac.addSecs(-1) << "abcdefg"; newMRow("Changed since first", manager) << manager << contacts << changed << ac << "abcdefg"; newMRow("Changed since second", manager) << manager << contacts << changed << bc << "abcdefg"; newMRow("Changed since third", manager) << manager << contacts << changed << cc << "abcdefg"; newMRow("Changed since fourth", manager) << manager << contacts << changed << dc << "abcdefg"; newMRow("Changed since after fourth", manager) << manager << contacts << changed << dc.addSecs(1) << "abcefg"; newMRow("Changed since first changed", manager) << manager << contacts << changed << am << "a"; newMRow("Changed since second changed", manager) << manager << contacts << changed << bm << "ab"; newMRow("Changed since third changed", manager) << manager << contacts << changed << cm << "abc"; newMRow("Changed since fourth changed", manager) << manager << contacts << changed << dm << "abcdefg"; // These are currently useless.. newMRow("Removed since before start", manager) << manager << contacts << removed << ac.addSecs(-1) << ""; newMRow("Removed since first", manager) << manager << contacts << removed << ac << ""; newMRow("Removed since second", manager) << manager << contacts << removed << bc << ""; newMRow("Removed since third", manager) << manager << contacts << removed << cc << ""; newMRow("Removed since fourth", manager) << manager << contacts << removed << dc << ""; newMRow("Removed since after fourth", manager) << manager << contacts << removed << dc.addSecs(1) << ""; } else { // Stop spam and asserts with a single row newMRow("Unsupported", manager) << manager << QList() << added << QDateTime() << QString(); } } } void tst_QContactManagerFiltering::changelogFiltering() { QFETCH(int, eventType); QFETCH(QDateTime, since); QFETCH(QString, expected); QFETCH(QContactManager*, cm); QFETCH(QList, contacts); if (cm->hasFeature(QContactManager::ChangeLogs)) { QList ids; QContactChangeLogFilter clf((QContactChangeLogFilter::EventType)eventType); clf.setSince(since); ids = cm->contactIds(clf); QString output = convertIds(contacts, ids, 'a', 'k'); // don't include the convenience filtering contacts QCOMPARE(output, expected); // unsorted? or sorted? } else { QSKIP("Changelogs not supported by this manager."); } } #endif #ifdef DETAIL_DEFINITION_SUPPORTED QPair tst_QContactManagerFiltering::definitionAndField(QContactManager *cm, QVariant::Type type, bool *nativelyFilterable) { QPair result; QString definitionName, fieldName; // step one: search for an existing definition with a field of the specified type QMap allDefs = cm->detailDefinitions(); QStringList defNames = allDefs.keys(); bool found = false; bool isNativelyFilterable = false; foreach (const QString& defName, defNames) { // check the current definition. QContactDetailDefinition def = allDefs.value(defName); // if unique, we cannot use this definition. if (def.isUnique()) { continue; } // if read only, we cannot use this definition. // special case these, since read-only is reported via details, not definitions... if (def.name() == QString(QLatin1String(QContactName::DefinitionName)) || def.name() == QString(QLatin1String(QContactPresence::DefinitionName)) || def.name() == QString(QLatin1String(QContactGlobalPresence::DefinitionName))) { continue; } // grab the fields and search for a field of the required type QMap allFields = def.fields(); QList fNames = allFields.keys(); foreach (const QString& fName, fNames) { QContactDetailFieldDefinition field = allFields.value(fName); if (field.dataType() == type) { // this field of the current definition is of the required type. definitionName = defName; fieldName = fName; found = true; // step two: check to see whether the definition/field is natively filterable QContactDetailFilter filter; filter.setDetailDefinitionName(definitionName, fieldName); bool isNativelyFilterable = cm->isFilterSupported(filter); if (isNativelyFilterable) { // we've found the optimal definition + field for our test. break; } } } if (found && isNativelyFilterable) { // we've found the optimal definition + field for our test. break; } } if (found) { // whether it is natively filterable or not, we found a definition that matches our requirements. result.first = definitionName; result.second = fieldName; *nativelyFilterable = isNativelyFilterable; return result; } // step three (or, if not step one): check to see whether the manager allows mutable definitions // no existing definition matched our requirements, but we might be able to add one that does. if (cm->supportedDataTypes().contains(type) && cm->hasFeature(QContactManager::MutableDefinitions)) { // ok, the manager does not have a definition matching our criteria, but we could probably add it. int defCount = detailDefinitionsAddedToManagers.values(cm).count(); QString generatedDefinitionName = QString("x-nokia-mobility-contacts-test-definition-") + QString::number((defCount+1)); // build a definition that matches the criteria. QContactDetailDefinition generatedDefinition; generatedDefinition.setName(generatedDefinitionName); QContactDetailFieldDefinition generatedField; generatedField.setDataType(type); QMap fields; fields.insert("generatedField", generatedField); generatedDefinition.setFields(fields); generatedDefinition.setUnique(false); // attempt to save it to the manager. if (cm->saveDetailDefinition(generatedDefinition)) { // successfully added definition. definitionName = generatedDefinitionName; fieldName = "generatedField"; detailDefinitionsAddedToManagers.insert(cm, definitionName); // cleanup stack. } } else { qWarning() << "Unable to perform tests involving detail values of the" << type << "type: not supported by manager:" << cm->managerName(); } result.first = definitionName; result.second = fieldName; *nativelyFilterable = false; return result; } #endif QList tst_QContactManagerFiltering::prepareModel(QContactManager *cm) { #ifdef DETAIL_DEFINITION_SUPPORTED /* Discover the definition and field names required for testing */ QMap > definitionDetails; // per value type string QPair defAndFieldNames; bool nativelyFilterable; // If the engine doesn't support changelogs, don't insert pauses. bool supportsChangelog = cm->hasFeature(QContactManager::ChangeLogs); const int napTime = supportsChangelog ? 2000 : 1; /* For our test actions: memory engine, add the "special" definitions. */ if (cm->managerName() == QString(QLatin1String("memory"))) { QContactDetailDefinition def; QContactDetailFieldDefinition field; QMap fields; // integer def.setName("IntegerDefinition"); field.setDataType(QVariant::Int); field.setAllowableValues(QVariantList()); fields.clear(); fields.insert("IntegerField", field); def.setFields(fields); defAndFieldNames = QPair("IntegerDefinition", "IntegerField"); definitionDetails.insert("Integer", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); cm->saveDetailDefinition(def, QContactType::TypeContact); // double def.setName("DoubleDefinition"); field.setDataType(QVariant::Double); field.setAllowableValues(QVariantList()); fields.clear(); fields.insert("DoubleField", field); def.setFields(fields); defAndFieldNames = QPair("DoubleDefinition", "DoubleField"); definitionDetails.insert("Double", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); cm->saveDetailDefinition(def, QContactType::TypeContact); // boolean def.setName("BooleanDefinition"); field.setDataType(QVariant::Bool); field.setAllowableValues(QVariantList()); fields.clear(); fields.insert("BooleanField", field); def.setFields(fields); defAndFieldNames = QPair("BooleanDefinition", "BooleanField"); definitionDetails.insert("Boolean", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); cm->saveDetailDefinition(def, QContactType::TypeContact); // date def.setName("DateDefinition"); field.setDataType(QVariant::Date); field.setAllowableValues(QVariantList()); fields.clear(); fields.insert("DateField", field); def.setFields(fields); defAndFieldNames = QPair("DateDefinition", "DateField"); definitionDetails.insert("Date", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); cm->saveDetailDefinition(def, QContactType::TypeContact); } /* String */ /* Override this to ensure we select a string field that supports sorting: defAndFieldNames = definitionAndField(cm, QVariant::String, &nativelyFilterable); if (!defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { definitionDetails.insert("String", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); */ definitionDetails.insert("String", qMakePair(QString(QLatin1String(QContactGuid::DefinitionName)), QString(QLatin1String(QContactGuid::FieldGuid)))); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); /* Integer */ defAndFieldNames = definitionAndField(cm, QVariant::Int, &nativelyFilterable); if (cm->managerName() != "memory" && !defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { // we don't insert for memory engine, as we already handled this above (for action filtering) definitionDetails.insert("Integer", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* Date time detail */ defAndFieldNames = definitionAndField(cm, QVariant::DateTime, &nativelyFilterable); if (!defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { definitionDetails.insert("DateTime", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* double detail */ defAndFieldNames = definitionAndField(cm, QVariant::Double, &nativelyFilterable); if (cm->managerName() != "memory" && !defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { // we don't insert for memory engine, as we already handled this above (for action filtering) definitionDetails.insert("Double", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* bool */ defAndFieldNames = definitionAndField(cm, QVariant::Bool, &nativelyFilterable); if (cm->managerName() != "memory" && !defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { // we don't insert for memory engine, as we already handled this above (for action filtering) definitionDetails.insert("Bool", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* long long */ defAndFieldNames = definitionAndField(cm, QVariant::LongLong, &nativelyFilterable); if (!defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { definitionDetails.insert("LongLong", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* unsigned long long */ defAndFieldNames = definitionAndField(cm, QVariant::ULongLong, &nativelyFilterable); if (!defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { definitionDetails.insert("ULongLong", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* date */ defAndFieldNames = definitionAndField(cm, QVariant::Date, &nativelyFilterable); if (cm->managerName() != "memory" && !defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { // we don't insert for memory engine, as we already handled this above (for action filtering) definitionDetails.insert("Date", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* time */ defAndFieldNames = definitionAndField(cm, QVariant::Time, &nativelyFilterable); if (!defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { definitionDetails.insert("Time", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* uint */ defAndFieldNames = definitionAndField(cm, QVariant::UInt, &nativelyFilterable); if (!defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { definitionDetails.insert("UInt", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); /* char */ defAndFieldNames = definitionAndField(cm, QVariant::Char, &nativelyFilterable); if (!defAndFieldNames.first.isEmpty() && !defAndFieldNames.second.isEmpty()) { definitionDetails.insert("Char", defAndFieldNames); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); } defAndFieldNames.first = QString(); defAndFieldNames.second = QString(); #else const int napTime = 1; #endif QMap > definitionDetails; //definitionDetails.insert("String", qMakePair(QContactOrganization::Type, static_cast(QContactOrganization::FieldName))); definitionDetails.insert("String", qMakePair(QContactGuid::Type, static_cast(QContactGuid::FieldGuid))); definitionDetails.insert("Integer", qMakePair(QContactPresence::Type, static_cast(QContactPresence::FieldPresenceState))); definitionDetails.insert("DateTime", qMakePair(QContactTimestamp::Type, static_cast(QContactTimestamp::FieldCreationTimestamp))); definitionDetails.insert("Date", qMakePair(QContactBirthday::Type, static_cast(QContactBirthday::FieldBirthday))); definitionDetails.insert("Bool", qMakePair(QContactFavorite::Type, static_cast(QContactFavorite::FieldFavorite))); // Only used by geolocation - not supported by qtcontacts-sqlite definitionDetails.insert("Double", qMakePair(QContactDetail::TypeUndefined, -1)); // Unused: definitionDetails.insert("LongLong", qMakePair(QContactDetail::TypeUndefined, -1)); definitionDetails.insert("ULongLong", qMakePair(QContactDetail::TypeUndefined, -1)); definitionDetails.insert("Time", qMakePair(QContactDetail::TypeUndefined, -1)); definitionDetails.insert("UInt", qMakePair(QContactDetail::TypeUndefined, -1)); definitionDetails.insert("Char", qMakePair(QContactDetail::TypeUndefined, -1)); defAndFieldNamesForTypePerManager.insert(cm, definitionDetails); /* Add some contacts */ QContact contactA, contactB, contactC, contactD; QContactName name; QContactPhoneNumber number; QContactDetail string(definitionDetails.value("String").first); QContactDetail integer(definitionDetails.value("Integer").first); QContactDetail datetime(definitionDetails.value("DateTime").first); QContactDetail dubble(definitionDetails.value("Double").first); QContactDetail boool(definitionDetails.value("Bool").first); QContactDetail llong(definitionDetails.value("LongLong").first); QContactDetail ullong(definitionDetails.value("ULongLong").first); QContactDetail date(definitionDetails.value("Date").first); QContactDetail time(definitionDetails.value("Time").first); QContactDetail uintt(definitionDetails.value("UInt").first); QContactDetail charr(definitionDetails.value("Char").first); name.setFirstName("Aaron"); name.setLastName("Aaronson"); #ifdef DETAIL_DEFINITION_SUPPORTED if (cm->detailDefinition(QContactName::DefinitionName).fields().contains(QContactName::FieldMiddleName)) #endif name.setMiddleName("Arne"); #ifdef DETAIL_DEFINITION_SUPPORTED if (cm->detailDefinition(QContactName::DefinitionName).fields().contains(QContactName::FieldPrefix)) #endif name.setPrefix("Sir"); #ifdef DETAIL_DEFINITION_SUPPORTED if (cm->detailDefinition(QContactName::DefinitionName).fields().contains(QContactName::FieldSuffix)) #endif name.setSuffix("Dr."); QContactNickname nick; nick.setNickname("Sir Aaron"); QContactEmailAddress emailAddr; emailAddr.setEmailAddress("Aaron@Aaronson.com"); number.setNumber("5551212"); string.setValue(definitionDetails.value("String").second, "Aaron Aaronson"); integer.setValue(definitionDetails.value("Integer").second, 3); // QContactPresence::PresenceBusy datetime.setValue(definitionDetails.value("DateTime").second, QDateTime(QDate(2009, 06, 29), QTime(16, 52, 23, 0))); boool.setValue(definitionDetails.value("Bool").second, true); ullong.setValue(definitionDetails.value("ULongLong").second, (qulonglong)120000000000LL); // 120B date.setValue(definitionDetails.value("Date").second, QDate(1988, 1, 26)); time.setValue(definitionDetails.value("Time").second, QTime(16,52,23,0)); contactA.saveDetail(&name); contactA.saveDetail(&nick); contactA.saveDetail(&emailAddr); contactA.saveDetail(&number); if (validDetailInfo(definitionDetails.value("String"))) contactA.saveDetail(&string); if (validDetailInfo(definitionDetails.value("Integer"))) contactA.saveDetail(&integer); if (validDetailInfo(definitionDetails.value("DateTime"))) contactA.saveDetail(&datetime); if (validDetailInfo(definitionDetails.value("Bool"))) contactA.saveDetail(&boool); if (validDetailInfo(definitionDetails.value("ULongLong"))) contactA.saveDetail(&ullong); if (validDetailInfo(definitionDetails.value("Date"))) contactA.saveDetail(&date); if (validDetailInfo(definitionDetails.value("Time"))) contactA.saveDetail(&time); name = QContactName(); name.setFirstName("Bob"); name.setLastName("Aaronsen"); nick.setNickname("Sir Bob"); number.setNumber("+55553456"); string.setValue(definitionDetails.value("String").second, "Bob Aaronsen"); integer.setValue(definitionDetails.value("Integer").second, 20); dubble.setValue(definitionDetails.value("Double").second, 4.0); boool.setValue(definitionDetails.value("Bool").second, false); ullong.setValue(definitionDetails.value("ULongLong").second, (qulonglong) 80000000000LL); // 80B uintt.setValue(definitionDetails.value("UInt").second, 4000000000u); // 4B date.setValue(definitionDetails.value("Date").second, QDate(2492, 5, 5)); time.setValue(definitionDetails.value("Time").second, QTime(15,52,23,0)); charr.setValue(definitionDetails.value("Char").second, QVariant(QChar('b'))); contactB.saveDetail(&name); contactB.saveDetail(&nick); contactB.saveDetail(&number); if (validDetailInfo(definitionDetails.value("String"))) contactB.saveDetail(&string); if (validDetailInfo(definitionDetails.value("Integer"))) contactB.saveDetail(&integer); if (validDetailInfo(definitionDetails.value("Double"))) contactB.saveDetail(&dubble); if (validDetailInfo(definitionDetails.value("Bool"))) contactB.saveDetail(&boool); if (validDetailInfo(definitionDetails.value("ULongLong"))) contactB.saveDetail(&ullong); if (validDetailInfo(definitionDetails.value("UInt"))) contactB.saveDetail(&uintt); if (validDetailInfo(definitionDetails.value("Date"))) contactB.saveDetail(&date); if (validDetailInfo(definitionDetails.value("Time"))) contactB.saveDetail(&time); if (validDetailInfo(definitionDetails.value("Char"))) contactB.saveDetail(&charr); name.setFirstName("Boris"); name.setLastName("Aaronsun"); string.setValue(definitionDetails.value("String").second, "Boris Aaronsun"); integer.setValue(definitionDetails.value("Integer").second, -20); datetime.setValue(definitionDetails.value("DateTime").second, QDateTime(QDate(2009, 06, 29), QTime(16, 54, 17, 0))); llong.setValue(definitionDetails.value("LongLong").second, (qlonglong)8000000000LL); // 8B charr.setValue(definitionDetails.value("Char").second, QVariant(QChar('c'))); contactC.saveDetail(&name); if (validDetailInfo(definitionDetails.value("String"))) contactC.saveDetail(&string); if (validDetailInfo(definitionDetails.value("Integer"))) contactC.saveDetail(&integer); if (validDetailInfo(definitionDetails.value("DateTime"))) contactC.saveDetail(&datetime); if (validDetailInfo(definitionDetails.value("Double"))) contactC.saveDetail(&dubble); if (validDetailInfo(definitionDetails.value("Bool"))) contactC.saveDetail(&boool); if (validDetailInfo(definitionDetails.value("LongLong"))) contactC.saveDetail(&llong); if (validDetailInfo(definitionDetails.value("ULongLong"))) contactC.saveDetail(&ullong); if (validDetailInfo(definitionDetails.value("Char"))) contactC.saveDetail(&charr); name.setFirstName("Dennis"); name.setLastName("FitzMacintyre"); string.setValue(definitionDetails.value("String").second, "Dennis FitzMacintyre"); dubble.setValue(definitionDetails.value("Double").second, -128.0); llong.setValue(definitionDetails.value("LongLong").second, (qlonglong)-14000000000LL); uintt.setValue(definitionDetails.value("UInt").second, 3000000000u); // 3B date.setValue(definitionDetails.value("Date").second, QDate(2770, 10, 1)); contactD.saveDetail(&name); if (validDetailInfo(definitionDetails.value("String"))) contactD.saveDetail(&string); if (validDetailInfo(definitionDetails.value("Double"))) contactD.saveDetail(&dubble); if (validDetailInfo(definitionDetails.value("LongLong"))) contactD.saveDetail(&llong); if (validDetailInfo(definitionDetails.value("UInt"))) contactD.saveDetail(&uintt); if (validDetailInfo(definitionDetails.value("Date"))) contactD.saveDetail(&date); qDebug() << "Generating contacts with different timestamps, please wait.."; int originalContactCount = cm->contactIds().count(); bool successfulSave = cm->saveContact(&contactA); Q_ASSERT_VERIFY(successfulSave); QTest::qSleep(napTime); successfulSave = cm->saveContact(&contactB); Q_ASSERT_VERIFY(successfulSave); QTest::qSleep(napTime); successfulSave = cm->saveContact(&contactC); Q_ASSERT_VERIFY(successfulSave); QTest::qSleep(napTime); successfulSave = cm->saveContact(&contactD); Q_ASSERT_VERIFY(successfulSave); QTest::qSleep(napTime); /* Now add some contacts specifically for multisorting */ QContact contactE,contactF,contactG; QContactName n; n.setFirstName("John"); n.setLastName("Smithee"); string.setValue(definitionDetails.value("String").second, ""); if (validDetailInfo(definitionDetails.value("String"))) contactE.saveDetail(&string); contactE.saveDetail(&n); n = QContactName(); n.setFirstName("John"); n.setLastName("Smithey"); contactF.saveDetail(&n); n = QContactName(); n.setFirstName("John"); n.setLastName("Smithy"); string.setValue(definitionDetails.value("String").second, "zzz"); if (validDetailInfo(definitionDetails.value("String"))) contactG.saveDetail(&string); contactG.saveDetail(&n); successfulSave = cm->saveContact(&contactE); Q_ASSERT_VERIFY(successfulSave); successfulSave = cm->saveContact(&contactF); Q_ASSERT_VERIFY(successfulSave); successfulSave = cm->saveContact(&contactG); Q_ASSERT_VERIFY(successfulSave); originalContactCount += 7; Q_FATAL_VERIFY(cm->contactIds().count() == originalContactCount); /* Now some for the locale aware sorting */ QContact contactH, contactI, contactJ, contactK; QContactName n2; n2.setFirstName("xander"); #ifdef CUSTOM_LABEL_SUPPORTED n2.setCustomLabel("xander"); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) n2.setValue(QContactName::FieldCustomLabel, "xander"); #endif contactH.saveDetail(&n2); n2.setFirstName("Xander"); #ifdef CUSTOM_LABEL_SUPPORTED n2.setCustomLabel("Xander"); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) n2.setValue(QContactName::FieldCustomLabel, "Xander"); #endif contactI.saveDetail(&n2); n2.setFirstName("xAnder"); #ifdef CUSTOM_LABEL_SUPPORTED n2.setCustomLabel("xAnder"); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) n2.setValue(QContactName::FieldCustomLabel, "xAnder"); #endif contactJ.saveDetail(&n2); n2.setFirstName("Yarrow"); #ifdef CUSTOM_LABEL_SUPPORTED n2.setCustomLabel("Yarrow"); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) n2.setValue(QContactName::FieldCustomLabel, "Yarrow"); #endif contactK.saveDetail(&n2); // XXX add äaut; or âum; etc to test those sort orders #ifdef COMPATIBLE_CONTACT_SUPPORTED contactH = cm->compatibleContact(contactH); contactI = cm->compatibleContact(contactI); contactJ = cm->compatibleContact(contactJ); contactK = cm->compatibleContact(contactK); #endif Q_ASSERT_VERIFY(cm->saveContact(&contactH)); Q_ASSERT_VERIFY(cm->saveContact(&contactI)); Q_ASSERT_VERIFY(cm->saveContact(&contactJ)); Q_ASSERT_VERIFY(cm->saveContact(&contactK)); /* Ensure the last modified times are different */ QTest::qSleep(napTime); QContactName modifiedName = contactC.detail(); #ifdef CUSTOM_LABEL_SUPPORTED modifiedName.setCustomLabel("Clarence"); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) modifiedName.setValue(QContactName::FieldCustomLabel, "Clarence"); #endif contactC.saveDetail(&modifiedName); cm->saveContact(&contactC); QTest::qSleep(napTime); modifiedName = contactB.detail(); #ifdef CUSTOM_LABEL_SUPPORTED modifiedName.setCustomLabel("Boris"); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) modifiedName.setValue(QContactName::FieldCustomLabel, "Boris"); #endif contactB.saveDetail(&modifiedName); cm->saveContact(&contactB); QTest::qSleep(napTime); modifiedName = contactA.detail(); #ifdef CUSTOM_LABEL_SUPPORTED modifiedName.setCustomLabel("Albert"); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) modifiedName.setValue(QContactName::FieldCustomLabel, "Albert"); #endif contactA.saveDetail(&modifiedName); cm->saveContact(&contactA); QTest::qSleep(napTime); /* Now some for convenience filtering */ QSet allDefs; allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); allDefs.insert(detailType()); // Contact L ---------------------------------------- QContact contactL; if (allDefs.contains(detailType())) { QContactAddress ladr; ladr.setStreet("streetstring road"); // Contact L matches streetstring. ladr.setLocality("testplace"); ladr.setRegion("somewhere"); contactL.saveDetail(&ladr); } if (allDefs.contains(detailType())) { QContactEmailAddress led; led.setEmailAddress("frad@test.domain"); contactL.saveDetail(&led); } if (allDefs.contains(detailType())) { QContactPhoneNumber lp; lp.setNumber("11111"); contactL.saveDetail(&lp); } if (allDefs.contains(detailType())) { QContactName ln; ln.setFirstName("Fradarick"); ln.setLastName("Gumboots"); contactL.saveDetail(&ln); } if (allDefs.contains(detailType())) { QContactTag lt; lt.setTag("Soccer"); contactL.saveDetail(<); } // Contact M ---------------------------------------- QContact contactM; if (allDefs.contains(detailType())) { QContactAddress madr; madr.setStreet("some road"); madr.setLocality("testplace"); madr.setRegion("somewhere"); contactM.saveDetail(&madr); } if (allDefs.contains(detailType())) { QContactEmailAddress med; med.setEmailAddress("frbd@test.com"); // Contact M matches @test.com contactM.saveDetail(&med); } if (allDefs.contains(detailType())) { QContactPhoneNumber mp; mp.setNumber("22222"); contactM.saveDetail(&mp); } if (allDefs.contains(detailType())) { QContactName mn; mn.setFirstName("Frbdbrick"); mn.setLastName("Gumboots"); contactM.saveDetail(&mn); } if (allDefs.contains(detailType())) { QContactTag mt; mt.setTag("Soccer"); contactM.saveDetail(&mt); } // Contact N ---------------------------------------- QContact contactN; if (allDefs.contains(detailType())) { QContactAddress nadr; nadr.setStreet("some road"); nadr.setLocality("testplace"); nadr.setRegion("somewhere"); contactN.saveDetail(&nadr); } if (allDefs.contains(detailType())) { QContactEmailAddress ned; ned.setEmailAddress("frcd@test.domain"); contactN.saveDetail(&ned); } if (allDefs.contains(detailType())) { QContactPhoneNumber np; np.setNumber("12345"); // Contact N matches 12345 contactN.saveDetail(&np); } if (allDefs.contains(detailType())) { QContactName nn; nn.setFirstName("Frcdcrick"); nn.setLastName("Gumboots"); contactN.saveDetail(&nn); } if (allDefs.contains(detailType())) { QContactTag nt; nt.setTag("Soccer"); contactN.saveDetail(&nt); } // Contact O ---------------------------------------- QContact contactO; if (allDefs.contains(detailType())) { QContactAddress oadr; oadr.setStreet("some road"); oadr.setLocality("testplace"); oadr.setRegion("somewhere"); contactO.saveDetail(&oadr); } if (allDefs.contains(detailType())) { QContactEmailAddress oed; oed.setEmailAddress("frdd@test.domain"); contactO.saveDetail(&oed); } if (allDefs.contains(detailType())) { QContactPhoneNumber op; op.setNumber("44444"); contactO.saveDetail(&op); } if (allDefs.contains(detailType())) { QContactName on; on.setFirstName("Freddy"); // Contact O matches Freddy on.setLastName("Gumboots"); contactO.saveDetail(&on); } if (allDefs.contains(detailType())) { QContactTag ot; ot.setTag("Soccer"); contactO.saveDetail(&ot); } // Contact P ---------------------------------------- QContact contactP; if (allDefs.contains(detailType())) { QContactAddress padr; padr.setStreet("some road"); padr.setLocality("testplace"); padr.setRegion("somewhere"); contactP.saveDetail(&padr); } if (allDefs.contains(detailType())) { QContactEmailAddress ped; ped.setEmailAddress("fred@test.domain"); contactP.saveDetail(&ped); } if (allDefs.contains(detailType())) { QContactPhoneNumber pp; pp.setNumber("55555"); contactP.saveDetail(&pp); } if (allDefs.contains(detailType())) { QContactName pn; pn.setFirstName("Frederick"); // Contact P matches Frederic (contains). pn.setLastName("Gumboots"); contactP.saveDetail(&pn); } if (allDefs.contains(detailType())) { QContactTag pt; pt.setTag("Soccer"); contactP.saveDetail(&pt); } // Contact Q ---------------------------------------- QContact contactQ; if (allDefs.contains(detailType())) { QContactAddress qadr; qadr.setStreet("some road"); qadr.setLocality("testplace"); qadr.setRegion("somewhere"); contactQ.saveDetail(&qadr); } if (allDefs.contains(detailType())) { QContactEmailAddress qed; qed.setEmailAddress("frfd@test.domain"); contactQ.saveDetail(&qed); } if (allDefs.contains(detailType())) { QContactPhoneNumber qp; qp.setNumber("66666"); contactQ.saveDetail(&qp); } if (allDefs.contains(detailType())) { QContactName qn; qn.setFirstName("Frfdfrick"); qn.setLastName("Gumboots"); contactQ.saveDetail(&qn); } if (allDefs.contains(detailType())) { QContactTag qt; qt.setTag("Soccer"); contactQ.saveDetail(&qt); } if (allDefs.contains(detailType())) { QContactFavorite qf; qf.setFavorite(true); // Contact Q matches favorite = true contactQ.saveDetail(&qf); } // Contact R ---------------------------------------- QContact contactR; if (allDefs.contains(detailType())) { QContactAddress radr; radr.setStreet("some road"); radr.setLocality("testplace"); radr.setRegion("somewhere"); contactR.saveDetail(&radr); } if (allDefs.contains(detailType())) { QContactEmailAddress red; red.setEmailAddress("frgd@test.domain"); contactR.saveDetail(&red); } if (allDefs.contains(detailType())) { QContactPhoneNumber rp; rp.setNumber("77777"); contactR.saveDetail(&rp); } if (allDefs.contains(detailType())) { QContactName rn; rn.setFirstName("Frgdgrick"); rn.setLastName("Gumboots"); contactR.saveDetail(&rn); } if (allDefs.contains(detailType())) { QContactTag rt; rt.setTag("Football"); // Contact R matches Football contactR.saveDetail(&rt); } #ifdef COMPATIBLE_CONTACT_SUPPORTED // --------------------- save. contactL = cm->compatibleContact(contactL); contactM = cm->compatibleContact(contactM); contactN = cm->compatibleContact(contactN); contactO = cm->compatibleContact(contactO); contactP = cm->compatibleContact(contactP); contactQ = cm->compatibleContact(contactQ); contactR = cm->compatibleContact(contactR); #endif Q_ASSERT_VERIFY(cm->saveContact(&contactL)); Q_ASSERT_VERIFY(cm->saveContact(&contactM)); Q_ASSERT_VERIFY(cm->saveContact(&contactN)); Q_ASSERT_VERIFY(cm->saveContact(&contactO)); Q_ASSERT_VERIFY(cm->saveContact(&contactP)); Q_ASSERT_VERIFY(cm->saveContact(&contactQ)); Q_ASSERT_VERIFY(cm->saveContact(&contactR)); // --------------------- end. /* Add our newly saved contacts to our internal list of added contacts */ contactsAddedToManagers.insert(cm, contactR.id()); contactsAddedToManagers.insert(cm, contactQ.id()); contactsAddedToManagers.insert(cm, contactP.id()); contactsAddedToManagers.insert(cm, contactO.id()); contactsAddedToManagers.insert(cm, contactN.id()); contactsAddedToManagers.insert(cm, contactM.id()); contactsAddedToManagers.insert(cm, contactL.id()); contactsAddedToManagers.insert(cm, contactK.id()); contactsAddedToManagers.insert(cm, contactJ.id()); contactsAddedToManagers.insert(cm, contactI.id()); contactsAddedToManagers.insert(cm, contactH.id()); contactsAddedToManagers.insert(cm, contactG.id()); contactsAddedToManagers.insert(cm, contactF.id()); contactsAddedToManagers.insert(cm, contactE.id()); contactsAddedToManagers.insert(cm, contactD.id()); contactsAddedToManagers.insert(cm, contactC.id()); contactsAddedToManagers.insert(cm, contactB.id()); contactsAddedToManagers.insert(cm, contactA.id()); /* Reload the contacts to pick up any changes */ contactA = cm->contact(retrievalId(contactA)); contactB = cm->contact(retrievalId(contactB)); contactC = cm->contact(retrievalId(contactC)); contactD = cm->contact(retrievalId(contactD)); contactE = cm->contact(retrievalId(contactE)); contactF = cm->contact(retrievalId(contactF)); contactG = cm->contact(retrievalId(contactG)); contactH = cm->contact(retrievalId(contactH)); contactI = cm->contact(retrievalId(contactI)); contactJ = cm->contact(retrievalId(contactJ)); contactK = cm->contact(retrievalId(contactK)); contactL = cm->contact(retrievalId(contactL)); contactM = cm->contact(retrievalId(contactM)); contactN = cm->contact(retrievalId(contactN)); contactO = cm->contact(retrievalId(contactO)); contactP = cm->contact(retrievalId(contactP)); contactQ = cm->contact(retrievalId(contactQ)); contactR = cm->contact(retrievalId(contactR)); QList list; if (!contactA.isEmpty()) list << contactA.id(); if (!contactB.isEmpty()) list << contactB.id(); if (!contactC.isEmpty()) list << contactC.id(); if (!contactD.isEmpty()) list << contactD.id(); if (!contactE.isEmpty()) list << contactE.id(); if (!contactF.isEmpty()) list << contactF.id(); if (!contactG.isEmpty()) list << contactG.id(); if (!contactH.isEmpty()) list << contactH.id(); if (!contactI.isEmpty()) list << contactI.id(); if (!contactJ.isEmpty()) list << contactJ.id(); if (!contactK.isEmpty()) list << contactK.id(); if (!contactL.isEmpty()) list << contactL.id(); if (!contactM.isEmpty()) list << contactM.id(); if (!contactN.isEmpty()) list << contactN.id(); if (!contactO.isEmpty()) list << contactO.id(); if (!contactP.isEmpty()) list << contactP.id(); if (!contactQ.isEmpty()) list << contactQ.id(); if (!contactR.isEmpty()) list << contactR.id(); return list; } /* ============ Utility functions ============= */ void tst_QContactManagerFiltering::dumpContactDifferences(const QContact& ca, const QContact& cb) { // Try to narrow down the differences QContact a(ca); QContact b(cb); QContactName n1 = a.detail(); QContactName n2 = b.detail(); // Check the name components in more detail QCOMPARE(n1.firstName(), n2.firstName()); QCOMPARE(n1.middleName(), n2.middleName()); QCOMPARE(n1.lastName(), n2.lastName()); QCOMPARE(n1.prefix(), n2.prefix()); QCOMPARE(n1.suffix(), n2.suffix()); #ifdef CUSTOM_LABEL_SUPPORTED QCOMPARE(n1.customLabel(), n2.customLabel()); #elif defined (CUSTOM_LABEL_STORAGE_SUPPORTED) QCOMPARE(n1.value(QContactName::FieldCustomLabel), n2.value(QContactName::FieldCustomLabel)); #endif #ifdef DISPLAY_LABEL_SUPPORTED // Check the display label QCOMPARE(a.displayLabel(), b.displayLabel()); #endif // Now look at the rest QList aDetails = a.details(); QList bDetails = b.details(); // They can be in any order, so loop // First remove any matches foreach(QContactDetail d, aDetails) { foreach(QContactDetail d2, bDetails) { if(d == d2) { a.removeDetail(&d); b.removeDetail(&d2); break; } } } // Now dump the extra details that were unmatched in A aDetails = a.details(); bDetails = b.details(); foreach (const QContactDetail &d, aDetails) { if (detailType(d) != detailType()) qDebug() << "A contact had extra detail:" << detailTypeName(d) << detailValues(d); } // and same for B foreach (const QContactDetail &d, bDetails) { if (detailType(d) != detailType()) qDebug() << "B contact had extra detail:" << detailTypeName(d) << detailValues(d); } QCOMPARE(b, a); } bool tst_QContactManagerFiltering::isSuperset(const QContact& ca, const QContact& cb) { // returns true if contact ca is a superset of contact cb // we use this test instead of equality because dynamic information // such as presence/location, and synthesised information such as // display label and (possibly) type, may differ between a contact // in memory and the contact in the managed store. QContact a(ca); QContact b(cb); QList aDetails = a.details(); QList bDetails = b.details(); // They can be in any order, so loop // First remove any matches foreach(QContactDetail d, aDetails) { foreach(QContactDetail d2, bDetails) { if(d == d2) { a.removeDetail(&d); b.removeDetail(&d2); break; } } } // check for contact type updates if (validContactType(a)) if (validContactType(b)) if (a.type() != b.type()) return false; // nonempty type is different. // Now check to see if b has any details remaining; if so, a is not a superset. // Note that the DisplayLabel and Type can never be removed. if (b.details().size() > 2 || (b.details().size() == 2 && (detailType(b.details().value(0)) != detailType() || detailType(b.details().value(1)) != detailType()))) return false; return true; } void tst_QContactManagerFiltering::dumpContact(const QContact& contact) { QContactManager m; qDebug() << "Contact: " << ContactId::toString(contact); QList details = contact.details(); foreach (const QContactDetail &d, details) { qDebug() << " " << detailTypeName(d) << ":"; qDebug() << " Vals:" << detailValues(d); } } void tst_QContactManagerFiltering::dumpContacts() { QContactManager m; QList ids = m.contactIds(); foreach (const QContactId &id, ids) { QContact c = m.contact(id); dumpContact(c); } } QTEST_GUILESS_MAIN(tst_QContactManagerFiltering) #include "tst_qcontactmanagerfiltering.moc" #include "qcontactmanager.h" qtcontacts-sqlite-0.3.19/tests/auto/synctransactions/000077500000000000000000000000001436373107600230175ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/auto/synctransactions/synctransactions.pro000066400000000000000000000013041436373107600271440ustar00rootroot00000000000000TARGET = tst_synctransactions include(../../common.pri) # We need access to the ContactManagerEngine header and moc output INCLUDEPATH += \ ../../../src/engine/ \ ../../../src/extensions/ HEADERS += \ ../../../src/engine/contactid_p.h \ ../../../src/extensions/contactmanagerengine.h \ ../../../src/extensions/qcontactcollectionchangesfetchrequest.h \ ../../../src/extensions/qcontactchangesfetchrequest.h \ ../../../src/extensions/qcontactchangessaverequest.h \ ../../../src/extensions/qcontactclearchangeflagsrequest.h \ ../../util.h \ testsyncadaptor.h SOURCES += \ ../../../src/engine/contactid.cpp \ tst_synctransactions.cpp \ testsyncadaptor.cpp qtcontacts-sqlite-0.3.19/tests/auto/synctransactions/testsyncadapter.cpp000066400000000000000000000404511436373107600267440ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Contact: Chris Adams * * 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 "testsyncadapter.h" #include "../../../src/extensions/twowaycontactsyncadapter_impl.h" #include "../../../src/extensions/qtcontacts-extensions.h" #include #include #include #include #include #define TSA_GUID_STRING(accountId, fname, lname) QString(accountId + ":" + fname + lname) namespace { QMap managerParameters() { QMap params; params.insert(QStringLiteral("autoTest"), QStringLiteral("true")); params.insert(QStringLiteral("mergePresenceChanges"), QStringLiteral("true")); return params; } } TestSyncAdapter::TestSyncAdapter(const QString &accountId, QObject *parent) : QObject(parent), TwoWayContactSyncAdapter(QStringLiteral("testsyncadapter"), managerParameters()) , m_accountId(accountId) { cleanUp(accountId); } TestSyncAdapter::~TestSyncAdapter() { cleanUp(m_accountId); } void TestSyncAdapter::cleanUp(const QString &accountId) { initSyncAdapter(accountId); readSyncStateData(&m_remoteSince[accountId], accountId, TwoWayContactSyncAdapter::ReadPartialState); purgeSyncStateData(accountId, true); } void TestSyncAdapter::addRemoteDuplicates(const QString &accountId, const QString &fname, const QString &lname, const QString &phone) { addRemoteContact(accountId, fname, lname, phone); addRemoteContact(accountId, fname, lname, phone); addRemoteContact(accountId, fname, lname, phone); } void TestSyncAdapter::mergeRemoteDuplicates(const QString &accountId) { Q_FOREACH (const QString &dupGuid, m_remoteServerDuplicates[accountId].values()) { m_remoteAddMods[accountId].remove(dupGuid); // shouldn't be any here anyway. m_remoteDeletions[accountId].append(m_remoteServerContacts[accountId].value(dupGuid)); m_remoteServerContacts[accountId].remove(dupGuid); } m_remoteServerDuplicates[accountId].clear(); } void TestSyncAdapter::addRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QString &phone, TestSyncAdapter::PhoneModifiability mod) { QContactName ncn; ncn.setFirstName(fname); ncn.setLastName(lname); QContactPhoneNumber ncp; ncp.setNumber(phone); if (mod == TestSyncAdapter::ExplicitlyModifiable) { ncp.setValue(QContactDetail__FieldModifiable, true); } else if (mod == TestSyncAdapter::ExplicitlyNonModifiable) { ncp.setValue(QContactDetail__FieldModifiable, false); } QContact newContact; newContact.saveDetail(&ncn); newContact.saveDetail(&ncp); const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (m_remoteServerContacts[accountId].contains(contactGuidStr)) { // this is an intentional duplicate. we have special handling for duplicates. QString duplicateGuidString = contactGuidStr + ":" + QString::number(m_remoteServerDuplicates[accountId].values(contactGuidStr).size() + 1); QContactGuid guid; guid.setGuid(duplicateGuidString); newContact.saveDetail(&guid); m_remoteServerDuplicates[accountId].insert(contactGuidStr, duplicateGuidString); m_remoteServerContacts[accountId].insert(duplicateGuidString, newContact); m_remoteAddMods[accountId].insert(duplicateGuidString); } else { QContactGuid guid; guid.setGuid(contactGuidStr); newContact.saveDetail(&guid); m_remoteServerContacts[accountId].insert(contactGuidStr, newContact); m_remoteAddMods[accountId].insert(contactGuidStr); } } void TestSyncAdapter::removeRemoteContact(const QString &accountId, const QString &fname, const QString &lname) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); QContact remContact = m_remoteServerContacts[accountId].value(contactGuidStr); // stop tracking the contact if we are currently tracking it. m_remoteAddMods[accountId].remove(contactGuidStr); // remove it from our remote cache m_remoteServerContacts[accountId].remove(contactGuidStr); // report the contact as deleted m_remoteDeletions[accountId].append(remContact); } void TestSyncAdapter::setRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QContact &contact) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); QContact setContact = contact; QContactGuid sguid = setContact.detail(); sguid.setGuid(contactGuidStr); setContact.saveDetail(&sguid); QContactOriginMetadata somd = setContact.detail(); somd.setGroupId(setContact.id().toString()); setContact.saveDetail(&somd); m_remoteServerContacts[accountId][contactGuidStr] = setContact; m_remoteAddMods[accountId].insert(contactGuidStr); } void TestSyncAdapter::changeRemoteContactPhone(const QString &accountId, const QString &fname, const QString &lname, const QString &modPhone) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (!m_remoteServerContacts[accountId].contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts[accountId].value(contactGuidStr); QContactPhoneNumber mcp = modContact.detail(); mcp.setNumber(modPhone); modContact.saveDetail(&mcp); m_remoteServerContacts[accountId][contactGuidStr] = modContact; m_remoteAddMods[accountId].insert(contactGuidStr); } void TestSyncAdapter::changeRemoteContactEmail(const QString &accountId, const QString &fname, const QString &lname, const QString &modEmail) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (!m_remoteServerContacts[accountId].contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts[accountId].value(contactGuidStr); QContactEmailAddress mce = modContact.detail(); mce.setEmailAddress(modEmail); modContact.saveDetail(&mce); m_remoteServerContacts[accountId][contactGuidStr] = modContact; m_remoteAddMods[accountId].insert(contactGuidStr); } void TestSyncAdapter::changeRemoteContactName(const QString &accountId, const QString &fname, const QString &lname, const QString &modfname, const QString &modlname) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (!m_remoteServerContacts[accountId].contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts[accountId].value(contactGuidStr); QContactName mcn = modContact.detail(); if (modfname.isEmpty() && modlname.isEmpty()) { modContact.removeDetail(&mcn); } else { mcn.setFirstName(modfname); mcn.setLastName(modlname); modContact.saveDetail(&mcn); } const QString modContactGuidStr(TSA_GUID_STRING(accountId, modfname, modlname)); m_remoteServerContacts[accountId].remove(contactGuidStr); m_remoteAddMods[accountId].remove(contactGuidStr); m_remoteServerContacts[accountId][modContactGuidStr] = modContact; m_remoteAddMods[accountId].insert(modContactGuidStr); } void TestSyncAdapter::performTwoWaySync(const QString &accountId) { // reset our state. m_downsyncWasRequired[accountId] = false; m_upsyncWasRequired[accountId] = false; // do the sync process as described in twowaycontactsyncadapter.h if (!initSyncAdapter(accountId)) { qWarning() << Q_FUNC_INFO << "couldn't init adapter"; emit failed(); return; } if (!readSyncStateData(&m_remoteSince[accountId], accountId, TwoWayContactSyncAdapter::ReadPartialState)) { qWarning() << Q_FUNC_INFO << "couldn't read sync state data"; emit failed(); return; } determineRemoteChanges(m_remoteSince[accountId], accountId); // continued in continueTwoWaySync(). } void TestSyncAdapter::determineRemoteChanges(const QDateTime &, const QString &accountId) { QTimer *simtimer = 0; if (!m_simulationTimers.contains(accountId)) { simtimer = new QTimer(this); simtimer->setSingleShot(true); simtimer->setInterval(200); // simulate network latency simtimer->setProperty("accountId", accountId); m_simulationTimers.insert(accountId, simtimer); } else { simtimer = m_simulationTimers.value(accountId); } connect(simtimer, SIGNAL(timeout()), this, SLOT(continueTwoWaySync())); simtimer->start(); } void TestSyncAdapter::continueTwoWaySync() { QTimer *simtimer = qobject_cast(sender()); simtimer->disconnect(this, SLOT(continueTwoWaySync())); QString accountId = simtimer->property("accountId").toString(); // continuing the sync process as described in twowaycontactsyncadapter.h if (m_remoteDeletions[accountId].isEmpty() && m_remoteAddMods[accountId].isEmpty()) { m_downsyncWasRequired[accountId] = false; } else { m_downsyncWasRequired[accountId] = true; } // call storeRemoteChanges anyway so that the state machine continues to work. // alternatively, we could set the state to StoredRemoteChanges manually, and skip // this call in the else block above, but we should test that it works properly anyway. QList remoteAddMods; QMap additions; foreach (const QString &contactGuidStr, m_remoteAddMods[accountId]) { remoteAddMods.append(m_remoteServerContacts[accountId].value(contactGuidStr)); if (remoteAddMods.last().id().isNull()) { additions.insert(remoteAddMods.count() - 1, contactGuidStr); } } if (!storeRemoteChanges(m_remoteDeletions[accountId], &remoteAddMods, accountId)) { qWarning() << Q_FUNC_INFO << "couldn't store remote changes"; emit failed(); return; } // Store the ID of any contact we added QMap::const_iterator ait = additions.constBegin(), aend = additions.constEnd(); for ( ; ait != aend; ++ait) { const QContact &added(remoteAddMods.at(ait.key())); const QString &contactGuidStr(ait.value()); m_remoteServerContacts[accountId][contactGuidStr].setId(added.id()); } m_modifiedIds[accountId].clear(); foreach (const QContact &stored, remoteAddMods) { m_modifiedIds[accountId].insert(stored.id()); } // clear our simulated remote changes deltas, as we've already reported / stored them. m_remoteDeletions[accountId].clear(); m_remoteAddMods[accountId].clear(); QList locallyAdded, locallyModified, locallyDeleted; QDateTime localSince; if (!determineLocalChanges(&localSince, &locallyAdded, &locallyModified, &locallyDeleted, accountId)) { qWarning() << Q_FUNC_INFO << "couldn't determine local changes"; emit failed(); return; } if (locallyAdded.isEmpty() && locallyModified.isEmpty() && locallyDeleted.isEmpty()) { m_upsyncWasRequired[accountId] = false; } else { m_upsyncWasRequired[accountId] = true; } upsyncLocalChanges(localSince, locallyAdded, locallyModified, locallyDeleted, accountId); // continued in finalizeTwoWaySync() } bool TestSyncAdapter::testAccountProvenance(const QContact &contact, const QString &accountId) { foreach (const QContact &remoteContact, m_remoteServerContacts[accountId]) { if (remoteContact.id() == contact.id()) { return true; } } return false; } void TestSyncAdapter::upsyncLocalChanges(const QDateTime &, const QList &locallyAdded, const QList &locallyModified, const QList &locallyDeleted, const QString &accountId) { // first, apply the local changes to our in memory store. foreach (const QContact &c, locallyAdded) { setRemoteContact(accountId, c.detail().firstName(), c.detail().lastName(), c); } foreach (const QContact &c, locallyModified) { // we cannot simply call setRemoteContact since the name might be modified or empty due to a previous test. Q_FOREACH (const QString &storedGuid, m_remoteServerContacts[m_accountId].keys()) { if (m_remoteServerContacts[m_accountId][storedGuid].id() == c.id()) { m_remoteServerContacts[m_accountId][storedGuid] = c; m_remoteAddMods[accountId].insert(storedGuid); } } } foreach (const QContact &c, locallyDeleted) { // we cannot simply call removeRemoteContact since the name might be modified or empty due to a previous test. QMap remoteServerContacts = m_remoteServerContacts[m_accountId]; Q_FOREACH (const QString &storedGuid, remoteServerContacts.keys()) { if (remoteServerContacts.value(storedGuid).id() == c.id()) { m_remoteServerContacts[m_accountId].remove(storedGuid); } } } // then trigger finalize after a simulated network delay. QTimer *simtimer = 0; if (!m_simulationTimers.contains(accountId)) { simtimer = new QTimer(this); simtimer->setSingleShot(true); simtimer->setInterval(200); // simulate network latency. simtimer->setProperty("accountId", accountId); m_simulationTimers.insert(accountId, simtimer); } else { simtimer = m_simulationTimers.value(accountId); } connect(simtimer, SIGNAL(timeout()), this, SLOT(finalizeTwoWaySync())); simtimer->start(); } void TestSyncAdapter::finalizeTwoWaySync() { QTimer *simtimer = qobject_cast(sender()); simtimer->disconnect(this, SLOT(finalizeTwoWaySync())); QString accountId = simtimer->property("accountId").toString(); if (!storeSyncStateData(accountId)) { qWarning() << Q_FUNC_INFO << "couldn't store sync state data"; emit failed(); return; } emit finished(); // succeeded. } bool TestSyncAdapter::upsyncWasRequired(const QString &accountId) const { return m_upsyncWasRequired[accountId]; } bool TestSyncAdapter::downsyncWasRequired(const QString &accountId) const { return m_downsyncWasRequired[accountId]; } QContact TestSyncAdapter::remoteContact(const QString &accountId, const QString &fname, const QString &lname) const { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); return m_remoteServerContacts[accountId].value(contactGuidStr); } QSet TestSyncAdapter::modifiedIds(const QString &accountId) const { return m_modifiedIds[accountId]; } qtcontacts-sqlite-0.3.19/tests/auto/synctransactions/testsyncadapter.h000066400000000000000000000122051436373107600264050ustar00rootroot00000000000000/* * Copyright (C) 2014 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef TESTSYNCADAPTER_H #define TESTSYNCADAPTER_H #include "../../../src/extensions/twowaycontactsyncadapter.h" #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class TestSyncAdapter : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdapter { Q_OBJECT public: TestSyncAdapter(const QString &accountId, QObject *parent = 0); ~TestSyncAdapter(); enum PhoneModifiability { ImplicitlyModifiable = 0, ExplicitlyModifiable, ExplicitlyNonModifiable }; // for testing purposes void addRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QString &phone, PhoneModifiability mod = ImplicitlyModifiable); void removeRemoteContact(const QString &accountId, const QString &fname, const QString &lname); void setRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QContact &contact); void changeRemoteContactPhone(const QString &accountId, const QString &fname, const QString &lname, const QString &modPhone); void changeRemoteContactEmail(const QString &accountId, const QString &fname, const QString &lname, const QString &modEmail); void changeRemoteContactName(const QString &accountId, const QString &fname, const QString &lname, const QString &modfname, const QString &modlname); void addRemoteDuplicates(const QString &accountId, const QString &fname, const QString &lname, const QString &phone); void mergeRemoteDuplicates(const QString &accountId); // triggering sync and checking state. void performTwoWaySync(const QString &accountId); bool upsyncWasRequired(const QString &accountId) const; bool downsyncWasRequired(const QString &accountId) const; QContact remoteContact(const QString &accountId, const QString &fname, const QString &lname) const; QSet modifiedIds(const QString &accountId) const; Q_SIGNALS: void finished(); void failed(); protected: // implementing the TWCSA interface void determineRemoteChanges(const QDateTime &remoteSince, const QString &accountId); bool testAccountProvenance(const QContact &contact, const QString &accountId); void upsyncLocalChanges(const QDateTime &localSince, const QList &locallyAdded, const QList &locallyModified, const QList &locallyDeleted, const QString &accountId); // simulating asynchronous network operations private Q_SLOTS: void continueTwoWaySync(); void finalizeTwoWaySync(); private: void cleanUp(const QString &accountId); QString m_accountId; // simulating server-side changes, per account: mutable QMap m_simulationTimers; mutable QMap m_downsyncWasRequired; mutable QMap m_upsyncWasRequired; mutable QMap m_remoteSince; mutable QMap > m_remoteDeletions; mutable QMap > m_remoteAddMods; // used to lookup into m_remoteServerContacts mutable QMap > m_remoteServerContacts; mutable QMap > m_modifiedIds; mutable QMap > m_remoteServerDuplicates; // accountId to originalGuid to duplicateGuids. }; #endif qtcontacts-sqlite-0.3.19/tests/auto/synctransactions/testsyncadaptor.cpp000066400000000000000000000544751436373107600267710ustar00rootroot00000000000000/* * Copyright (C) 2014 - 2015 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * 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 "testsyncadaptor.h" #include "../../../src/extensions/twowaycontactsyncadaptor_impl.h" #include "../../../src/extensions/qtcontacts-extensions.h" #include #include #include #include #include #include #include #include #define TSA_GUID_STRING(accountId, applicationName, fname, lname) QString(accountId + ":" + applicationName + ":" + fname + lname) namespace { QMap managerParameters() { QMap params; params.insert(QStringLiteral("autoTest"), QStringLiteral("true")); params.insert(QStringLiteral("mergePresenceChanges"), QStringLiteral("true")); return params; } QContact updateContactEtag(const QContact &c) { const QList extendedDetails = c.details(); for (const QContactExtendedDetail &ed : extendedDetails) { if (ed.name() == QStringLiteral("etag")) { QContactExtendedDetail updatedEtag = ed; updatedEtag.setData(QUuid::createUuid().toString()); QContact ret = c; ret.saveDetail(&updatedEtag, QContact::IgnoreAccessConstraints); return ret; } } QContactExtendedDetail etag; etag.setData(QUuid::createUuid().toString()); QContact ret = c; ret.saveDetail(&etag, QContact::IgnoreAccessConstraints); return ret; } QContactCollection updateCollectionCtag(const QContactCollection &c) { QContactCollection ret = c; ret.setExtendedMetaData(QStringLiteral("ctag"), QUuid::createUuid().toString()); return ret; } } TestSyncAdaptor::TestSyncAdaptor(int accountId, const QString &applicationName, QContactManager &manager, QObject *parent) : QObject(parent), TwoWayContactSyncAdaptor(accountId, applicationName, manager) , m_accountId(accountId) , m_applicationName(applicationName) { cleanUp(); QContact alice; QContactName an = alice.detail(); an.setFirstName("Alice"); an.setLastName("Wonderland"); an.setValue(QContactDetail__FieldModifiable, false); alice.saveDetail(&an); QContactPhoneNumber ap = alice.detail(); ap.setNumber("123123123"); ap.setValue(QContactDetail__FieldModifiable, false); alice.saveDetail(&ap); QContactGuid ag; ag.setGuid(TSA_GUID_STRING(m_accountId, m_applicationName, an.firstName(), an.lastName())); alice.saveDetail(&ag); m_alice = updateContactEtag(alice); QContact bob; QContactName bn = bob.detail(); bn.setFirstName("Bob"); bn.setLastName("Constructor"); bn.setValue(QContactDetail__FieldModifiable, false); bob.saveDetail(&bn); QContactEmailAddress be = bob.detail(); be.setEmailAddress("bob@constructor.tld"); be.setValue(QContactDetail__FieldModifiable, false); bob.saveDetail(&be); QContactGuid bg; bg.setGuid(TSA_GUID_STRING(m_accountId, m_applicationName, bn.firstName(), bn.lastName())); bob.saveDetail(&bg); m_bob = updateContactEtag(bob); QContactCollection emptyCollection; emptyCollection.setMetaData(QContactCollection::KeyName, QStringLiteral("Empty")); emptyCollection.setMetaData(QContactCollection::KeyDescription, QStringLiteral("An empty, read-only collection")); emptyCollection.setMetaData(QContactCollection::KeyColor, QStringLiteral("red")); emptyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, accountId); emptyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, applicationName); emptyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); emptyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/addressbooks/empty")); m_emptyCollection = updateCollectionCtag(emptyCollection); QContactCollection readonlyCollection; readonlyCollection.setMetaData(QContactCollection::KeyName, QStringLiteral("ReadOnly")); readonlyCollection.setMetaData(QContactCollection::KeyDescription, QStringLiteral("A non-empty, non-aggregable, read-only collection")); readonlyCollection.setMetaData(QContactCollection::KeyColor, QStringLiteral("blue")); readonlyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, accountId); readonlyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, applicationName); readonlyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, false); readonlyCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/addressbooks/readonly")); m_readOnlyCollection = updateCollectionCtag(readonlyCollection); QContactCollection readwriteCollection; readwriteCollection.setMetaData(QContactCollection::KeyName, QStringLiteral("ReadWrite")); readwriteCollection.setMetaData(QContactCollection::KeyDescription, QStringLiteral("A normal, aggregable, read-write collection")); readwriteCollection.setMetaData(QContactCollection::KeyColor, QStringLiteral("green")); readwriteCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, accountId); readwriteCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, applicationName); readwriteCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); readwriteCollection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/addressbooks/readwrite")); m_readWriteCollection = updateCollectionCtag(readwriteCollection); } TestSyncAdaptor::~TestSyncAdaptor() { cleanUp(); } void TestSyncAdaptor::cleanUp() { removeAllCollections(); } void TestSyncAdaptor::addRemoteDuplicates(const QString &fname, const QString &lname, const QString &phone) { addRemoteContact(fname, lname, phone); addRemoteContact(fname, lname, phone); addRemoteContact(fname, lname, phone); } void TestSyncAdaptor::mergeRemoteDuplicates() { Q_FOREACH (const QString &dupGuid, m_remoteServerDuplicates.values()) { m_remoteAdditions.remove(dupGuid); // shouldn't be any here anyway. m_remoteModifications.remove(dupGuid); // shouldn't be any here anyway. m_remoteDeletions.append(m_remoteServerContacts.value(dupGuid)); m_remoteServerContacts.remove(dupGuid); } m_remoteServerDuplicates.clear(); } void TestSyncAdaptor::addRemoteContact(const QString &fname, const QString &lname, const QString &phone, TestSyncAdaptor::PhoneModifiability mod) { QContact newContact; QContactName ncn; ncn.setFirstName(fname); ncn.setLastName(lname); newContact.saveDetail(&ncn); QContactPhoneNumber ncp; ncp.setNumber(phone); if (mod == TestSyncAdaptor::ExplicitlyModifiable) { ncp.setValue(QContactDetail__FieldModifiable, true); } else if (mod == TestSyncAdaptor::ExplicitlyNonModifiable) { ncp.setValue(QContactDetail__FieldModifiable, false); } newContact.saveDetail(&ncp); QContactStatusFlags nfl; nfl.setFlag(QContactStatusFlags::IsAdded, true); newContact.saveDetail(&nfl); newContact = updateContactEtag(newContact); const QString contactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, fname, lname)); if (m_remoteServerContacts.contains(contactGuidStr)) { // this is an intentional duplicate. we have special handling for duplicates. QString duplicateGuidString = contactGuidStr + "#" + QString::number(m_remoteServerDuplicates.values(contactGuidStr).size() + 1); QContactGuid guid; guid.setGuid(duplicateGuidString); newContact.saveDetail(&guid); m_remoteServerDuplicates.insert(contactGuidStr, duplicateGuidString); m_remoteServerContacts.insert(duplicateGuidString, newContact); m_remoteAdditions.insert(duplicateGuidString); } else { QContactGuid guid; guid.setGuid(contactGuidStr); newContact.saveDetail(&guid); m_remoteServerContacts.insert(contactGuidStr, newContact); m_remoteAdditions.insert(contactGuidStr); } } void TestSyncAdaptor::removeRemoteContact(const QString &fname, const QString &lname) { const QString contactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, fname, lname)); QContact remContact = m_remoteServerContacts.value(contactGuidStr); QContactStatusFlags rfl = remContact.detail(); rfl.setFlag(QContactStatusFlags::IsAdded, false); rfl.setFlag(QContactStatusFlags::IsModified, false); rfl.setFlag(QContactStatusFlags::IsDeleted, true); remContact.saveDetail(&rfl, QContact::IgnoreAccessConstraints); // stop tracking the contact if we are currently tracking it. m_remoteAdditions.remove(contactGuidStr); m_remoteModifications.remove(contactGuidStr); // remove it from our remote cache m_remoteServerContacts.remove(contactGuidStr); // report the contact as deleted m_remoteDeletions.append(remContact); } QContact TestSyncAdaptor::setRemoteContact(const QString &fname, const QString &lname, const QContact &contact) { const QString contactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, fname, lname)); QContact setContact = contact; QContactGuid sguid = setContact.detail(); sguid.setGuid(contactGuidStr); setContact.saveDetail(&sguid, QContact::IgnoreAccessConstraints); QContactOriginMetadata somd = setContact.detail(); somd.setGroupId(setContact.id().toString()); setContact.saveDetail(&somd, QContact::IgnoreAccessConstraints); const QContact newContact = updateContactEtag(setContact); m_remoteServerContacts[contactGuidStr] = newContact; return newContact; } void TestSyncAdaptor::changeRemoteContactPhone(const QString &fname, const QString &lname, const QString &modPhone) { const QString contactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, fname, lname)); if (!m_remoteServerContacts.contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts.value(contactGuidStr); QContactPhoneNumber mcp = modContact.detail(); mcp.setNumber(modPhone); modContact.saveDetail(&mcp); QContactStatusFlags flags = modContact.detail(); flags.setFlag(QContactStatusFlags::IsModified, true); modContact.saveDetail(&flags, QContact::IgnoreAccessConstraints); m_remoteServerContacts[contactGuidStr] = modContact; m_remoteModifications.insert(contactGuidStr); } void TestSyncAdaptor::changeRemoteContactEmail(const QString &fname, const QString &lname, const QString &modEmail) { const QString contactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, fname, lname)); if (!m_remoteServerContacts.contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts.value(contactGuidStr); QContactEmailAddress mce = modContact.detail(); mce.setEmailAddress(modEmail); modContact.saveDetail(&mce); QContactStatusFlags flags = modContact.detail(); flags.setFlag(QContactStatusFlags::IsModified, true); modContact.saveDetail(&flags, QContact::IgnoreAccessConstraints); m_remoteServerContacts[contactGuidStr] = modContact; m_remoteModifications.insert(contactGuidStr); } void TestSyncAdaptor::changeRemoteContactName(const QString &fname, const QString &lname, const QString &modfname, const QString &modlname) { const QString contactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, fname, lname)); if (!m_remoteServerContacts.contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts.value(contactGuidStr); QContactName mcn = modContact.detail(); if (modfname.isEmpty() && modlname.isEmpty()) { modContact.removeDetail(&mcn); } else { mcn.setFirstName(modfname); mcn.setLastName(modlname); modContact.saveDetail(&mcn); } QContactStatusFlags flags = modContact.detail(); flags.setFlag(QContactStatusFlags::IsModified, true); modContact.saveDetail(&flags, QContact::IgnoreAccessConstraints); const QString modContactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, modfname, modlname)); m_remoteServerContacts.remove(contactGuidStr); m_remoteModifications.remove(contactGuidStr); m_remoteServerContacts[modContactGuidStr] = modContact; m_remoteModifications.insert(modContactGuidStr); } bool TestSyncAdaptor::upsyncWasRequired() const { return m_upsyncWasRequired; } bool TestSyncAdaptor::downsyncWasRequired() const { return m_downsyncWasRequired; } QContact TestSyncAdaptor::remoteContact(const QString &fname, const QString &lname) const { const QString contactGuidStr(TSA_GUID_STRING(m_accountId, m_applicationName, fname, lname)); return m_remoteServerContacts.value(contactGuidStr); } QSet TestSyncAdaptor::modifiedIds() const { return m_modifiedIds; } void TestSyncAdaptor::performTwoWaySync() { // reset our state. m_downsyncWasRequired = false; m_upsyncWasRequired = false; startSync(); } bool TestSyncAdaptor::determineRemoteCollections() { QList remoteCollections; remoteCollections.append(m_emptyCollection); remoteCollections.append(m_readOnlyCollection); if (!m_readWriteCollectionDeleted) { remoteCollections.append(m_readWriteCollection); } // simulate sending a network request to remote server. QTimer::singleShot(250, this, [this, remoteCollections] { this->remoteCollectionsDetermined(remoteCollections); }); return true; } bool TestSyncAdaptor::deleteRemoteCollection(const QContactCollection &collection) { // simulate sending a network request to the remote server. QTimer::singleShot(250, this, [this, collection] { if (collection.metaData(QContactCollection::KeyName).toString() == QStringLiteral("ReadWrite")) { this->m_readWriteCollectionDeleted = true; this->remoteCollectionDeleted(collection); } else { qWarning() << "TestSyncAdaptor: unable to delete read-only collection: " << collection.metaData(QContactCollection::KeyName).toString(); syncOperationError(); } }); return true; } bool TestSyncAdaptor::determineRemoteContacts(const QContactCollection &collection) { // simulate a request to the server. QTimer::singleShot(250, this, [this, collection] { if (collection.metaData(QContactCollection::KeyName).toString() == QStringLiteral("ReadWrite")) { if (this->m_readWriteCollectionDeleted) { qWarning() << "TestSyncAdaptor: unable to determine contacts from deleted collection"; syncOperationError(); } else { remoteContactsDetermined(collection, this->m_remoteServerContacts.values()); } } else if (collection.metaData(QContactCollection::KeyName).toString() == QStringLiteral("ReadOnly")) { const QList fixed { m_alice, m_bob }; remoteContactsDetermined(collection, fixed); } else if (collection.metaData(QContactCollection::KeyName).toString() == QStringLiteral("Empty")) { remoteContactsDetermined(collection, QList()); } else { // unknown / nonexistent collection. qWarning() << "TestSyncAdaptor: unknown collection, cannot determine contacts"; syncOperationError(); } }); return true; } bool TestSyncAdaptor::storeLocalChangesRemotely( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { // simulate a request to the server. QTimer::singleShot(250, this, [this, collection, addedContacts, modifiedContacts, deletedContacts] { if (collection.metaData(QContactCollection::KeyName).toString() == QStringLiteral("ReadWrite")) { if (this->m_readWriteCollectionDeleted) { qWarning() << "TestSyncAdaptor: unable to store local changes to deleted collection"; syncOperationError(); } else { // we return the updated (with etags) contacts to twcsa in order to update the database. QList updatedAdded; QList updatedModified; // apply the local changes to our in memory store. m_readWriteCollection = updateCollectionCtag(collection); foreach (const QContact &c, addedContacts) { updatedAdded.append(setRemoteContact( c.detail().firstName(), c.detail().lastName(), c)); } foreach (const QContact &c, modifiedContacts) { bool found = false; Q_FOREACH (const QString &storedGuid, m_remoteServerContacts.keys()) { if (c.detail().guid() == storedGuid) { // this modified contact exists. if it was originally // added on the server, it may not yet have a QContactId // in our memory store, so we should set it. if (m_remoteServerContacts[storedGuid].id().isNull()) { m_remoteServerContacts[storedGuid].setId(c.id()); } } if (m_remoteServerContacts[storedGuid].id() == c.id()) { found = true; const QContact updated = updateContactEtag(c); m_remoteServerContacts[storedGuid] = updated; updatedModified.append(updated); break; } } if (!found) { qWarning() << "TestSyncAdaptor: unable to apply modification to nonexistent remote contact:" << c.id() << " : " << c.detail().firstName() << " " << c.detail().lastName(); syncOperationError(); return; } } foreach (const QContact &c, deletedContacts) { // we cannot simply call removeRemoteContact since the name might be modified or empty due to a previous test. bool found = false; QMap remoteServerContacts = m_remoteServerContacts; Q_FOREACH (const QString &storedGuid, remoteServerContacts.keys()) { if (c.detail().guid() == storedGuid) { // this deleted contact exists. if it was originally // added on the server, it may not yet have a QContactId // in our memory store, so we should set it. if (m_remoteServerContacts[storedGuid].id().isNull()) { m_remoteServerContacts[storedGuid].setId(c.id()); } } if (remoteServerContacts.value(storedGuid).id() == c.id()) { found = true; m_remoteServerContacts.remove(storedGuid); } } if (!found) { qWarning() << "TestSyncAdaptor: unable to apply deletion to nonexistent remote contact:" << c.id() << " : " << c.detail().firstName() << " " << c.detail().lastName(); syncOperationError(); return; } } // successfully updated remote data. return the results (with updated ctag/etags). localChangesStoredRemotely(m_readWriteCollection, updatedAdded, updatedModified); } } else { qWarning() << "TestSyncAdaptor: unable to store local changes to read-only (or non-existent) remote collection"; syncOperationError(); } }); return true; } void TestSyncAdaptor::syncFinishedSuccessfully() { emit finished(); } void TestSyncAdaptor::syncFinishedWithError() { emit failed(); } qtcontacts-sqlite-0.3.19/tests/auto/synctransactions/testsyncadaptor.h000066400000000000000000000121241436373107600264170ustar00rootroot00000000000000/* * Copyright (C) 2014 - 2015 Jolla Ltd. * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef TESTSYNCADAPTOR_H #define TESTSYNCADAPTOR_H #include "../../../src/extensions/twowaycontactsyncadaptor.h" #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class TestSyncAdaptor : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdaptor { Q_OBJECT public: TestSyncAdaptor(int accountId, const QString &applicationName, QContactManager &manager, QObject *parent = 0); ~TestSyncAdaptor(); enum PhoneModifiability { ImplicitlyModifiable = 0, ExplicitlyModifiable, ExplicitlyNonModifiable }; // for testing purposes void addRemoteContact(const QString &fname, const QString &lname, const QString &phone, PhoneModifiability mod = ImplicitlyModifiable); void removeRemoteContact(const QString &fname, const QString &lname); QContact setRemoteContact(const QString &fname, const QString &lname, const QContact &contact); void changeRemoteContactPhone(const QString &fname, const QString &lname, const QString &modPhone); void changeRemoteContactEmail(const QString &fname, const QString &lname, const QString &modEmail); void changeRemoteContactName(const QString &fname, const QString &lname, const QString &modfname, const QString &modlname); void addRemoteDuplicates(const QString &fname, const QString &lname, const QString &phone); void mergeRemoteDuplicates(); // triggering sync and checking state. void performTwoWaySync(); bool upsyncWasRequired() const; bool downsyncWasRequired() const; QContact remoteContact(const QString &fname, const QString &lname) const; QSet modifiedIds() const; Q_SIGNALS: void finished(); void failed(); protected: // implementing the TWCSA interface bool determineRemoteCollections(); bool deleteRemoteCollection(const QContactCollection &collection); bool determineRemoteContacts(const QContactCollection &collection); bool storeLocalChangesRemotely( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts); void syncFinishedSuccessfully(); void syncFinishedWithError(); private: void cleanUp(); int m_accountId; QString m_applicationName; // we simulate 3 collections: // one of which is empty and is read-only // one of which has content and is read-only // one of which has content and is read-write QContactCollection m_emptyCollection; QContactCollection m_readOnlyCollection; QContactCollection m_readWriteCollection; bool m_readWriteCollectionDeleted = false; // the readonly non-empty collection has two fixed contacts: QContact m_alice; QContact m_bob; // simulating server-side changes: mutable bool m_downsyncWasRequired = false; mutable bool m_upsyncWasRequired = false; mutable QList m_remoteDeletions; mutable QSet m_remoteAdditions; // guids used to lookup into m_remoteServerContacts mutable QSet m_remoteModifications; // guids used to lookup into m_remoteServerContacts mutable QMap m_remoteServerContacts; // guid to contact mutable QSet m_modifiedIds; mutable QMultiMap m_remoteServerDuplicates; // originalGuid to duplicateGuids. }; #endif qtcontacts-sqlite-0.3.19/tests/auto/synctransactions/tst_synctransactions.cpp000066400000000000000000003106131436373107600300260ustar00rootroot00000000000000/* * Copyright (C) 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include #include #include "../../util.h" #include "testsyncadaptor.h" #include "qtcontacts-extensions.h" #include "qcontactcollectionchangesfetchrequest.h" #include "qcontactcollectionchangesfetchrequest_impl.h" #include "qcontactchangesfetchrequest.h" #include "qcontactchangesfetchrequest_impl.h" #include "qcontactchangessaverequest.h" #include "qcontactchangessaverequest_impl.h" #include "qcontactclearchangeflagsrequest.h" #include "qcontactclearchangeflagsrequest_impl.h" class tst_synctransactions : public QObject { Q_OBJECT public: tst_synctransactions(); virtual ~tst_synctransactions(); public slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); public slots: void addColAccumulationSlot(const QList &ids); void addAccumulationSlot(const QList &ids); private slots: void singleCollection_noContacts(); void singleCollection_addedContacts(); void singleCollection_multipleCycles(); void singleCollection_unhandledChanges(); void multipleCollections(); void syncRequests(); void twcsa_nodelta(); void twcsa_delta(); void twcsa_oneway(); private: void waitForSignalPropagation(); QContactManager *m_cm; QSet m_createdColIds; QSet m_createdIds; QByteArray aggregateAddressbookId() { return QtContactsSqliteExtensions::aggregateCollectionId(m_cm->managerUri()).localId(); } QByteArray localAddressbookId() { return QtContactsSqliteExtensions::localCollectionId(m_cm->managerUri()).localId(); } }; tst_synctransactions::tst_synctransactions() : m_cm(0) { QMap parameters; parameters.insert(QString::fromLatin1("autoTest"), QString::fromLatin1("true")); parameters.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("true")); m_cm = new QContactManager(QString::fromLatin1("org.nemomobile.contacts.sqlite"), parameters); QTest::qWait(250); // creating self contact etc will cause some signals to be emitted. ignore them. QObject::connect(m_cm, &QContactManager::collectionsAdded, this, &tst_synctransactions::addColAccumulationSlot); QObject::connect(m_cm, &QContactManager::contactsAdded, this, &tst_synctransactions::addAccumulationSlot); } tst_synctransactions::~tst_synctransactions() { } void tst_synctransactions::initTestCase() { registerIdType(); /* Make sure the DB is empty */ QContactCollectionFilter allCollections; m_cm->removeContacts(m_cm->contactIds(allCollections)); waitForSignalPropagation(); } void tst_synctransactions::init() { m_createdColIds.clear(); m_createdIds.clear(); } void tst_synctransactions::cleanupTestCase() { } void tst_synctransactions::cleanup() { QContactManager::Error err = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); waitForSignalPropagation(); if (!m_createdIds.isEmpty()) { // purge them one at a time, to avoid "contacts from different collections in single batch" errors. for (const QContactId &cid : m_createdIds) { QContact doomed = m_cm->contact(cid); if (!doomed.id().isNull() && doomed.collectionId().localId() != aggregateAddressbookId()) { if (!m_cm->removeContact(cid)) { qWarning() << "Failed to cleanup:" << QString::fromLatin1(cid.localId()); } cme->clearChangeFlags(QList() << cid, &err); } } m_createdIds.clear(); } if (!m_createdColIds.isEmpty()) { for (const QContactCollectionId &colId : m_createdColIds.toList()) { m_cm->removeCollection(colId); cme->clearChangeFlags(colId, &err); } m_createdColIds.clear(); } cme->clearChangeFlags(QContactCollectionId(m_cm->managerUri(), localAddressbookId()), &err); waitForSignalPropagation(); } void tst_synctransactions::waitForSignalPropagation() { // Signals are routed via DBUS, so we need to wait for them to arrive QTest::qWait(50); } void tst_synctransactions::addColAccumulationSlot(const QList &ids) { foreach (const QContactCollectionId &id, ids) { m_createdColIds.insert(id); } } void tst_synctransactions::addAccumulationSlot(const QList &ids) { foreach (const QContactId &id, ids) { m_createdIds.insert(id); } } void tst_synctransactions::singleCollection_noContacts() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; QContactCollectionId remoteAddressbookId; // ensure that initially, no changes are detected. { QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; QVERIFY(cme->fetchCollectionChanges( 0, QStringLiteral("tst_synctransactions"), &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(addedCollections.count(), 0); QCOMPARE(modifiedCollections.count(), 0); QCOMPARE(deletedCollections.count(), 0); QCOMPARE(unmodifiedCollections.count(), 0); } // simulate a sync cycle which results in an empty remote addressbook being added. { QHash *> additions; QHash *> modifications; QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_synctransactions"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QList addedCollectionContacts; additions.insert(&remoteAddressbook, &addedCollectionContacts); const QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy policy( QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges); QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); QVERIFY(!remoteAddressbook.id().isNull()); // id should have been set during save operation. remoteAddressbookId = remoteAddressbook.id(); } // ensure that no changes are detected, but the collection is reported as unmodified. { QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; QVERIFY(cme->fetchCollectionChanges( 5, QStringLiteral("tst_synctransactions"), &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(addedCollections.count(), 0); QCOMPARE(modifiedCollections.count(), 0); QCOMPARE(deletedCollections.count(), 0); QCOMPARE(unmodifiedCollections.count(), 1); QCOMPARE(unmodifiedCollections.first().id(), remoteAddressbookId); } // and ensure that no contact changes are reported for that collection { QList addedContacts; QList modifiedContacts; QList deletedContacts; QList unmodifiedContacts; QVERIFY(cme->fetchContactChanges( remoteAddressbookId, &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 0); } // clean up. QVERIFY(m_cm->removeCollection(remoteAddressbookId)); QVERIFY(cme->clearChangeFlags(remoteAddressbookId, &err)); } void tst_synctransactions::singleCollection_addedContacts() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; QContactCollectionId remoteAddressbookId; QContactId remoteContactId; // ensure that initially, no changes are detected. { QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; QVERIFY(cme->fetchCollectionChanges( 0, QStringLiteral("tst_synctransactions"), &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(addedCollections.count(), 0); QCOMPARE(modifiedCollections.count(), 0); QCOMPARE(deletedCollections.count(), 0); QCOMPARE(unmodifiedCollections.count(), 0); } // simulate a sync cycle which results in a non-empty remote addressbook being added. { QHash *> additions; QHash *> modifications; QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_synctransactions"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QContact syncAlice; QContactName san; san.setFirstName("Alice"); san.setMiddleName("In"); san.setLastName("Wonderland"); syncAlice.saveDetail(&san); QContactPhoneNumber saph; saph.setNumber("123454321"); syncAlice.saveDetail(&saph); QContactEmailAddress saem; saem.setEmailAddress("alice@wonderland.tld"); syncAlice.saveDetail(&saem); QContactStatusFlags saf; saf.setFlag(QContactStatusFlags::IsAdded, true); syncAlice.saveDetail(&saf); QList addedCollectionContacts; addedCollectionContacts.append(syncAlice); additions.insert(&remoteAddressbook, &addedCollectionContacts); const QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy policy( QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges); QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); QVERIFY(!remoteAddressbook.id().isNull()); // id should have been set during save operation. QVERIFY(!addedCollectionContacts.first().id().isNull()); // id should have been set during save operation. remoteAddressbookId = remoteAddressbook.id(); remoteContactId = addedCollectionContacts.first().id(); } // ensure that no changes are detected, but the collection is reported as unmodified. { QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; QVERIFY(cme->fetchCollectionChanges( 5, QStringLiteral("tst_synctransactions"), &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(addedCollections.count(), 0); QCOMPARE(modifiedCollections.count(), 0); QCOMPARE(deletedCollections.count(), 0); QCOMPARE(unmodifiedCollections.count(), 1); QCOMPARE(unmodifiedCollections.first().id(), remoteAddressbookId); } // and ensure that no contact changes are reported for that collection, // but the remote contact is reported as unmodified. { QList addedContacts; QList modifiedContacts; QList deletedContacts; QList unmodifiedContacts; QVERIFY(cme->fetchContactChanges( remoteAddressbookId, &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 1); QCOMPARE(unmodifiedContacts.first().id(), remoteContactId); } // clean up. QVERIFY(m_cm->removeCollection(remoteAddressbookId)); QVERIFY(cme->clearChangeFlags(remoteAddressbookId, &err)); } void tst_synctransactions::singleCollection_multipleCycles() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; QHash *> additions; QHash *> modifications; QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_synctransactions"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); remoteAddressbook.setExtendedMetaData(QStringLiteral("SyncToken"), "synctoken-one"); QContact syncAlice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); syncAlice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("123454321"); syncAlice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice@wonderland.tld"); syncAlice.saveDetail(&aem); QContactStatusFlags af; af.setFlag(QContactStatusFlags::IsAdded, true); syncAlice.saveDetail(&af); QContact syncBob; QContactName bn; bn.setFirstName("Bob"); bn.setMiddleName("The"); bn.setLastName("Constructor"); syncBob.saveDetail(&bn); QContactPhoneNumber bph; bph.setNumber("543212345"); syncBob.saveDetail(&bph); QContactEmailAddress bem; bem.setEmailAddress("bob@construction.tld"); syncBob.saveDetail(&bem); QContactStatusFlags bf; bf.setFlag(QContactStatusFlags::IsAdded, true); syncBob.saveDetail(&bf); QList addedCollectionContacts; addedCollectionContacts.append(syncAlice); addedCollectionContacts.append(syncBob); additions.insert(&remoteAddressbook, &addedCollectionContacts); const QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy policy( QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges); // initial sync cycle: remote has a non-empty addressbook. QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); QCOMPARE(err, QContactManager::NoError); QVERIFY(!remoteAddressbook.id().isNull()); // id should have been set during save operation. QVERIFY(!addedCollectionContacts.at(0).id().isNull()); // id should have been set during save operation. QVERIFY(!addedCollectionContacts.at(1).id().isNull()); // id should have been set during save operation. QCOMPARE(addedCollectionContacts.at(0).collectionId(), remoteAddressbook.id()); QCOMPARE(addedCollectionContacts.at(1).collectionId(), remoteAddressbook.id()); syncAlice = addedCollectionContacts.at(0); syncBob = addedCollectionContacts.at(1); // wait a while. not necessary but for timestamp debugging purposes... QTest::qWait(250); // now perform some local modifications: // add a contact QContact syncCharlie; syncCharlie.setCollectionId(remoteAddressbook.id()); QContactName cn; cn.setFirstName("Charlie"); cn.setMiddleName("The"); cn.setLastName("Horse"); syncCharlie.saveDetail(&cn); QContactPhoneNumber cph; cph.setNumber("987656789"); syncCharlie.saveDetail(&cph); QContactEmailAddress cem; cem.setEmailAddress("charlie@horse.tld"); syncCharlie.saveDetail(&cem); QVERIFY(m_cm->saveContact(&syncCharlie)); // delete a contact QVERIFY(m_cm->removeContact(syncBob.id())); // modify a contact syncAlice = m_cm->contact(syncAlice.id()); aph = syncAlice.detail(); aph.setNumber("111111111"); QVERIFY(syncAlice.saveDetail(&aph)); QVERIFY(m_cm->saveContact(&syncAlice)); // now perform a second sync cycle. // first, retrieve local changes we need to push to remote server. QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; QVERIFY(cme->fetchCollectionChanges( 5, "tst_synctransactions", &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedCollections.count(), 0); QCOMPARE(modifiedCollections.count(), 0); QCOMPARE(deletedCollections.count(), 0); QCOMPARE(unmodifiedCollections.count(), 1); QCOMPARE(unmodifiedCollections.first().extendedMetaData().value( QStringLiteral("SyncToken")).toString(), QStringLiteral("synctoken-one")); QList addedContacts; QList modifiedContacts; QList deletedContacts; QList unmodifiedContacts; QVERIFY(cme->fetchContactChanges( remoteAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 1); QCOMPARE(modifiedContacts.count(), 1); QCOMPARE(deletedContacts.count(), 1); QCOMPARE(unmodifiedContacts.count(), 0); QCOMPARE(addedContacts.first().id(), syncCharlie.id()); QCOMPARE(deletedContacts.first().id(), syncBob.id()); QCOMPARE(modifiedContacts.first().id(), syncAlice.id()); // at this point, Bob should have been marked as deleted, // and should not be accessible using the normal access API. QContact deletedBob = m_cm->contact(syncBob.id()); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); QVERIFY(deletedBob.id().isNull()); // but we should still be able to access deleted Bob via specific filter. QContactCollectionFilter allCollections; QList deletedContactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContactIds.size(), 1); QVERIFY(deletedContactIds.contains(syncBob.id())); deletedContacts.clear(); deletedContacts = m_cm->contacts(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContacts.size(), 1); QCOMPARE(deletedContacts.first().detail().number(), QStringLiteral("543212345")); // Bob's phone number. // now fetch changes from the remote server, and calculate the delta. // in this case, we simulate that the user added a hobby on the remote server // for contact Alice, and deleted contact Charlie, and these changes need // to be stored to the local database. syncAlice = modifiedContacts.first(); QContactHobby ah; ah.setHobby("Tennis"); syncAlice.saveDetail(&ah); af = syncAlice.detail(); af.setFlag(QContactStatusFlags::IsModified, true); syncAlice.saveDetail(&af, QContact::IgnoreAccessConstraints); syncCharlie = addedContacts.first(); QContactStatusFlags cf = syncCharlie.detail(); cf.setFlag(QContactStatusFlags::IsDeleted, true); syncCharlie.saveDetail(&cf, QContact::IgnoreAccessConstraints); // specify an updated ctag for the addressbook. remoteAddressbook.setExtendedMetaData(QStringLiteral("SyncToken"), "synctoken-two"); // write the remote changes to the local database. additions.clear(); modifications.clear(); QList modifiedCollectionContacts; modifiedCollectionContacts.append(syncAlice); // modification modifiedCollectionContacts.append(syncCharlie); // deletion modifications.insert(&remoteAddressbook, &modifiedCollectionContacts); QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); // Alice should have been updated with the new hobby. // The other details should not have been changed. syncAlice = m_cm->contact(syncAlice.id()); QCOMPARE(syncAlice.detail().hobby(), QStringLiteral("Tennis")); QCOMPARE(syncAlice.detail().number(), QStringLiteral("111111111")); // we should no longer be able to access the deleted contacts, // as the clearChangeFlags parameter was "true" in the above method call. deletedContactIds.clear(); deletedContactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContactIds.size(), 0); // now perform another sync cycle. // there should be no local changes reported since the last clearChangeFlags() // (in this case, since the last storeChanges() call). addedCollections.clear(); modifiedCollections.clear(); deletedCollections.clear(); unmodifiedCollections.clear(); QVERIFY(cme->fetchCollectionChanges( 5, "tst_synctransactions", &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedCollections.count(), 0); QCOMPARE(modifiedCollections.count(), 0); QCOMPARE(deletedCollections.count(), 0); QCOMPARE(unmodifiedCollections.count(), 1); QCOMPARE(unmodifiedCollections.first().extendedMetaData().value( QStringLiteral("SyncToken")).toString(), QStringLiteral("synctoken-two")); addedContacts.clear(); modifiedContacts.clear(); deletedContacts.clear(); unmodifiedContacts.clear(); QVERIFY(cme->fetchContactChanges( remoteAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(addedContacts.size(), 0); QCOMPARE(modifiedContacts.size(), 0); QCOMPARE(deletedContacts.size(), 0); QCOMPARE(unmodifiedContacts.size(), 1); QCOMPARE(unmodifiedContacts.first().id(), syncAlice.id()); syncAlice = unmodifiedContacts.first(); // report remote deletion of the entire collection and store locally. additions.clear(); modifications.clear(); QVERIFY(cme->storeChanges( &additions, &modifications, QList() << remoteAddressbook.id(), policy, true, &err)); // attempting to fetch the collection should fail QContactCollection deletedCollection = m_cm->collection(remoteAddressbook.id()); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); QVERIFY(deletedCollection.id().isNull()); // attempting to fetch deleted contacts should return no results. // the deletion of the contacts as a result of the deletion of the collection // will in this case be applied immediately (and purged) due to the // clearChangeFlags=true parameter to the above storeChanges() call. deletedContactIds.clear(); deletedContactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContactIds.size(), 0); } void tst_synctransactions::singleCollection_unhandledChanges() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; QHash *> additions; QHash *> modifications; QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_synctransactions"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QContact syncAlice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); syncAlice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("123454321"); syncAlice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice@wonderland.tld"); syncAlice.saveDetail(&aem); QContactStatusFlags af; af.setFlag(QContactStatusFlags::IsAdded, true); syncAlice.saveDetail(&af); QContact syncBob; QContactName bn; bn.setFirstName("Bob"); bn.setMiddleName("The"); bn.setLastName("Constructor"); syncBob.saveDetail(&bn); QContactPhoneNumber bph; bph.setNumber("543212345"); syncBob.saveDetail(&bph); QContactEmailAddress bem; bem.setEmailAddress("bob@construction.tld"); syncBob.saveDetail(&bem); QContactStatusFlags bf; bf.setFlag(QContactStatusFlags::IsAdded, true); syncBob.saveDetail(&bf); QList addedCollectionContacts; addedCollectionContacts.append(syncAlice); addedCollectionContacts.append(syncBob); additions.insert(&remoteAddressbook, &addedCollectionContacts); const QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy policy( QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges); // initial sync cycle: remote has a non-empty addressbook. QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); QCOMPARE(err, QContactManager::NoError); QVERIFY(!remoteAddressbook.id().isNull()); // id should have been set during save operation. QVERIFY(!addedCollectionContacts.at(0).id().isNull()); // id should have been set during save operation. QVERIFY(!addedCollectionContacts.at(1).id().isNull()); // id should have been set during save operation. QCOMPARE(addedCollectionContacts.at(0).collectionId(), remoteAddressbook.id()); QCOMPARE(addedCollectionContacts.at(1).collectionId(), remoteAddressbook.id()); syncAlice = addedCollectionContacts.at(0); syncBob = addedCollectionContacts.at(1); // wait a while. not necessary but for timestamp debugging purposes... QTest::qWait(250); // now perform a local modification: // add a contact QContact syncCharlie; syncCharlie.setCollectionId(remoteAddressbook.id()); QContactName cn; cn.setFirstName("Charlie"); cn.setMiddleName("The"); cn.setLastName("Horse"); syncCharlie.saveDetail(&cn); QContactPhoneNumber cph; cph.setNumber("987656789"); syncCharlie.saveDetail(&cph); QContactEmailAddress cem; cem.setEmailAddress("charlie@horse.tld"); syncCharlie.saveDetail(&cem); QVERIFY(m_cm->saveContact(&syncCharlie)); // now begin a new sync cycle. fetch local changes for push to remote server. // this should report the local addition of the Charlie contact. QList addedContacts; QList modifiedContacts; QList deletedContacts; QList unmodifiedContacts; QVERIFY(cme->fetchContactChanges( remoteAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 1); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 2); QCOMPARE(unmodifiedContacts.at(0).id(), syncAlice.id()); QCOMPARE(unmodifiedContacts.at(1).id(), syncBob.id()); QCOMPARE(addedContacts.first().id(), syncCharlie.id()); syncAlice = unmodifiedContacts.at(0); syncBob = unmodifiedContacts.at(1); syncCharlie = addedContacts.first(); // now we simulate the case where: // while the sync plugin is upsyncing the local addition to the remote server, // the device user modifies another contact locally. This modification is // "unhandled" in the current sync cycle, as the sync plugin doesn't know that // this change exists, yet. syncAlice = m_cm->contact(syncAlice.id()); aph = syncAlice.detail(); aph.setNumber("111111111"); QVERIFY(syncAlice.saveDetail(&aph)); QContactHobby ah = syncAlice.detail(); ah.setHobby("Tennis"); QVERIFY(syncAlice.saveDetail(&ah)); aem = syncAlice.detail(); QVERIFY(syncAlice.removeDetail(&aem)); QVERIFY(m_cm->saveContact(&syncAlice)); // now the sync plugin has successfully upsynced the local addition change. // it now downsyncs the remote change: deletion of Bob. bf = syncBob.detail(); bf.setFlag(QContactStatusFlags::IsAdded, false); bf.setFlag(QContactStatusFlags::IsDeleted, true); syncBob.saveDetail(&bf, QContact::IgnoreAccessConstraints); // write the remote changes to the local database. additions.clear(); modifications.clear(); QList modifiedCollectionContacts; modifiedCollectionContacts.append(syncBob); // deletion modifications.insert(&remoteAddressbook, &modifiedCollectionContacts); QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); QCOMPARE(err, QContactManager::NoError); // the previous sync cycle is completed. // now ensure that the previously unhandled change is reported // during the next sync cycle. addedContacts.clear(); modifiedContacts.clear(); deletedContacts.clear(); unmodifiedContacts.clear(); QVERIFY(cme->fetchContactChanges( remoteAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 1); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 1); QCOMPARE(modifiedContacts.first().id(), syncAlice.id()); QCOMPARE(unmodifiedContacts.first().id(), syncCharlie.id()); // ensure the specific changes are reported. syncAlice = modifiedContacts.first(); QCOMPARE(syncAlice.detail().hobby(), ah.hobby()); QVERIFY(syncAlice.detail().value(QContactDetail__FieldChangeFlags).toInt() & QContactDetail__ChangeFlag_IsAdded); QCOMPARE(syncAlice.detail().number(), aph.number()); QVERIFY(syncAlice.detail().value(QContactDetail__FieldChangeFlags).toInt() & QContactDetail__ChangeFlag_IsModified); QVERIFY(syncAlice.detail().value(QContactDetail__FieldChangeFlags).toInt() & QContactDetail__ChangeFlag_IsDeleted); // clean up. QVERIFY(m_cm->removeCollection(remoteAddressbook.id())); QVERIFY(cme->clearChangeFlags(remoteAddressbook.id(), &err)); } void tst_synctransactions::multipleCollections() { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_cm); QContactManager::Error err = QContactManager::NoError; QHash *> additions; QHash *> modifications; QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_synctransactions"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QContactCollection anotherAddressbook; anotherAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("another")); anotherAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_synctransactions"); anotherAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); anotherAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/another"); QContact syncAlice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); syncAlice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("123454321"); syncAlice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice@wonderland.tld"); syncAlice.saveDetail(&aem); QContactStatusFlags af; af.setFlag(QContactStatusFlags::IsAdded, true); syncAlice.saveDetail(&af); QContact syncBob; QContactName bn; bn.setFirstName("Bob"); bn.setMiddleName("The"); bn.setLastName("Constructor"); syncBob.saveDetail(&bn); QContactPhoneNumber bph; bph.setNumber("543212345"); syncBob.saveDetail(&bph); QContactEmailAddress bem; bem.setEmailAddress("bob@construction.tld"); syncBob.saveDetail(&bem); QContactStatusFlags bf; bf.setFlag(QContactStatusFlags::IsAdded, true); syncBob.saveDetail(&bf); QList addedCollectionContacts; addedCollectionContacts.append(syncAlice); addedCollectionContacts.append(syncBob); additions.insert(&remoteAddressbook, &addedCollectionContacts); QList emptyCollectionContacts; additions.insert(&anotherAddressbook, &emptyCollectionContacts); const QtContactsSqliteExtensions::ContactManagerEngine::ConflictResolutionPolicy policy( QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges); // initial sync cycle: remote has a non-empty addressbook. QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); QCOMPARE(err, QContactManager::NoError); QVERIFY(!remoteAddressbook.id().isNull()); // id should have been set during save operation. QVERIFY(!anotherAddressbook.id().isNull()); // id should have been set during save operation. QVERIFY(!addedCollectionContacts.at(0).id().isNull()); // id should have been set during save operation. QVERIFY(!addedCollectionContacts.at(1).id().isNull()); // id should have been set during save operation. QCOMPARE(addedCollectionContacts.at(0).collectionId(), remoteAddressbook.id()); QCOMPARE(addedCollectionContacts.at(1).collectionId(), remoteAddressbook.id()); syncAlice = addedCollectionContacts.at(0); syncBob = addedCollectionContacts.at(1); // wait a while. not necessary but for timestamp debugging purposes... QTest::qWait(250); // modify an addressbook locally. anotherAddressbook.setMetaData(QContactCollection::KeyDescription, QStringLiteral("another test addressbook")); QVERIFY(m_cm->saveCollection(&anotherAddressbook)); // and add a contact to it locally. QContact syncCharlie; syncCharlie.setCollectionId(anotherAddressbook.id()); QContactName cn; cn.setFirstName("Charlie"); cn.setMiddleName("The"); cn.setLastName("Horse"); syncCharlie.saveDetail(&cn); QContactPhoneNumber cph; cph.setNumber("987656789"); syncCharlie.saveDetail(&cph); QContactEmailAddress cem; cem.setEmailAddress("charlie@horse.tld"); syncCharlie.saveDetail(&cem); QVERIFY(m_cm->saveContact(&syncCharlie)); // also simulate a local deletion of a contact in the other addressbook. QVERIFY(m_cm->removeContact(syncBob.id())); // begin a new sync cycle // first, fetch local collection changes using the sync API. // note that the remoteAddressbook will be reported as unmodified // even though its content changed, as this API only reports // changes to collection metadata. QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; QVERIFY(cme->fetchCollectionChanges( 5, QString(), // should be able to fetch by accountId &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedCollections.size(), 0); QCOMPARE(modifiedCollections.size(), 1); QCOMPARE(deletedCollections.size(), 0); QCOMPARE(unmodifiedCollections.size(), 1); QCOMPARE(modifiedCollections.at(0).id(), anotherAddressbook.id()); QCOMPARE(unmodifiedCollections.at(0).id(), remoteAddressbook.id()); // then fetch local contact changes within each collection. QList addedContacts; QList modifiedContacts; QList deletedContacts; QList unmodifiedContacts; QVERIFY(cme->fetchContactChanges( remoteAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 1); QCOMPARE(unmodifiedContacts.count(), 1); QCOMPARE(deletedContacts.at(0).id(), syncBob.id()); QCOMPARE(unmodifiedContacts.at(0).id(), syncAlice.id()); addedContacts.clear(); modifiedContacts.clear(); deletedContacts.clear(); unmodifiedContacts.clear(); QVERIFY(cme->fetchContactChanges( anotherAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 1); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 0); QCOMPARE(addedContacts.at(0).id(), syncCharlie.id()); // note: performing that operation multiple times should return the same results, // as fetching changes should not clear any change flags which are set. addedContacts.clear(); modifiedContacts.clear(); deletedContacts.clear(); unmodifiedContacts.clear(); QVERIFY(cme->fetchContactChanges( remoteAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 1); QCOMPARE(unmodifiedContacts.count(), 1); QCOMPARE(deletedContacts.at(0).id(), syncBob.id()); QCOMPARE(unmodifiedContacts.at(0).id(), syncAlice.id()); addedContacts.clear(); modifiedContacts.clear(); deletedContacts.clear(); unmodifiedContacts.clear(); QVERIFY(cme->fetchContactChanges( anotherAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 1); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 0); QCOMPARE(addedContacts.at(0).id(), syncCharlie.id()); // finally, simulate storing remote changes to the local database. // in this simulated sync cycle, no remote changes occurred, so just clear the change flags // for the two synced addressbooks. This should also purge the deleted Bob contact. QVERIFY(cme->clearChangeFlags(anotherAddressbook.id(), &err)); QCOMPARE(err, QContactManager::NoError); QVERIFY(cme->clearChangeFlags(remoteAddressbook.id(), &err)); QCOMPARE(err, QContactManager::NoError); // now simulate local deletion of the anotherAddressbook. QVERIFY(m_cm->removeCollection(anotherAddressbook.id())); // the contact within that collection should be marked as deleted // and thus not retrievable using the normal API unless the specific // IsDeleted filter is set. QContact deletedContact = m_cm->contact(syncCharlie.id()); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); QVERIFY(deletedContact.id().isNull()); QContactIdFilter idFilter; idFilter.setIds(QList() << syncCharlie.id()); deletedContacts = m_cm->contacts(idFilter & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContacts.size(), 1); QCOMPARE(deletedContacts.at(0).id(), syncCharlie.id()); QCOMPARE(deletedContacts.at(0).detail().number(), syncCharlie.detail().number()); QContactCollectionFilter allCollections; deletedContacts = m_cm->contacts(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContacts.size(), 1); // should not include Bob, who should have been purged due to clearChangeFlags(). QCOMPARE(deletedContacts.at(0).id(), syncCharlie.id()); // now simulate another sync cycle. // step one: get local collection changes. addedCollections.clear(); modifiedCollections.clear(); deletedCollections.clear(); unmodifiedCollections.clear(); QVERIFY(cme->fetchCollectionChanges( 0, QStringLiteral("tst_synctransactions"), // should be able to fetch by applicationName &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedCollections.size(), 0); QCOMPARE(modifiedCollections.size(), 0); QCOMPARE(deletedCollections.size(), 1); QCOMPARE(unmodifiedCollections.size(), 1); QCOMPARE(deletedCollections.at(0).id(), anotherAddressbook.id()); QCOMPARE(unmodifiedCollections.at(0).id(), remoteAddressbook.id()); // step two: get local contact changes. addedContacts.clear(); modifiedContacts.clear(); deletedContacts.clear(); unmodifiedContacts.clear(); QVERIFY(cme->fetchContactChanges( remoteAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 0); QCOMPARE(unmodifiedContacts.count(), 1); QCOMPARE(unmodifiedContacts.at(0).id(), syncAlice.id()); syncAlice = unmodifiedContacts.at(0); addedContacts.clear(); modifiedContacts.clear(); deletedContacts.clear(); unmodifiedContacts.clear(); QVERIFY(cme->fetchContactChanges( anotherAddressbook.id(), &addedContacts, &modifiedContacts, &deletedContacts, &unmodifiedContacts, &err)); QCOMPARE(err, QContactManager::NoError); QCOMPARE(addedContacts.count(), 0); QCOMPARE(modifiedContacts.count(), 0); QCOMPARE(deletedContacts.count(), 1); QCOMPARE(unmodifiedContacts.count(), 0); QCOMPARE(deletedContacts.at(0).id(), syncCharlie.id()); // step three: store remote changes to local database. QContactHobby ah; ah.setHobby("Tennis"); QVERIFY(syncAlice.saveDetail(&ah)); af = syncAlice.detail(); af.setFlag(QContactStatusFlags::IsAdded, false); af.setFlag(QContactStatusFlags::IsModified, true); QVERIFY(syncAlice.saveDetail(&af, QContact::IgnoreAccessConstraints)); additions.clear(); modifications.clear(); QList modifiedCollectionContacts; modifiedCollectionContacts.append(syncAlice); modifications.insert(&remoteAddressbook, &modifiedCollectionContacts); QVERIFY(cme->storeChanges( &additions, &modifications, QList(), policy, true, &err)); QCOMPARE(err, QContactManager::NoError); QVERIFY(cme->clearChangeFlags(anotherAddressbook.id(), &err)); QCOMPARE(err, QContactManager::NoError); // the above operations should have cleared change flags, causing purge of Charlie etc. deletedContacts = m_cm->contacts(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContacts.size(), 0); // clean up. QVERIFY(m_cm->removeCollection(remoteAddressbook.id())); QVERIFY(cme->clearChangeFlags(remoteAddressbook.id(), &err)); } void tst_synctransactions::syncRequests() { // Now test that the sync transaction request classes work properly. // This test does the same that the singleCollection_multipleCycles() // test does, but with requests rather than raw engine calls. QContactCollectionFilter allCollections; QContactCollectionId remoteAddressbookId; QContactId aliceId, bobId, charlieId; { QContactCollection remoteAddressbook; remoteAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("test")); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, "tst_synctransactions"); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); remoteAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/test"); QContact syncAlice; QContactName an; an.setFirstName("Alice"); an.setMiddleName("In"); an.setLastName("Wonderland"); syncAlice.saveDetail(&an); QContactPhoneNumber aph; aph.setNumber("123454321"); syncAlice.saveDetail(&aph); QContactEmailAddress aem; aem.setEmailAddress("alice@wonderland.tld"); syncAlice.saveDetail(&aem); QContactStatusFlags af; af.setFlag(QContactStatusFlags::IsAdded, true); syncAlice.saveDetail(&af); QContact syncBob; QContactName bn; bn.setFirstName("Bob"); bn.setMiddleName("The"); bn.setLastName("Constructor"); syncBob.saveDetail(&bn); QContactPhoneNumber bph; bph.setNumber("543212345"); syncBob.saveDetail(&bph); QContactEmailAddress bem; bem.setEmailAddress("bob@construction.tld"); syncBob.saveDetail(&bem); QContactStatusFlags bf; bf.setFlag(QContactStatusFlags::IsAdded, true); syncBob.saveDetail(&bf); QList addedCollectionContacts; addedCollectionContacts.append(syncAlice); addedCollectionContacts.append(syncBob); QHash > additions; additions.insert(remoteAddressbook, addedCollectionContacts); QContactChangesSaveRequest *csr = new QContactChangesSaveRequest; csr->setManager(m_cm); csr->setAddedCollections(additions); csr->setClearChangeFlags(true); csr->start(); QVERIFY(csr->waitForFinished(5000)); QCOMPARE(csr->error(), QContactManager::NoError); // ensure that the values have been updated as a result of the operation // e.g. to include ids etc. remoteAddressbookId = csr->addedCollections().keys().first().id(); QVERIFY(!remoteAddressbookId.isNull()); aliceId = csr->addedCollections().value(csr->addedCollections().keys().first()).first().id(); bobId = csr->addedCollections().value(csr->addedCollections().keys().first()).last().id(); QVERIFY(!aliceId.isNull()); QVERIFY(!bobId.isNull()); QVERIFY(aliceId != bobId); QContact reloadAlice = m_cm->contact(aliceId); QCOMPARE(m_cm->error(), QContactManager::NoError); QCOMPARE(reloadAlice.detail().number(), syncAlice.detail().number()); QCOMPARE(reloadAlice.detail().emailAddress(), syncAlice.detail().emailAddress()); QContact reloadBob = m_cm->contact(bobId); QCOMPARE(m_cm->error(), QContactManager::NoError); QCOMPARE(reloadBob.detail().number(), syncBob.detail().number()); QCOMPARE(reloadBob.detail().emailAddress(), syncBob.detail().emailAddress()); } { // now perform some local modifications: // add a contact QContact syncCharlie; syncCharlie.setCollectionId(remoteAddressbookId); QContactName cn; cn.setFirstName("Charlie"); cn.setMiddleName("The"); cn.setLastName("Horse"); syncCharlie.saveDetail(&cn); QContactPhoneNumber cph; cph.setNumber("987656789"); syncCharlie.saveDetail(&cph); QContactEmailAddress cem; cem.setEmailAddress("charlie@horse.tld"); syncCharlie.saveDetail(&cem); QVERIFY(m_cm->saveContact(&syncCharlie)); charlieId = syncCharlie.id(); // delete a contact QVERIFY(m_cm->removeContact(bobId)); // modify a contact QContact syncAlice = m_cm->contact(aliceId); QContactPhoneNumber aph = syncAlice.detail(); aph.setNumber("111111111"); QVERIFY(syncAlice.saveDetail(&aph)); QVERIFY(m_cm->saveContact(&syncAlice)); } { // now perform a second sync cycle. // first, retrieve local collection metadata changes we need to push to remote server. QContactCollectionChangesFetchRequest *ccfr = new QContactCollectionChangesFetchRequest; ccfr->setManager(m_cm); ccfr->setApplicationName(QStringLiteral("tst_synctransactions")); ccfr->start(); QVERIFY(ccfr->waitForFinished(5000)); QCOMPARE(ccfr->error(), QContactManager::NoError); QVERIFY(ccfr->addedCollections().isEmpty()); QVERIFY(ccfr->modifiedCollections().isEmpty()); QVERIFY(ccfr->removedCollections().isEmpty()); QCOMPARE(ccfr->unmodifiedCollections().size(), 1); QCOMPARE(ccfr->unmodifiedCollections().first().id(), remoteAddressbookId); // second, retrieve local contact changes we need to push to the remote server. QContactChangesFetchRequest *cfr = new QContactChangesFetchRequest; cfr->setManager(m_cm); cfr->setCollectionId(remoteAddressbookId); cfr->start(); QVERIFY(cfr->waitForFinished(5000)); QCOMPARE(cfr->error(), QContactManager::NoError); QCOMPARE(cfr->addedContacts().size(), 1); QCOMPARE(cfr->addedContacts().first().id(), charlieId); QCOMPARE(cfr->modifiedContacts().size(), 1); QCOMPARE(cfr->modifiedContacts().first().id(), aliceId); QCOMPARE(cfr->removedContacts().size(), 1); QCOMPARE(cfr->removedContacts().first().id(), bobId); QCOMPARE(cfr->unmodifiedContacts().size(), 0); // at this point, Bob should have been marked as deleted, // and should not be accessible using the normal access API. QContact deletedBob = m_cm->contact(bobId); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); QVERIFY(deletedBob.id().isNull()); // but we should still be able to access deleted Bob via specific filter. QList deletedContactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContactIds.size(), 1); QVERIFY(deletedContactIds.contains(bobId)); QList deletedContacts = m_cm->contacts(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContacts.size(), 1); QCOMPARE(deletedContacts.first().detail().number(), QStringLiteral("543212345")); // Bob's phone number. // third, fetch changes from the remote server, and calculate the delta. // in this case, we simulate that the user added a hobby on the remote server // for contact Alice, and deleted contact Charlie, and these changes need // to be stored to the local database. QContact syncAlice = cfr->modifiedContacts().first(); QContactHobby ah; ah.setHobby("Tennis"); syncAlice.saveDetail(&ah); QContactStatusFlags af = syncAlice.detail(); af.setFlag(QContactStatusFlags::IsModified, true); syncAlice.saveDetail(&af, QContact::IgnoreAccessConstraints); QContact syncCharlie = cfr->addedContacts().first(); QContactStatusFlags cf = syncCharlie.detail(); cf.setFlag(QContactStatusFlags::IsDeleted, true); syncCharlie.saveDetail(&cf, QContact::IgnoreAccessConstraints); QContactCollection remoteAddressbook = m_cm->collection(remoteAddressbookId); QHash > modifications; modifications.insert(remoteAddressbook, QList() << syncAlice << syncCharlie); QContactChangesSaveRequest *csr = new QContactChangesSaveRequest; csr->setManager(m_cm); csr->setClearChangeFlags(true); csr->setModifiedCollections(modifications); csr->start(); QVERIFY(csr->waitForFinished(5000)); QCOMPARE(csr->error(), QContactManager::NoError); // Alice should have been updated with the new hobby. // The other details should not have been changed. syncAlice = m_cm->contact(aliceId); QCOMPARE(syncAlice.detail().hobby(), QStringLiteral("Tennis")); QCOMPARE(syncAlice.detail().number(), QStringLiteral("111111111")); // we should no longer be able to access the deleted contacts, // as the clearChangeFlags parameter was "true" in the above request. deletedContactIds.clear(); deletedContactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContactIds.size(), 0); } { // now perform another sync cycle. // there should be no local changes reported since the last clearChangeFlags() // (in this case, since the last storeChanges() call). // first, retrieve local collection metadata changes we need to push to remote server. QContactCollectionChangesFetchRequest *ccfr = new QContactCollectionChangesFetchRequest; ccfr->setManager(m_cm); ccfr->setApplicationName(QStringLiteral("tst_synctransactions")); ccfr->start(); QVERIFY(ccfr->waitForFinished(5000)); QCOMPARE(ccfr->error(), QContactManager::NoError); QVERIFY(ccfr->addedCollections().isEmpty()); QVERIFY(ccfr->modifiedCollections().isEmpty()); QVERIFY(ccfr->removedCollections().isEmpty()); QCOMPARE(ccfr->unmodifiedCollections().size(), 1); QCOMPARE(ccfr->unmodifiedCollections().first().id(), remoteAddressbookId); // second, retrieve local contact changes we need to push to the remote server. QContactChangesFetchRequest *cfr = new QContactChangesFetchRequest; cfr->setManager(m_cm); cfr->setCollectionId(remoteAddressbookId); cfr->start(); QVERIFY(cfr->waitForFinished(5000)); QCOMPARE(cfr->error(), QContactManager::NoError); QCOMPARE(cfr->addedContacts().size(), 0); QCOMPARE(cfr->modifiedContacts().size(), 0); QCOMPARE(cfr->removedContacts().size(), 0); QCOMPARE(cfr->unmodifiedContacts().size(), 1); QCOMPARE(cfr->unmodifiedContacts().first().id(), aliceId); // third, report remote changes and store locally // in this case, we simulate remote deletion of the entire collection. QContactChangesSaveRequest *csr = new QContactChangesSaveRequest; csr->setManager(m_cm); csr->setClearChangeFlags(true); csr->setRemovedCollections(QList() << remoteAddressbookId); csr->start(); QVERIFY(csr->waitForFinished(5000)); QCOMPARE(csr->error(), QContactManager::NoError); // attempting to fetch the collection should fail QContactCollection deletedCollection = m_cm->collection(remoteAddressbookId); QCOMPARE(m_cm->error(), QContactManager::DoesNotExistError); QVERIFY(deletedCollection.id().isNull()); // attempting to fetch deleted contacts should return no results. // the deletion of the contacts as a result of the deletion of the collection // will in this case be applied immediately (and purged) due to the // clearChangeFlags=true parameter to the above storeChanges() call. QList deletedContactIds = m_cm->contactIds(allCollections & QContactStatusFlags::matchFlag(QContactStatusFlags::IsDeleted, QContactFilter::MatchContains)); QCOMPARE(deletedContactIds.size(), 0); } } bool haveExpectedContent(const QContact &c, const QString &phone, TestSyncAdaptor::PhoneModifiability modifiability, const QString &email) { const QContactPhoneNumber &phn(c.detail()); TestSyncAdaptor::PhoneModifiability modif = TestSyncAdaptor::ImplicitlyModifiable; if (phn.values().contains(QContactDetail__FieldModifiable)) { modif = phn.value(QContactDetail__FieldModifiable) ? TestSyncAdaptor::ExplicitlyModifiable : TestSyncAdaptor::ExplicitlyNonModifiable; } return phn.number() == phone && modif == modifiability && c.detail().emailAddress() == email; } void tst_synctransactions::twcsa_nodelta() { // construct a sync adaptor, and prefill its read-write collection with 3 contacts. const int accountId = 3; const QString applicationName = QStringLiteral("tst_synctransactions::twcsa_nodelta"); TestSyncAdaptor tsa(accountId, applicationName, *m_cm); tsa.addRemoteContact("John", "One", "1111111", TestSyncAdaptor::ImplicitlyModifiable); tsa.addRemoteContact("Luke", "Two", "2222222", TestSyncAdaptor::ExplicitlyModifiable); tsa.addRemoteContact("Mark", "Three", "3333333", TestSyncAdaptor::ExplicitlyNonModifiable); // perform the initial sync cycle. QSignalSpy finishedSpy(&tsa, SIGNAL(finished())); QSignalSpy failedSpy(&tsa, SIGNAL(failed())); tsa.performTwoWaySync(); QTRY_COMPARE(failedSpy.count() + finishedSpy.count(), 1); QTRY_COMPARE(finishedSpy.count(), 1); // should have 8 more contacts than we had before: // two built-in contacts from non-aggregable read-only collection, // and then 3 constituent contacts from the read-write collection, plus 3 aggregates. QContactCollectionFilter allCollections; QList allContacts = m_cm->contacts(allCollections); QContactId aliceId, bobId; QContactId johnId, lukeId, markId; QContactId aggJohnId, aggLukeId, aggMarkId; for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); const bool isAggregate = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size() > 0; const QString fname = c.detail().firstName(); if (!isAggregate && fname == QStringLiteral("Alice")) aliceId = c.id(); if (isAggregate && fname == QStringLiteral("Alice")) QCOMPARE(isAggregate, false); // force failure. if (!isAggregate && fname == QStringLiteral("Bob")) bobId = c.id(); if (isAggregate && fname == QStringLiteral("Bob")) QCOMPARE(isAggregate, false); // force failure. if (!isAggregate && fname == QStringLiteral("John")) johnId = c.id(); if (isAggregate && fname == QStringLiteral("John")) aggJohnId = c.id(); if (!isAggregate && fname == QStringLiteral("Luke")) lukeId = c.id(); if (isAggregate && fname == QStringLiteral("Luke")) aggLukeId = c.id(); if (!isAggregate && fname == QStringLiteral("Mark")) markId = c.id(); if (isAggregate && fname == QStringLiteral("Mark")) aggMarkId = c.id(); } QCOMPARE(allContacts.size(), 8); QVERIFY(aliceId != QContactId()); QVERIFY(bobId != QContactId()); QVERIFY(johnId != QContactId()); QVERIFY(lukeId != QContactId()); QVERIFY(markId != QContactId()); QVERIFY(aggJohnId != QContactId()); QVERIFY(aggLukeId != QContactId()); QVERIFY(aggMarkId != QContactId()); // ensure that the collections themselves have been downsynced QContactCollection emptyCollection, readonlyCollection, readwriteCollection; QString emptyCollectionCtag, readonlyCollectionCtag, readwriteCollectionCtag; for (const QContactCollection &c : m_cm->collections()) { const int collectionAccountId = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); const QString collectionAppName = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); const QString collectionName = c.metaData(QContactCollection::KeyName).toString(); if (collectionAccountId == accountId && collectionAppName == applicationName) { if (collectionName == QStringLiteral("Empty")) { emptyCollection = c; emptyCollectionCtag = c.extendedMetaData(QStringLiteral("ctag")).toString(); } else if (collectionName == QStringLiteral("ReadOnly")) { readonlyCollection = c; readonlyCollectionCtag = c.extendedMetaData(QStringLiteral("ctag")).toString(); } else { QCOMPARE(collectionName, QStringLiteral("ReadWrite")); readwriteCollection = c; readwriteCollectionCtag = c.extendedMetaData(QStringLiteral("ctag")).toString(); } } } QVERIFY(!emptyCollection.id().isNull()); QVERIFY(!readonlyCollection.id().isNull()); QVERIFY(!readwriteCollection.id().isNull()); QVERIFY(!emptyCollectionCtag.isEmpty()); QVERIFY(!readonlyCollectionCtag.isEmpty()); QVERIFY(!readwriteCollectionCtag.isEmpty()); // ensure that the downsynced contacts have the data we expect // note that aggregate contact details are explicitly non-modifiable always. QVERIFY(haveExpectedContent(m_cm->contact(aliceId), QStringLiteral("123123123"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(bobId), QString(), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("bob@constructor.tld"))); QVERIFY(haveExpectedContent(m_cm->contact(johnId), QStringLiteral("1111111"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(aggJohnId), QStringLiteral("1111111"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(lukeId), QStringLiteral("2222222"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(aggLukeId), QStringLiteral("2222222"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(markId), QStringLiteral("3333333"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(aggMarkId), QStringLiteral("3333333"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); // and ensure they belong to the collections we expect QCOMPARE(m_cm->contact(aliceId).collectionId(), readonlyCollection.id()); QCOMPARE(m_cm->contact(bobId).collectionId(), readonlyCollection.id()); QCOMPARE(m_cm->contact(johnId).collectionId(), readwriteCollection.id()); QCOMPARE(m_cm->contact(lukeId).collectionId(), readwriteCollection.id()); QCOMPARE(m_cm->contact(markId).collectionId(), readwriteCollection.id()); QCOMPARE(m_cm->contact(aggJohnId).collectionId(), QContactCollectionId(m_cm->managerUri(), aggregateAddressbookId())); QCOMPARE(m_cm->contact(aggLukeId).collectionId(), QContactCollectionId(m_cm->managerUri(), aggregateAddressbookId())); QCOMPARE(m_cm->contact(aggMarkId).collectionId(), QContactCollectionId(m_cm->managerUri(), aggregateAddressbookId())); // simulate a local addition and local modification QContact matthew; QContactName mn; mn.setFirstName(QStringLiteral("Matthew")); mn.setLastName(QStringLiteral("Four")); matthew.saveDetail(&mn); QContactPhoneNumber mp; mp.setNumber(QStringLiteral("4444444")); matthew.saveDetail(&mn); matthew.saveDetail(&mp); matthew.setCollectionId(readwriteCollection.id()); QVERIFY(m_cm->saveContact(&matthew)); QContact mark = m_cm->contact(markId); QContactEmailAddress me; me.setEmailAddress(QStringLiteral("mark@three.tld")); mark.saveDetail(&me); QVERIFY(m_cm->saveContact(&mark)); // simulate a remote modification, and a remote deletion. tsa.changeRemoteContactPhone(QStringLiteral("John"), QStringLiteral("One"), QStringLiteral("1111123")); tsa.removeRemoteContact(QStringLiteral("Luke"), QStringLiteral("Two")); // now perform another sync cycle tsa.performTwoWaySync(); QTRY_COMPARE(failedSpy.count() + finishedSpy.count(), 2); QTRY_COMPARE(finishedSpy.count(), 2); // ensure that the local database now contains the appropriate information allContacts = m_cm->contacts(allCollections); QContactId matthewId, aggMatthewId; for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); const bool isAggregate = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size() > 0; const QString fname = c.detail().firstName(); if (!isAggregate && fname == QStringLiteral("Alice")) QCOMPARE(c.id(), aliceId); // id should not have changed else if (isAggregate && fname == QStringLiteral("Alice")) QCOMPARE(isAggregate, false); // force failure. else if (!isAggregate && fname == QStringLiteral("Bob")) QCOMPARE(c.id(), bobId); // id should not have changed else if (isAggregate && fname == QStringLiteral("Bob")) QCOMPARE(isAggregate, false); // force failure. else if (!isAggregate && fname == QStringLiteral("John")) QCOMPARE(c.id(), johnId); // id should not have changed else if (isAggregate && fname == QStringLiteral("John")) QCOMPARE(c.id(), aggJohnId); // id should not have changed else if (!isAggregate && fname == QStringLiteral("Mark")) QCOMPARE(c.id(), markId); // id should not have changed else if (isAggregate && fname == QStringLiteral("Mark")) QCOMPARE(c.id(), aggMarkId); // id should not have changed else if (!isAggregate && fname == QStringLiteral("Matthew")) matthewId = c.id(); else if (isAggregate && fname == QStringLiteral("Matthew")) aggMatthewId = c.id(); else QCOMPARE(fname, QStringLiteral("Alice")); // force failure, unknown/deleted contact seen. } QCOMPARE(allContacts.size(), 8); QVERIFY(aggMarkId != QContactId()); QVERIFY(aggMatthewId != QContactId()); QCOMPARE(m_cm->contact(johnId).detail().number(), QStringLiteral("1111123")); QCOMPARE(m_cm->contact(aggJohnId).detail().number(), QStringLiteral("1111123")); QCOMPARE(m_cm->contact(johnId).details().size(), 1); QCOMPARE(m_cm->contact(aggJohnId).details().size(), 1); // check the collections are available and that the ctag of the readwrite collection has changed. for (const QContactCollection &c : m_cm->collections()) { const int collectionAccountId = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); const QString collectionAppName = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); const QString collectionName = c.metaData(QContactCollection::KeyName).toString(); if (collectionAccountId == accountId && collectionAppName == applicationName) { if (collectionName == QStringLiteral("Empty")) { emptyCollection = c; QCOMPARE(c.extendedMetaData(QStringLiteral("ctag")).toString(), emptyCollectionCtag); } else if (collectionName == QStringLiteral("ReadOnly")) { readonlyCollection = c; QCOMPARE(c.extendedMetaData(QStringLiteral("ctag")).toString(), readonlyCollectionCtag); } else { QCOMPARE(collectionName, QStringLiteral("ReadWrite")); readwriteCollection = c; QVERIFY(c.extendedMetaData(QStringLiteral("ctag")).toString() != readwriteCollectionCtag); readwriteCollectionCtag = c.extendedMetaData(QStringLiteral("ctag")).toString(); } } } // and ensure that the remote database contains the appropriate information QContact remoteMatthew = tsa.remoteContact(QStringLiteral("Matthew"), QStringLiteral("Four")); QCOMPARE(remoteMatthew.detail().number(), matthew.detail().number()); QContact remoteMark = tsa.remoteContact(QStringLiteral("Mark"), QStringLiteral("Three")); QCOMPARE(remoteMark.detail().emailAddress(), mark.detail().emailAddress()); QContact remoteJohn = tsa.remoteContact(QStringLiteral("John"), QStringLiteral("One")); QCOMPARE(remoteJohn.detail().number(), QStringLiteral("1111123")); QContact remoteLuke = tsa.remoteContact(QStringLiteral("Luke"), QStringLiteral("Two")); QCOMPARE(remoteLuke.details().size(), 0); // contact was deleted from remote, shouldn't exist. // delete the read-write collection locally QVERIFY(m_cm->removeCollection(readwriteCollection.id())); // now perform another sync cycle tsa.performTwoWaySync(); QTRY_COMPARE(failedSpy.count() + finishedSpy.count(), 3); QTRY_COMPARE(finishedSpy.count(), 3); // ensure that the local database now contains the appropriate information allContacts = m_cm->contacts(allCollections); for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); const bool isAggregate = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size() > 0; const QString fname = c.detail().firstName(); if (!isAggregate && fname == QStringLiteral("Alice")) QCOMPARE(c.id(), aliceId); // id should not have changed else if (isAggregate && fname == QStringLiteral("Alice")) QCOMPARE(isAggregate, false); // force failure. else if (!isAggregate && fname == QStringLiteral("Bob")) QCOMPARE(c.id(), bobId); // id should not have changed else if (isAggregate && fname == QStringLiteral("Bob")) QCOMPARE(isAggregate, false); // force failure. else QVERIFY(fname == QStringLiteral("Alice") || fname == QStringLiteral("Bob")); // force failure. } QCOMPARE(allContacts.size(), 2); // the readwrite collection should no longer exist locally for (const QContactCollection &c : m_cm->collections()) { const int collectionAccountId = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); const QString collectionAppName = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); const QString collectionName = c.metaData(QContactCollection::KeyName).toString(); if (collectionAccountId == accountId && collectionAppName == applicationName) { if (collectionName == QStringLiteral("Empty")) { emptyCollection = c; QCOMPARE(c.extendedMetaData(QStringLiteral("ctag")).toString(), emptyCollectionCtag); } else if (collectionName == QStringLiteral("ReadOnly")) { readonlyCollection = c; QCOMPARE(c.extendedMetaData(QStringLiteral("ctag")).toString(), readonlyCollectionCtag); } else { // force a failure, the other collection should have been deleted. QVERIFY(collectionName == QStringLiteral("Empty") || collectionName == QStringLiteral("ReadOnly")); } } } // now perform another sync cycle without performing any changes either locally or remotely. tsa.performTwoWaySync(); QTRY_COMPARE(failedSpy.count() + finishedSpy.count(), 4); QTRY_COMPARE(finishedSpy.count(), 4); // no changes should have occurred. allContacts = m_cm->contacts(allCollections); for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); const bool isAggregate = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size() > 0; const QString fname = c.detail().firstName(); if (!isAggregate && fname == QStringLiteral("Alice")) QCOMPARE(c.id(), aliceId); // id should not have changed else if (isAggregate && fname == QStringLiteral("Alice")) QCOMPARE(isAggregate, false); // force failure. else if (!isAggregate && fname == QStringLiteral("Bob")) QCOMPARE(c.id(), bobId); // id should not have changed else if (isAggregate && fname == QStringLiteral("Bob")) QCOMPARE(isAggregate, false); // force failure. else QVERIFY(fname == QStringLiteral("Alice") || fname == QStringLiteral("Bob")); // force failure. } QCOMPARE(allContacts.size(), 2); for (const QContactCollection &c : m_cm->collections()) { const int collectionAccountId = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); const QString collectionAppName = c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); const QString collectionName = c.metaData(QContactCollection::KeyName).toString(); if (collectionAccountId == accountId && collectionAppName == applicationName) { if (collectionName == QStringLiteral("Empty")) { emptyCollection = c; QCOMPARE(c.extendedMetaData(QStringLiteral("ctag")).toString(), emptyCollectionCtag); } else if (collectionName == QStringLiteral("ReadOnly")) { readonlyCollection = c; QCOMPARE(c.extendedMetaData(QStringLiteral("ctag")).toString(), readonlyCollectionCtag); } else { // force a failure, the other collection should have been deleted. QVERIFY(collectionName == QStringLiteral("Empty") || collectionName == QStringLiteral("ReadOnly")); } } } } void tst_synctransactions::twcsa_delta() { // TODO: a sync plugin which supports delta sync. } void tst_synctransactions::twcsa_oneway() { // TODO: a sync plugin which only supports to-device sync. } /* void tst_Aggregation::TestSyncAdaptor() { QContactDetailFilter allSyncTargets; setFilterDetail(allSyncTargets, QContactSyncTarget::FieldSyncTarget); QList originalIds = m_cm->contactIds(allSyncTargets); // add some contacts remotely, and downsync them. It should not result in an upsync. QString accountId(QStringLiteral("1")); TestSyncAdaptor tsa(accountId); tsa.addRemoteContact(accountId, "John", "TsaOne", "1111111", TestSyncAdaptor::ImplicitlyModifiable); tsa.addRemoteContact(accountId, "Bob", "TsaTwo", "2222222", TestSyncAdaptor::ExplicitlyModifiable); tsa.addRemoteContact(accountId, "Mark", "TsaThree", "3333333", TestSyncAdaptor::ExplicitlyNonModifiable); QSignalSpy finishedSpy(&tsa, SIGNAL(finished())); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 1); // should have 6 more contacts than we had before (aggregate+synctarget x 3) QList allContacts = m_cm->contacts(allSyncTargets); QContactId tsaOneStcId, tsaOneAggId, tsaTwoStcId, tsaTwoAggId, tsaThreeStcId, tsaThreeAggId; for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); if (originalIds.contains(c.id())) { allContacts.removeAt(i); } else { bool isAggregate = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size() > 0; if (c.detail().firstName() == QStringLiteral("John")) { if (isAggregate) { tsaOneAggId = c.id(); } else { tsaOneStcId = c.id(); } } else if (c.detail().firstName() == QStringLiteral("Bob")) { if (isAggregate) { tsaTwoAggId = c.id(); } else { tsaTwoStcId = c.id(); } } else if (c.detail().firstName() == QStringLiteral("Mark")) { if (isAggregate) { tsaThreeAggId = c.id(); } else { tsaThreeStcId = c.id(); } } } } QCOMPARE(allContacts.size(), 6); QVERIFY(tsaOneStcId != QContactId()); QVERIFY(tsaOneAggId != QContactId()); QVERIFY(tsaTwoStcId != QContactId()); QVERIFY(tsaTwoAggId != QContactId()); QVERIFY(tsaThreeStcId != QContactId()); QVERIFY(tsaThreeAggId != QContactId()); // verify that the added IDs were reported QSet reportedIds(tsa.modifiedIds(accountId)); QCOMPARE(reportedIds.size(), 3); QVERIFY(reportedIds.contains(tsaOneStcId)); QVERIFY(reportedIds.contains(tsaTwoStcId)); QVERIFY(reportedIds.contains(tsaThreeStcId)); // and no upsync of local changes should be required (there shouldn't have been any local changes). QVERIFY(!tsa.upsyncWasRequired(accountId)); QVERIFY(tsa.downsyncWasRequired(accountId)); // ensure that the downsynced contacts have the data we expect // note that aggregate contacts don't have a modifiability flag, since by definition // the modification would "actually" occur to some constituent contact detail, // and thus are considered by the haveExpectedContent function to be ImplicitlyModifiable. QVERIFY(haveExpectedContent(m_cm->contact(tsaOneStcId), QStringLiteral("1111111"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaOneAggId), QStringLiteral("1111111"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoStcId), QStringLiteral("2222222"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoAggId), QStringLiteral("2222222"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaThreeStcId), QStringLiteral("3333333"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaThreeAggId), QStringLiteral("3333333"), TestSyncAdaptor::ImplicitlyModifiable, QString())); // now modify tsaTwo's aggregate - should cause the creation of an incidental local. // triggering update should then not require downsync but would require upsync. QContact tsaTwoAggregate = m_cm->contact(tsaTwoAggId); QContactEmailAddress tsaTwoEmail; tsaTwoEmail.setEmailAddress("bob@tsatwo.com"); tsaTwoAggregate.saveDetail(&tsaTwoEmail); // new detail, will cause creation of incidental local. m_cm->saveContact(&tsaTwoAggregate); allContacts = m_cm->contacts(allSyncTargets); QContactId tsaTwoLocalId; for (int i = allContacts.size() - 1; i >= 0; --i) { if (originalIds.contains(allContacts[i].id())) { allContacts.removeAt(i); } else if (allContacts[i].detail().firstName() == QStringLiteral("Bob") && allContacts[i].id() != tsaTwoAggId && allContacts[i].id() != tsaTwoStcId) { tsaTwoLocalId = allContacts[i].id(); } } QCOMPARE(allContacts.size(), 7); // new incidental local. QVERIFY(tsaTwoLocalId != QContactId()); // perform the sync. tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 2); // downsync should not have been required, but upsync should have been. QVERIFY(!tsa.downsyncWasRequired(accountId)); QVERIFY(tsa.upsyncWasRequired(accountId)); // ensure that the contacts have the data we expect QVERIFY(haveExpectedContent(m_cm->contact(tsaOneStcId), QStringLiteral("1111111"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaOneAggId), QStringLiteral("1111111"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoStcId), QStringLiteral("2222222"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoLocalId), QString(), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("bob@tsatwo.com"))); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoAggId), QStringLiteral("2222222"), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("bob@tsatwo.com"))); QVERIFY(haveExpectedContent(m_cm->contact(tsaThreeStcId), QStringLiteral("3333333"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaThreeAggId), QStringLiteral("3333333"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(tsa.remoteContact(accountId, QStringLiteral("Bob"), QStringLiteral("TsaTwo")), QStringLiteral("2222222"), TestSyncAdaptor::ExplicitlyModifiable, QStringLiteral("bob@tsatwo.com"))); // modify both locally and remotely. // we modify the phone number locally (ie, the synctarget constituent) // and the email address remotely (ie, the local constituent) // and test to ensure that everything is resolved/updated correctly. QContact tsaTwoStc = m_cm->contact(tsaTwoStcId); QContactPhoneNumber tsaTwoStcPhn = tsaTwoStc.detail(); tsaTwoStcPhn.setNumber("2222229"); tsaTwoStc.saveDetail(&tsaTwoStcPhn); QVERIFY(m_cm->saveContact(&tsaTwoStc)); tsa.changeRemoteContactEmail(accountId, "Bob", "TsaTwo", "bob2@tsatwo.com"); // perform the sync. tsa.performTwoWaySync(accountId); // ensure that the per-account separation is maintained properly for out of band data etc. tsa.addRemoteContact(QStringLiteral("2"), QStringLiteral("Jerry"), QStringLiteral("TsaTwoTwo"), QStringLiteral("555")); tsa.performTwoWaySync(QStringLiteral("2")); QTRY_COMPARE(finishedSpy.count(), 4); // wait for both to finish tsa.removeRemoteContact(QStringLiteral("2"), QStringLiteral("Jerry"), QStringLiteral("TsaTwoTwo")); tsa.performTwoWaySync(QStringLiteral("2")); QTRY_COMPARE(finishedSpy.count(), 5); // downsync and upsync should have been required in the original sync for the "main" account. QVERIFY(tsa.downsyncWasRequired(accountId)); QVERIFY(tsa.upsyncWasRequired(accountId)); // ensure that the contacts have the data we expect QVERIFY(haveExpectedContent(m_cm->contact(tsaOneStcId), QStringLiteral("1111111"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaOneAggId), QStringLiteral("1111111"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoStcId), QStringLiteral("2222229"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoLocalId), QString(), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("bob2@tsatwo.com"))); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoAggId), QStringLiteral("2222229"), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("bob2@tsatwo.com"))); QVERIFY(haveExpectedContent(m_cm->contact(tsaThreeStcId), QStringLiteral("3333333"), TestSyncAdaptor::ExplicitlyNonModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaThreeAggId), QStringLiteral("3333333"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(tsa.remoteContact(accountId, QStringLiteral("Bob"), QStringLiteral("TsaTwo")), QStringLiteral("2222229"), TestSyncAdaptor::ExplicitlyModifiable, QStringLiteral("bob2@tsatwo.com"))); // remove a contact locally, ensure that the removal is upsynced. QVERIFY(tsa.remoteContact(accountId, QStringLiteral("Mark"), QStringLiteral("TsaThree")) != QContact()); QVERIFY(m_cm->removeContact(tsaThreeAggId)); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 6); QVERIFY(tsa.downsyncWasRequired(accountId)); // the previously upsynced changes which were applied will be returned, hence will be downsynced; but discarded as nonsubstantial / already applied. QVERIFY(tsa.upsyncWasRequired(accountId)); // ensure that the contacts have the data we expect QVERIFY(haveExpectedContent(m_cm->contact(tsaOneStcId), QStringLiteral("1111111"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaOneAggId), QStringLiteral("1111111"), TestSyncAdaptor::ImplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoStcId), QStringLiteral("2222229"), TestSyncAdaptor::ExplicitlyModifiable, QString())); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoLocalId), QString(), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("bob2@tsatwo.com"))); QVERIFY(haveExpectedContent(m_cm->contact(tsaTwoAggId), QStringLiteral("2222229"), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("bob2@tsatwo.com"))); QVERIFY(haveExpectedContent(tsa.remoteContact(accountId, QStringLiteral("Bob"), QStringLiteral("TsaTwo")), QStringLiteral("2222229"), TestSyncAdaptor::ExplicitlyModifiable, QStringLiteral("bob2@tsatwo.com"))); QVERIFY(tsa.remoteContact(accountId, QStringLiteral("Mark"), QStringLiteral("TsaThree")) == QContact()); // deleted remotely. // add a contact locally, ensure that the addition is upsynced. QContact tsaFourLocal; QContactName tsaFourName; tsaFourName.setFirstName("Jennifer"); tsaFourName.setLastName("TsaFour"); QContactEmailAddress tsaFourEmail; tsaFourEmail.setEmailAddress("jennifer@tsafour.com"); tsaFourLocal.saveDetail(&tsaFourName); tsaFourLocal.saveDetail(&tsaFourEmail); QVERIFY(m_cm->saveContact(&tsaFourLocal)); QContactId tsaFourLocalId = tsaFourLocal.id(); QContactId tsaFourAggId; allContacts = m_cm->contacts(allSyncTargets); for (int i = allContacts.size() - 1; i >= 0; --i) { if (originalIds.contains(allContacts[i].id())) { allContacts.removeAt(i); } else if (allContacts[i].detail().firstName() == QStringLiteral("Jennifer") && allContacts[i].id() != tsaFourLocalId) { tsaFourAggId = allContacts[i].id(); } } QVERIFY(tsaFourAggId != QContactId()); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 7); QVERIFY(!tsa.downsyncWasRequired(accountId)); // there were no remote changes QVERIFY(tsa.upsyncWasRequired(accountId)); QVERIFY(haveExpectedContent(tsa.remoteContact(accountId, QStringLiteral("Jennifer"), QStringLiteral("TsaFour")), QString(), TestSyncAdaptor::ImplicitlyModifiable, QStringLiteral("jennifer@tsafour.com"))); // remove some contacts remotely, ensure the removals are downsynced. QTest::qWait(1); tsa.removeRemoteContact(accountId, QStringLiteral("Bob"), QStringLiteral("TsaTwo")); QVERIFY(tsa.remoteContact(accountId, QStringLiteral("Bob"), QStringLiteral("TsaTwo")) == QContact()); tsa.removeRemoteContact(accountId, QStringLiteral("Jennifer"), QStringLiteral("TsaFour")); QVERIFY(tsa.remoteContact(accountId, QStringLiteral("Jennifer"), QStringLiteral("TsaFour")) == QContact()); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 8); QVERIFY(tsa.downsyncWasRequired(accountId)); QVERIFY(!tsa.upsyncWasRequired(accountId)); QList allIds = m_cm->contactIds(allSyncTargets); // the sync target constituents of two and four should be removed. // the local constituents (and the aggregates) should remain. QVERIFY(!allIds.contains(tsaTwoStcId)); // the local constituent of tsaTwo should remain even though it's incidental. QVERIFY(allIds.contains(tsaTwoLocalId)); QVERIFY(allIds.contains(tsaTwoAggId)); // and the tsaFour contact was originally a pure-local addition, so shouldn't be removed. // it may, after all, have been synced up to other sync sources. // Note: we should NOT sync up either tsaTwo or tsaFour in subsequent syncs. QVERIFY(allIds.contains(tsaFourLocalId)); QVERIFY(allIds.contains(tsaFourAggId)); // modify two and four locally, and ensure they don't get synced up. tsaFourLocal = m_cm->contact(tsaFourLocalId); tsaFourEmail = tsaFourLocal.detail(); tsaFourEmail.setEmailAddress("jennifer2@tsafour.com"); tsaFourLocal.saveDetail(&tsaFourEmail); m_cm->saveContact(&tsaFourLocal); tsaTwoAggregate = m_cm->contact(tsaTwoAggId); tsaTwoEmail = tsaTwoAggregate.detail(); tsaTwoEmail.setEmailAddress("bob3@tsatwo.com"); tsaTwoAggregate.saveDetail(&tsaTwoEmail); m_cm->saveContact(&tsaTwoAggregate); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 9); QVERIFY(!tsa.downsyncWasRequired(accountId)); // no remote changes since last sync, and last sync didn't upsync any changes. QVERIFY(!tsa.upsyncWasRequired(accountId)); // changes shouldn't have been upsynced. // modify (the only remaining) remote contact, delete the local contacts. // the conflict should be resolved in favour of the local device, and the // removal should be upsynced. TODO: support different conflict resolutions. tsa.changeRemoteContactPhone(accountId, QStringLiteral("John"), QStringLiteral("TsaOne"), QStringLiteral("1111112")); QVERIFY(m_cm->removeContact(tsaOneAggId)); QVERIFY(m_cm->removeContact(tsaTwoAggId)); QVERIFY(m_cm->removeContact(tsaFourAggId)); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 10); QVERIFY(tsa.downsyncWasRequired(accountId)); QVERIFY(tsa.upsyncWasRequired(accountId)); allIds = m_cm->contactIds(allSyncTargets); QVERIFY(!allIds.contains(tsaOneStcId)); QVERIFY(!allIds.contains(tsaOneAggId)); QVERIFY(!allIds.contains(tsaTwoLocalId)); QVERIFY(!allIds.contains(tsaTwoAggId)); QVERIFY(!allIds.contains(tsaFourLocalId)); QVERIFY(!allIds.contains(tsaFourAggId)); // should be back to original set of ids prior to test. QCOMPARE(originalIds.size(), allIds.size()); foreach (const QContactId &id, allIds) { QVERIFY(originalIds.contains(id)); } // now add a new contact remotely, which has a name. // remove the name locally, and modify it remotely - this will cause a "composed detail" modification update. tsa.addRemoteContact(accountId, "John", "TsaFive", "555555", TestSyncAdaptor::ImplicitlyModifiable); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 11); allIds = m_cm->contactIds(allSyncTargets); QCOMPARE(allIds.size(), originalIds.size() + 2); // remote + aggregate allContacts = m_cm->contacts(allSyncTargets); QContact tsaFiveStc, tsaFiveAgg; for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); if (originalIds.contains(c.id())) { allContacts.removeAt(i); } else { bool isAggregate = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size() > 0; if (isAggregate) { tsaFiveAgg = c; } else { tsaFiveStc = c; } } } QVERIFY(tsaFiveAgg.id() != QContactId()); QVERIFY(tsaFiveStc.id() != QContactId()); // now remove the name on the local copy QContactName tsaFiveName = tsaFiveStc.detail(); tsaFiveStc.removeDetail(&tsaFiveName); QVERIFY(m_cm->saveContact(&tsaFiveStc)); // now modify the name on the remote server, and trigger sync. // during this process, the local contact will NOT have a QContactName detail // so the modification pair will be . This used to trigger a bug. tsa.changeRemoteContactName(accountId, "John", "TsaFive", "Jonathan", "TsaFive"); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 12); // ensure that the contact contains the data we expect // currently, we only support PreserveLocalChanges conflict resolution tsaFiveStc = m_cm->contact(tsaFiveStc.id()); QCOMPARE(tsaFiveStc.detail().firstName(), QString()); QCOMPARE(tsaFiveStc.detail().lastName(), QString()); QCOMPARE(tsaFiveStc.detail().number(), QStringLiteral("555555")); // now do the same test as above, but this time remove the name remotely and modify it locally. tsa.addRemoteContact(accountId, "James", "TsaSix", "666666", TestSyncAdaptor::ImplicitlyModifiable); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 13); allIds = m_cm->contactIds(allSyncTargets); QCOMPARE(allIds.size(), originalIds.size() + 4); // remote + aggregate for Five and Six allContacts = m_cm->contacts(allSyncTargets); QContact tsaSixStc, tsaSixAgg; for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); if (originalIds.contains(c.id())) { allContacts.removeAt(i); } else { bool isAggregate = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size() > 0; if (isAggregate && c.id() != tsaFiveAgg.id()) { tsaSixAgg = c; } else if (!isAggregate && c.id() != tsaFiveStc.id()){ tsaSixStc = c; } } } QVERIFY(tsaSixAgg.id() != QContactId()); QVERIFY(tsaSixStc.id() != QContactId()); // now modify the name on the local copy QContactName tsaSixName = tsaSixStc.detail(); tsaSixName.setFirstName("Jimmy"); tsaSixStc.saveDetail(&tsaSixName); QVERIFY(m_cm->saveContact(&tsaSixStc)); // now remove the name on the remote server, and trigger sync. // during this process, the remote contact will NOT have a QContactName detail // so the modification pair will be . tsa.changeRemoteContactName(accountId, "James", "TsaSix", "", ""); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 14); // ensure that the contact contains the data we expect // currently, we only support PreserveLocalChanges conflict resolution tsaSixStc = m_cm->contact(tsaSixStc.id()); QCOMPARE(tsaSixStc.detail().firstName(), QStringLiteral("Jimmy")); QCOMPARE(tsaSixStc.detail().lastName(), QStringLiteral("TsaSix")); QCOMPARE(tsaSixStc.detail().number(), QStringLiteral("666666")); // partial clean up, locally remove all contact aggregates from sync adapter. QVERIFY(m_cm->removeContact(tsaFiveAgg.id())); QVERIFY(m_cm->removeContact(tsaSixAgg.id())); QCOMPARE(m_cm->contactIds(allSyncTargets).size(), originalIds.size()); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 15); // the following test ensures that "remote duplicate removal" works correctly. // - have multiple duplicated contacts server-side // - sync them down // - remove all but one duplicate server-side // - sync the changes (removals) // - ensure that the removals are applied correctly device-side. tsa.addRemoteDuplicates(accountId, "John", "Duplicate", "1234321"); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 16); allContacts = m_cm->contacts(allSyncTargets); int syncTargetConstituentsCount = 0, aggCount = 0; QContact tsaSevenAgg; for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); if (originalIds.contains(c.id())) { allContacts.removeAt(i); } else { int tempAggCount = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size(); if (tempAggCount > 0) { aggCount = tempAggCount; tsaSevenAgg = c; } else { syncTargetConstituentsCount += 1; } } } QVERIFY(tsaSevenAgg.id() != QContactId()); QCOMPARE(aggCount, 3); // the aggregate should aggregate the 3 duplicates QCOMPARE(syncTargetConstituentsCount, 3); // there should be 3 duplicates tsa.mergeRemoteDuplicates(accountId); tsa.performTwoWaySync(accountId); QTRY_COMPARE(finishedSpy.count(), 17); allContacts = m_cm->contacts(allSyncTargets); syncTargetConstituentsCount = 0; aggCount = 0; for (int i = allContacts.size() - 1; i >= 0; --i) { const QContact &c(allContacts[i]); if (originalIds.contains(c.id())) { allContacts.removeAt(i); } else { int tempAggCount = c.relatedContacts(QStringLiteral("Aggregates"), QContactRelationship::Second).size(); if (tempAggCount > 0) { tsaSevenAgg = c; aggCount = tempAggCount; } else { syncTargetConstituentsCount += 1; } } } QCOMPARE(aggCount, 1); // now there should be just one sync target constituent. QCOMPARE(syncTargetConstituentsCount, 1); // clean up. QVERIFY(m_cm->removeContact(tsaSevenAgg.id())); QCOMPARE(m_cm->contactIds(allSyncTargets).size(), originalIds.size()); tsa.removeAllContacts(); } */ QTEST_GUILESS_MAIN(tst_synctransactions) #include "tst_synctransactions.moc" qtcontacts-sqlite-0.3.19/tests/benchmarks/000077500000000000000000000000001436373107600205575ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/benchmarks/benchmarks.pro000066400000000000000000000001161436373107600234140ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ fetchtimes \ #deltadetection qtcontacts-sqlite-0.3.19/tests/benchmarks/deltadetection/000077500000000000000000000000001436373107600235475ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/benchmarks/deltadetection/deltadetection.pro000066400000000000000000000005311436373107600272600ustar00rootroot00000000000000include(../../../config.pri) TEMPLATE = app TARGET = deltadetection QT = \ core \ testlib SOURCES = main.cpp deltasyncadapter.cpp HEADERS = deltasyncadapter.h ../../../src/extensions/contactmanagerengine.h INCLUDEPATH += ../../../src/extensions ../../../src/engine/ target.path = /opt/tests/qtcontacts-sqlite-qt5 INSTALLS += target qtcontacts-sqlite-0.3.19/tests/benchmarks/deltadetection/deltasyncadapter.cpp000066400000000000000000000426461436373107600276160ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * 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 "deltasyncadapter.h" #include "../../../src/extensions/twowaycontactsyncadapter_impl.h" #include "../../../src/extensions/qtcontacts-extensions.h" #include #include #include #include #include #define TSA_GUID_STRING(accountId, fname, lname) QString(accountId + ":" + fname + lname) namespace { QContactPhoneNumber generatePhoneNumber(QStringList *seen) { QContactPhoneNumber ret; int random = qrand(); int random2 = qrand(); if (random % 2 == 0) { ret.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); } if (random % 3 == 0) { ret.setContexts(QContactDetail::ContextHome); } else { ret.setContexts(QContactDetail::ContextWork); } QString number; if (random % 5 == 0) { // 8 digit phone number number = QStringLiteral("%1%2%3%4%5%6%7%8") .arg(random%10).arg(random2%9).arg(random%8) .arg(random2%7).arg(random%6).arg(random2%5) .arg(random%4).arg(random2%3); } else { // 9 digit phone number number = QStringLiteral("%1%2%3%4%5%6%7%8%9") .arg(random2%10).arg(random2%9).arg(random2%8) .arg(random%7).arg(random%6).arg(random%5) .arg(random2%4).arg(random2%3).arg(random%2); } ret.setNumber(number); if (seen->contains(number)) { return generatePhoneNumber(seen); } seen->append(number); return ret; } QMap managerParameters() { QMap params; params.insert(QStringLiteral("autoTest"), QStringLiteral("true")); params.insert(QStringLiteral("mergePresenceChanges"), QStringLiteral("true")); return params; } } DeltaSyncAdapter::DeltaSyncAdapter(const QString &accountId, QObject *parent) : QObject(parent), TwoWayContactSyncAdapter(QStringLiteral("testsyncadapter"), managerParameters()) , m_accountId(accountId) { cleanUp(accountId); } DeltaSyncAdapter::~DeltaSyncAdapter() { cleanUp(m_accountId); } void DeltaSyncAdapter::cleanUp(const QString &accountId) { initSyncAdapter(accountId); readSyncStateData(&m_remoteSince[accountId], accountId, TwoWayContactSyncAdapter::ReadPartialState); purgeSyncStateData(accountId, true); } void DeltaSyncAdapter::addRemoteDuplicates(const QString &accountId, const QString &fname, const QString &lname, const QString &phone) { addRemoteContact(accountId, fname, lname, phone); addRemoteContact(accountId, fname, lname, phone); addRemoteContact(accountId, fname, lname, phone); } void DeltaSyncAdapter::mergeRemoteDuplicates(const QString &accountId) { Q_FOREACH (const QString &dupGuid, m_remoteServerDuplicates[accountId].values()) { m_remoteAddMods[accountId].remove(dupGuid); // shouldn't be any here anyway. m_remoteDeletions[accountId].append(m_remoteServerContacts[accountId].value(dupGuid)); m_remoteServerContacts[accountId].remove(dupGuid); } m_remoteServerDuplicates[accountId].clear(); } void DeltaSyncAdapter::addRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QString &phone, DeltaSyncAdapter::PhoneModifiability mod) { QContactName ncn; ncn.setFirstName(fname); ncn.setLastName(lname); QContactPhoneNumber ncp; ncp.setNumber(phone); if (mod == DeltaSyncAdapter::ExplicitlyModifiable) { ncp.setValue(QContactDetail__FieldModifiable, true); } else if (mod == DeltaSyncAdapter::ExplicitlyNonModifiable) { ncp.setValue(QContactDetail__FieldModifiable, false); } QContact newContact; newContact.saveDetail(&ncn); if (phone == QStringLiteral("insert250phones")) { QStringList seen; for (int i = 0; i < 250; ++i) { QContactPhoneNumber p = generatePhoneNumber(&seen); newContact.saveDetail(&p); } } else { newContact.saveDetail(&ncp); } const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (m_remoteServerContacts[accountId].contains(contactGuidStr)) { // this is an intentional duplicate. we have special handling for duplicates. QString duplicateGuidString = contactGuidStr + ":" + QString::number(m_remoteServerDuplicates[accountId].values(contactGuidStr).size() + 1); QContactGuid guid; guid.setGuid(duplicateGuidString); newContact.saveDetail(&guid); m_remoteServerDuplicates[accountId].insert(contactGuidStr, duplicateGuidString); m_remoteServerContacts[accountId].insert(duplicateGuidString, newContact); m_remoteAddMods[accountId].insert(duplicateGuidString); } else { QContactGuid guid; guid.setGuid(contactGuidStr); newContact.saveDetail(&guid); m_remoteServerContacts[accountId].insert(contactGuidStr, newContact); m_remoteAddMods[accountId].insert(contactGuidStr); } } void DeltaSyncAdapter::removeRemoteContact(const QString &accountId, const QString &fname, const QString &lname) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); QContact remContact = m_remoteServerContacts[accountId].value(contactGuidStr); // stop tracking the contact if we are currently tracking it. m_remoteAddMods[accountId].remove(contactGuidStr); // remove it from our remote cache m_remoteServerContacts[accountId].remove(contactGuidStr); // report the contact as deleted m_remoteDeletions[accountId].append(remContact); } void DeltaSyncAdapter::setRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QContact &contact) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); QContact setContact = contact; QContactGuid sguid = setContact.detail(); sguid.setGuid(contactGuidStr); setContact.saveDetail(&sguid); QContactOriginMetadata somd = setContact.detail(); somd.setGroupId(setContact.id().toString()); setContact.saveDetail(&somd); m_remoteServerContacts[accountId][contactGuidStr] = setContact; m_remoteAddMods[accountId].insert(contactGuidStr); } void DeltaSyncAdapter::changeRemoteContactPhone(const QString &accountId, const QString &fname, const QString &lname, const QString &modPhone) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (!m_remoteServerContacts[accountId].contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts[accountId].value(contactGuidStr); if (modPhone == QStringLiteral("modify10phones")) { QList allPhones = modContact.details(); if (allPhones.size() < 10) { qWarning() << "Modifying 10 phones but have only:" << allPhones.size() << "to modify! aborting!"; return; } int stride = allPhones.size() / 10; for (int i = 0; i < allPhones.size(); i+=stride) { QContactPhoneNumber mcp = allPhones.at(i); mcp.setNumber(mcp.number() + QString::number(i)); modContact.saveDetail(&mcp); } QContactPhoneNumber removeOne = allPhones.at(qMax(allPhones.size() - 8, 0)); modContact.removeDetail(&removeOne); } else if (modPhone == QStringLiteral("modifyallphones")) { QList allPhones = modContact.details(); for (int i = 0; i < allPhones.size(); ++i) { QContactPhoneNumber mcp = allPhones.at(i); mcp.setNumber(mcp.number() + QString::number(i)); modContact.saveDetail(&mcp); } QContactPhoneNumber removeOne = allPhones.at(qMax(allPhones.size() - 8, 0)); modContact.removeDetail(&removeOne); } else { QContactPhoneNumber mcp = modContact.detail(); mcp.setNumber(modPhone); modContact.saveDetail(&mcp); } m_remoteServerContacts[accountId][contactGuidStr] = modContact; m_remoteAddMods[accountId].insert(contactGuidStr); } void DeltaSyncAdapter::changeRemoteContactEmail(const QString &accountId, const QString &fname, const QString &lname, const QString &modEmail) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (!m_remoteServerContacts[accountId].contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts[accountId].value(contactGuidStr); QContactEmailAddress mce = modContact.detail(); mce.setEmailAddress(modEmail); modContact.saveDetail(&mce); m_remoteServerContacts[accountId][contactGuidStr] = modContact; m_remoteAddMods[accountId].insert(contactGuidStr); } void DeltaSyncAdapter::changeRemoteContactName(const QString &accountId, const QString &fname, const QString &lname, const QString &modfname, const QString &modlname) { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); if (!m_remoteServerContacts[accountId].contains(contactGuidStr)) { qWarning() << "Contact:" << contactGuidStr << "doesn't exist remotely!"; return; } QContact modContact = m_remoteServerContacts[accountId].value(contactGuidStr); QContactName mcn = modContact.detail(); if (modfname.isEmpty() && modlname.isEmpty()) { modContact.removeDetail(&mcn); } else { mcn.setFirstName(modfname); mcn.setLastName(modlname); modContact.saveDetail(&mcn); } const QString modContactGuidStr(TSA_GUID_STRING(accountId, modfname, modlname)); m_remoteServerContacts[accountId].remove(contactGuidStr); m_remoteAddMods[accountId].remove(contactGuidStr); m_remoteServerContacts[accountId][modContactGuidStr] = modContact; m_remoteAddMods[accountId].insert(modContactGuidStr); } void DeltaSyncAdapter::performTwoWaySync(const QString &accountId) { // reset our state. m_downsyncWasRequired[accountId] = false; m_upsyncWasRequired[accountId] = false; // do the sync process as described in twowaycontactsyncadapter.h if (!initSyncAdapter(accountId)) { qWarning() << Q_FUNC_INFO << "couldn't init adapter"; emit failed(); return; } if (!readSyncStateData(&m_remoteSince[accountId], accountId, TwoWayContactSyncAdapter::ReadPartialState)) { qWarning() << Q_FUNC_INFO << "couldn't read sync state data"; emit failed(); return; } determineRemoteChanges(m_remoteSince[accountId], accountId); // continued in continueTwoWaySync(). } void DeltaSyncAdapter::determineRemoteChanges(const QDateTime &, const QString &accountId) { // continuing the sync process as described in twowaycontactsyncadapter.h if (m_remoteDeletions[accountId].isEmpty() && m_remoteAddMods[accountId].isEmpty()) { m_downsyncWasRequired[accountId] = false; } else { m_downsyncWasRequired[accountId] = true; } // call storeRemoteChanges anyway so that the state machine continues to work. // alternatively, we could set the state to StoredRemoteChanges manually, and skip // this call in the else block above, but we should test that it works properly anyway. QList remoteAddMods; QMap additions; foreach (const QString &contactGuidStr, m_remoteAddMods[accountId]) { remoteAddMods.append(m_remoteServerContacts[accountId].value(contactGuidStr)); if (remoteAddMods.last().id().isNull()) { additions.insert(remoteAddMods.count() - 1, contactGuidStr); } } if (!storeRemoteChanges(m_remoteDeletions[accountId], &remoteAddMods, accountId)) { qWarning() << Q_FUNC_INFO << "couldn't store remote changes"; emit failed(); return; } // Store the ID of any contact we added QMap::const_iterator ait = additions.constBegin(), aend = additions.constEnd(); for ( ; ait != aend; ++ait) { const QContact &added(remoteAddMods.at(ait.key())); const QString &contactGuidStr(ait.value()); m_remoteServerContacts[accountId][contactGuidStr].setId(added.id()); } m_modifiedIds[accountId].clear(); foreach (const QContact &stored, remoteAddMods) { m_modifiedIds[accountId].insert(stored.id()); } // clear our simulated remote changes deltas, as we've already reported / stored them. m_remoteDeletions[accountId].clear(); m_remoteAddMods[accountId].clear(); QList locallyAdded, locallyModified, locallyDeleted; QDateTime localSince; if (!determineLocalChanges(&localSince, &locallyAdded, &locallyModified, &locallyDeleted, accountId)) { qWarning() << Q_FUNC_INFO << "couldn't determine local changes"; emit failed(); return; } if (locallyAdded.isEmpty() && locallyModified.isEmpty() && locallyDeleted.isEmpty()) { m_upsyncWasRequired[accountId] = false; } else { m_upsyncWasRequired[accountId] = true; } upsyncLocalChanges(localSince, locallyAdded, locallyModified, locallyDeleted, accountId); } bool DeltaSyncAdapter::testAccountProvenance(const QContact &contact, const QString &accountId) { foreach (const QContact &remoteContact, m_remoteServerContacts[accountId]) { if (remoteContact.id() == contact.id()) { return true; } } return false; } void DeltaSyncAdapter::upsyncLocalChanges(const QDateTime &, const QList &locallyAdded, const QList &locallyModified, const QList &locallyDeleted, const QString &accountId) { // first, apply the local changes to our in memory store. foreach (const QContact &c, locallyAdded) { setRemoteContact(accountId, c.detail().firstName(), c.detail().lastName(), c); } foreach (const QContact &c, locallyModified) { // we cannot simply call setRemoteContact since the name might be modified or empty due to a previous test. Q_FOREACH (const QString &storedGuid, m_remoteServerContacts[m_accountId].keys()) { if (m_remoteServerContacts[m_accountId][storedGuid].id() == c.id()) { m_remoteServerContacts[m_accountId][storedGuid] = c; m_remoteAddMods[accountId].insert(storedGuid); } } } foreach (const QContact &c, locallyDeleted) { // we cannot simply call removeRemoteContact since the name might be modified or empty due to a previous test. QMap remoteServerContacts = m_remoteServerContacts[m_accountId]; Q_FOREACH (const QString &storedGuid, remoteServerContacts.keys()) { if (remoteServerContacts.value(storedGuid).id() == c.id()) { m_remoteServerContacts[m_accountId].remove(storedGuid); } } } // then finalize the sync if (!storeSyncStateData(accountId)) { qWarning() << Q_FUNC_INFO << "couldn't store sync state data"; emit failed(); return; } emit finished(); // succeeded. } bool DeltaSyncAdapter::upsyncWasRequired(const QString &accountId) const { return m_upsyncWasRequired[accountId]; } bool DeltaSyncAdapter::downsyncWasRequired(const QString &accountId) const { return m_downsyncWasRequired[accountId]; } QContact DeltaSyncAdapter::remoteContact(const QString &accountId, const QString &fname, const QString &lname) const { const QString contactGuidStr(TSA_GUID_STRING(accountId, fname, lname)); return m_remoteServerContacts[accountId].value(contactGuidStr); } QSet DeltaSyncAdapter::modifiedIds(const QString &accountId) const { return m_modifiedIds[accountId]; } qtcontacts-sqlite-0.3.19/tests/benchmarks/deltadetection/deltasyncadapter.h000066400000000000000000000120361436373107600272510ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef DELTASYNCADAPTER_H #define DELTASYNCADAPTER_H #include "../../../src/extensions/twowaycontactsyncadapter.h" #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class DeltaSyncAdapter : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdapter { Q_OBJECT public: DeltaSyncAdapter(const QString &accountId, QObject *parent = 0); ~DeltaSyncAdapter(); enum PhoneModifiability { ImplicitlyModifiable = 0, ExplicitlyModifiable, ExplicitlyNonModifiable }; // for testing purposes void addRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QString &phone, PhoneModifiability mod = ImplicitlyModifiable); void removeRemoteContact(const QString &accountId, const QString &fname, const QString &lname); void setRemoteContact(const QString &accountId, const QString &fname, const QString &lname, const QContact &contact); void changeRemoteContactPhone(const QString &accountId, const QString &fname, const QString &lname, const QString &modPhone); void changeRemoteContactEmail(const QString &accountId, const QString &fname, const QString &lname, const QString &modEmail); void changeRemoteContactName(const QString &accountId, const QString &fname, const QString &lname, const QString &modfname, const QString &modlname); void addRemoteDuplicates(const QString &accountId, const QString &fname, const QString &lname, const QString &phone); void mergeRemoteDuplicates(const QString &accountId); // triggering sync and checking state. void performTwoWaySync(const QString &accountId); bool upsyncWasRequired(const QString &accountId) const; bool downsyncWasRequired(const QString &accountId) const; QContact remoteContact(const QString &accountId, const QString &fname, const QString &lname) const; QSet modifiedIds(const QString &accountId) const; Q_SIGNALS: void finished(); void failed(); protected: // implementing the TWCSA interface void determineRemoteChanges(const QDateTime &remoteSince, const QString &accountId); bool testAccountProvenance(const QContact &contact, const QString &accountId); void upsyncLocalChanges(const QDateTime &localSince, const QList &locallyAdded, const QList &locallyModified, const QList &locallyDeleted, const QString &accountId); private: void cleanUp(const QString &accountId); QString m_accountId; // simulating server-side changes, per account: mutable QMap m_simulationTimers; mutable QMap m_downsyncWasRequired; mutable QMap m_upsyncWasRequired; mutable QMap m_remoteSince; mutable QMap > m_remoteDeletions; mutable QMap > m_remoteAddMods; // used to lookup into m_remoteServerContacts mutable QMap > m_remoteServerContacts; mutable QMap > m_modifiedIds; mutable QMap > m_remoteServerDuplicates; // accountId to originalGuid to duplicateGuids. }; #endif // DELTASYNCADAPTER_H qtcontacts-sqlite-0.3.19/tests/benchmarks/deltadetection/main.cpp000066400000000000000000000070231436373107600252010ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include #include #include #include "deltasyncadapter.h" #include "../../util.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); qsrand(42); // we want consistent runs, with comparable phone numbers. QString accountId(QStringLiteral("1")); DeltaSyncAdapter dsa(accountId); dsa.addRemoteContact(accountId, "First", "Contact", "1111111", DeltaSyncAdapter::ImplicitlyModifiable); dsa.addRemoteContact(accountId, "Second", "Contact", "insert250phones", DeltaSyncAdapter::ExplicitlyModifiable); dsa.addRemoteContact(accountId, "Third", "Contact", "3333333", DeltaSyncAdapter::ExplicitlyNonModifiable); qWarning() << "================================ performing first sync"; QElapsedTimer et; et.start(); dsa.performTwoWaySync(accountId); qDebug() << "first sync took:" << et.elapsed() << "milliseconds."; dsa.changeRemoteContactPhone(accountId, QStringLiteral("First"), QStringLiteral("Contact"), QStringLiteral("1111112")); //dsa.changeRemoteContactPhone(accountId, QStringLiteral("Second"), QStringLiteral("Contact"), QStringLiteral("modify10phones")); dsa.changeRemoteContactPhone(accountId, QStringLiteral("Second"), QStringLiteral("Contact"), QStringLiteral("modifyallphones")); qWarning() << "================================ performing second sync"; et.restart(); dsa.performTwoWaySync(accountId); qDebug() << "second sync took:" << et.elapsed() << "milliseconds."; dsa.removeRemoteContact(accountId, "First", "Contact"); dsa.removeRemoteContact(accountId, "Second", "Contact"); dsa.removeRemoteContact(accountId, "Third", "Contact"); qWarning() << "================================ performing third sync"; et.restart(); dsa.performTwoWaySync(accountId); qDebug() << "third sync took:" << et.elapsed() << "milliseconds."; return 0; } qtcontacts-sqlite-0.3.19/tests/benchmarks/fetchtimes/000077500000000000000000000000001436373107600227125ustar00rootroot00000000000000qtcontacts-sqlite-0.3.19/tests/benchmarks/fetchtimes/fetchtimes.pro000066400000000000000000000003211436373107600255630ustar00rootroot00000000000000include(../../../config.pri) TEMPLATE = app TARGET = fetchtimes QT = core SOURCES = main.cpp INCLUDEPATH += $$PWD/../../../src/extensions/ target.path = /opt/tests/qtcontacts-sqlite-qt5 INSTALLS += target qtcontacts-sqlite-0.3.19/tests/benchmarks/fetchtimes/main.cpp000066400000000000000000002613041436373107600243500ustar00rootroot00000000000000/* * Copyright (C) 2013 - 2019 Jolla Ltd. * Copyright (C) 2019 - 2020 Open Mobile Platform LLC. * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "qtcontacts-extensions.h" #include "qtcontacts-extensions_impl.h" #include "qtcontacts-extensions_manager_impl.h" #include "contactmanagerengine.h" QTCONTACTS_USE_NAMESPACE #include static QContactId retrievalId(const QContact &contact) { return contact.id(); } static QStringList generateNonOverlappingFirstNamesList() { QStringList retn; retn << "Zach" << "Zane" << "Zinedine" << "Zockey" << "Yann" << "Yedrez" << "Yarrow" << "Yelter" << "Ximmy" << "Xascha" << "Xanthar" << "Xachy" << "William" << "Wally" << "Weston" << "Wulther" << "Vernon" << "Veston" << "Victoria" << "Vuitton" << "Urqhart" << "Uelela" << "Ulrich" << "Umpty" << "Timothy" << "Tigga" << "Tabitha" << "Texter" << "Stan" << "Steve" << "Sophie" << "Siphonie" << "Richard" << "Rafael" << "Rachael" << "Rascal" << "Quirky" << "Quilton" << "Quentin" << "Quarreller"; return retn; } static QStringList generateNonOverlappingLastNamesList() { QStringList retn; retn << "Quilter" << "Qualifa" << "Quarrier" << "Quickson" << "Rigger" << "Render" << "Ranger" << "Reader" << "Sailor" << "Smith" << "Salter" << "Shelfer" << "Tailor" << "Tasker" << "Toppler" << "Tipster" << "Underhill" << "Umpire" << "Upperhill" << "Uppsland" << "Vintner" << "Vester" << "Victor" << "Vacationer" << "Wicker" << "Whaler" << "Whistler" << "Wolf" << "Xylophone" << "Xabu" << "Xanadu" << "Xatti" << "Yeoman" << "Yesman" << "Yelper" << "Yachtsman" << "Zimmerman" << "Zomething" << "Zeltic" << "Zephyr"; return retn; } static QStringList generateFirstNamesList() { QStringList retn; retn << "Alexandria" << "Andrew" << "Adrien" << "Amos" << "Bob" << "Bronte" << "Barry" << "Braxton" << "Clarence" << "Chandler" << "Chris" << "Chantelle" << "Dominic" << "Diedre" << "David" << "Derrick" << "Eric" << "Esther" << "Eddie" << "Eean" << "Felicity" << "Fred" << "Fletcher" << "Farraday" << "Gary" << "Gertrude" << "Gerry" << "Germaine" << "Hillary" << "Henry" << "Hans" << "Haddock" << "Jacob" << "Jane" << "Jackson" << "Jennifer" << "Larry" << "Lilliane" << "Lambert" << "Lilly" << "Mary" << "Mark" << "Mirriam" << "Matthew" << "Nathene" << "Nicholas" << "Ned" << "Norris" << "Othello" << "Oscar" << "Olaf" << "Odinsdottur" << "Penny" << "Peter" << "Patrick" << "Pilborough"; return retn; } static QStringList generateMiddleNamesList() { QStringList retn; retn << "Aubrey" << "Cody" << "Taylor" << "Leslie"; return retn; } static QStringList generateLastNamesList() { QStringList retn; retn << "Arkady" << "Addleman" << "Axeman" << "Applegrower" << "Anderson" << "Baker" << "Bremmer" << "Bedlam" << "Barrymore" << "Battery" << "Cutter" << "Cooper" << "Cutler" << "Catcher" << "Capemaker" << "Driller" << "Dyer" << "Diver" << "Daytona" << "Duster" << "Eeler" << "Eckhart" << "Eggsman" << "Empty" << "Ellersly" << "Farmer" << "Farrier" << "Foster" << "Farseer" << "Fairtime" << "Grower" << "Gaston" << "Gerriman" << "Gipsland" << "Guilder" << "Helper" << "Hogfarmer" << "Harriet" << "Hope" << "Huxley" << "Inker" << "Innman" << "Ipland" << "Instiller" << "Innis" << "Joker" << "Jackson" << "Jolt" << "Jockey" << "Jerriman"; return retn; } static QStringList generatePhoneNumbersList() { QStringList retn; retn << "111222" << "111333" << "111444" << "111555" << "111666" << "111777" << "111888" << "111999" << "222333" << "222444" << "222555" << "222666" << "222777" << "222888" << "222999" << "333444" << "333555" << "333666" << "333777" << "333888" << "333999" << "444555" << "444666" << "444777" << "444888" << "444999" << "555666" << "555777" << "555888" << "555999" << "666111" << "666222" << "666333" << "666444" << "666555" << "777111" << "777222" << "777333" << "777444" << "777555" << "777666" << "888111" << "888222" << "888333" << "888444" << "888555" << "888666" << "888777" << "999111" << "999222" << "999333" << "999444" << "999555" << "999666" << "999777" << "999888" << "999999"; return retn; } static QStringList generateEmailProvidersList() { QStringList retn; retn << "@test.com" << "@testing.com" << "@testers.com" << "@test.org" << "@testing.org" << "@testers.org" << "@test.net" << "@testing.net" << "@testers.net" << "@test.fi" << "@testing.fi" << "@testers.fi" << "@test.com.au" << "@testing.com.au" << "@testers.com.au" << "@test.co.uk" << "@testing.co.uk" << "@testers.co.uk" << "@test.co.jp" << "@test.co.jp" << "@testers.co.jp"; return retn; } static QStringList generateAvatarsList() { QStringList retn; retn << "-smiling.jpg" << "-laughing.jpg" << "-surprised.jpg" << "-smiling.png" << "-laughing.png" << "-surprised.png" << "-curious.jpg" << "-joking.jpg" << "-grinning.jpg" << "-curious.png" << "-joking.png" << "-grinning.png"; return retn; } static QStringList generateHobbiesList() { QStringList retn; retn << "tennis" << "soccer" << "squash" << "volleyball" << "chess" << "photography" << "painting" << "sketching"; return retn; } QContact generateContact(const QContactCollectionId &collectionId = QContactCollectionId(), bool possiblyAggregate = false) { static const QStringList firstNames(generateFirstNamesList()); static const QStringList middleNames(generateMiddleNamesList()); static const QStringList lastNames(generateLastNamesList()); static const QStringList nonOverlappingFirstNames(generateNonOverlappingFirstNamesList()); static const QStringList nonOverlappingLastNames(generateNonOverlappingLastNamesList()); static const QStringList phoneNumbers(generatePhoneNumbersList()); static const QStringList emailProviders(generateEmailProvidersList()); static const QStringList avatars(generateAvatarsList()); static const QStringList hobbies(generateHobbiesList()); // we randomly determine whether to generate various details // to ensure that we have heterogeneous contacts in the db. QContact retn; retn.setCollectionId(collectionId); int random = qrand(); bool preventAggregate = (!collectionId.isNull() && !possiblyAggregate); // We always have a name. Select an overlapping name if the sync target // is something other than "local" and possiblyAggregate is true. QContactName name; name.setFirstName(preventAggregate ? nonOverlappingFirstNames.at(random % nonOverlappingFirstNames.size()) : firstNames.at(random % firstNames.size())); name.setLastName(preventAggregate ? nonOverlappingLastNames.at(random % nonOverlappingLastNames.size()) : lastNames.at(random % lastNames.size())); if ((random % 6) == 0) name.setMiddleName(middleNames.at(random % middleNames.size())); if ((random % 17) == 0) name.setPrefix(QLatin1String("Dr.")); retn.saveDetail(&name); // Favorite if ((random % 31) == 0) { QContactFavorite fav; fav.setFavorite(true); retn.saveDetail(&fav); } // Phone number if ((random % 3) == 0) { QContactPhoneNumber phn; QString randomPhn = phoneNumbers.at(random % phoneNumbers.size()); phn.setNumber(preventAggregate ? QString(QString::number(random % 500000) + randomPhn) : randomPhn); if ((random % 9) == 0) phn.setContexts(QContactDetail::ContextWork); retn.saveDetail(&phn); } // Email if ((random % 2) == 0) { QContactEmailAddress em; em.setEmailAddress(QString(QLatin1String("%1%2%3%4")) .arg(preventAggregate ? QString(QString::number(random % 500000) + QString::fromLatin1(collectionId.localId())) : QString()) .arg(name.firstName()).arg(name.lastName()) .arg(emailProviders.at(random % emailProviders.size()))); if (random % 9) em.setContexts(QContactDetail::ContextWork); retn.saveDetail(&em); } // Avatar if ((random % 5) == 0) { QContactAvatar av; av.setImageUrl(name.firstName() + avatars.at(random % avatars.size())); retn.saveDetail(&av); } // Hobby if ((random % 21) == 0) { QContactHobby h1; h1.setHobby(hobbies.at(random % hobbies.size())); retn.saveDetail(&h1); int newRandom = qrand(); if ((newRandom % 2) == 0) { QContactHobby h2; h2.setHobby(hobbies.at(newRandom % hobbies.size())); retn.saveDetail(&h2); } } return retn; } static qint64 aggregatedPresenceUpdate(QContactManager &manager, bool quickMode) { qint64 elapsedTimeTotal = 0; QElapsedTimer syncTimer; // This presence update benchmark should show whether presence update // time/cost scales linearly (we hope) or exponentially (which would be bad) // with the number of contacts in database if many of them are aggregated. // About of the "morePrefillData" contacts should share an aggregate // with one of the "prefillData" contacts. // This also benchmarks the effect of the per-contact-size (number of details // in each contact which change) of the update, and the effect of using // a filter mask to reduce the amount of work to be done. qDebug() << "--------"; qDebug() << "Performing scaling aggregated batch (connectivity change) presence update tests:"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("aggregatedPresenceUpdate")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/aggregatedPresenceUpdate"); manager.saveCollection(&testAddressbook); QContactCollection testAddressbook2; testAddressbook2.setMetaData(QContactCollection::KeyName, QStringLiteral("aggregatedPresenceUpdate2")); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/aggregatedPresenceUpdate2"); manager.saveCollection(&testAddressbook2); // prefill the database const int prefillCount = quickMode ? 250 : 500; QList prefillData; prefillData.reserve(prefillCount); for (int i = 0; i < prefillCount; ++i) { prefillData.append(generateContact(testAddressbook.id())); } qDebug() << " prefilling database with" << prefillData.size() << "contacts... this will take a while..."; manager.saveContacts(&prefillData); QList deleteIds; for (const QContact &c : prefillData) { deleteIds.append(c.id()); } qDebug() << " generating aggregated prefill data, please wait..."; QList morePrefillData; for (int i = 0; i < prefillCount; ++i) { if (i % 2 == 0) { morePrefillData.append(generateContact(testAddressbook2.id(), false)); // false = don't aggregate. } else { morePrefillData.append(generateContact(testAddressbook2.id(), true)); // false = don't aggregate. } } manager.saveContacts(&morePrefillData); for (const QContact &c : morePrefillData) { deleteIds.append(c.id()); } // now do the update QList contactsToUpdate; QDateTime timestamp = QDateTime::currentDateTime(); QStringList presenceAvatars = generateAvatarsList(); for (int j = 0; j < morePrefillData.size(); ++j) { QContact curr = morePrefillData.at(j); QString genstr = QString::number(j) + "5"; QContactPresence cp = curr.detail(); QContactNickname nn = curr.detail(); QContactAvatar av = curr.detail(); cp.setNickname(genstr); cp.setCustomMessage(genstr); cp.setTimestamp(timestamp); cp.setPresenceState(static_cast((qrand() % 4) + 1)); nn.setNickname(nn.nickname() + genstr); av.setImageUrl(genstr + presenceAvatars.at(qrand() % presenceAvatars.size())); curr.saveDetail(&cp); curr.saveDetail(&nn); curr.saveDetail(&av); contactsToUpdate.append(curr); } // perform a batch save. syncTimer.start(); manager.saveContacts(&contactsToUpdate); qint64 presenceElapsed = syncTimer.elapsed(); int totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " update ( batch of" << contactsToUpdate.size() << ") presence+nick+avatar (with" << totalAggregatesInDatabase << "existing in database, partial overlap):" << presenceElapsed << "milliseconds (" << ((1.0 * presenceElapsed) / (1.0 * contactsToUpdate.size())) << " msec per updated contact )"; elapsedTimeTotal += presenceElapsed; // now test just update the presence status (not nickname or avatar details). morePrefillData = contactsToUpdate; contactsToUpdate.clear(); for (int j = 0; j < morePrefillData.size(); ++j) { QContact curr = morePrefillData.at(j); QContactPresence cp = curr.detail(); cp.setPresenceState(static_cast((qrand() % 4) + 1)); curr.saveDetail(&cp); contactsToUpdate.append(curr); } // perform a batch save. syncTimer.start(); manager.saveContacts(&contactsToUpdate); presenceElapsed = syncTimer.elapsed(); totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " update ( batch of" << contactsToUpdate.size() << ") presence only (with" << totalAggregatesInDatabase << "existing in database, partial overlap):" << presenceElapsed << "milliseconds (" << ((1.0 * presenceElapsed) / (1.0 * contactsToUpdate.size())) << " msec per updated contact )"; elapsedTimeTotal += presenceElapsed; // also pass a "detail type mask" to the update. This allows the backend // to perform optimisation based upon which details are modified. morePrefillData = contactsToUpdate; contactsToUpdate.clear(); for (int j = 0; j < morePrefillData.size(); ++j) { QContact curr = morePrefillData.at(j); QContactPresence cp = curr.detail(); cp.setPresenceState(static_cast((qrand() % 4) + 1)); curr.saveDetail(&cp); contactsToUpdate.append(curr); } // perform a batch save. QList typeMask; typeMask << QContactDetail::TypePresence; syncTimer.start(); manager.saveContacts(&contactsToUpdate, typeMask); presenceElapsed = syncTimer.elapsed(); totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " update ( batch of" << contactsToUpdate.size() << ") masked presence only (with" << totalAggregatesInDatabase << "existing in database, partial overlap):" << presenceElapsed << "milliseconds (" << ((1.0 * presenceElapsed) / (1.0 * contactsToUpdate.size())) << " msec per updated contact )"; elapsedTimeTotal += presenceElapsed; QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); qDebug() << " deleted" << deleteIds.size() << "contacts in" << deleteTime << "milliseconds"; elapsedTimeTotal += deleteTime; syncTimer.start(); manager.removeCollection(testAddressbook.id()); manager.removeCollection(testAddressbook2.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); cme->clearChangeFlags(testAddressbook2.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 2 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 nonAggregatedPresenceUpdate(QContactManager &manager, bool quickMode) { qint64 elapsedTimeTotal = 0; QElapsedTimer syncTimer; // this presence update benchmark should show whether presence update // time/cost scales linearly (we hope) or exponentially (which would be bad) // with the number of contacts in database even if they are unrelated / non-aggregated. qDebug() << "--------"; qDebug() << "Performing scaling non-aggregated batch (connectivity change) presence update tests:"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("nonAggregatedPresenceUpdate")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/nonAggregatedPresenceUpdate"); manager.saveCollection(&testAddressbook); QContactCollection testAddressbook2; testAddressbook2.setMetaData(QContactCollection::KeyName, QStringLiteral("nonAggregatedPresenceUpdate2")); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/nonAggregatedPresenceUpdate2"); manager.saveCollection(&testAddressbook2); // prefill the database const int prefillCount = quickMode ? 250 : 500; QList prefillData; prefillData.reserve(prefillCount); for (int i = 0; i < prefillCount; ++i) { prefillData.append(generateContact(testAddressbook.id())); } qDebug() << " prefilling database with" << prefillData.size() << "contacts... this will take a while..."; manager.saveContacts(&prefillData); QList deleteIds; for (const QContact &c : prefillData) { deleteIds.append(c.id()); } qDebug() << " generating non-overlapping / non-aggregated prefill data, please wait..."; QList morePrefillData; for (int i = 0; i < prefillCount; ++i) { morePrefillData.append(generateContact(testAddressbook2.id(), false)); // false = don't aggregate. } manager.saveContacts(&morePrefillData); for (const QContact &c : morePrefillData) { deleteIds.append(c.id()); } // now do the update of only one set of those contacts QList contactsToUpdate; QDateTime timestamp = QDateTime::currentDateTime(); QStringList presenceAvatars = generateAvatarsList(); for (int j = 0; j < morePrefillData.size(); ++j) { QContact curr = morePrefillData.at(j); QString genstr = QString::number(j) + "4"; QContactPresence cp = curr.detail(); QContactNickname nn = curr.detail(); QContactAvatar av = curr.detail(); cp.setNickname(genstr); cp.setCustomMessage(genstr); cp.setTimestamp(timestamp); cp.setPresenceState(static_cast((qrand() % 4) + 1)); nn.setNickname(nn.nickname() + genstr); av.setImageUrl(genstr + presenceAvatars.at(qrand() % presenceAvatars.size())); curr.saveDetail(&cp); curr.saveDetail(&nn); curr.saveDetail(&av); contactsToUpdate.append(curr); } // perform a batch save. syncTimer.start(); manager.saveContacts(&contactsToUpdate); qint64 presenceElapsed = syncTimer.elapsed(); int totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " update ( batch of" << contactsToUpdate.size() << ") presence+nick+avatar (with" << totalAggregatesInDatabase << "existing in database, no overlap):" << presenceElapsed << "milliseconds (" << ((1.0 * presenceElapsed) / (1.0 * contactsToUpdate.size())) << " msec per updated contact )"; elapsedTimeTotal += presenceElapsed; QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); qDebug() << " deleted" << deleteIds.size() << "contacts in" << deleteTime << "milliseconds"; elapsedTimeTotal += deleteTime; syncTimer.start(); manager.removeCollection(testAddressbook.id()); manager.removeCollection(testAddressbook2.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); cme->clearChangeFlags(testAddressbook2.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 2 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 scalingPresenceUpdate(QContactManager &manager, bool quickMode) { qint64 elapsedTimeTotal = 0; QElapsedTimer syncTimer; // this presence update benchmark should show whether presence update // time/cost scales linearly (we hope) or exponentially (which would be bad) // with the number of contacts in database and the number of updates. qDebug() << "--------"; qDebug() << "Performing scaling entire batch (connectivity change) presence update tests:"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("scalingPresenceUpdate")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/scalingPresenceUpdate"); manager.saveCollection(&testAddressbook); // prefill the database const int prefillCount = quickMode ? 250 : 500; QList prefillData; prefillData.reserve(prefillCount); for (int i = 0; i < prefillCount; ++i) { prefillData.append(generateContact(testAddressbook.id())); } qDebug() << " prefilling database with" << prefillData.size() << "contacts... this will take a while..."; manager.saveContacts(&prefillData); QList deleteIds; for (const QContact &c : prefillData) { deleteIds.append(c.id()); } // now do the updates and save. QList contactsToUpdate; QDateTime timestamp = QDateTime::currentDateTime(); QStringList presenceAvatars = generateAvatarsList(); for (int j = 0; j < prefillData.size(); ++j) { QContact curr = prefillData.at(j); QString genstr = QString::number(j) + "3"; QContactPresence cp = curr.detail(); QContactNickname nn = curr.detail(); QContactAvatar av = curr.detail(); cp.setNickname(genstr); cp.setCustomMessage(genstr); cp.setTimestamp(timestamp); cp.setPresenceState(static_cast((qrand() % 4) + 1)); nn.setNickname(nn.nickname() + genstr); av.setImageUrl(genstr + presenceAvatars.at(qrand() % presenceAvatars.size())); curr.saveDetail(&cp); curr.saveDetail(&nn); curr.saveDetail(&av); contactsToUpdate.append(curr); } // perform a batch save. syncTimer.start(); manager.saveContacts(&contactsToUpdate); qint64 presenceElapsed = syncTimer.elapsed(); int totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " update ( batch of" << contactsToUpdate.size() << ") presence+nick+avatar (with" << totalAggregatesInDatabase << "existing in database, all overlap):" << presenceElapsed << "milliseconds (" << ((1.0 * presenceElapsed) / (1.0 * contactsToUpdate.size())) << " msec per updated contact )"; elapsedTimeTotal += presenceElapsed; QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); qDebug() << " deleted" << deleteIds.size() << "contacts in" << deleteTime << "milliseconds"; elapsedTimeTotal += deleteTime; syncTimer.start(); manager.removeCollection(testAddressbook.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 1 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 entireBatchPresenceUpdate(QContactManager &manager, bool quickMode) { qint64 elapsedTimeTotal = 0; QElapsedTimer syncTimer; // in the second presence update test, we update ALL of the contacts // This simulates having a large number of contacts from a single source (eg, a social network) // where (due to changed connectivity status) presence updates for the entire set become available. qDebug() << "--------"; qDebug() << "Performing entire batch (connectivity change) presence update tests:"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("entireBatchPresenceUpdate")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/entireBatchPresenceUpdate"); manager.saveCollection(&testAddressbook); // prefill the database const int prefillCount = quickMode ? 100 : 250; QList prefillData; prefillData.reserve(prefillCount); for (int i = 0; i < prefillCount; ++i) { prefillData.append(generateContact(testAddressbook.id())); } qDebug() << " prefilling database with" << prefillData.size() << "contacts... this will take a while..."; manager.saveContacts(&prefillData); QList deleteIds; for (const QContact &c : prefillData) { deleteIds.append(c.id()); } QList contactsToUpdate; QDateTime timestamp = QDateTime::currentDateTime(); QStringList presenceAvatars = generateAvatarsList(); for (int j = 0; j < prefillData.size(); ++j) { QContact curr = prefillData.at(j); QString genstr = QString::number(j) + "2"; QContactPresence cp = curr.detail(); QContactNickname nn = curr.detail(); QContactAvatar av = curr.detail(); cp.setNickname(genstr); cp.setCustomMessage(genstr); cp.setTimestamp(timestamp); cp.setPresenceState(static_cast((qrand() % 4) + 1)); nn.setNickname(nn.nickname() + genstr); av.setImageUrl(genstr + presenceAvatars.at(qrand() % presenceAvatars.size())); curr.saveDetail(&cp); curr.saveDetail(&nn); curr.saveDetail(&av); contactsToUpdate.append(curr); } // perform a batch save. syncTimer.start(); manager.saveContacts(&contactsToUpdate); qint64 presenceElapsed = syncTimer.elapsed(); int totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " update ( batch of" << contactsToUpdate.size() << ") presence+nick+avatar (with" << totalAggregatesInDatabase << "existing in database, all overlap):" << presenceElapsed << "milliseconds (" << ((1.0 * presenceElapsed) / (1.0 * contactsToUpdate.size())) << " msec per updated contact )"; elapsedTimeTotal += presenceElapsed; QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); qDebug() << " deleted" << deleteIds.size() << "contacts in" << deleteTime << "milliseconds"; elapsedTimeTotal += deleteTime; syncTimer.start(); manager.removeCollection(testAddressbook.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 1 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 smallBatchPresenceUpdate(QContactManager &manager, bool quickMode) { qint64 elapsedTimeTotal = 0; QElapsedTimer syncTimer; // The next test is about updating existing contacts, amongst a large set. // We're especially interested in presence updates, as these are common. qDebug() << "--------"; qDebug() << "Performing small batch presence update tests:"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("smallBatchPresenceUpdate")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/smallBatchPresenceUpdate"); manager.saveCollection(&testAddressbook); // prefill the database const int prefillCount = quickMode ? 100 : 250; QList prefillData; prefillData.reserve(prefillCount); for (int i = 0; i < prefillCount; ++i) { prefillData.append(generateContact(testAddressbook.id())); } qDebug() << " prefilling database with" << prefillData.size() << "contacts... this will take a while..."; manager.saveContacts(&prefillData); QList deleteIds; for (const QContact &c : prefillData) { deleteIds.append(c.id()); } // in the first presence update test, we update a small number of contacts. QStringList presenceAvatars = generateAvatarsList(); QList contactsToUpdate; const int smallBatchSize = quickMode ? 5 : 10; for (int i = 0; i < smallBatchSize; ++i) { contactsToUpdate.append(prefillData.at(prefillData.size() - 1 - i)); } // modify the presence, nickname and avatar of the test data for (int j = 0; j < contactsToUpdate.size(); ++j) { QString genstr = QString::number(j); QContact curr = contactsToUpdate[j]; QContactPresence cp = curr.detail(); QContactNickname nn = curr.detail(); QContactAvatar av = curr.detail(); cp.setNickname(genstr); cp.setCustomMessage(genstr); cp.setTimestamp(QDateTime::currentDateTime()); cp.setPresenceState(static_cast(qrand() % 4)); nn.setNickname(nn.nickname() + genstr); av.setImageUrl(genstr + presenceAvatars.at(qrand() % presenceAvatars.size())); curr.saveDetail(&cp); curr.saveDetail(&nn); curr.saveDetail(&av); contactsToUpdate.replace(j, curr); } // perform a batch save. syncTimer.start(); manager.saveContacts(&contactsToUpdate); qint64 presenceElapsed = syncTimer.elapsed(); int totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " update ( batch of" << contactsToUpdate.size() << ") presence+nick+avatar (with" << totalAggregatesInDatabase << "existing in database, all overlap):" << presenceElapsed << "milliseconds (" << ((1.0 * presenceElapsed) / (1.0 * contactsToUpdate.size())) << " msec per updated contact )"; elapsedTimeTotal += presenceElapsed; QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); qDebug() << " deleted" << deleteIds.size() << "contacts in" << deleteTime << "milliseconds"; elapsedTimeTotal += deleteTime; syncTimer.start(); manager.removeCollection(testAddressbook.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 1 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 aggregationOperations(QContactManager &manager, bool quickMode) { qint64 elapsedTimeTotal = 0; QElapsedTimer syncTimer; // The next test is about saving contacts which should get aggregated into others. // Aggregation is an expensive operation, so we expect these save operations to take longer. qDebug() << "--------"; qDebug() << "Performing aggregation tests"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("aggregationOperations")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/aggregationOperations"); manager.saveCollection(&testAddressbook); QContactCollection testAddressbook2; testAddressbook2.setMetaData(QContactCollection::KeyName, QStringLiteral("aggregationOperations2")); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/aggregationOperations2"); manager.saveCollection(&testAddressbook2); // prefill the database const int prefillCount = quickMode ? 100 : 250; QList prefillData; prefillData.reserve(prefillCount); for (int i = 0; i < prefillCount; ++i) { prefillData.append(generateContact(testAddressbook.id())); } qDebug() << " prefilling database with" << prefillData.size() << "contacts... this will take a while..."; manager.saveContacts(&prefillData); QList deleteIds; for (const QContact &c : prefillData) { deleteIds.append(c.id()); } // generate contacts which will be aggregated into the prefill contacts. const int aggregateCount = quickMode ? 50 : 100; QList contactsToAggregate; for (int i = 0; i < aggregateCount; ++i) { QContact existingContact = prefillData.at(prefillData.size() - 1 - i); QContact contactToAggregate; contactToAggregate.setCollectionId(testAddressbook.id()); QContactName aggName = existingContact.detail(); // ensures it'll get aggregated QContactOnlineAccount newOnlineAcct; // new data, which should get promoted up etc. newOnlineAcct.setAccountUri(QString(QLatin1String("aggregationOperations%1@fetchtimes.benchmark")).arg(i)); contactToAggregate.saveDetail(&aggName); contactToAggregate.saveDetail(&newOnlineAcct); contactsToAggregate.append(contactToAggregate); } syncTimer.start(); manager.saveContacts(&contactsToAggregate); qint64 aggregationElapsed = syncTimer.elapsed(); int totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " average time for aggregation of" << contactsToAggregate.size() << "contacts (with" << totalAggregatesInDatabase << "existing in database):" << aggregationElapsed << "milliseconds (" << ((1.0 * aggregationElapsed) / (1.0 * contactsToAggregate.size())) << " msec per aggregated contact )"; elapsedTimeTotal += aggregationElapsed; for (const QContact &c : contactsToAggregate) { deleteIds.append(c.id()); } // Now perform the test again, this time with more aggregates, to test nonlinearity. contactsToAggregate.clear(); const int high = prefillData.size() / 2, low = high / 2; for (int i = low; i < high; ++i) { QContact existingContact = prefillData.at(prefillData.size() - 1 - i); QContact contactToAggregate; contactToAggregate.setCollectionId(testAddressbook2.id()); QContactName aggName = existingContact.detail(); // ensures it'll get aggregated QContactOnlineAccount newOnlineAcct; // new data, which should get promoted up etc. newOnlineAcct.setAccountUri(QString(QLatin1String("aggregationOperations%1@fetchtimes.benchmark")).arg(i)); contactToAggregate.saveDetail(&aggName); contactToAggregate.saveDetail(&newOnlineAcct); contactsToAggregate.append(contactToAggregate); } syncTimer.start(); manager.saveContacts(&contactsToAggregate); aggregationElapsed = syncTimer.elapsed(); totalAggregatesInDatabase = manager.contactIds().count(); qDebug() << " average time for aggregation of" << contactsToAggregate.size() << "contacts (with" << totalAggregatesInDatabase << "existing in database):" << aggregationElapsed << "milliseconds (" << ((1.0 * aggregationElapsed) / (1.0 * contactsToAggregate.size())) << " msec per aggregated contact )"; elapsedTimeTotal += aggregationElapsed; for (const QContact &c : contactsToAggregate) { deleteIds.append(c.id()); } QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); qDebug() << " deleted" << deleteIds.size() << "contacts in" << deleteTime << "milliseconds"; elapsedTimeTotal += deleteTime; syncTimer.start(); manager.removeCollection(testAddressbook.id()); manager.removeCollection(testAddressbook2.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); cme->clearChangeFlags(testAddressbook2.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 2 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 smallBatchWithExistingData(QContactManager &manager, bool quickMode) { qint64 elapsedTimeTotal = 0; QElapsedTimer syncTimer; // these tests are slightly different to the others. They operate on much smaller // batches, but occur after the database has already been prefilled with some data. qDebug() << "--------"; qDebug() << "Performing test of small batch updates with large existing data set"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("smallBatchWithExistingData")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/smallBatchWithExistingData"); manager.saveCollection(&testAddressbook); QList smallerNbrContacts; if (quickMode) { smallerNbrContacts << 20; } else { smallerNbrContacts << 1 << 2 << 5 << 10 << 20 << 50; } QList > smallerTestData; qDebug() << " generating smaller test data for prefilled timings..."; for (int i = 0; i < smallerNbrContacts.size(); ++i) { int howMany = smallerNbrContacts.at(i); QList newTestData; newTestData.reserve(howMany); for (int j = 0; j < howMany; ++j) { newTestData.append(generateContact(testAddressbook.id())); } smallerTestData.append(newTestData); } // prefill the database const int prefillCount = quickMode ? 100 : 250; QList prefillData; prefillData.reserve(prefillCount); for (int i = 0; i < prefillCount; ++i) { prefillData.append(generateContact(testAddressbook.id())); } qDebug() << " prefilling database with" << prefillData.size() << "contacts... this will take a while..."; manager.saveContacts(&prefillData); qDebug() << " now performing timings (shouldn't get aggregated)..."; for (int i = 0; i < smallerTestData.size(); ++i) { QList td = smallerTestData.at(i); qint64 ste = 0; qDebug() << " performing tests for" << td.size() << "contacts:"; syncTimer.start(); manager.saveContacts(&td); ste = syncTimer.elapsed(); qDebug() << " saving took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; QContactFetchHint fh; syncTimer.start(); QList readContacts = manager.contacts(QContactFilter(), QList(), fh); ste = syncTimer.elapsed(); qDebug() << " reading all (" << readContacts.size() << "), all details, took" << ste << "milliseconds"; elapsedTimeTotal += ste; fh.setDetailTypesHint(QList() << QContactDisplayLabel::Type << QContactName::Type << QContactAvatar::Type << QContactPhoneNumber::Type << QContactEmailAddress::Type); syncTimer.start(); readContacts = manager.contacts(QContactFilter(), QList(), fh); ste = syncTimer.elapsed(); qDebug() << " reading all, common details, took" << ste << "milliseconds"; elapsedTimeTotal += ste; fh.setOptimizationHints(QContactFetchHint::NoRelationships); fh.setDetailTypesHint(QList()); syncTimer.start(); readContacts = manager.contacts(QContactFilter(), QList(), fh); ste = syncTimer.elapsed(); qDebug() << " reading all, no relationships, took" << ste << "milliseconds"; elapsedTimeTotal += ste; fh.setDetailTypesHint(QList() << QContactDisplayLabel::Type << QContactName::Type << QContactAvatar::Type); syncTimer.start(); readContacts = manager.contacts(QContactFilter(), QList(), fh); ste = syncTimer.elapsed(); qDebug() << " reading all, display details + no rels, took" << ste << "milliseconds"; elapsedTimeTotal += ste; QContactDetailFilter firstNameStartsA; firstNameStartsA.setDetailType(QContactName::Type, QContactName::FieldFirstName); firstNameStartsA.setValue("A"); firstNameStartsA.setMatchFlags(QContactDetailFilter::MatchStartsWith); fh.setDetailTypesHint(QList()); syncTimer.start(); readContacts = manager.contacts(firstNameStartsA, QList(), fh); ste = syncTimer.elapsed(); qDebug() << " reading filtered (" << readContacts.size() << "), no relationships, took" << ste << "milliseconds"; elapsedTimeTotal += ste; QList idsToRemove; for (int j = 0; j < td.size(); ++j) { idsToRemove.append(retrievalId(td.at(j))); } QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(idsToRemove); cme->clearChangeFlags(idsToRemove, &purgeError); ste = syncTimer.elapsed(); qDebug() << " removing test data took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; } qDebug() << " removing prefill data"; QList deleteIds; for (const QContact &c : prefillData) { deleteIds.append(c.id()); } QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); elapsedTimeTotal += deleteTime; qDebug() << " removing prefill data took" << deleteTime << "milliseconds"; syncTimer.start(); manager.removeCollection(testAddressbook.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 1 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 synchronousOperations(QContactManager &manager, bool quickMode) { // Time some synchronous operations. First, generate the test data. QElapsedTimer syncTimer; qint64 elapsedTimeTotal = 0; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("synchronousOperations")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/synchronousOperations"); manager.saveCollection(&testAddressbook); QList nbrContacts; if (quickMode) { nbrContacts << 100; } else { nbrContacts << 10 << 100 << 200 << 500 << 1000; } QList > testData; qDebug() << "--------"; qDebug() << "Performing basic synchronous operations"; qDebug() << " generating test data for timings..."; for (int i = 0; i < nbrContacts.size(); ++i) { int howMany = nbrContacts.at(i); QList newTestData; newTestData.reserve(howMany); for (int j = 0; j < howMany; ++j) { // Use testing sync target, so 'local' won't be modified into 'was_local' via aggregation newTestData.append(generateContact(testAddressbook.id())); } testData.append(newTestData); } // Perform the timings - these all create new contacts and assume an "empty" initial database for (int i = 0; i < testData.size(); ++i) { QList td = testData.at(i); qint64 ste = 0; qDebug() << " -> performing tests for" << td.size() << "contacts:"; syncTimer.start(); manager.saveContacts(&td); ste = syncTimer.elapsed(); qDebug() << " saving took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; QContactCollectionFilter testingFilter; testingFilter.setCollectionId(testAddressbook.id()); QContactFetchHint fh; syncTimer.start(); QList readContacts = manager.contacts(testingFilter, QList(), fh); ste = syncTimer.elapsed(); if (readContacts.size() != td.size()) { qWarning() << "Invalid retrieval count:" << readContacts.size() << "expecting:" << td.size(); } qDebug() << " reading all (" << readContacts.size() << "), all details, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; fh.setDetailTypesHint(QList() << QContactDisplayLabel::Type << QContactName::Type << QContactAvatar::Type << QContactPhoneNumber::Type << QContactEmailAddress::Type); syncTimer.start(); readContacts = manager.contacts(testingFilter, QList(), fh); ste = syncTimer.elapsed(); if (readContacts.size() != td.size()) { qWarning() << "Invalid retrieval count:" << readContacts.size() << "expecting:" << td.size(); } qDebug() << " reading all, common details, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; fh.setOptimizationHints(QContactFetchHint::NoRelationships); fh.setDetailTypesHint(QList()); syncTimer.start(); readContacts = manager.contacts(testingFilter, QList(), fh); ste = syncTimer.elapsed(); if (readContacts.size() != td.size()) { qWarning() << "Invalid retrieval count:" << readContacts.size() << "expecting:" << td.size(); } qDebug() << " reading all, no relationships, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; fh.setDetailTypesHint(QList() << QContactDisplayLabel::Type << QContactName::Type << QContactAvatar::Type); syncTimer.start(); readContacts = manager.contacts(testingFilter, QList(), fh); ste = syncTimer.elapsed(); if (readContacts.size() != td.size()) { qWarning() << "Invalid retrieval count:" << readContacts.size() << "expecting:" << td.size(); } qDebug() << " reading all, display details + no rels, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; // Read the contacts, selected by ID QList idsToRetrieve; for (int j = 0; j < td.size(); ++j) { idsToRetrieve.append(retrievalId(td.at(j))); } syncTimer.start(); readContacts = manager.contacts(idsToRetrieve, fh, 0); ste = syncTimer.elapsed(); if (readContacts.size() != td.size()) { qWarning() << "Invalid retrieval count:" << readContacts.size() << "expecting:" << td.size(); } qDebug() << " reading all by IDs, display details + no rels, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; // Read the same set using ID filtering QContactIdFilter idFilter; idFilter.setIds(idsToRetrieve); syncTimer.start(); readContacts = manager.contacts(idFilter, QList(), fh); ste = syncTimer.elapsed(); if (readContacts.size() != td.size()) { qWarning() << "Invalid retrieval count:" << readContacts.size() << "expecting:" << td.size(); } qDebug() << " reading all by ID filter, display details + no rels, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; // Read the same set, but filter everything out using syncTarget QContactCollectionFilter aggregateFilter; aggregateFilter.setCollectionId(QContactCollectionId(manager.managerUri(), QByteArrayLiteral("col-1"))); // aggregate collection id. syncTimer.start(); readContacts = manager.contacts(idFilter & aggregateFilter, QList(), fh); ste = syncTimer.elapsed(); if (readContacts.size() != 0) { qWarning() << "Invalid retrieval count:" << readContacts.size() << "expecting:" << 0; } qDebug() << " reading all by ID filter & aggregate, display details + no rels, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; QContactDetailFilter firstNameStartsA; firstNameStartsA.setDetailType(QContactName::Type, QContactName::FieldFirstName); firstNameStartsA.setValue("A"); firstNameStartsA.setMatchFlags(QContactDetailFilter::MatchStartsWith); fh.setDetailTypesHint(QList()); syncTimer.start(); readContacts = manager.contacts(firstNameStartsA, QList(), fh); ste = syncTimer.elapsed(); qDebug() << " reading filtered (" << readContacts.size() << "), no relationships, took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; QList idsToRemove; for (int j = 0; j < td.size(); ++j) { idsToRemove.append(retrievalId(td.at(j))); } QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(idsToRemove); cme->clearChangeFlags(idsToRemove, &purgeError); ste = syncTimer.elapsed(); qDebug() << " removing test data took" << ste << "milliseconds (" << ((1.0 * ste) / (1.0 * td.size())) << "msec per contact )"; elapsedTimeTotal += ste; } QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeCollection(testAddressbook.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 1 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return elapsedTimeTotal; } static qint64 performAsynchronousFetch(QContactManager &manager, bool quickMode) { const int repeatCount = quickMode ? 1 : 3; // test caching effects qint64 elapsedTimeTotal = 0; QContactFetchRequest request; request.setManager(&manager); // Fetch all, no optimization hints for (int i = 0; i < repeatCount; ++i) { QElapsedTimer timer; timer.start(); request.start(); request.waitForFinished(); qint64 elapsed = timer.elapsed(); qDebug() << " " << i << ": Fetch completed in" << elapsed << "ms"; elapsedTimeTotal += elapsed; } // Skip relationships QContactFetchHint hint; hint.setOptimizationHints(QContactFetchHint::NoRelationships); request.setFetchHint(hint); for (int i = 0; i < repeatCount; ++i) { QElapsedTimer timer; timer.start(); request.start(); request.waitForFinished(); qint64 elapsed = timer.elapsed(); qDebug() << " " << i << ": No-relationships fetch completed in" << elapsed << "ms"; elapsedTimeTotal += elapsed; } // Reduce data access hint.setDetailTypesHint(QList() << QContactName::Type << QContactAddress::Type); request.setFetchHint(hint); for (int i = 0; i < repeatCount; ++i) { QElapsedTimer timer; timer.start(); request.start(); request.waitForFinished(); qint64 elapsed = timer.elapsed(); qDebug() << " " << i << ": Reduced data fetch completed in" << elapsed << "ms"; elapsedTimeTotal += elapsed; } // Reduce number of results hint.setMaxCountHint(request.contacts().count() / 8); request.setFetchHint(hint); for (int i = 0; i < repeatCount; ++i) { QElapsedTimer timer; timer.start(); request.start(); request.waitForFinished(); qint64 elapsed = timer.elapsed(); qDebug() << " " << i << ": Max count fetch completed in" << elapsed << "ms"; elapsedTimeTotal += elapsed; } return elapsedTimeTotal; } static qint64 asynchronousOperations(QContactManager &manager, bool quickMode) { const int numberContacts = quickMode ? 100 : 1000; QElapsedTimer totalTimeTimer; totalTimeTimer.start(); qDebug() << "--------"; qDebug() << "Performing asynchronous fetch with empty database"; qint64 requestTime = performAsynchronousFetch(manager, quickMode); qDebug() << " asynchronous fetch requests took:" << requestTime << "milliseconds"; qDebug() << "--------"; qDebug() << "Performing asynchronous save of" << numberContacts << "contacts"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("asynchronousOperations")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/asynchronousOperations"); manager.saveCollection(&testAddressbook); QList testData; for (int i = 0; i < numberContacts; ++i) { testData.append(generateContact(testAddressbook.id())); } QElapsedTimer storeTimer; storeTimer.start(); QContactSaveRequest sreq; sreq.setManager(&manager); sreq.setContacts(testData); sreq.start(); sreq.waitForFinished(); QList savedContacts = sreq.contacts(); qint64 storeTime = storeTimer.elapsed(); qDebug() << " saved" << numberContacts << "contacts in" << storeTime << "milliseconds"; qDebug() << "--------"; qDebug() << "Performing asynchronous fetch with filled database"; requestTime = performAsynchronousFetch(manager, quickMode); qDebug() << " asynchronous fetch requests took:" << requestTime << "milliseconds"; qDebug() << "--------"; qDebug() << "Performing asynchronous remove with filled database"; QList deleteIds; for (const QContact &c : savedContacts) { deleteIds.append(c.id()); } QElapsedTimer deleteTimer; deleteTimer.start(); QContactRemoveRequest rreq; rreq.setManager(&manager); rreq.setContactIds(deleteIds); rreq.start(); rreq.waitForFinished(); qint64 deleteTime = deleteTimer.elapsed(); qDebug() << " asynchronous remove request took" << deleteTime << "milliseconds"; QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); deleteTimer.start(); manager.removeCollection(testAddressbook.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); qint64 colDeleteTime = deleteTimer.elapsed(); qDebug() << " deleted 1 addressbooks in" << colDeleteTime << "milliseconds"; return totalTimeTimer.elapsed(); } qint64 simpleFilterAndSort(QContactManager &manager, bool quickMode) { // Now we perform a simple create+filter+sort test, where contacts are saved in small chunks. qDebug() << "--------"; // create test collections for this benchmark. QContactCollection testAddressbook; testAddressbook.setMetaData(QContactCollection::KeyName, QStringLiteral("simpleFilterAndSort")); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 5); testAddressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/simpleFilterAndSort"); manager.saveCollection(&testAddressbook); QContactCollection testAddressbook2; testAddressbook2.setMetaData(QContactCollection::KeyName, QStringLiteral("simpleFilterAndSort2")); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 6); testAddressbook2.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, "/addressbooks/simpleFilterAndSort2"); manager.saveCollection(&testAddressbook2); qDebug() << "Starting save (chunks) / fetch (filter + sort) / delete (all) test..."; QList testData, testData2; const int chunkSize = quickMode ? 25 : 50; const int prefillCount = quickMode ? 250 : 1000; for (int i = 0; i < prefillCount/2; ++i) { testData.append(generateContact(testAddressbook.id(), true)); testData2.append(generateContact(testAddressbook2.id(), true)); } QList > chunks, chunks2; for (int i = 0; i < testData.size(); i += chunkSize) { QList chunk, chunk2; for (int j = 0; j < chunkSize && ((i+j) < testData.size()); ++j) { chunk.append(testData[i+j]); chunk2.append(testData2[i+j]); } chunks.append(chunk); chunks2.append(chunk2); } QContactFetchHint listDisplayFetchHint; listDisplayFetchHint.setDetailTypesHint(QList() << QContactDisplayLabel::Type << QContactName::Type << QContactAvatar::Type); QContactSortOrder sort; sort.setDetailType(QContactDisplayLabel::Type, QContactDisplayLabel__FieldLabelGroup); QContactDetailFilter phoneFilter; phoneFilter.setDetailType(QContactPhoneNumber::Type); // existence filter, don't care about value. QContactCollectionFilter aggregateFilter; aggregateFilter.setCollectionId(QContactCollectionId(manager.managerUri(), QByteArrayLiteral("col-1"))); // aggregate collection id. qDebug() << " storing" << prefillCount << "contacts... this will take a while..."; QElapsedTimer syncTimer; syncTimer.start(); for (int i = 0; i < chunks.size(); ++i) { manager.saveContacts(&chunks[i]); } for (int i = 0; i < chunks2.size(); ++i) { manager.saveContacts(&chunks2[i]); } qint64 saveTime = syncTimer.elapsed(); qDebug() << " stored" << (testData.size()+testData2.size()) << "contacts in" << saveTime << "milliseconds"; qDebug() << " retrieving aggregate contacts with filter, sort order, and fetch hint applied"; syncTimer.start(); QList filteredSorted = manager.contacts(aggregateFilter & phoneFilter, sort, listDisplayFetchHint); qint64 fetchTime = syncTimer.elapsed(); qDebug() << " retrieved" << filteredSorted.size() << "contacts in" << fetchTime << "milliseconds"; QList deleteIds; for (const QList &chunk : chunks) { for (const QContact &c : chunk) { deleteIds.append(c.id()); } } for (const QList &chunk2 : chunks2) { for (const QContact &c : chunk2) { deleteIds.append(c.id()); } } QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); syncTimer.start(); manager.removeContacts(deleteIds); cme->clearChangeFlags(deleteIds, &purgeError); qint64 deleteTime = syncTimer.elapsed(); qDebug() << " deleted" << deleteIds.size() << "contacts in" << deleteTime << "milliseconds"; if (filteredSorted.size() == 0) { qWarning() << "Zero aggregate contacts found. Are you sure you're running with privileged permissions?"; } syncTimer.start(); manager.removeCollection(testAddressbook.id()); manager.removeCollection(testAddressbook2.id()); cme->clearChangeFlags(testAddressbook.id(), &purgeError); cme->clearChangeFlags(testAddressbook2.id(), &purgeError); qint64 colDeleteTime = syncTimer.elapsed(); qDebug() << " deleted 2 addressbooks in" << colDeleteTime << "milliseconds"; // note: we omit this collection deletion time from the benchmark. return saveTime + fetchTime + deleteTime; } void generateQueryPlanTestDataContacts( int count, bool aggregate, const QContactCollection &col, QContactManager &manager, QtContactsSqliteExtensions::ContactManagerEngine *cme) { QList contacts; for (int i = 0; i < count; ++i) { contacts.append(generateContact(col.id(), aggregate)); } if (!manager.saveContacts(&contacts)) { qWarning() << "Failed to save contacts into collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } QList clearChangeFlags; for (int i = 0; i < contacts.size(); ++i) { if (i % 29 == 0) { // set deleted flag QContact del = contacts.at(i); if (!manager.removeContact(del.id())) { qWarning() << "Failed to delete contact at index: " << i << " from collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } else if (i % 23 == 0) { // nothing, leave added flag as-is. } else if (i % 17 == 0) { // set modified flag. QContact mod = contacts.at(i); QContactPhoneNumber extraph; extraph.setNumber(mod.detail().number() + QStringLiteral("1232123%1").arg(i)); mod.saveDetail(&extraph, QContact::IgnoreAccessConstraints); QContactEmailAddress extraem; extraem.setEmailAddress(QStringLiteral("extra.email.%1@server.tld.%2") .arg(i).arg(col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString())); mod.saveDetail(&extraem, QContact::IgnoreAccessConstraints); QContactGuid guid; guid.setGuid(QUuid::createUuid().toString()); mod.saveDetail(&guid, QContact::IgnoreAccessConstraints); if (!manager.saveContact(&mod)) { qWarning() << "Failed to save contact modification at index: " << i << " into collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } else if (i % 23 != 0) { clearChangeFlags.append(contacts.at(i).id()); } } QContactManager::Error err = QContactManager::NoError; if (!cme->clearChangeFlags(clearChangeFlags, &err)) { qWarning() << "Failed to clear contact change flags for collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } qint64 generateQueryPlanTestData(QContactManager &manager, int numberOfContacts) { QElapsedTimer timer; timer.start(); const int local = 188, a1c1 = 250, a1c2 = 100, a2c1 = 150, a2c2 = 18, a2c3 = 25, a2c4 = 80, a2c5 = 500, a0c1 = 42, a4c1 = 200; const int totalNumberOfContacts = local + a1c1 + a1c2 + a2c1 + a2c2 + a2c3 + a2c4 + a2c5 + a0c1 + a4c1; const int scaledNumberOfContacts = numberOfContacts > 0 ? numberOfContacts : 1553; const double ratio = ((double)scaledNumberOfContacts) / ((double)totalNumberOfContacts); QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); { QContactCollection col = manager.collection(QtContactsSqliteExtensions::localCollectionId(manager.managerUri())); generateQueryPlanTestDataContacts(qRound(ratio * local), false, col, manager, cme); } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("User Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Description of User Contacts addressbook")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 1); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("carddav")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/carddav/user/1/addressbooks/contacts/")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a1c1), true, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Shared Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Description of Shared Contacts addressbook")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 1); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("carddav")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/carddav/user/1/addressbooks/shared_contacts/")); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a1c2), false, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Google Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Default contacts addressbook in Google Contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 2); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("google-contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/path/?user=someUser&addressbook=default")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a2c1), true, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Google Recent Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Recent contacts addressbook in Google Contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 2); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("google-contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/path/?user=someUser&addressbook=recent")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a2c2), false, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Soccer Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Soccer contacts addressbook in Google Contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 2); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("google-contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/path/?user=someUser&addressbook=soccer")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a2c3), false, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Work Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Work contacts addressbook in Google Contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 2); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("google-contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/path/?user=someUser&addressbook=work")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a2c4), true, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Plus Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Google Plus contacts addressbook in Google Contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 2); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("google-contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("/path/?user=someUser&addressbook=plus")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a2c5), true, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Application-specific Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Some application-specific contacts which should not be aggregated")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("application")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, false); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a0c1), false, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } { QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Exchange Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Contacts from Exchange ActiveSync account")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, 4); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("exchange")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, QStringLiteral("2:3")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (manager.saveCollection(&col)) { generateQueryPlanTestDataContacts(qRound(ratio * a4c1), true, col, manager, cme); } else { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); } } return timer.elapsed(); } qint64 performReadQueryPlanTestData(QContactManager &manager) { qDebug() << "Starting perform read query plan test data..."; QElapsedTimer syncTimer; syncTimer.start(); const QList contacts = manager.contacts(); const qint64 elapsed = syncTimer.elapsed(); qDebug() << "Took: " << elapsed << "ms to read " << contacts.size() << " contacts"; QContactDetailFilter firstNameStartsA; firstNameStartsA.setDetailType(QContactName::Type, QContactName::FieldFirstName); firstNameStartsA.setValue("A"); firstNameStartsA.setMatchFlags(QContactDetailFilter::MatchStartsWith); syncTimer.start(); const QList filteredContacts = manager.contacts(firstNameStartsA, QList(), QContactFetchHint()); const qint64 filteredElapsed = syncTimer.elapsed(); qDebug() << "Took: " << filteredElapsed << "ms to read " << filteredContacts.size() << " contacts via filter"; return elapsed+filteredElapsed; } qint64 performQueryPlanOperations(QContactManager &manager) { qDebug() << "Starting perform query plan operations test..."; QElapsedTimer timer; timer.start(); QContactCollection col; col.setMetaData(QContactCollection::KeyName, QStringLiteral("Other Contacts")); col.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Some other contacts")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QStringLiteral("application")); col.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); if (!manager.saveCollection(&col)) { qWarning() << "Failed to save collection: " << col.metaData(QContactCollection::KeyName) << " : " << col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString(); return -1; } QContact localContact; QContactName lcn; lcn.setFirstName("Alice"); lcn.setLastName("Wonderland"); QContactPhoneNumber lcp; lcp.setNumber("123456789"); QContactEmailAddress lce; lce.setEmailAddress("alice@wonderland.tld"); QContactAddress lca; lca.setStreet("1 Rabbit Hole Way"); lca.setLocality("Underground"); lca.setRegion("Wonderland"); lca.setCountry("Fantasy"); localContact.saveDetail(&lcn); localContact.saveDetail(&lcp); localContact.saveDetail(&lce); localContact.saveDetail(&lca); QContact otherContact; otherContact.setCollectionId(col.id()); QContactPhoneNumber ocp; ocp.setNumber("987654321"); QContactEmailAddress oce; oce.setEmailAddress("alice.wonderland@madhatter.tld"); QContactHobby och; och.setHobby("Dreaming"); otherContact.saveDetail(&lcn); otherContact.saveDetail(&ocp); otherContact.saveDetail(&oce); otherContact.saveDetail(&och); qDebug() << " storing local contact...."; QElapsedTimer syncTimer; syncTimer.start(); if (!manager.saveContact(&localContact)) { qWarning() << "Failed to save local contact!"; return -1; } qint64 saveTime = syncTimer.elapsed(); qDebug() << " saved local contact in:" << saveTime << "milliseconds"; qDebug() << " storing other contact...."; syncTimer.start(); if (!manager.saveContact(&otherContact)) { qWarning() << "Failed to save other contact!"; return -1; } saveTime = syncTimer.elapsed(); qDebug() << " saved other contact in:" << saveTime << "milliseconds"; qDebug() << " fetching aggregate contacts..."; syncTimer.start(); const QList contacts = manager.contacts(); qint64 readTime = syncTimer.elapsed(); qDebug() << " read" << contacts.size() << "aggregate contacts in" << readTime << "milliseconds"; const qint64 totalTime = timer.elapsed(); // clean up. QContactManager::Error purgeError = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(manager); const QContactId localId = localContact.id(); manager.removeContact(localId); cme->clearChangeFlags(QList() << localId, &purgeError); const QContactCollectionId colId = col.id(); manager.removeCollection(colId); cme->clearChangeFlags(colId, &purgeError); return totalTime; } int main(int argc, char *argv[]) { QCoreApplication application(argc, argv); const QStringList &args(application.arguments()); QStringList functionArgs; if (args.size() <= 1) { qDebug() << "usage: fetchtimes [--stable] [--quick] --help|--all|--function="; return 0; } else if (args.contains("--help") || args.contains("-h")) { qDebug() << "usage: fetchtimes [--stable] --help|--all|--quick|"; qDebug() << "If --stable is specified, a stable prng seed will be used."; qDebug() << "If --quick is specified, the benchmark will complete more quickly (but results will have higher variance)"; qDebug() << "Available functions:"; qDebug() << " simpleFilterAndSort"; qDebug() << " asynchronousOperations"; qDebug() << " synchronousOperations"; qDebug() << " smallBatchWithExistingData"; qDebug() << " aggregationOperations"; qDebug() << " smallBatchPresenceUpdate"; qDebug() << " entireBatchPresenceUpdate"; qDebug() << " scalingPresenceUpdate"; qDebug() << " nonAggregatedPresenceUpdate"; qDebug() << " aggregatedPresenceUpdate"; return 0; } // remember also to set: //mcetool --set-never-blank=enabled //mcetool --set-cpu-scaling-governor=interactive (automatic/performance) //mcetool --set-power-saving-mode=disabled //mcetool --set-low-power-mode=disabled for (int i = 0; i < args.size(); ++i) { if (args.at(i).startsWith(QStringLiteral("--function="))) { functionArgs.append(args.at(i).mid(11)); } else if (args.at(i).compare(QStringLiteral("-f")) == 0 && args.size() > (i+1)) { i = i+1; functionArgs.append(args.at(i)); } } const bool queryPlan(args.contains(QStringLiteral("--queryPlan"))); const bool testData(args.contains(QStringLiteral("--testData"))); const bool readTestData(args.contains(QStringLiteral("--readTestData"))); const bool quickMode(args.contains(QStringLiteral("-q")) || args.contains(QStringLiteral("--quick"))); const bool stable(args.contains(QStringLiteral("-s")) || args.contains(QStringLiteral("--stable"))); const bool runAll(args.contains(QStringLiteral("-a")) || args.contains(QStringLiteral("--all"))); QMap parameters; parameters.insert(QString::fromLatin1("autoTest"), QString::fromLatin1("true")); parameters.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("false")); QContactManager manager(QString::fromLatin1("org.nemomobile.contacts.sqlite"), parameters); QList aggregateIds = manager.contactIds(); // ensure the database has been created. if (!aggregateIds.isEmpty()) { qWarning() << "Database not empty at beginning of test! Contains:" << aggregateIds.size() << "aggregate contacts!"; } qint64 elapsedTimeTotal = 0; clock_t startTicks = clock(); if (queryPlan) { // hidden/undocumented feature: perform two writes and one read // which we will use to inspect the query plans. qsrand(42); elapsedTimeTotal = performQueryPlanOperations(manager); } else if (readTestData) { // hidden/undocumented feature: time read all contacts from database. qsrand(42); elapsedTimeTotal = performReadQueryPlanTestData(manager); } else if (testData) { // hidden/undocumented feature: fill database with random data // which we then use to generate the query plan. qsrand(42); elapsedTimeTotal = generateQueryPlanTestData(manager, args.last().toInt()); } else { qsrand(stable ? 42 : QDateTime::currentDateTime().time().second()); elapsedTimeTotal += (runAll || functionArgs.contains("simpleFilterAndSort")) ? simpleFilterAndSort(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("asynchronousOperations")) ? asynchronousOperations(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("synchronousOperations")) ? synchronousOperations(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("smallBatchWithExistingData")) ? smallBatchWithExistingData(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("aggregationOperations")) ? aggregationOperations(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("smallBatchPresenceUpdate")) ? smallBatchPresenceUpdate(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("entireBatchPresenceUpdate")) ? entireBatchPresenceUpdate(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("scalingPresenceUpdate")) ? scalingPresenceUpdate(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("nonAggregatedPresenceUpdate")) ? nonAggregatedPresenceUpdate(manager, quickMode) : 0; elapsedTimeTotal += (runAll || functionArgs.contains("aggregatedPresenceUpdate")) ? aggregatedPresenceUpdate(manager, quickMode) : 0; } clock_t endTicks = clock(); qDebug() << "\n\nCumulative elapsed time:" << elapsedTimeTotal << "milliseconds, with: " << (endTicks - startTicks) << " clock ticks."; return 0; } qtcontacts-sqlite-0.3.19/tests/common.pri000066400000000000000000000005121436373107600204440ustar00rootroot00000000000000include(../config.pri) QT = \ core \ testlib TEMPLATE = app CONFIG -= app_bundle INCLUDEPATH += $$PWD/../src/extensions target.path = /opt/tests/qtcontacts-sqlite-qt5 INSTALLS += target check.commands = "$${PWD}/run_test.sh $$shadowed($${PWD})/.. ./$${TARGET}" check.depends = $${TARGET} QMAKE_EXTRA_TARGETS += check qtcontacts-sqlite-0.3.19/tests/qcontactmanagerdataholder.h000066400000000000000000000103131436373107600240100ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Mobility Components. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef QCONTACTMANAGERDATAHOLDER_H #define QCONTACTMANAGERDATAHOLDER_H #include #include #include #include #include #include "qcontactmanager.h" #include "util.h" // // W A R N I N G // ------------- // // This file is not part of the Qt API. It exists purely as an // implementation detail. This header file may change from version to // version without notice, or even be removed. // // We mean it. // QT_BEGIN_NAMESPACE_CONTACTS class QContact; class QContactManagerDataHolder { public: QContactManagerDataHolder(bool nonprivileged) { QStringList managerNames = QContactManager::availableManagers(); foreach(const QString& mgr, managerNames) { // Only test the qtcontacts-sqlite engine if (mgr != QLatin1String("qtcontacts:org.nemomobile.contacts.sqlite")) continue; QMap params; params.insert("autoTest", "true"); params.insert("mergePresenceChanges", "false"); if (nonprivileged) { params.insert("nonprivileged", "true"); } QString mgrUri = QContactManager::buildUri(mgr, params); QContactManager* cm = QContactManager::fromUri(mgrUri); if (cm) { qDebug() << "Saving contacts for" << mgrUri; QList contacts = cm->contacts(); savedContacts.insert(cm->managerName(),contacts); QList ids; foreach(const QContact& contact, contacts) ids.append(retrievalId(contact)); cm->removeContacts(ids, 0); delete cm; } } } ~QContactManagerDataHolder() { foreach(const QString& mgrUri, savedContacts.keys()) { QContactManager* cm = QContactManager::fromUri(mgrUri); if (cm) { qDebug() << "Restoring contacts for" << mgrUri; QList contacts = savedContacts.value(mgrUri); cm->saveContacts(&contacts, 0); // XXX this doesn't restore relationships.. delete cm; } } } private: QMap > savedContacts; }; QT_END_NAMESPACE_CONTACTS #endif qtcontacts-sqlite-0.3.19/tests/run_test.sh000077500000000000000000000016061436373107600206470ustar00rootroot00000000000000#! /bin/sh # Usage: # run_test.sh # # Run TEST_PROGRAM within a proper environment; this includes a D-Bus session # and any environment variable needed for the succesful completion of the test. # # If the TEST_WRAPPER environment variable is set, then it will be executed and # the test program will be passed as an argument to it; this can be useful, for # example, to run the tests in valgrind or strace. set -e TOP_BUILD_DIR="$1" TEST_PROGRAM="$2" export LC_ALL=C export QT_PLUGIN_PATH="${TOP_BUILD_DIR}/src/engine/" OUTPUT=$(dbus-daemon --session --print-address '' --print-pid '' --fork) export DBUS_SESSION_BUS_ADDRESS=$(echo "$OUTPUT" | head -1) DBUS_DAEMON_PID=$(echo "$OUTPUT" | tail -1) cleanUp() { echo "Killing the temporary D-Bus daemon" kill "$DBUS_DAEMON_PID" } trap cleanUp EXIT INT TERM $TEST_WRAPPER $TEST_PROGRAM trap - EXIT cleanUp qtcontacts-sqlite-0.3.19/tests/tests.pro000066400000000000000000000002371436373107600203300ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ auto \ benchmarks tests_xml.path = /opt/tests/qtcontacts-sqlite-qt5/ tests_xml.files = tests.xml INSTALLS += tests_xml qtcontacts-sqlite-0.3.19/tests/tests.xml000066400000000000000000000121451436373107600203310ustar00rootroot00000000000000 QtContacts SQLite backend automatic tests Backend correctness automatic tests DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "rm -rf /home/$DEVICEUSER/.local/share/system/privileged/Contacts/qtcontacts-sqlite-test" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_aggregation" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "rm -rf /home/$DEVICEUSER/.local/share/system/privileged/Contacts/qtcontacts-sqlite-test" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_synctransactions" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "rm -rf /home/$DEVICEUSER/.local/share/system/privileged/Contacts/qtcontacts-sqlite-test" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_displaylabelgroups" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "rm -rf /home/$DEVICEUSER/.local/share/system/privileged/Contacts/qtcontacts-sqlite-test" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_detailfetchrequest" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "rm -rf /home/$DEVICEUSER/.local/share/system/privileged/Contacts/qtcontacts-sqlite-test" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_qcontactmanager" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "rm -rf /home/$DEVICEUSER/.local/share/system/Contacts/qtcontacts-sqlite-test" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_qcontactmanagerfiltering" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_database -perfcounter tickcounter" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_memorytable" $DEVICEUSER' DEVICEUSER=$(getent passwd $(grep "^UID_MIN" /etc/login.defs | tr -s " " | cut -d " " -f2) | sed 's/:.*//') bash -c '/usr/sbin/run-blts-root /bin/su -g privileged -c "/opt/tests/qtcontacts-sqlite-qt5/tst_phonenumber" $DEVICEUSER' qtcontacts-sqlite-0.3.19/tests/util.h000066400000000000000000000213411436373107600175710ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). ** Copyright (C) 2020 Open Mobile Platform LLC. ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Mobility Components. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef QTCONTACTS_SQLITE_UTIL_H #define QTCONTACTS_SQLITE_UTIL_H #include #include #include #include #include "../../../src/engine/contactid_p.h" #include "../../../src/extensions/qtcontacts-extensions_impl.h" #include "../../../src/extensions/qtcontacts-extensions_manager_impl.h" #include "../../../src/extensions/qcontactdeactivated.h" #include "../../../src/extensions/qcontactdeactivated_impl.h" #include "../../../src/extensions/qcontactundelete.h" #include "../../../src/extensions/qcontactundelete_impl.h" #include "../../../src/extensions/qcontactoriginmetadata.h" #include "../../../src/extensions/qcontactoriginmetadata_impl.h" #include "../../../src/extensions/qcontactstatusflags.h" #include "../../../src/extensions/qcontactstatusflags_impl.h" #include "../../../src/extensions/contactmanagerengine.h" // qtpim doesn't support the customLabel field natively, but qtcontact-sqlite provides it #define CUSTOM_LABEL_STORAGE_SUPPORTED // qtpim doesn't support the displayLabelGroup field natively, but qtcontacts-sqlite provides it #define DISPLAY_LABEL_GROUP_STORAGE_SUPPORTED #define QTRY_WAIT(code, __expr) \ do { \ const int __step = 50; \ const int __timeout = 5000; \ if (!(__expr)) { \ QTest::qWait(0); \ } \ for (int __i = 0; __i < __timeout && !(__expr); __i+=__step) { \ do { code } while(0); \ QTest::qWait(__step); \ } \ } while(0) #define QCONTACTMANAGER_REMOVE_VERSIONS_FROM_URI(params) params.remove(QString::fromLatin1(QTCONTACTS_VERSION_NAME)); \ params.remove(QString::fromLatin1(QTCONTACTS_IMPLEMENTATION_VERSION_NAME)) QTCONTACTS_USE_NAMESPACE Q_DECLARE_METATYPE(QList) void registerIdType() { qRegisterMetaType("QContactId"); qRegisterMetaType >("QList"); } const char *collectionsAddedSignal = SIGNAL(collectionsAdded(QList)); const char *collectionsChangedSignal = SIGNAL(collectionsChanged(QList)); const char *collectionsRemovedSignal = SIGNAL(collectionsRemoved(QList)); const char *contactsAddedSignal = SIGNAL(contactsAdded(QList)); const char *contactsChangedSignal = SIGNAL(contactsChanged(QList, QList)); const char *contactsPresenceChangedSignal = SIGNAL(contactsPresenceChanged(QList)); const char *contactsRemovedSignal = SIGNAL(contactsRemoved(QList)); const char *relationshipsAddedSignal = SIGNAL(relationshipsAdded(QList)); const char *relationshipsRemovedSignal = SIGNAL(relationshipsRemoved(QList)); const char *selfContactIdChangedSignal = SIGNAL(selfContactIdChanged(QContactId,QContactId)); const QContactId &retrievalId(const QContactId &id) { return id; } QContactId retrievalId(const QContact &contact) { return retrievalId(contact.id()); } QContactId removalId(const QContact &contact) { return retrievalId(contact); } typedef QList DetailList; DetailList::value_type detailType(const QContactDetail &detail) { return detail.type(); } template DetailList::value_type detailType() { return T::Type; } QString detailTypeName(const QContactDetail &detail) { // We could create the table to print this, but I'm not bothering now... return QString::number(detail.type()); } bool validDetailType(QContactDetail::DetailType type) { return (type != QContactDetail::TypeUndefined); } bool validDetailType(const QContactDetail &detail) { return validDetailType(detail.type()); } typedef QMap DetailMap; DetailMap detailValues(const QContactDetail &detail, bool includeProvenance = true) { DetailMap rv(detail.values()); if (!includeProvenance) { DetailMap::iterator it = rv.begin(); while (it != rv.end()) { if (it.key() == QContactDetail::FieldProvenance) { it = rv.erase(it); } else { ++it; } } } return rv; } bool validContactType(const QContact &contact) { return (contact.type() == QContactType::TypeContact); } template void setFilterDetail(QContactDetailFilter &filter, F field) { filter.setDetailType(T::Type, field); } template void setFilterDetail(QContactDetailFilter &filter, T type, F field) { filter.setDetailType(type, field); } template void setFilterDetail(QContactDetailRangeFilter &filter, F field) { filter.setDetailType(T::Type, field); } template void setFilterDetail(QContactDetailRangeFilter &filter, T type, F field) { filter.setDetailType(type, field); } template void setFilterDetail(QContactDetailFilter &filter) { filter.setDetailType(T::Type); } template void setFilterValue(QContactDetailFilter &filter, T value) { filter.setValue(value); } template void setSortDetail(QContactSortOrder &sort, F field) { sort.setDetailType(T::Type, field); } template void setSortDetail(QContactSortOrder &sort, T type, F field) { sort.setDetailType(type, field); } template QString relationshipString(F fn) { return fn(); } template void setFilterType(QContactRelationshipFilter &filter, T type) { filter.setRelationshipType(relationshipString(type)); } void setFilterContactId(QContactRelationshipFilter &filter, const QContactId &contactId) { filter.setRelatedContactId(contactId); } QContactRelationship makeRelationship(const QContactId &firstId, const QContactId &secondId) { QContactRelationship relationship; relationship.setFirst(firstId); relationship.setSecond(secondId); return relationship; } template QContactRelationship makeRelationship(T type, const QContactId &firstId, const QContactId &secondId) { QContactRelationship relationship(makeRelationship(firstId, secondId)); relationship.setRelationshipType(relationshipString(type)); return relationship; } QContactRelationship makeRelationship(const QString &type, const QContactId &firstId, const QContactId &secondId) { QContactRelationship relationship(makeRelationship(firstId, secondId)); relationship.setRelationshipType(type); return relationship; } const QContact &relatedContact(const QContact &contact) { return contact; } QContactId relatedContactId(const QContact &contact) { return contact.id(); } QList relatedContactIds(const QList &contacts) { QList rv; foreach (const QContact &contact, contacts) { rv.append(contact.id()); } return rv; } #endif