pax_global_header00006660000000000000000000000064147576175700014535gustar00rootroot0000000000000052 comment=cb04d167c87d86b6cdc34dd4c3282c7b8be254ee nemo-qml-plugin-contacts-0.3.32/000077500000000000000000000000001475761757000164575ustar00rootroot00000000000000nemo-qml-plugin-contacts-0.3.32/.gitignore000066400000000000000000000002241475761757000204450ustar00rootroot00000000000000Makefile /RPMS documentation.list *.o *.so *.so.* *.moc moc_* /translations/nemo-qml-plugin-contacts.ts /translations/nemo-qml-plugin-contacts_*.qm nemo-qml-plugin-contacts-0.3.32/LICENSE.BSD000066400000000000000000000026531475761757000201010ustar00rootroot00000000000000Redistribution 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. nemo-qml-plugin-contacts-0.3.32/config.pri000066400000000000000000000002271475761757000204410ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += Qt$${QT_MAJOR_VERSION}Contacts Qt$${QT_MAJOR_VERSION}Versit qtcontacts-sqlite-qt$${QT_MAJOR_VERSION}-extensions nemo-qml-plugin-contacts-0.3.32/contacts.pro000066400000000000000000000002631475761757000210200ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = lib src tools tests translations doc src.depends = lib tools.depends = src tests.depends = src OTHER_FILES += rpm/nemo-qml-plugin-contacts-qt5.spec nemo-qml-plugin-contacts-0.3.32/doc/000077500000000000000000000000001475761757000172245ustar00rootroot00000000000000nemo-qml-plugin-contacts-0.3.32/doc/doc.pro000066400000000000000000000005111475761757000205100ustar00rootroot00000000000000TEMPLATE = aux CONFIG += sailfish-qdoc-template SAILFISH_QDOC.project = nemo-qml-plugin-contacts SAILFISH_QDOC.config = nemo-qml-plugin-contacts.qdocconf SAILFISH_QDOC.style = offline SAILFISH_QDOC.path = /usr/share/doc/nemo-qml-plugin-contacts OTHER_FILES += $$PWD/sailfish-contacts.qdocconf \ $$PWD/index.qdoc nemo-qml-plugin-contacts-0.3.32/doc/index.qdoc000066400000000000000000000004411475761757000212020ustar00rootroot00000000000000/*! \qmlmodule org.nemomobile.contacts 1.0 \title org.nemomobile.contacts QML Types This module includes the following types: */ /*! \page index.html \title Nemo QML Plugin Contacts This module includes the following types: \generatelist{qmltypesbymodule org.nemomobile.contacts} */ nemo-qml-plugin-contacts-0.3.32/doc/nemo-qml-plugin-contacts.qdocconf000066400000000000000000000035411475761757000256020ustar00rootroot00000000000000project = nemo-qml-plugin-contacts description = Nemo QML Plugin Contacts Reference Documentation versionsym = version = 0.3 url = $BASE_URL/nemo-qml-plugin-contacts sourcedirs += $$PWD/../src $$PWD/../doc headerdirs += $$PWD/../src qhp.projects = NemoQmlPluginContacts outputformats = HTML outputdir = $$PWD/html base = file:$$PWD/html tagfile = $$PWD/html/nemo-qml-plugin-contacts.tags qhp.NemoQmlPluginContacts.file = nemo-qml-plugin-contacts.qhp qhp.NemoQmlPluginContacts.namespace = org.nemomobile.contacts.0.3 qhp.NemoQmlPluginContacts.virtualFolder = nemo-qml-plugin-contacts qhp.NemoQmlPluginContacts.indexTitle = Nemo QML Plugin Contacts qhp.NemoQmlPluginContacts.indexRoot = qhp.NemoQmlPluginContacts.filterAttributes = nemo-qml-plugin-contacts 0.3 qhp.NemoQmlPluginContacts.subprojects = qmltypes qhp.NemoQmlPluginContacts.subprojects.qmltypes.title = QML Types qhp.NemoQmlPluginContacts.subprojects.qmltypes.indexTitle = org.nemomobile.contacts QML Types qhp.NemoQmlPluginContacts.subprojects.qmltypes.selectors = qmlclass qhp.NemoQmlPluginContacts.subprojects.qmltypes.sortPages = true qhp.NemoQmlPluginContacts.customFilters.NemoQmlPluginContacts.name = org.nemomobile.contacts 0.3 qhp.NemoQmlPluginContacts.customFilters.NemoQmlPluginContacts.filterAttributes = nemo-qml-plugin-contacts 0.3 HTML.footer += \ "
\n" \ "

© 2023 Jolla Ltd.

\n" \ "

All other trademarks are property of their respective owners.

\n" \ "

\n" \ " This document may be used under the terms of the " \ " GNU Free Documentation License version 1.3" \ " as published by the Free Software Foundation." \ "

\n" \ "
\n" navigation.homepage = "Nemo QML Plugin Contacts" nemo-qml-plugin-contacts-0.3.32/lib/000077500000000000000000000000001475761757000172255ustar00rootroot00000000000000nemo-qml-plugin-contacts-0.3.32/lib/cacheconfiguration.cpp000066400000000000000000000213461475761757000235720ustar00rootroot00000000000000/* * Copyright (c) 2014 - 2020 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 "cacheconfiguration.h" #include #ifdef HAS_MLITE #include #endif #ifdef HAS_QGSETTINGS #include #endif class CacheConfigurationPrivate : public QObject { Q_OBJECT public: CacheConfigurationPrivate(CacheConfiguration *q); CacheConfiguration *q_ptr; CacheConfiguration::DisplayLabelOrder m_displayLabelOrder; QString m_sortProperty; QString m_groupProperty; #ifdef HAS_MLITE MGConfItem m_displayLabelOrderConf; MGConfItem m_sortPropertyConf; MGConfItem m_groupPropertyConf; void onDisplayLabelOrderChanged(); void onSortPropertyChanged(); void onGroupPropertyChanged(); #endif #ifdef HAS_QGSETTINGS QGSettings *m_propertyConf; void onConfigPropertyChanged(const QString &key); #endif // HAS_QGSETTINGS }; CacheConfigurationPrivate::CacheConfigurationPrivate(CacheConfiguration *q) : q_ptr(q) , m_displayLabelOrder(CacheConfiguration::FirstNameFirst) , m_sortProperty(QString::fromLatin1("firstName")) , m_groupProperty(QString::fromLatin1("firstName")) #ifdef HAS_MLITE , m_displayLabelOrderConf(QLatin1String("/org/nemomobile/contacts/display_label_order")) , m_sortPropertyConf(QLatin1String("/org/nemomobile/contacts/sort_property")) , m_groupPropertyConf(QLatin1String("/org/nemomobile/contacts/group_property")) #endif #ifdef HAS_QGSETTINGS , m_propertyConf(new QGSettings("org.nemomobile.contacts", "/org/nemomobile/contacts/")) #endif // HAS_QGSETTINGS { #ifdef HAS_MLITE connect(&m_displayLabelOrderConf, &MGConfItem::valueChanged, this, &CacheConfigurationPrivate::onDisplayLabelOrderChanged); QVariant displayLabelOrder = m_displayLabelOrderConf.value(); if (displayLabelOrder.isValid()) m_displayLabelOrder = static_cast(displayLabelOrder.toInt()); connect(&m_sortPropertyConf, &MGConfItem::valueChanged, this, &CacheConfigurationPrivate::onSortPropertyChanged); QVariant sortPropertyConf = m_sortPropertyConf.value(); if (sortPropertyConf.isValid()) m_sortProperty = sortPropertyConf.toString(); connect(&m_groupPropertyConf, &MGConfItem::valueChanged, this, &CacheConfigurationPrivate::onGroupPropertyChanged); QVariant groupPropertyConf = m_groupPropertyConf.value(); if (groupPropertyConf.isValid()) m_groupProperty = groupPropertyConf.toString(); #endif #ifdef HAS_QGSETTINGS connect(m_propertyConf, &QGSettings::changed, this, &CacheConfigurationPrivate::onConfigPropertyChanged); QVariant displayLabelOrder = m_propertyConf->get(QStringLiteral("display-label-order")); if (displayLabelOrder.isValid()) m_displayLabelOrder = static_cast(displayLabelOrder.toInt()); QVariant sortPropertyConf = m_propertyConf->get(QStringLiteral("sort-property")); if (sortPropertyConf.isValid()) m_sortProperty = sortPropertyConf.toString(); QVariant groupPropertyConf = m_propertyConf->get(QStringLiteral("group-property")); if (groupPropertyConf.isValid()) m_groupProperty = groupPropertyConf.toString(); #endif } #ifdef HAS_MLITE void CacheConfigurationPrivate::onDisplayLabelOrderChanged() { QVariant displayLabelOrder = m_displayLabelOrderConf.value(); if (displayLabelOrder.isValid() && displayLabelOrder.toInt() != m_displayLabelOrder) { m_displayLabelOrder = static_cast(displayLabelOrder.toInt()); emit q_ptr->displayLabelOrderChanged(m_displayLabelOrder); } } void CacheConfigurationPrivate::onSortPropertyChanged() { QVariant sortProperty = m_sortPropertyConf.value(); if (sortProperty.isValid() && sortProperty.toString() != m_sortProperty) { const QString newProperty(sortProperty.toString()); if ((newProperty != QString::fromLatin1("firstName")) && (newProperty != QString::fromLatin1("lastName"))) { qWarning() << "Invalid sort property configuration:" << newProperty; return; } m_sortProperty = newProperty; emit q_ptr->sortPropertyChanged(m_sortProperty); } } void CacheConfigurationPrivate::onGroupPropertyChanged() { QVariant groupProperty = m_groupPropertyConf.value(); if (groupProperty.isValid() && groupProperty.toString() != m_groupProperty) { const QString newProperty(groupProperty.toString()); if ((newProperty != QString::fromLatin1("firstName")) && (newProperty != QString::fromLatin1("lastName"))) { qWarning() << "Invalid group property configuration:" << newProperty; return; } m_groupProperty = newProperty; emit q_ptr->groupPropertyChanged(m_groupProperty); } } #endif #ifdef HAS_QGSETTINGS void CacheConfigurationPrivate::onConfigPropertyChanged(const QString &key) { if (key == QLatin1String("display-label-order")) { QVariant displayLabelOrder = m_propertyConf->get(QStringLiteral("display-label-order")); if (displayLabelOrder.isValid() && displayLabelOrder.toInt() != m_displayLabelOrder) { m_displayLabelOrder = static_cast(displayLabelOrder.toInt()); emit q_ptr->displayLabelOrderChanged(m_displayLabelOrder); } } else if (key == QLatin1String("sort-property")) { QVariant sortProperty = m_propertyConf->get(QStringLiteral("sort-property")); if (sortProperty.isValid() && sortProperty.toString() != m_sortProperty) { const QString newProperty(sortProperty.toString()); if ((newProperty != QString::fromLatin1("firstName")) && (newProperty != QString::fromLatin1("lastName"))) { qWarning() << "Invalid sort property configuration:" << newProperty; return; } m_sortProperty = newProperty; emit q_ptr->sortPropertyChanged(m_sortProperty); } } else if (key == QLatin1String("group-property")) { QVariant groupProperty = m_propertyConf->get(QStringLiteral("group-property")); if (groupProperty.isValid() && groupProperty.toString() != m_groupProperty) { const QString newProperty(groupProperty.toString()); if ((newProperty != QString::fromLatin1("firstName")) && (newProperty != QString::fromLatin1("lastName"))) { qWarning() << "Invalid group property configuration:" << newProperty; return; } m_groupProperty = newProperty; emit q_ptr->groupPropertyChanged(m_groupProperty); } } } #endif CacheConfiguration::CacheConfiguration() : d_ptr(new CacheConfigurationPrivate(this)) { } CacheConfiguration::~CacheConfiguration() { delete d_ptr; } CacheConfiguration::DisplayLabelOrder CacheConfiguration::displayLabelOrder() const { return d_ptr->m_displayLabelOrder; } QString CacheConfiguration::sortProperty() const { return d_ptr->m_sortProperty; } QString CacheConfiguration::groupProperty() const { return d_ptr->m_groupProperty; } #include "cacheconfiguration.moc" nemo-qml-plugin-contacts-0.3.32/lib/cacheconfiguration.h000066400000000000000000000047371475761757000232440ustar00rootroot00000000000000/* * Copyright (c) 2014 - 2020 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 SEASIDE_CACHE_CONFIGURATION_H #define SEASIDE_CACHE_CONFIGURATION_H #include "contactcacheexport.h" #include #include class CacheConfigurationPrivate; class CONTACTCACHE_EXPORT CacheConfiguration : public QObject { Q_OBJECT public: enum DisplayLabelOrder { FirstNameFirst = 0, LastNameFirst }; CacheConfiguration(); virtual ~CacheConfiguration(); DisplayLabelOrder displayLabelOrder() const; QString sortProperty() const; QString groupProperty() const; signals: void displayLabelOrderChanged(CacheConfiguration::DisplayLabelOrder order); void sortPropertyChanged(const QString &sortProperty); void groupPropertyChanged(const QString &groupProperty); private: CacheConfigurationPrivate *d_ptr; }; #endif // SEASIDE_CACHE_CONFIGURATION_H nemo-qml-plugin-contacts-0.3.32/lib/contactcacheexport.h000066400000000000000000000035541475761757000232660ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 CONTACTCACHEEXPORT_H #define CONTACTCACHEEXPORT_H #include #ifdef CONTACTCACHE_BUILD #define CONTACTCACHE_EXPORT Q_DECL_EXPORT #else #define CONTACTCACHE_EXPORT Q_DECL_IMPORT #endif #endif nemo-qml-plugin-contacts-0.3.32/lib/lib.pri000066400000000000000000000030531475761757000205100ustar00rootroot00000000000000include(../config.pri) CONFIG += qt hide_symbols CONFIG += link_pkgconfig packagesExist(mlite$${QT_MAJOR_VERSION}) { PKGCONFIG += mlite$${QT_MAJOR_VERSION} DEFINES += HAS_MLITE } else { packagesExist(gsettings-qt) { message("use of gsettings") PKGCONFIG += gsettings-qt DEFINES += HAS_QGSETTINGS } else { warning("Neither mlite nor gsettings available. Some functionality may not work as expected.") } } packagesExist(mce) { PKGCONFIG += mce DEFINES += HAS_MCE } else { warning("mce not available. Some functionality may not work as expected.") } PKGCONFIG += mlocale$${QT_MAJOR_VERSION} LIBS += -lphonenumber # We need access to QtContacts private headers QT += contacts-private # We need the moc output for ContactManagerEngine from sqlite-extensions extensionsIncludePath = $$system(pkg-config --cflags-only-I qtcontacts-sqlite-qt$${QT_MAJOR_VERSION}-extensions) VPATH += $$replace(extensionsIncludePath, -I, ) HEADERS += \ contactmanagerengine.h \ qcontactclearchangeflagsrequest.h SOURCES += \ $$PWD/cacheconfiguration.cpp \ $$PWD/seasidecache.cpp \ $$PWD/seasideexport.cpp \ $$PWD/seasideimport.cpp \ $$PWD/seasidecontactbuilder.cpp \ $$PWD/seasidepropertyhandler.cpp PUBLIC_HEADERS = \ $$PWD/cacheconfiguration.h \ $$PWD/contactcacheexport.h \ $$PWD/seasidecache.h \ $$PWD/seasideexport.h \ $$PWD/seasideimport.h \ $$PWD/seasidecontactbuilder.h \ $$PWD/synchronizelists.h \ $$PWD/seasidepropertyhandler.h HEADERS += $$PUBLIC_HEADERS nemo-qml-plugin-contacts-0.3.32/lib/lib.pro000066400000000000000000000015001475761757000205110ustar00rootroot00000000000000include(lib.pri) TEMPLATE = lib # 'contacts' is too generic for the target name - use 'contactcache' TARGET = contactcache-qt$${QT_MAJOR_VERSION} target.path = $$[QT_INSTALL_LIBS] INSTALLS += target DEFINES += CONTACTCACHE_BUILD CONFIG += create_pc create_prl no_install_prl QT -= gui QT += core dbus develheaders.path = /usr/include/$$TARGET develheaders.files = $$PUBLIC_HEADERS QMAKE_PKGCONFIG_NAME = $$TARGET QMAKE_PKGCONFIG_DESCRIPTION = Sailfish OS contact cache library QMAKE_PKGCONFIG_LIBDIR = $$target.path QMAKE_PKGCONFIG_INCDIR = $$develheaders.path QMAKE_PKGCONFIG_DESTDIR = pkgconfig QMAKE_PKGCONFIG_REQUIRES = Qt$${QT_MAJOR_VERSION}Core Qt$${QT_MAJOR_VERSION}Contacts Qt$${QT_MAJOR_VERSION}Versit qtcontacts-sqlite-qt$${QT_MAJOR_VERSION}-extensions QMAKE_PKGCONFIG_VERSION = $$VERSION INSTALLS += develheaders nemo-qml-plugin-contacts-0.3.32/lib/seasidecache.cpp000066400000000000000000004021261475761757000223370ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 "seasidecache.h" #include "synchronizelists.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAS_MCE #include #include #endif #include QTVERSIT_USE_NAMESPACE namespace { Q_GLOBAL_STATIC(CacheConfiguration, cacheConfig) ML10N::MLocale mLocale; const QString aggregateRelationshipType = QContactRelationship::Aggregates(); const QString isNotRelationshipType = QString::fromLatin1("IsNot"); // Find the script that all letters in the name belong to, else yield Unknown QChar::Script nameScript(const QString &name) { QChar::Script script(QChar::Script_Unknown); if (!name.isEmpty()) { QString::const_iterator it = name.begin(), end = name.end(); for ( ; it != end; ++it) { const QChar::Category charCategory((*it).category()); if (charCategory >= QChar::Letter_Uppercase && charCategory <= QChar::Letter_Other) { const QChar::Script charScript((*it).script()); if (script == QChar::Script_Unknown) { script = charScript; } else if (charScript != script) { return QChar::Script_Unknown; } } } } return script; } QChar::Script nameScript(const QString &firstName, const QString &lastName) { if (firstName.isEmpty()) { return nameScript(lastName); } else if (lastName.isEmpty()) { return nameScript(firstName); } QChar::Script firstScript(nameScript(firstName)); if (firstScript != QChar::Script_Unknown) { QChar::Script lastScript(nameScript(lastName)); if (lastScript == firstScript) { return lastScript; } } return QChar::Script_Unknown; } bool nameScriptImpliesFamilyFirst(const QString &firstName, const QString &lastName) { switch (nameScript(firstName, lastName)) { // These scripts are used by cultures that conform to the family-name-first nameing convention: case QChar::Script_Han: case QChar::Script_Lao: case QChar::Script_Hangul: case QChar::Script_Khmer: case QChar::Script_Mongolian: case QChar::Script_Hiragana: case QChar::Script_Katakana: case QChar::Script_Bopomofo: case QChar::Script_Yi: return true; default: return false; } } QString managerName() { return QString::fromLatin1("org.nemomobile.contacts.sqlite"); } QMap managerParameters() { QMap rv; // Report presence changes independently from other contact changes rv.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("false")); if (!qgetenv("LIBCONTACTS_TEST_MODE").isEmpty()) { rv.insert(QString::fromLatin1("autoTest"), QString::fromLatin1("true")); } return rv; } Q_GLOBAL_STATIC_WITH_ARGS(QContactManager, manager, (managerName(), managerParameters())) typedef QList DetailList; template QContactDetail::DetailType detailType() { return T::Type; } QContactDetail::DetailType detailType(const QContactDetail &detail) { return detail.type(); } template void setDetailType(Filter &filter, Field field) { filter.setDetailType(T::Type, field); } DetailList detailTypesHint(const QContactFetchHint &hint) { return hint.detailTypesHint(); } void setDetailTypesHint(QContactFetchHint &hint, const DetailList &types) { hint.setDetailTypesHint(types); } QContactFetchHint basicFetchHint() { QContactFetchHint fetchHint; // We generally have no use for these things: fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships | QContactFetchHint::NoActionPreferences | QContactFetchHint::NoBinaryBlobs); return fetchHint; } QContactFetchHint presenceFetchHint() { QContactFetchHint fetchHint(basicFetchHint()); setDetailTypesHint(fetchHint, DetailList() << detailType() << detailType() << detailType()); return fetchHint; } DetailList displayDetails() { DetailList types; types << detailType() << detailType() << detailType(); return types; } DetailList contactsTableDetails() { DetailList types; // These details are reported in every query types << detailType() << detailType(); return types; } QContactFetchHint metadataFetchHint(quint32 fetchTypes = 0) { QContactFetchHint fetchHint(basicFetchHint()); // Include all detail types which come from the main contacts table DetailList types(contactsTableDetails()); // Include common details used for display purposes // (including nickname, as some contacts have no other name) types << displayDetails(); if (fetchTypes & SeasideCache::FetchAccountUri) { types << detailType(); } if (fetchTypes & SeasideCache::FetchPhoneNumber) { types << detailType(); } if (fetchTypes & SeasideCache::FetchEmailAddress) { types << detailType(); } if (fetchTypes & SeasideCache::FetchOrganization) { types << detailType(); } if (fetchTypes & SeasideCache::FetchAvatar) { types << detailType(); } if (fetchTypes & SeasideCache::FetchFavorite) { types << detailType(); } if (fetchTypes & SeasideCache::FetchGender) { types << detailType(); } setDetailTypesHint(fetchHint, types); return fetchHint; } QContactFetchHint onlineFetchHint(quint32 fetchTypes = 0) { QContactFetchHint fetchHint(metadataFetchHint(fetchTypes)); // We also need global presence state setDetailTypesHint(fetchHint, detailTypesHint(fetchHint) << detailType()); return fetchHint; } QContactFetchHint favoriteFetchHint(quint32 fetchTypes = 0) { // We also need avatar info return onlineFetchHint(fetchTypes | SeasideCache::FetchAvatar | SeasideCache::FetchFavorite); } QContactFetchHint extendedMetadataFetchHint(quint32 fetchTypes) { QContactFetchHint fetchHint(basicFetchHint()); DetailList types; // Only query for the specific types we need if (fetchTypes & SeasideCache::FetchAccountUri) { types << detailType(); } if (fetchTypes & SeasideCache::FetchPhoneNumber) { types << detailType(); } if (fetchTypes & SeasideCache::FetchEmailAddress) { types << detailType(); } if (fetchTypes & SeasideCache::FetchOrganization) { types << detailType(); } if (fetchTypes & SeasideCache::FetchAvatar) { types << detailType(); } if (fetchTypes & SeasideCache::FetchFavorite) { types << detailType(); } if (fetchTypes & SeasideCache::FetchGender) { types << detailType(); } setDetailTypesHint(fetchHint, types); return fetchHint; } QContactFilter allFilter() { return QContactFilter(); } QContactFilter favoriteFilter() { return QContactFavorite::match(); } typedef QPair StringPair; QList addressPairs(const QContactPhoneNumber &phoneNumber) { QList rv; const QString normalized(SeasideCache::normalizePhoneNumber(phoneNumber.number())); if (!normalized.isEmpty()) { const QChar plus(QChar::fromLatin1('+')); if (normalized.startsWith(plus)) { // Also index the complete form of this number rv.append(qMakePair(QString(), normalized)); } // Always index the minimized form of the number const QString minimized(SeasideCache::minimizePhoneNumber(normalized)); rv.append(qMakePair(QString(), minimized)); } return rv; } StringPair addressPair(const QContactEmailAddress &emailAddress) { return qMakePair(emailAddress.emailAddress().toLower(), QString()); } StringPair addressPair(const QContactOnlineAccount &account) { StringPair address = qMakePair(account.value(QContactOnlineAccount__FieldAccountPath), account.accountUri().toLower()); return !address.first.isEmpty() && !address.second.isEmpty() ? address : StringPair(); } bool validAddressPair(const StringPair &address) { return (!address.first.isEmpty() || !address.second.isEmpty()); } QList internalIds(const QList &ids) { QList rv; rv.reserve(ids.count()); foreach (const QContactId &id, ids) { rv.append(SeasideCache::internalId(id)); } return rv; } QString::const_iterator firstDtmfChar(QString::const_iterator it, QString::const_iterator end) { static const QString dtmfChars(QString::fromLatin1("pPwWxX#*")); for ( ; it != end; ++it) { if (dtmfChars.contains(*it)) return it; } return end; } const int ExactMatch = 100; int matchLength(const QString &lhs, const QString &rhs) { if (lhs.isEmpty() || rhs.isEmpty()) return 0; QString::const_iterator lbegin = lhs.constBegin(), lend = lhs.constEnd(); QString::const_iterator rbegin = rhs.constBegin(), rend = rhs.constEnd(); // Do these numbers contain DTMF elements? QString::const_iterator ldtmf = firstDtmfChar(lbegin, lend); QString::const_iterator rdtmf = firstDtmfChar(rbegin, rend); QString::const_iterator lit, rit; bool processDtmf = false; int matchLength = 0; if ((ldtmf != lbegin) && (rdtmf != rbegin)) { // Start match length calculation at the last non-DTMF digit lit = ldtmf - 1; rit = rdtmf - 1; while (*lit == *rit) { ++matchLength; --lit; --rit; if ((lit == lbegin) || (rit == rbegin)) { if (*lit == *rit) { ++matchLength; if ((lit == lbegin) && (rit == rbegin)) { // We have a complete, exact match - this must be the best match return ExactMatch; } else { // We matched all of one number - continue looking in the DTMF part processDtmf = true; } } break; } } } else { // Process the DTMF section for a match processDtmf = true; } // Have we got a match? if ((matchLength >= QtContactsSqliteExtensions::DefaultMaximumPhoneNumberCharacters) || processDtmf) { // See if the match continues into the DTMF area QString::const_iterator lit = ldtmf; QString::const_iterator rit = rdtmf; for ( ; (lit != lend) && (rit != rend); ++lit, ++rit) { if ((*lit).toLower() != (*rit).toLower()) break; ++matchLength; } } return matchLength; } int bestPhoneNumberMatchLength(const QContact &contact, const QString &match) { int bestMatchLength = 0; foreach (const QContactPhoneNumber& phone, contact.details()) { bestMatchLength = qMax(bestMatchLength, matchLength(SeasideCache::normalizePhoneNumber(phone.number()), match)); if (bestMatchLength == ExactMatch) { return ExactMatch; } } return bestMatchLength; } } SeasideCache *SeasideCache::instancePtr = 0; int SeasideCache::contactDisplayLabelGroupCount = 0; QStringList SeasideCache::allContactDisplayLabelGroups = QStringList(); QTranslator *SeasideCache::engEnTranslator = 0; QTranslator *SeasideCache::translator = 0; QContactManager* SeasideCache::manager() { return ::manager(); } SeasideCache* SeasideCache::instance() { if (!instancePtr) { instancePtr = new SeasideCache; } return instancePtr; } QContactId SeasideCache::apiId(const QContact &contact) { return contact.id(); } QContactId SeasideCache::apiId(quint32 iid) { return QtContactsSqliteExtensions::apiContactId(iid, manager()->managerUri()); } bool SeasideCache::validId(const QContactId &id) { return !id.isNull(); } quint32 SeasideCache::internalId(const QContact &contact) { return internalId(contact.id()); } quint32 SeasideCache::internalId(const QContactId &id) { return QtContactsSqliteExtensions::internalContactId(id); } SeasideCache::SeasideCache() : m_syncFilter(FilterNone) , m_populated(0) , m_cacheIndex(0) , m_queryIndex(0) , m_fetchProcessedCount(0) , m_fetchByIdProcessedCount(0) , m_keepPopulated(false) , m_populateProgress(Unpopulated) , m_populating(0) , m_fetchTypes(0) , m_extraFetchTypes(0) , m_dataTypesFetched(0) , m_updatesPending(false) , m_refreshRequired(false) , m_contactsUpdated(false) , m_displayOff(false) { m_timer.start(); m_fetchPostponed.invalidate(); CacheConfiguration *config(cacheConfig()); connect(config, &CacheConfiguration::displayLabelOrderChanged, this, &SeasideCache::displayLabelOrderChanged); connect(config, &CacheConfiguration::sortPropertyChanged, this, &SeasideCache::sortPropertyChanged); #ifdef HAS_MCE // Is this a GUI application? If so, we want to defer some processing when the display is off if (qApp && qApp->property("applicationDisplayName").isValid()) { // Only QGuiApplication has this property if (!QDBusConnection::systemBus().connect(MCE_SERVICE, MCE_SIGNAL_PATH, MCE_SIGNAL_IF, MCE_DISPLAY_SIG, this, SLOT(displayStatusChanged(QString)))) { qWarning() << "Unable to connect to MCE displayStatusChanged signal"; } } #endif QContactManager *mgr(manager()); // The contactsPresenceChanged signal is not exported by QContactManager, so we // need to find it from the manager's engine object typedef QtContactsSqliteExtensions::ContactManagerEngine EngineType; EngineType *cme = dynamic_cast(QContactManagerData::managerData(mgr)->m_engine); if (cme) { connect(cme, &EngineType::displayLabelGroupsChanged, this, &SeasideCache::displayLabelGroupsChanged); displayLabelGroupsChanged(cme->displayLabelGroups()); connect(cme, &EngineType::contactsPresenceChanged, this, &SeasideCache::contactsPresenceChanged); } else { qWarning() << "Unable to retrieve contact manager engine"; } connect(mgr, &QContactManager::dataChanged, this, &SeasideCache::dataChanged); connect(mgr, &QContactManager::contactsAdded, this, &SeasideCache::contactsAdded); connect(mgr, &QContactManager::contactsChanged, this, &SeasideCache::contactsChanged); connect(mgr, &QContactManager::contactsRemoved, this, &SeasideCache::contactsRemoved); connect(&m_fetchRequest, &QContactFetchRequest::resultsAvailable, this, &SeasideCache::contactsAvailable); connect(&m_fetchByIdRequest, &QContactFetchByIdRequest::resultsAvailable, this, &SeasideCache::contactsAvailable); connect(&m_contactIdRequest, &QContactIdFetchRequest::resultsAvailable, this, &SeasideCache::contactIdsAvailable); connect(&m_relationshipsFetchRequest, &QContactRelationshipFetchRequest::resultsAvailable, this, &SeasideCache::relationshipsAvailable); connect(&m_fetchRequest, &QContactFetchRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_fetchByIdRequest, &QContactFetchByIdRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_contactIdRequest, &QContactIdFetchRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_relationshipsFetchRequest, &QContactRelationshipFetchRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_clearChangeFlagsRequest, &QContactClearChangeFlagsRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_removeRequest, &QContactRemoveRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_saveRequest, &QContactSaveRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_relationshipSaveRequest, &QContactRelationshipSaveRequest::stateChanged, this, &SeasideCache::requestStateChanged); connect(&m_relationshipRemoveRequest, &QContactRelationshipRemoveRequest::stateChanged, this, &SeasideCache::requestStateChanged); m_fetchRequest.setManager(mgr); m_fetchByIdRequest.setManager(mgr); m_contactIdRequest.setManager(mgr); m_relationshipsFetchRequest.setManager(mgr); m_clearChangeFlagsRequest.setManager(mgr); m_removeRequest.setManager(mgr); m_saveRequest.setManager(mgr); m_relationshipSaveRequest.setManager(mgr); m_relationshipRemoveRequest.setManager(mgr); setSortOrder(sortProperty()); } SeasideCache::~SeasideCache() { if (instancePtr == this) instancePtr = 0; } void SeasideCache::checkForExpiry() { if (!instancePtr) return; if (instancePtr->m_users.isEmpty() && !QCoreApplication::closingDown()) { bool unused = true; for (int i = 0; i < FilterTypesCount; ++i) { unused &= instancePtr->m_models[i].isEmpty(); } if (unused) { instancePtr->m_expiryTimer.start(30000, instancePtr); } } } void SeasideCache::registerModel(ListModel *model, FilterType type, FetchDataType requiredTypes, FetchDataType extraTypes) { // Ensure the cache has been instantiated instance(); instancePtr->m_expiryTimer.stop(); for (int i = 0; i < FilterTypesCount; ++i) instancePtr->m_models[i].removeAll(model); instancePtr->m_models[type].append(model); instancePtr->keepPopulated(requiredTypes & SeasideCache::FetchTypesMask, extraTypes & SeasideCache::FetchTypesMask); if (requiredTypes & SeasideCache::FetchTypesMask) { // If we have filtered models, they will need a contact ID refresh after the cache is populated instancePtr->m_refreshRequired = true; } } void SeasideCache::unregisterModel(ListModel *model) { if (!instancePtr) return; for (int i = 0; i < FilterTypesCount; ++i) instancePtr->m_models[i].removeAll(model); checkForExpiry(); } void SeasideCache::registerUser(QObject *user) { // Ensure the cache has been instantiated instance(); instancePtr->m_expiryTimer.stop(); instancePtr->m_users.insert(user); } void SeasideCache::unregisterUser(QObject *user) { if (!instancePtr) return; instancePtr->m_users.remove(user); checkForExpiry(); } void SeasideCache::registerDisplayLabelGroupChangeListener(SeasideDisplayLabelGroupChangeListener *listener) { // Ensure the cache has been instantiated instance(); instancePtr->m_displayLabelGroupChangeListeners.append(listener); } void SeasideCache::unregisterDisplayLabelGroupChangeListener(SeasideDisplayLabelGroupChangeListener *listener) { if (!instancePtr) return; instancePtr->m_displayLabelGroupChangeListeners.removeAll(listener); } void SeasideCache::registerChangeListener(ChangeListener *listener, FetchDataType requiredTypes, FetchDataType extraTypes) { // Ensure the cache has been instantiated instance(); instancePtr->m_changeListeners.append(listener); instancePtr->keepPopulated(requiredTypes, extraTypes); } void SeasideCache::unregisterChangeListener(ChangeListener *listener) { if (!instancePtr) return; instancePtr->m_changeListeners.removeAll(listener); } void SeasideCache::unregisterResolveListener(ResolveListener *listener) { if (!instancePtr) return; QHash::iterator it = instancePtr->m_resolveAddresses.begin(); while (it != instancePtr->m_resolveAddresses.end()) { if (it.value().listener == listener) { it.key()->cancel(); delete it.key(); it = instancePtr->m_resolveAddresses.erase(it); } else { ++it; } } QList::iterator it2 = instancePtr->m_unknownAddresses.begin(); while (it2 != instancePtr->m_unknownAddresses.end()) { if (it2->listener == listener) { it2 = instancePtr->m_unknownAddresses.erase(it2); } else { ++it2; } } QList::iterator it3 = instancePtr->m_unknownResolveAddresses.begin(); while (it3 != instancePtr->m_unknownResolveAddresses.end()) { if (it3->listener == listener) { it3 = instancePtr->m_unknownResolveAddresses.erase(it3); } else { ++it3; } } QSet::iterator it4 = instancePtr->m_pendingResolve.begin(); while (it4 != instancePtr->m_pendingResolve.end()) { if (it4->listener == listener) { it4 = instancePtr->m_pendingResolve.erase(it4); } else { ++it4; } } } QString SeasideCache::displayLabelGroup(const CacheItem *cacheItem) { if (!cacheItem) return QString(); return cacheItem->displayLabelGroup; } QStringList SeasideCache::allDisplayLabelGroups() { // Ensure the cache has been instantiated instance(); return allContactDisplayLabelGroups; } QHash > SeasideCache::displayLabelGroupMembers() { if (instancePtr) return instancePtr->m_contactDisplayLabelGroups; return QHash >(); } SeasideCache::DisplayLabelOrder SeasideCache::displayLabelOrder() { return static_cast(cacheConfig()->displayLabelOrder()); } QString SeasideCache::sortProperty() { return cacheConfig()->sortProperty(); } QString SeasideCache::groupProperty() { return cacheConfig()->groupProperty(); } int SeasideCache::contactId(const QContact &contact) { quint32 internal = internalId(contact); return static_cast(internal); } int SeasideCache::contactId(const QContactId &contactId) { quint32 internal = internalId(contactId); return static_cast(internal); } SeasideCache::CacheItem *SeasideCache::itemById(const QContactId &id, bool requireComplete) { if (!validId(id)) return nullptr; // Ensure the cache has been instantiated instance(); quint32 iid = internalId(id); CacheItem *item = nullptr; QHash::iterator it = instancePtr->m_people.find(iid); if (it != instancePtr->m_people.end()) { item = &(*it); } else { // Insert a new item into the cache if the one doesn't exist. item = &(instancePtr->m_people[iid]); item->iid = iid; item->contactState = ContactAbsent; item->contact.setId(id); } if (requireComplete) { ensureCompletion(item); } return item; } SeasideCache::CacheItem *SeasideCache::itemById(int id, bool requireComplete) { if (id != 0) { QContactId contactId(apiId(static_cast(id))); if (!contactId.isNull()) { return itemById(contactId, requireComplete); } } return nullptr; } SeasideCache::CacheItem *SeasideCache::existingItem(const QContactId &id) { return existingItem(internalId(id)); } SeasideCache::CacheItem *SeasideCache::existingItem(quint32 iid) { // Ensure the cache has been instantiated instance(); QHash::iterator it = instancePtr->m_people.find(iid); return it != instancePtr->m_people.end() ? &(*it) : nullptr; } QContact SeasideCache::contactById(const QContactId &id) { // Ensure the cache has been instantiated instance(); quint32 iid = internalId(id); return instancePtr->m_people.value(iid, CacheItem()).contact; } void SeasideCache::ensureCompletion(CacheItem *cacheItem) { if (cacheItem->contactState < ContactRequested) { refreshContact(cacheItem); } } void SeasideCache::refreshContact(CacheItem *cacheItem) { // Ensure the cache has been instantiated instance(); cacheItem->contactState = ContactRequested; instancePtr->m_changedContacts.append(cacheItem->apiId()); instancePtr->fetchContacts(); } SeasideCache::CacheItem *SeasideCache::itemByPhoneNumber(const QString &number, bool requireComplete) { const QString normalized(normalizePhoneNumber(number)); if (normalized.isEmpty()) return 0; // Ensure the cache has been instantiated instance(); const QChar plus(QChar::fromLatin1('+')); if (normalized.startsWith(plus)) { // See if there is a match for the complete form of this number if (CacheItem *item = instancePtr->itemMatchingPhoneNumber(normalized, normalized, requireComplete)) { return item; } } const QString minimized(minimizePhoneNumber(normalized)); if (((instancePtr->m_fetchTypes & SeasideCache::FetchPhoneNumber) == 0) && !instancePtr->m_resolvedPhoneNumbers.contains(minimized)) { // We haven't previously queried this number, so there may be more matches than any // that we already have cached; return 0 to force a query return 0; } return instancePtr->itemMatchingPhoneNumber(minimized, normalized, requireComplete); } SeasideCache::CacheItem *SeasideCache::itemByEmailAddress(const QString &email, bool requireComplete) { if (email.trimmed().isEmpty()) return 0; // Ensure the cache has been instantiated instance(); QHash::const_iterator it = instancePtr->m_emailAddressIds.find(email.toLower()); if (it != instancePtr->m_emailAddressIds.end()) return itemById(*it, requireComplete); return 0; } SeasideCache::CacheItem *SeasideCache::itemByOnlineAccount(const QString &localUid, const QString &remoteUid, bool requireComplete) { if (localUid.trimmed().isEmpty() || remoteUid.trimmed().isEmpty()) return 0; // Ensure the cache has been instantiated instance(); QPair address = qMakePair(localUid, remoteUid.toLower()); QHash, quint32>::const_iterator it = instancePtr->m_onlineAccountIds.find(address); if (it != instancePtr->m_onlineAccountIds.end()) return itemById(*it, requireComplete); return 0; } SeasideCache::CacheItem *SeasideCache::resolvePhoneNumber(ResolveListener *listener, const QString &number, bool requireComplete) { // Ensure the cache has been instantiated instance(); CacheItem *item = itemByPhoneNumber(number, requireComplete); if (!item) { // Don't bother trying to resolve an invalid number const QString normalized(normalizePhoneNumber(number)); if (!normalized.isEmpty()) { instancePtr->resolveAddress(listener, QString(), number, requireComplete); } else { // Report this address is unknown ResolveData data; data.second = number; data.listener = listener; instancePtr->m_unknownResolveAddresses.append(data); instancePtr->requestUpdate(); } } else if (requireComplete) { ensureCompletion(item); } return item; } SeasideCache::CacheItem *SeasideCache::resolveEmailAddress(ResolveListener *listener, const QString &address, bool requireComplete) { // Ensure the cache has been instantiated instance(); CacheItem *item = itemByEmailAddress(address, requireComplete); if (!item) { instancePtr->resolveAddress(listener, address, QString(), requireComplete); } else if (requireComplete) { ensureCompletion(item); } return item; } SeasideCache::CacheItem *SeasideCache::resolveOnlineAccount(ResolveListener *listener, const QString &localUid, const QString &remoteUid, bool requireComplete) { // Ensure the cache has been instantiated instance(); CacheItem *item = itemByOnlineAccount(localUid, remoteUid, requireComplete); if (!item) { instancePtr->resolveAddress(listener, localUid, remoteUid, requireComplete); } else if (requireComplete) { ensureCompletion(item); } return item; } QContactId SeasideCache::selfContactId() { return manager()->selfContactId(); } void SeasideCache::requestUpdate() { if (!m_updatesPending) { QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); m_updatesPending = true; } } bool SeasideCache::saveContact(const QContact &contact) { return saveContacts(QList() << contact); } bool SeasideCache::saveContacts(const QList &contacts) { // Ensure the cache has been instantiated instance(); for (const QContact &contact : contacts) { const QContactId id = apiId(contact); if (validId(id)) { instancePtr->m_contactsToSave[contact.collectionId()][id] = contact; instancePtr->contactDataChanged(internalId(id)); } else { instancePtr->m_contactsToCreate.append(contact); } } instancePtr->requestUpdate(); instancePtr->updateSectionBucketIndexCaches(); return true; } void SeasideCache::contactDataChanged(quint32 iid) { instancePtr->contactDataChanged(iid, FilterFavorites); instancePtr->contactDataChanged(iid, FilterAll); } void SeasideCache::contactDataChanged(quint32 iid, FilterType filter) { int row = contactIndex(iid, filter); if (row != -1) { QList &models = m_models[filter]; for (int i = 0; i < models.count(); ++i) { models.at(i)->sourceDataChanged(row, row); } } } bool SeasideCache::removeContact(const QContact &contact) { return removeContacts(QList() << contact); } bool SeasideCache::removeContacts(const QList &contacts) { // Ensure the cache has been instantiated instance(); bool allSucceeded = true; QSet modifiedDisplayLabelGroups; for (const QContact &contact : contacts) { const QContactId id = apiId(contact); if (!validId(id)) { allSucceeded = false; continue; } if (contact.collectionId() == localCollectionId()) { instancePtr->m_localContactsToRemove.append(id); } instancePtr->m_contactsToRemove[contact.collectionId()].append(id); quint32 iid = internalId(id); instancePtr->removeContactData(iid, FilterFavorites); instancePtr->removeContactData(iid, FilterAll); const QString group(displayLabelGroup(existingItem(iid))); instancePtr->removeFromContactDisplayLabelGroup(iid, group, &modifiedDisplayLabelGroups); } instancePtr->notifyDisplayLabelGroupsChanged(modifiedDisplayLabelGroups); instancePtr->updateSectionBucketIndexCaches(); instancePtr->requestUpdate(); return allSucceeded; } void SeasideCache::removeContactData(quint32 iid, FilterType filter) { int row = contactIndex(iid, filter); if (row == -1) return; QList &models = m_models[filter]; for (int i = 0; i < models.count(); ++i) models.at(i)->sourceAboutToRemoveItems(row, row); m_contacts[filter].removeAt(row); for (int i = 0; i < models.count(); ++i) models.at(i)->sourceItemsRemoved(); } bool SeasideCache::fetchConstituents(const QContact &contact) { QContactId personId(contact.id()); if (!validId(personId)) return false; // Ensure the cache has been instantiated instance(); if (!instancePtr->m_contactsToFetchConstituents.contains(personId)) { instancePtr->m_contactsToFetchConstituents.append(personId); instancePtr->requestUpdate(); } return true; } bool SeasideCache::fetchMergeCandidates(const QContact &contact) { QContactId personId(contact.id()); if (!validId(personId)) return false; // Ensure the cache has been instantiated instance(); if (!instancePtr->m_contactsToFetchCandidates.contains(personId)) { instancePtr->m_contactsToFetchCandidates.append(personId); instancePtr->requestUpdate(); } return true; } const QList *SeasideCache::contacts(FilterType type) { // Ensure the cache has been instantiated instance(); return &instancePtr->m_contacts[type]; } bool SeasideCache::isPopulated(FilterType filterType) { if (!instancePtr) return false; return instancePtr->m_populated & (1 << filterType); } QString SeasideCache::getPrimaryName(const QContact &contact) { const QContactName nameDetail = contact.detail(); return primaryName(nameDetail.firstName(), nameDetail.lastName()); } QString SeasideCache::getSecondaryName(const QContact &contact) { const QContactName nameDetail = contact.detail(); return secondaryName(nameDetail.firstName(), nameDetail.lastName()); } QString SeasideCache::primaryName(const QString &firstName, const QString &lastName) { if (firstName.isEmpty() && lastName.isEmpty()) { return QString(); } const bool familyNameFirst(displayLabelOrder() == LastNameFirst || nameScriptImpliesFamilyFirst(firstName, lastName)); return familyNameFirst ? lastName : firstName; } QString SeasideCache::secondaryName(const QString &firstName, const QString &lastName) { const bool familyNameFirst(displayLabelOrder() == LastNameFirst || nameScriptImpliesFamilyFirst(firstName, lastName)); return familyNameFirst ? firstName : lastName; } static bool needsSpaceBetweenNames(const QString &first, const QString &second) { if (first.isEmpty() || second.isEmpty()) { return false; } return first[first.length()-1].script() != QChar::Script_Han || second[0].script() != QChar::Script_Han; } template void updateNameDetail(F1 getter, F2 setter, QContactName *nameDetail, const QString &value) { QString existing((nameDetail->*getter)()); if (!existing.isEmpty()) { existing.append(QChar::fromLatin1(' ')); } (nameDetail->*setter)(existing + value); } QString SeasideCache::placeholderDisplayLabel() { //: The display label for a contact which has no name or nickname. //% "(Unnamed)" return qtTrId("nemo_contacts-la-placeholder_display_label"); } void SeasideCache::decomposeDisplayLabel(const QString &formattedDisplayLabel, QContactName *nameDetail) { if (!translator) { engEnTranslator = new QTranslator(qApp); engEnTranslator->load(QString::fromLatin1("nemo-qml-plugin-contacts_eng_en"), QString::fromLatin1("/usr/share/translations")); qApp->installTranslator(engEnTranslator); translator = new QTranslator(qApp); translator->load(QLocale(), QString::fromLatin1("nemo-qml-plugin-contacts"), QString::fromLatin1("-"), QString::fromLatin1("/usr/share/translations")); qApp->installTranslator(translator); } // Try to parse the structure from the formatted name // TODO: Use MBreakIterator for localized splitting #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStringList tokens(formattedDisplayLabel.split(QChar::fromLatin1(' '), Qt::SkipEmptyParts)); #else QStringList tokens(formattedDisplayLabel.split(QChar::fromLatin1(' '), QString::SkipEmptyParts)); #endif if (tokens.count() >= 2) { QString format; if (tokens.count() == 2) { //: Format string for allocating 2 tokens to name parts - 2 characters from the set [FMLPS] //% "FL" format = qtTrId("nemo_contacts_name_structure_2_tokens"); } else if (tokens.count() == 3) { //: Format string for allocating 3 tokens to name parts - 3 characters from the set [FMLPS] //% "FML" format = qtTrId("nemo_contacts_name_structure_3_tokens"); } else if (tokens.count() > 3) { //: Format string for allocating 4 tokens to name parts - 4 characters from the set [FMLPS] //% "FFML" format = qtTrId("nemo_contacts_name_structure_4_tokens"); // Coalesce the leading tokens together to limit the possibilities int excess = tokens.count() - 4; if (excess > 0) { QString first(tokens.takeFirst()); while (--excess >= 0) { QString nextNamePart = tokens.takeFirst(); first += (needsSpaceBetweenNames(first, nextNamePart) ? QChar::fromLatin1(' ') : QString()) + nextNamePart; } tokens.prepend(first); } } if (format.length() != tokens.length()) { qWarning() << "Invalid structure format for" << tokens.count() << "tokens:" << format; } else { foreach (const QChar &part, format) { const QString token(tokens.takeFirst()); switch (part.toUpper().toLatin1()) { case 'F': updateNameDetail(&QContactName::firstName, &QContactName::setFirstName, nameDetail, token); break; case 'M': updateNameDetail(&QContactName::middleName, &QContactName::setMiddleName, nameDetail, token); break; case 'L': updateNameDetail(&QContactName::lastName, &QContactName::setLastName, nameDetail, token); break; case 'P': updateNameDetail(&QContactName::prefix, &QContactName::setPrefix, nameDetail, token); break; case 'S': updateNameDetail(&QContactName::suffix, &QContactName::setSuffix, nameDetail, token); break; default: qWarning() << "Invalid structure format character:" << part; } } } } } // small helper to avoid inconvenience QString SeasideCache::generateDisplayLabel(const QContact &contact, DisplayLabelOrder order, bool fallbackToNonNameDetails) { QString displayLabel = contact.detail().label(); if (!displayLabel.isEmpty()) { return displayLabel; } QContactName name = contact.detail(); QString nameStr1(name.firstName()); QString nameStr2(name.lastName()); const bool familyNameFirst(order == LastNameFirst || nameScriptImpliesFamilyFirst(nameStr1, nameStr2)); if (familyNameFirst) { nameStr1 = name.lastName(); nameStr2 = name.firstName(); } if (!nameStr1.isEmpty()) displayLabel.append(nameStr1); if (!nameStr2.isEmpty()) { if (needsSpaceBetweenNames(nameStr1, nameStr2)) { displayLabel.append(" "); } displayLabel.append(nameStr2); } if (!displayLabel.isEmpty() || !fallbackToNonNameDetails) { return displayLabel; } // Try to generate a label from the contact details, in our preferred order displayLabel = generateDisplayLabelFromNonNameDetails(contact); if (!displayLabel.isEmpty()) { return displayLabel; } return placeholderDisplayLabel(); } QString SeasideCache::generateDisplayLabelFromNonNameDetails(const QContact &contact) { foreach (const QContactNickname& nickname, contact.details()) { if (!nickname.nickname().isEmpty()) { return nickname.nickname(); } } foreach (const QContactGlobalPresence& gp, contact.details()) { // should only be one of these, but qtct is strange, and doesn't list it as a unique detail in the schema... if (!gp.nickname().isEmpty()) { return gp.nickname(); } } foreach (const QContactPresence& presence, contact.details()) { if (!presence.nickname().isEmpty()) { return presence.nickname(); } } // If this contact has organization details but no name, it probably represents that organization directly QContactOrganization company = contact.detail(); if (!company.name().isEmpty()) { return company.name(); } // If none of the detail fields provides a label, fallback to the backend's label string, in // preference to using any of the addressing details directly const QString displayLabel = contact.detail().label(); if (!displayLabel.isEmpty()) { return displayLabel; } foreach (const QContactOnlineAccount& account, contact.details()) { if (!account.accountUri().isEmpty()) { return account.accountUri(); } } foreach (const QContactEmailAddress& email, contact.details()) { if (!email.emailAddress().isEmpty()) { return email.emailAddress(); } } foreach (const QContactPhoneNumber& phone, contact.details()) { if (!phone.number().isEmpty()) return phone.number(); } return QString(); } static bool avatarUrlWithMetadata(const QContact &contact, QUrl &matchingUrl, const QString &metadataFragment = QString()) { static const QString coverMetadata(QString::fromLatin1("cover")); static const QString localMetadata(QString::fromLatin1("local")); static const QString fileScheme(QString::fromLatin1("file")); QList avatarDetails = contact.details(); QMap newestAvatarsMap; // Find the last modified avatar for each metadata type. for (int i = 0; i < avatarDetails.size(); ++i) { const QContactAvatar &av(avatarDetails[i]); const QString metadata(av.value(QContactAvatar::FieldMetaData).toString()); if (!metadataFragment.isEmpty() && !metadata.startsWith(metadataFragment)) { // this avatar doesn't match the metadata requirement. ignore it. continue; } auto latestAvatarIt = newestAvatarsMap.find(metadata); if (latestAvatarIt == newestAvatarsMap.end()) { newestAvatarsMap.insert(metadata, av); } else { if (av.value(QContactDetail__FieldModified).toDateTime() > latestAvatarIt.value().value(QContactDetail__FieldModified).toDateTime()) { latestAvatarIt.value() = av; } } } int fallbackScore = 0; QUrl fallbackUrl; // Select an appropriate avatar from the list of last modified avatars. QList latestAvatars = newestAvatarsMap.values(); for (int i = 0; i < latestAvatars.size(); ++i) { const QContactAvatar &av(latestAvatars[i]); const QString metadata(av.value(QContactAvatar::FieldMetaData).toString()); const QUrl avatarImageUrl = av.imageUrl(); if (metadata == localMetadata) { // We have a local avatar record - use the image it specifies matchingUrl = avatarImageUrl; return true; } else { // queue it as fallback if its score is better than the best fallback seen so far. // prefer local file system images over remote urls, and prefer normal avatars // over "cover" (background image) type avatars. const bool remote(!avatarImageUrl.scheme().isEmpty() && avatarImageUrl.scheme() != fileScheme); int score = remote ? 3 : 4; if (metadata == coverMetadata) { score -= 2; } if (score > fallbackScore) { fallbackUrl = avatarImageUrl; fallbackScore = score; } } } if (!fallbackUrl.isEmpty()) { matchingUrl = fallbackUrl; return true; } // no matching avatar image. return false; } QUrl SeasideCache::filteredAvatarUrl(const QContact &contact, const QStringList &metadataFragments) { QUrl matchingUrl; if (metadataFragments.isEmpty()) { if (avatarUrlWithMetadata(contact, matchingUrl)) { return matchingUrl; } } foreach (const QString &metadataFragment, metadataFragments) { if (avatarUrlWithMetadata(contact, matchingUrl, metadataFragment)) { return matchingUrl; } } return QUrl(); } bool SeasideCache::removeLocalAvatarFile(const QContact &contact, const QContactAvatar &avatar) { if (avatar.isEmpty() || contact.collectionId() != localCollectionId()) { return false; } const QString avatarPath = avatar.imageUrl().isLocalFile() ? avatar.imageUrl().toLocalFile() : avatar.imageUrl().toString(); // Check that the avatar is a system-generated file before deleting it, to avoid deleting // user-created files. static const QString dataPath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).value(0); static const QString avatarCachePath = QString("%1/data/avatars").arg(dataPath); static const QString avatarSystemPath = QString("%1/system").arg(dataPath); if (avatarPath.startsWith(avatarCachePath) || avatarPath.startsWith(avatarSystemPath)) { return QFile::remove(avatarPath); } return false; } QString SeasideCache::normalizePhoneNumber(const QString &input, bool validate) { QtContactsSqliteExtensions::NormalizePhoneNumberFlags normalizeFlags(QtContactsSqliteExtensions::KeepPhoneNumberDialString); if (validate) { // If the number if not valid, return empty normalizeFlags |= QtContactsSqliteExtensions::ValidatePhoneNumber; } return QtContactsSqliteExtensions::normalizePhoneNumber(input, normalizeFlags); } QString SeasideCache::minimizePhoneNumber(const QString &input, bool validate) { // TODO: use a configuration variable to make this configurable const int maxCharacters = QtContactsSqliteExtensions::DefaultMaximumPhoneNumberCharacters; QString validated(normalizePhoneNumber(input, validate)); if (validated.isEmpty()) return validated; return QtContactsSqliteExtensions::minimizePhoneNumber(validated, maxCharacters); } QContactCollectionId SeasideCache::aggregateCollectionId() { return QtContactsSqliteExtensions::aggregateCollectionId(manager()->managerUri()); } QContactCollectionId SeasideCache::localCollectionId() { return QtContactsSqliteExtensions::localCollectionId(manager()->managerUri()); } QContactFilter SeasideCache::filterForMergeCandidates(const QContact &contact) const { // Find any contacts that we might merge with the supplied contact QContactFilter rv; QContactName name(contact.detail()); const QString firstName(name.firstName().trimmed()); const QString lastName(name.lastName().trimmed()); if (firstName.isEmpty() && lastName.isEmpty()) { // Use the displayLabel to match with const QString label(contact.detail().label().trimmed()); if (!label.isEmpty()) { // Partial match to first name QContactDetailFilter firstNameFilter; setDetailType(firstNameFilter, QContactName::FieldFirstName); firstNameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); firstNameFilter.setValue(label); rv = rv | firstNameFilter; // Partial match to last name QContactDetailFilter lastNameFilter; setDetailType(lastNameFilter, QContactName::FieldLastName); lastNameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); lastNameFilter.setValue(label); rv = rv | lastNameFilter; // Partial match to nickname QContactDetailFilter nicknameFilter; setDetailType(nicknameFilter, QContactNickname::FieldNickname); nicknameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); nicknameFilter.setValue(label); rv = rv | nicknameFilter; } } else { if (!firstName.isEmpty()) { // Partial match to first name QContactDetailFilter nameFilter; setDetailType(nameFilter, QContactName::FieldFirstName); nameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); nameFilter.setValue(firstName); rv = rv | nameFilter; // Partial match to first name in the nickname QContactDetailFilter nicknameFilter; setDetailType(nicknameFilter, QContactNickname::FieldNickname); nicknameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); nicknameFilter.setValue(firstName); rv = rv | nicknameFilter; if (firstName.length() > 3) { // Also look for shortened forms of this name, such as 'Timothy' => 'Tim' QContactDetailFilter shortFilter; setDetailType(shortFilter, QContactName::FieldFirstName); shortFilter.setMatchFlags(QContactFilter::MatchStartsWith | QContactFilter::MatchFixedString); shortFilter.setValue(firstName.left(3)); rv = rv | shortFilter; } } if (!lastName.isEmpty()) { // Partial match to last name QContactDetailFilter nameFilter; setDetailType(nameFilter, QContactName::FieldLastName); nameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); nameFilter.setValue(lastName); rv = rv | nameFilter; // Partial match to last name in the nickname QContactDetailFilter nicknameFilter; setDetailType(nicknameFilter, QContactNickname::FieldNickname); nicknameFilter.setMatchFlags(QContactFilter::MatchContains | QContactFilter::MatchFixedString); nicknameFilter.setValue(lastName); rv = rv | nicknameFilter; } } // Phone number match foreach (const QContactPhoneNumber &phoneNumber, contact.details()) { const QString number(phoneNumber.number().trimmed()); if (number.isEmpty()) continue; rv = rv | QContactPhoneNumber::match(number); } // Email address match foreach (const QContactEmailAddress &emailAddress, contact.details()) { QString address(emailAddress.emailAddress().trimmed()); int index = address.indexOf(QChar::fromLatin1('@')); if (index > 0) { // Match any address that is the same up to the @ symbol address = address.left(index).trimmed(); } if (address.isEmpty()) continue; QContactDetailFilter filter; setDetailType(filter, QContactEmailAddress::FieldEmailAddress); filter.setMatchFlags((index > 0 ? QContactFilter::MatchStartsWith : QContactFilter::MatchExactly) | QContactFilter::MatchFixedString); filter.setValue(address); rv = rv | filter; } // Account URI match foreach (const QContactOnlineAccount &account, contact.details()) { QString uri(account.accountUri().trimmed()); int index = uri.indexOf(QChar::fromLatin1('@')); if (index > 0) { // Match any account URI that is the same up to the @ symbol uri = uri.left(index).trimmed(); } if (uri.isEmpty()) continue; QContactDetailFilter filter; setDetailType(filter, QContactOnlineAccount::FieldAccountUri); filter.setMatchFlags((index > 0 ? QContactFilter::MatchStartsWith : QContactFilter::MatchExactly) | QContactFilter::MatchFixedString); filter.setValue(uri); rv = rv | filter; } // If we know the contact gender rule out mismatches QContactGender gender(contact.detail()); if (gender.gender() != QContactGender::GenderUnspecified) { QContactDetailFilter matchFilter; setDetailType(matchFilter, QContactGender::FieldGender); matchFilter.setValue(gender.gender()); QContactDetailFilter unknownFilter; setDetailType(unknownFilter, QContactGender::FieldGender); unknownFilter.setValue(QContactGender::GenderUnspecified); rv = rv & (matchFilter | unknownFilter); } // Only return aggregate contact IDs return rv & SeasideCache::aggregateFilter(); } void SeasideCache::startRequest(bool *idleProcessing) { bool requestPending = false; // Test these conditions in priority order // Start by loading the favorites model, because it's so small and // the user is likely to want to interact with it. if (m_keepPopulated && (m_populateProgress == Unpopulated)) { if (m_fetchRequest.isActive()) { requestPending = true; } else { m_fetchRequest.setFilter(favoriteFilter()); m_fetchRequest.setFetchHint(favoriteFetchHint(m_fetchTypes)); m_fetchRequest.setSorting(m_sortOrder); qDebug() << "Starting favorites query at" << m_timer.elapsed() << "ms"; m_fetchRequest.start(); m_fetchProcessedCount = 0; m_populateProgress = FetchFavorites; m_dataTypesFetched |= m_fetchTypes; m_populating = true; } } const int maxPriorityIds = 20; // Next priority is refreshing small numbers of contacts, // because these likely came from UI elements calling ensureCompletion() if (!m_changedContacts.isEmpty() && m_changedContacts.count() < maxPriorityIds) { if (m_fetchRequest.isActive()) { requestPending = true; } else { QContactIdFilter filter; filter.setIds(m_changedContacts); m_changedContacts.clear(); // A local ID filter will fetch all contacts, rather than just aggregates; // we only want to retrieve aggregate contacts that have changed m_fetchRequest.setFilter(filter & aggregateFilter()); m_fetchRequest.setFetchHint(basicFetchHint()); m_fetchRequest.setSorting(QList()); m_fetchRequest.start(); m_fetchProcessedCount = 0; } } // Then populate the rest of the cache before doing anything else. if (m_keepPopulated && (m_populateProgress != Populated)) { if (m_fetchRequest.isActive()) { requestPending = true; } else { if (m_populateProgress == FetchMetadata) { // Query for all contacts // Request the metadata of all contacts (only data from the primary table, and any // other details required to determine whether the contacts matches the filter) m_fetchRequest.setFilter(allFilter()); m_fetchRequest.setFetchHint(metadataFetchHint(m_fetchTypes | SeasideCache::FetchGender)); m_fetchRequest.setSorting(m_sortOrder); qDebug() << "Starting metadata query at" << m_timer.elapsed() << "ms"; m_fetchRequest.start(); m_fetchProcessedCount = 0; m_populating = true; } } // Do nothing else until the cache is populated return; } if (m_refreshRequired) { // We can't refresh the IDs til all contacts have been appended if (m_contactsToAppend.isEmpty()) { if (m_contactIdRequest.isActive()) { requestPending = true; } else { m_refreshRequired = false; m_syncFilter = FilterFavorites; m_contactIdRequest.setFilter(favoriteFilter()); m_contactIdRequest.setSorting(m_sortOrder); m_contactIdRequest.start(); } } } else if (m_syncFilter == FilterAll) { if (m_contactIdRequest.isActive()) { requestPending = true; } else { if (m_syncFilter == FilterAll) { m_contactIdRequest.setFilter(allFilter()); m_contactIdRequest.setSorting(m_sortOrder); } m_contactIdRequest.start(); } } if (!m_relationshipsToSave.isEmpty() || !m_relationshipsToRemove.isEmpty()) { // this has to be before contact saves are processed so that the disaggregation flow // works properly if (!m_relationshipsToSave.isEmpty()) { if (!m_relationshipSaveRequest.isActive()) { m_relationshipSaveRequest.setRelationships(m_relationshipsToSave); m_relationshipSaveRequest.start(); m_relationshipsToSave.clear(); } } if (!m_relationshipsToRemove.isEmpty()) { if (!m_relationshipRemoveRequest.isActive()) { m_relationshipRemoveRequest.setRelationships(m_relationshipsToRemove); m_relationshipRemoveRequest.start(); m_relationshipsToRemove.clear(); } } // do not proceed with other tasks, even if we couldn't start a new request return; } if (!m_contactsToRemove.isEmpty()) { if (m_removeRequest.isActive()) { requestPending = true; } else { // Make per-collection remove requests, as backend does not allow batch removal of // contacts from different collections. for (auto it = m_contactsToRemove.begin(); it != m_contactsToRemove.end();) { const QList &contactsToRemove = it.value(); if (!contactsToRemove.isEmpty()) { m_removeRequest.setContactIds(contactsToRemove); m_removeRequest.start(); m_contactsToRemove.erase(it); break; } else { ++it; } } } } else if (!m_localContactsToRemove.isEmpty() && !m_removeRequest.isActive()) { if (m_clearChangeFlagsRequest.state() == QContactAbstractRequest::ActiveState) { requestPending = true; } else { // When local contacts are removed, their flags must also be cleared so that they are // removed from the database. m_clearChangeFlagsRequest.setContactIds(m_localContactsToRemove); m_clearChangeFlagsRequest.start(); m_localContactsToRemove.clear(); } } if (!m_contactsToCreate.isEmpty() || !m_contactsToSave.isEmpty()) { if (m_saveRequest.isActive()) { requestPending = true; } else { // Make per-collection save requests, as backend does not allow batch saving of // contacts from different collections. if (!m_contactsToCreate.isEmpty()) { m_saveRequest.setContacts(m_contactsToCreate); m_saveRequest.start(); m_contactsToCreate.clear(); } else if (!m_contactsToSave.isEmpty()) { for (auto it = m_contactsToSave.begin(); it != m_contactsToSave.end();) { const QHash &contactsToSave = it.value(); if (!contactsToSave.isEmpty()) { m_saveRequest.setContacts(contactsToSave.values()); m_saveRequest.start(); m_contactsToSave.erase(it); break; } else { ++it; } } } } } if (!m_constituentIds.isEmpty()) { if (m_fetchByIdRequest.isActive()) { requestPending = true; } else { // Fetch the constituent information (even if they're already in the // cache, because we don't update non-aggregates on change notifications) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_fetchByIdRequest.setIds(m_constituentIds.values()); #else m_fetchByIdRequest.setIds(m_constituentIds.toList()); #endif m_fetchByIdRequest.start(); m_fetchByIdProcessedCount = 0; } } if (!m_contactsToFetchConstituents.isEmpty()) { if (m_relationshipsFetchRequest.isActive()) { requestPending = true; } else { QContactId aggregateId = m_contactsToFetchConstituents.first(); // Find the constituents of this contact m_relationshipsFetchRequest.setFirst(aggregateId); m_relationshipsFetchRequest.setRelationshipType(QContactRelationship::Aggregates()); m_relationshipsFetchRequest.start(); } } if (!m_contactsToFetchCandidates.isEmpty()) { if (m_contactIdRequest.isActive()) { requestPending = true; } else { QContactId contactId(m_contactsToFetchCandidates.first()); const QContact contact(contactById(contactId)); // Find candidates to merge with this contact m_contactIdRequest.setFilter(filterForMergeCandidates(contact)); m_contactIdRequest.setSorting(m_sortOrder); m_contactIdRequest.start(); } } if (m_fetchTypes) { quint32 unfetchedTypes = m_fetchTypes & ~m_dataTypesFetched & SeasideCache::FetchTypesMask; if (unfetchedTypes) { if (m_fetchRequest.isActive()) { requestPending = true; } else { // Fetch the missing data types for whichever contacts need them m_fetchRequest.setSorting(m_sortOrder); if (unfetchedTypes == SeasideCache::FetchPhoneNumber) { m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasPhoneNumber, QContactFilter::MatchContains)); } else if (unfetchedTypes == SeasideCache::FetchEmailAddress) { m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasEmailAddress, QContactFilter::MatchContains)); } else if (unfetchedTypes == SeasideCache::FetchAccountUri) { m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasOnlineAccount, QContactFilter::MatchContains)); m_fetchRequest.setSorting(m_onlineSortOrder); } else { m_fetchRequest.setFilter(allFilter()); } m_fetchRequest.setFetchHint(extendedMetadataFetchHint(unfetchedTypes)); m_fetchRequest.start(); m_fetchProcessedCount = 0; m_dataTypesFetched |= unfetchedTypes; } } } if (!m_changedContacts.isEmpty()) { if (m_fetchRequest.isActive()) { requestPending = true; } else if (!m_displayOff) { // If we request too many IDs we will exceed the SQLite bound variables limit // The actual limit is over 800, but we should reduce further to increase interactivity const int maxRequestIds = 200; QContactIdFilter filter; if (m_changedContacts.count() > maxRequestIds) { filter.setIds(m_changedContacts.mid(0, maxRequestIds)); m_changedContacts = m_changedContacts.mid(maxRequestIds); } else { filter.setIds(m_changedContacts); m_changedContacts.clear(); } // A local ID filter will fetch all contacts, rather than just aggregates; // we only want to retrieve aggregate contacts that have changed m_fetchRequest.setFilter(filter & aggregateFilter()); m_fetchRequest.setFetchHint(basicFetchHint()); m_fetchRequest.setSorting(QList()); m_fetchRequest.start(); m_fetchProcessedCount = 0; } } if (!m_presenceChangedContacts.isEmpty()) { if (m_fetchRequest.isActive()) { requestPending = true; } else if (!m_displayOff) { const int maxRequestIds = 200; QContactIdFilter filter; if (m_presenceChangedContacts.count() > maxRequestIds) { filter.setIds(m_presenceChangedContacts.mid(0, maxRequestIds)); m_presenceChangedContacts = m_presenceChangedContacts.mid(maxRequestIds); } else { filter.setIds(m_presenceChangedContacts); m_presenceChangedContacts.clear(); } m_fetchRequest.setFilter(filter & aggregateFilter()); m_fetchRequest.setFetchHint(presenceFetchHint()); m_fetchRequest.setSorting(QList()); m_fetchRequest.start(); m_fetchProcessedCount = 0; } } if (requestPending) { // Don't proceed if we were unable to start one of the above requests return; } // No remaining work is pending - do we have any background task requests? if (m_extraFetchTypes) { quint32 unfetchedTypes = m_extraFetchTypes & ~m_dataTypesFetched & SeasideCache::FetchTypesMask; if (unfetchedTypes) { if (m_fetchRequest.isActive()) { requestPending = true; } else { quint32 fetchType = 0; // Load extra data items that we want to be able to search on, if not already fetched if (unfetchedTypes & SeasideCache::FetchOrganization) { // since this uses allFilter(), might as well grab // all the missing detail types fetchType = unfetchedTypes; m_fetchRequest.setFilter(allFilter()); } else if (unfetchedTypes & SeasideCache::FetchPhoneNumber) { fetchType = SeasideCache::FetchPhoneNumber; m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasPhoneNumber, QContactFilter::MatchContains)); } else if (unfetchedTypes & SeasideCache::FetchEmailAddress) { fetchType = SeasideCache::FetchEmailAddress; m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasEmailAddress, QContactFilter::MatchContains)); } else { fetchType = SeasideCache::FetchAccountUri; m_fetchRequest.setFilter(QContactStatusFlags::matchFlag(QContactStatusFlags::HasOnlineAccount, QContactFilter::MatchContains)); } m_fetchRequest.setFetchHint(extendedMetadataFetchHint(fetchType)); m_fetchRequest.start(); m_fetchProcessedCount = 0; m_dataTypesFetched |= fetchType; } } } if (!requestPending) { // Nothing to do - proceeed with idle processing *idleProcessing = true; } } bool SeasideCache::event(QEvent *event) { if (event->type() != QEvent::UpdateRequest) return QObject::event(event); m_updatesPending = false; bool idleProcessing = false; startRequest(&idleProcessing); // Report any unknown addresses while (!m_unknownResolveAddresses.isEmpty()) { const ResolveData &resolve = m_unknownResolveAddresses.takeFirst(); resolve.listener->addressResolved(resolve.first, resolve.second, 0); } if (!m_contactsToAppend.isEmpty() || !m_contactsToUpdate.isEmpty()) { applyPendingContactUpdates(); // Send another event to trigger further processing requestUpdate(); return true; } if (idleProcessing) { // Remove expired contacts when all other activity has been processed if (!m_expiredContacts.isEmpty()) { QList removeIds; QHash::const_iterator it = m_expiredContacts.constBegin(), end = m_expiredContacts.constEnd(); for ( ; it != end; ++it) { if (it.value() < 0) { quint32 iid = internalId(it.key()); removeIds.append(iid); } } m_expiredContacts.clear(); QSet modifiedGroups; // Before removal, ensure none of these contacts are in name groups foreach (quint32 iid, removeIds) { if (CacheItem *item = existingItem(iid)) { removeFromContactDisplayLabelGroup(item->iid, item->displayLabelGroup, &modifiedGroups); } } notifyDisplayLabelGroupsChanged(modifiedGroups); // Remove the contacts from the cache foreach (quint32 iid, removeIds) { QHash::iterator cacheItem = m_people.find(iid); if (cacheItem != m_people.end()) { delete cacheItem->itemData; m_people.erase(cacheItem); } } updateSectionBucketIndexCaches(); } } return true; } void SeasideCache::timerEvent(QTimerEvent *event) { if (event->timerId() == m_fetchTimer.timerId()) { // If the display is off, defer these fetches until they can be seen if (!m_displayOff) { fetchContacts(); } } if (event->timerId() == m_expiryTimer.timerId()) { m_expiryTimer.stop(); instancePtr = 0; deleteLater(); } } void SeasideCache::contactsAdded(const QList &ids) { // These additions may change address resolutions, so we may need to process them const bool relevant(m_keepPopulated || !instancePtr->m_changeListeners.isEmpty()); if (relevant) { updateContacts(ids, &m_changedContacts); } } void SeasideCache::contactsChanged(const QList &ids, const QList &typesChanged) { Q_UNUSED(typesChanged) if (m_keepPopulated) { updateContacts(ids, &m_changedContacts); } else { // Update these contacts if they're already in the cache QList presentIds; foreach (const QContactId &id, ids) { if (existingItem(id)) { presentIds.append(id); } } updateContacts(presentIds, &m_changedContacts); } } void SeasideCache::contactsPresenceChanged(const QList &ids) { if (m_keepPopulated) { updateContacts(ids, &m_presenceChangedContacts); } else { // Update these contacts if they're already in the cache QList presentIds; foreach (const QContactId &id, ids) { if (existingItem(id)) { presentIds.append(id); } } updateContacts(presentIds, &m_presenceChangedContacts); } } void SeasideCache::contactsRemoved(const QList &ids) { QList presentIds; foreach (const QContactId &id, ids) { if (CacheItem *item = existingItem(id)) { // Report this item is about to be removed foreach (ChangeListener *listener, m_changeListeners) { listener->itemAboutToBeRemoved(item); } ItemListener *listener = item->listeners; while (listener) { ItemListener *next = listener->next; listener->itemAboutToBeRemoved(item); listener = next; } item->listeners = 0; // Remove the links to addressible details updateContactIndexing(item->contact, QContact(), item->iid, QSet(), item); // Delete the avatar file assets of removed local contacts. foreach (const QContactAvatar &avatar, item->contact.details()) { removeLocalAvatarFile(item->contact, avatar); } if (!m_keepPopulated) { presentIds.append(id); } } } if (m_keepPopulated) { m_refreshRequired = true; } else { // Remove these contacts if they're already in the cache; they won't be removed by syncing foreach (const QContactId &id, presentIds) { m_expiredContacts[id] += -1; } } requestUpdate(); } void SeasideCache::dataChanged() { QList contactIds; typedef QHash::iterator iterator; for (iterator it = m_people.begin(); it != m_people.end(); ++it) { if (it->contactState != ContactAbsent) contactIds.append(it->apiId()); } updateContacts(contactIds, &m_changedContacts); // The backend will automatically update, but notify the models of the change. for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (int j = 0; j < models.count(); ++j) { ListModel *model = models.at(j); model->updateGroupProperty(); model->sourceItemsChanged(); model->sourceDataChanged(0, m_contacts[i].size()); model->updateSectionBucketIndexCache(); } } // Update the sorted list order m_refreshRequired = true; requestUpdate(); } void SeasideCache::fetchContacts() { static const int WaitIntervalMs = 250; if (m_fetchRequest.isActive()) { // The current fetch is still active - we may as well continue to accumulate m_fetchTimer.start(WaitIntervalMs, this); } else { m_fetchTimer.stop(); m_fetchPostponed.invalidate(); // Fetch any changed contacts immediately if (m_contactsUpdated) { m_contactsUpdated = false; if (m_keepPopulated) { // Refresh our contact sets in case sorting has changed m_refreshRequired = true; } } requestUpdate(); } } void SeasideCache::updateContacts(const QList &contactIds, QList *updateList) { // Wait for new changes to be reported static const int PostponementIntervalMs = 500; // Maximum wait until we fetch all changes previously reported static const int MaxPostponementMs = 5000; if (!contactIds.isEmpty()) { m_contactsUpdated = true; updateList->append(contactIds); // If the display is off, defer fetching these changes if (!m_displayOff) { if (m_fetchPostponed.isValid()) { // We are waiting to accumulate further changes int remainder = MaxPostponementMs - m_fetchPostponed.elapsed(); if (remainder > 0) { // We can postpone further m_fetchTimer.start(std::min(remainder, PostponementIntervalMs), this); } } else { // Wait for further changes before we query for the ones we have now m_fetchPostponed.restart(); m_fetchTimer.start(PostponementIntervalMs, this); } } } } void SeasideCache::updateCache(CacheItem *item, const QContact &contact, bool partialFetch, bool initialInsert) { if (item->contactState < ContactRequested) { item->contactState = partialFetch ? ContactPartial : ContactComplete; } else if (!partialFetch) { // Don't set a complete contact back after a partial update item->contactState = ContactComplete; } // Preserve the value of HasValidOnlineAccount, which is held only in the cache const int hasValidFlagValue = item->statusFlags & HasValidOnlineAccount; item->statusFlags = contact.detail().flagsValue() | hasValidFlagValue; if (item->itemData) { item->itemData->updateContact(contact, &item->contact, item->contactState); } else { item->contact = contact; } // If a valid display label was previously generated with name details, don't override with // non-name details. const bool fallbackToNonNameDetails = item->displayLabel.isEmpty(); const QString displayLabel = generateDisplayLabel(item->contact, displayLabelOrder(), fallbackToNonNameDetails); if (!displayLabel.isEmpty()) { item->displayLabel = displayLabel; } const QString displayLabelGroup = contact.detail().value(QContactDisplayLabel__FieldLabelGroup).toString(); if (!displayLabelGroup.isEmpty()) { item->displayLabelGroup = displayLabelGroup; } if (!initialInsert) { reportItemUpdated(item); } } void SeasideCache::reportItemUpdated(CacheItem *item) { // Report the change to this contact ItemListener *listener = item->listeners; while (listener) { listener->itemUpdated(item); listener = listener->next; } foreach (ChangeListener *listener, m_changeListeners) { listener->itemUpdated(item); } } void SeasideCache::resolveUnknownAddresses(const QString &first, const QString &second, CacheItem *item) { QList::iterator it = instancePtr->m_unknownAddresses.begin(); while (it != instancePtr->m_unknownAddresses.end()) { bool resolved = false; if (first == QString()) { // This is a phone number - test in normalized form resolved = (it->first == QString()) && (it->compare == second); } else if (second == QString()) { // Email address - compare in lowercased form resolved = (it->compare == first) && (it->second == QString()); } else { // Online account - compare URI in lowercased form resolved = (it->first == first) && (it->compare == second); } if (resolved) { // Inform the listener of resolution it->listener->addressResolved(it->first, it->second, item); // Do we need to request completion as well? if (it->requireComplete) { ensureCompletion(item); } it = instancePtr->m_unknownAddresses.erase(it); } else { ++it; } } } bool SeasideCache::updateContactIndexing(const QContact &oldContact, const QContact &contact, quint32 iid, const QSet &queryDetailTypes, CacheItem *item) { if (oldContact.collectionId() != aggregateCollectionId() && contact.collectionId() != aggregateCollectionId()) { return false; } bool modified = false; QSet oldAddresses; if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { // Addresses which are no longer in the contact should be de-indexed foreach (const QContactPhoneNumber &phoneNumber, oldContact.details()) { foreach (const StringPair &address, addressPairs(phoneNumber)) { if (validAddressPair(address)) oldAddresses.insert(address); } } // Update our address indexes for any address details in this contact foreach (const QContactPhoneNumber &phoneNumber, contact.details()) { foreach (const StringPair &address, addressPairs(phoneNumber)) { if (!validAddressPair(address)) continue; if (!oldAddresses.remove(address)) { // This address was not previously recorded modified = true; resolveUnknownAddresses(address.first, address.second, item); } CachedPhoneNumber cachedPhoneNumber(normalizePhoneNumber(phoneNumber.number()), iid); if (contact.collectionId() == aggregateCollectionId()) { if (!m_phoneNumberIds.contains(address.second, cachedPhoneNumber)) m_phoneNumberIds.insert(address.second, cachedPhoneNumber); } } } // Remove any addresses no longer available for this contact if (!oldAddresses.isEmpty()) { modified = true; foreach (const StringPair &address, oldAddresses) { m_phoneNumberIds.remove(address.second); } oldAddresses.clear(); } } if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { foreach (const QContactEmailAddress &emailAddress, oldContact.details()) { const StringPair address(addressPair(emailAddress)); if (validAddressPair(address)) oldAddresses.insert(address); } foreach (const QContactEmailAddress &emailAddress, contact.details()) { const StringPair address(addressPair(emailAddress)); if (!validAddressPair(address)) continue; if (!oldAddresses.remove(address)) { modified = true; resolveUnknownAddresses(address.first, address.second, item); } if (contact.collectionId() == aggregateCollectionId()) { m_emailAddressIds[address.first] = iid; } } if (!oldAddresses.isEmpty()) { modified = true; foreach (const StringPair &address, oldAddresses) { m_emailAddressIds.remove(address.first); } oldAddresses.clear(); } } if (queryDetailTypes.isEmpty() || queryDetailTypes.contains(detailType())) { foreach (const QContactOnlineAccount &account, oldContact.details()) { const StringPair address(addressPair(account)); if (validAddressPair(address)) oldAddresses.insert(address); } // Keep track of whether this contact has any valid IM accounts bool hasValid = false; foreach (const QContactOnlineAccount &account, contact.details()) { const StringPair address(addressPair(account)); if (!validAddressPair(address)) continue; if (!oldAddresses.remove(address)) { modified = true; resolveUnknownAddresses(address.first, address.second, item); } if (contact.collectionId() == aggregateCollectionId()) { m_onlineAccountIds[address] = iid; } hasValid = true; } if (hasValid) { item->statusFlags |= HasValidOnlineAccount; } else { item->statusFlags &= ~HasValidOnlineAccount; } if (!oldAddresses.isEmpty()) { modified = true; foreach (const StringPair &address, oldAddresses) { m_onlineAccountIds.remove(address); } oldAddresses.clear(); } } return modified; } void updateDetailsFromCache(QContact &contact, SeasideCache::CacheItem *item, const QSet &queryDetailTypes) { // Copy any existing detail types that are in the current record to the new instance foreach (const QContactDetail &existing, item->contact.details()) { const QContactDetail::DetailType existingType(detailType(existing)); static const DetailList contactsTableTypes(contactsTableDetails()); // The queried contact already contains any types in the contacts table, and those // types explicitly fetched by the query if (!queryDetailTypes.contains(existingType) && !contactsTableTypes.contains(existingType)) { QContactDetail copy(existing); contact.saveDetail(©); } } } void SeasideCache::contactsAvailable() { QContactAbstractRequest *request = static_cast(sender()); QList contacts; QContactFetchHint fetchHint; if (request == &m_fetchByIdRequest) { contacts = m_fetchByIdRequest.contacts(); if (m_fetchByIdProcessedCount) { contacts = contacts.mid(m_fetchByIdProcessedCount); } m_fetchByIdProcessedCount += contacts.count(); fetchHint = m_fetchByIdRequest.fetchHint(); } else { contacts = m_fetchRequest.contacts(); if (m_fetchProcessedCount) { contacts = contacts.mid(m_fetchProcessedCount); } m_fetchProcessedCount += contacts.count(); fetchHint = m_fetchRequest.fetchHint(); } if (contacts.isEmpty()) return; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QSet queryDetailTypes = QSet(detailTypesHint(fetchHint).begin(), detailTypesHint(fetchHint).end()); #else QSet queryDetailTypes = detailTypesHint(fetchHint).toSet(); #endif if (request == &m_fetchRequest && m_populating) { Q_ASSERT(m_populateProgress > Unpopulated && m_populateProgress < Populated); FilterType type(m_populateProgress == FetchFavorites ? FilterFavorites : FilterAll); QHash, QList > >::iterator it = m_contactsToAppend.find(type); if (it != m_contactsToAppend.end()) { // All populate queries have the same detail types, so we can append this list to the existing one it.value().second.append(contacts); } else { m_contactsToAppend.insert(type, qMakePair(queryDetailTypes, contacts)); } requestUpdate(); } else { if (contacts.count() == 1 || request == &m_fetchByIdRequest) { // Process these results immediately applyContactUpdates(contacts, queryDetailTypes); // note: can cause out-of-order since this doesn't result in refresh request. TODO: remove this line? updateSectionBucketIndexCaches(); } else { // Add these contacts to the list to be progressively appended QList, QList > >::iterator it = m_contactsToUpdate.begin(), end = m_contactsToUpdate.end(); for ( ; it != end; ++it) { if ((*it).first == queryDetailTypes) { (*it).second.append(contacts); break; } } if (it == end) { m_contactsToUpdate.append(qMakePair(queryDetailTypes, contacts)); } requestUpdate(); } } } void SeasideCache::applyPendingContactUpdates() { if (!m_contactsToAppend.isEmpty()) { // Insert the contacts in the order they're requested QHash, QList > >::iterator end = m_contactsToAppend.end(), it = end; if ((it = m_contactsToAppend.find(FilterFavorites)) != end) { } else if ((it = m_contactsToAppend.find(FilterAll)) != end) { } Q_ASSERT(it != end); FilterType type = it.key(); QSet &detailTypes((*it).first); const bool partialFetch = !detailTypes.isEmpty(); QList &appendedContacts((*it).second); const int maxBatchSize = 200; const int minBatchSize = 50; if (appendedContacts.count() < maxBatchSize) { // For a small number of contacts, append all at once appendContacts(appendedContacts, type, partialFetch, detailTypes); appendedContacts.clear(); } else { // Append progressively in batches appendContacts(appendedContacts.mid(0, minBatchSize), type, partialFetch, detailTypes); appendedContacts = appendedContacts.mid(minBatchSize); } if (appendedContacts.isEmpty()) { m_contactsToAppend.erase(it); // This list has been processed - have we finished populating the group? if (type == FilterFavorites && (m_populateProgress != FetchFavorites)) { makePopulated(FilterFavorites); qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; } else if (type == FilterAll && (m_populateProgress != FetchMetadata)) { makePopulated(FilterNone); makePopulated(FilterAll); qDebug() << "All queried in" << m_timer.elapsed() << "ms"; } updateSectionBucketIndexCaches(); } } else { QList, QList > >::iterator it = m_contactsToUpdate.begin(); QSet &detailTypes((*it).first); // Update a single contact at a time; the update can cause numerous QML bindings // to be re-evaluated, so even a single contact update might be a slow operation QList &updatedContacts((*it).second); applyContactUpdates(QList() << updatedContacts.takeFirst(), detailTypes); if (updatedContacts.isEmpty()) { m_contactsToUpdate.erase(it); updateSectionBucketIndexCaches(); } } } void SeasideCache::updateSectionBucketIndexCaches() { for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (ListModel *model : models) { model->updateSectionBucketIndexCache(); } } } void SeasideCache::applyContactUpdates(const QList &contacts, const QSet &queryDetailTypes) { QSet modifiedGroups; const bool partialFetch = !queryDetailTypes.isEmpty(); foreach (QContact contact, contacts) { quint32 iid = internalId(contact); QString oldDisplayLabelGroup; QString oldDisplayLabel; CacheItem *item = existingItem(iid); if (!item) { // We haven't seen this contact before item = &(m_people[iid]); item->iid = iid; } else { oldDisplayLabelGroup = item->displayLabelGroup; oldDisplayLabel = item->displayLabel; if (partialFetch) { // Update our new instance with any details not returned by the current query updateDetailsFromCache(contact, item, queryDetailTypes); } } bool roleDataChanged = false; // This is a simplification of reality, should we test more changes? if (!partialFetch || queryDetailTypes.contains(detailType())) { roleDataChanged |= (contact.details() != item->contact.details()); } if (!partialFetch || queryDetailTypes.contains(detailType())) { roleDataChanged |= (contact.detail() != item->contact.detail()); } roleDataChanged |= updateContactIndexing(item->contact, contact, iid, queryDetailTypes, item); updateCache(item, contact, partialFetch, false); roleDataChanged |= (item->displayLabel != oldDisplayLabel); // do this even if !roleDataChanged as name groups are affected by other display label changes if (item->displayLabelGroup != oldDisplayLabelGroup) { if (!ignoreContactForDisplayLabelGroups(item->contact)) { addToContactDisplayLabelGroup(item->iid, item->displayLabelGroup, &modifiedGroups); removeFromContactDisplayLabelGroup(item->iid, oldDisplayLabelGroup, &modifiedGroups); } } if (roleDataChanged) { instancePtr->contactDataChanged(item->iid); } } notifyDisplayLabelGroupsChanged(modifiedGroups); } void SeasideCache::addToContactDisplayLabelGroup(quint32 iid, const QString &group, QSet *modifiedGroups) { if (!group.isEmpty()) { QSet &set(m_contactDisplayLabelGroups[group]); if (!set.contains(iid)) { set.insert(iid); if (modifiedGroups && !m_displayLabelGroupChangeListeners.isEmpty()) { modifiedGroups->insert(group); } } } } void SeasideCache::removeFromContactDisplayLabelGroup(quint32 iid, const QString &group, QSet *modifiedGroups) { if (!group.isEmpty()) { QSet &set(m_contactDisplayLabelGroups[group]); if (set.remove(iid)) { if (modifiedGroups && !m_displayLabelGroupChangeListeners.isEmpty()) { modifiedGroups->insert(group); } } } } void SeasideCache::notifyDisplayLabelGroupsChanged(const QSet &groups) { if (groups.isEmpty() || m_displayLabelGroupChangeListeners.isEmpty()) return; QHash > updates; foreach (const QString &group, groups) updates.insert(group, m_contactDisplayLabelGroups[group]); for (int i = 0; i < m_displayLabelGroupChangeListeners.count(); ++i) m_displayLabelGroupChangeListeners[i]->displayLabelGroupsUpdated(updates); } void SeasideCache::contactIdsAvailable() { if (m_syncFilter == FilterNone) { if (!m_contactsToFetchCandidates.isEmpty()) { foreach (const QContactId &id, m_contactIdRequest.ids()) { m_candidateIds.insert(id); } } } else { synchronizeList(this, m_contacts[m_syncFilter], m_cacheIndex, internalIds(m_contactIdRequest.ids()), m_queryIndex); } } void SeasideCache::relationshipsAvailable() { static const QString aggregatesRelationship = QContactRelationship::Aggregates(); foreach (const QContactRelationship &rel, m_relationshipsFetchRequest.relationships()) { if (rel.relationshipType() == aggregatesRelationship) { m_constituentIds.insert(rel.second()); } } } void SeasideCache::removeRange(FilterType filter, int index, int count) { QList &cacheIds = m_contacts[filter]; QList &models = m_models[filter]; for (int i = 0; i < models.count(); ++i) models[i]->sourceAboutToRemoveItems(index, index + count - 1); for (int i = 0; i < count; ++i) { if (filter == FilterAll) { const quint32 iid = cacheIds.at(index); m_expiredContacts[apiId(iid)] -= 1; } cacheIds.removeAt(index); } for (int i = 0; i < models.count(); ++i) { models[i]->sourceItemsRemoved(); models[i]->updateSectionBucketIndexCache(); } } int SeasideCache::insertRange(FilterType filter, int index, int count, const QList &queryIds, int queryIndex) { QList &cacheIds = m_contacts[filter]; QList &models = m_models[filter]; const quint32 selfId = internalId(manager()->selfContactId()); int end = index + count - 1; for (int i = 0; i < models.count(); ++i) models[i]->sourceAboutToInsertItems(index, end); for (int i = 0; i < count; ++i) { quint32 iid = queryIds.at(queryIndex + i); if (iid == selfId) continue; if (filter == FilterAll) { const QContactId apiId = SeasideCache::apiId(iid); m_expiredContacts[apiId] += 1; } cacheIds.insert(index + i, iid); } for (int i = 0; i < models.count(); ++i) { models[i]->sourceItemsInserted(index, end); models[i]->updateSectionBucketIndexCache(); } return end - index + 1; } void SeasideCache::appendContacts(const QList &contacts, FilterType filterType, bool partialFetch, const QSet &queryDetailTypes) { if (!contacts.isEmpty()) { QList &cacheIds = m_contacts[filterType]; QList &models = m_models[filterType]; cacheIds.reserve(contacts.count()); const int begin = cacheIds.count(); int end = cacheIds.count() + contacts.count() - 1; if (begin <= end) { QSet modifiedGroups; for (int i = 0; i < models.count(); ++i) models.at(i)->sourceAboutToInsertItems(begin, end); foreach (QContact contact, contacts) { quint32 iid = internalId(contact); cacheIds.append(iid); CacheItem *item = existingItem(iid); if (!item) { item = &(m_people[iid]); item->iid = iid; } else { if (partialFetch) { // Update our new instance with any details not returned by the current query updateDetailsFromCache(contact, item, queryDetailTypes); } } updateContactIndexing(item->contact, contact, iid, queryDetailTypes, item); updateCache(item, contact, partialFetch, true); if (filterType == FilterAll) { addToContactDisplayLabelGroup(iid, displayLabelGroup(item), &modifiedGroups); } } for (int i = 0; i < models.count(); ++i) models.at(i)->sourceItemsInserted(begin, end); notifyDisplayLabelGroupsChanged(modifiedGroups); } } } void SeasideCache::notifySaveContactComplete(int constituentId, int aggregateId) { for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (int j = 0; j < models.count(); ++j) { ListModel *model = models.at(j); model->saveContactComplete(constituentId, aggregateId); } } } void SeasideCache::requestStateChanged(QContactAbstractRequest::State state) { if (state != QContactAbstractRequest::FinishedState) return; if (sender() == &m_clearChangeFlagsRequest) { // Not a subclass of QContactAbstractRequest, so handle separately. if (m_clearChangeFlagsRequest.error() != QContactManager::NoError) { qWarning() << "ClearChangeFlagsRequest error:" << m_clearChangeFlagsRequest.error(); } return; } QContactAbstractRequest *request = static_cast(sender()); if (request->error() != QContactManager::NoError) { qWarning() << "Contact request" << request->type() << "error:" << request->error(); } if (request == &m_relationshipsFetchRequest) { if (!m_contactsToFetchConstituents.isEmpty()) { QContactId aggregateId = m_contactsToFetchConstituents.takeFirst(); if (!m_constituentIds.isEmpty()) { m_contactsToLinkTo.append(aggregateId); } else { // We didn't find any constituents - report the empty list CacheItem *cacheItem = itemById(aggregateId); if (cacheItem->itemData) { cacheItem->itemData->constituentsFetched(QList()); } updateConstituentAggregations(cacheItem->apiId()); } } } else if (request == &m_fetchByIdRequest) { if (!m_contactsToLinkTo.isEmpty()) { // Report these results QContactId aggregateId = m_contactsToLinkTo.takeFirst(); CacheItem *cacheItem = itemById(aggregateId); QList constituentIds; foreach (const QContactId &id, m_constituentIds) { constituentIds.append(internalId(id)); } m_constituentIds.clear(); if (cacheItem->itemData) { cacheItem->itemData->constituentsFetched(constituentIds); } updateConstituentAggregations(cacheItem->apiId()); } } else if (request == &m_contactIdRequest) { if (m_syncFilter != FilterNone) { // We have completed fetching this filter set completeSynchronizeList(this, m_contacts[m_syncFilter], m_cacheIndex, internalIds(m_contactIdRequest.ids()), m_queryIndex); // Notify models of completed updates QList &models = m_models[m_syncFilter]; for (int i = 0; i < models.count(); ++i) models.at(i)->sourceItemsChanged(); if (m_syncFilter == FilterFavorites) { // Next, query for all contacts (including favorites) m_syncFilter = FilterAll; } else if (m_syncFilter == FilterAll) { m_syncFilter = FilterNone; } } else if (!m_contactsToFetchCandidates.isEmpty()) { // Report these results QContactId contactId = m_contactsToFetchCandidates.takeFirst(); CacheItem *cacheItem = itemById(contactId); const quint32 contactIid = internalId(contactId); QList candidateIds; foreach (const QContactId &id, m_candidateIds) { // Exclude the original source contact const quint32 iid = internalId(id); if (iid != contactIid) { candidateIds.append(iid); } } m_candidateIds.clear(); if (cacheItem->itemData) { cacheItem->itemData->mergeCandidatesFetched(candidateIds); } } else { qWarning() << "ID fetch completed with no filter?"; } } else if (request == &m_relationshipSaveRequest || request == &m_relationshipRemoveRequest) { bool completed = false; QList relationships; if (request == &m_relationshipSaveRequest) { relationships = m_relationshipSaveRequest.relationships(); completed = !m_relationshipRemoveRequest.isActive(); } else { relationships = m_relationshipRemoveRequest.relationships(); completed = !m_relationshipSaveRequest.isActive(); } foreach (const QContactRelationship &relationship, relationships) { m_aggregatedContacts.insert(relationship.first()); } if (completed) { foreach (const QContactId &contactId, m_aggregatedContacts) { CacheItem *cacheItem = itemById(contactId); if (cacheItem && cacheItem->itemData) cacheItem->itemData->aggregationOperationCompleted(); } // We need to update these modified contacts immediately foreach (const QContactId &id, m_aggregatedContacts) m_changedContacts.append(id); fetchContacts(); m_aggregatedContacts.clear(); } } else if (request == &m_fetchRequest) { if (m_populating) { Q_ASSERT(m_populateProgress > Unpopulated && m_populateProgress < Populated); if (m_populateProgress == FetchFavorites) { if (m_contactsToAppend.find(FilterFavorites) == m_contactsToAppend.end()) { // No pending contacts, the models are now populated makePopulated(FilterFavorites); qDebug() << "Favorites queried in" << m_timer.elapsed() << "ms"; } m_populateProgress = FetchMetadata; } else if (m_populateProgress == FetchMetadata) { if (m_contactsToAppend.find(FilterAll) == m_contactsToAppend.end()) { makePopulated(FilterNone); makePopulated(FilterAll); qDebug() << "All queried in" << m_timer.elapsed() << "ms"; } m_populateProgress = Populated; } m_populating = false; } } else if (request == &m_saveRequest) { for (int i = 0; i < m_saveRequest.contacts().size(); ++i) { const QContact c = m_saveRequest.contacts().at(i); if (m_saveRequest.errorMap().value(i) != QContactManager::NoError) { notifySaveContactComplete(-1, -1); } else if (c.collectionId() == aggregateCollectionId()) { // In case an aggregate is saved rather than a local constituent, // no need to look up the aggregate via a relationship fetch request. notifySaveContactComplete(-1, internalId(c)); } else { // Get the aggregate associated with this contact. QContactRelationshipFetchRequest *rfr = new QContactRelationshipFetchRequest(this); rfr->setManager(m_saveRequest.manager()); rfr->setRelationshipType(QContactRelationship::Aggregates()); rfr->setSecond(c.id()); connect(rfr, &QContactAbstractRequest::stateChanged, this, [this, c, rfr] { if (rfr->state() == QContactAbstractRequest::FinishedState) { rfr->deleteLater(); if (rfr->relationships().size()) { const quint32 constituentId = internalId(rfr->relationships().at(0).second()); const quint32 aggregateId = internalId(rfr->relationships().at(0).first()); this->notifySaveContactComplete(constituentId, aggregateId); } else { // error: cannot retrieve aggregate for newly created constituent. this->notifySaveContactComplete(internalId(c), -1); } } }); rfr->start(); } } } // See if there are any more requests to dispatch requestUpdate(); } void SeasideCache::addressRequestStateChanged(QContactAbstractRequest::State state) { if (state != QContactAbstractRequest::FinishedState) return; // results are complete, so record them in the cache QContactFetchRequest *request = static_cast(sender()); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QSet queryDetailTypes = QSet(detailTypesHint(request->fetchHint()).begin(), detailTypesHint(request->fetchHint()).end()); #else QSet queryDetailTypes = detailTypesHint(request->fetchHint()).toSet(); #endif applyContactUpdates(request->contacts(), queryDetailTypes); // now figure out which address was being resolved and resolve it QHash::iterator it = instancePtr->m_resolveAddresses.find(request); if (it == instancePtr->m_resolveAddresses.end()) { qWarning() << "Got stateChanged for unknown request"; return; } ResolveData data(it.value()); if (data.first == QString()) { // We have now queried this phone number m_resolvedPhoneNumbers.insert(minimizePhoneNumber(data.second)); } CacheItem *item = 0; const QList &resolvedContacts = it.key()->contacts(); if (!resolvedContacts.isEmpty()) { if (resolvedContacts.count() == 1 && data.first != QString()) { // Return the result because it is the only resolved contact; however still filter out // false positive phone number matches item = itemById(apiId(resolvedContacts.first()), false); } else { // Lookup the result in our updated indexes if (data.first == QString()) { item = itemByPhoneNumber(data.second, false); } else if (data.second == QString()) { item = itemByEmailAddress(data.first, false); } else { item = itemByOnlineAccount(data.first, data.second, false); } } } else { // This address is unknown - keep it for later resolution if (data.first == QString()) { // Compare this phone number in minimized form data.compare = minimizePhoneNumber(data.second); } else if (data.second == QString()) { // Compare this email address in lowercased form data.compare = data.first.toLower(); } else { // Compare this account URI in lowercased form data.compare = data.second.toLower(); } m_unknownAddresses.append(data); } m_pendingResolve.remove(data); data.listener->addressResolved(data.first, data.second, item); delete it.key(); m_resolveAddresses.erase(it); } void SeasideCache::makePopulated(FilterType filter) { m_populated |= (1 << filter); QList &models = m_models[filter]; for (int i = 0; i < models.count(); ++i) models.at(i)->makePopulated(); } void SeasideCache::setSortOrder(const QString &property) { bool firstNameFirst = (property == QString::fromLatin1("firstName")); QContactSortOrder firstNameOrder; setDetailType(firstNameOrder, QContactName::FieldFirstName); firstNameOrder.setCaseSensitivity(Qt::CaseInsensitive); firstNameOrder.setDirection(Qt::AscendingOrder); firstNameOrder.setBlankPolicy(QContactSortOrder::BlanksFirst); QContactSortOrder lastNameOrder; setDetailType(lastNameOrder, QContactName::FieldLastName); lastNameOrder.setCaseSensitivity(Qt::CaseInsensitive); lastNameOrder.setDirection(Qt::AscendingOrder); lastNameOrder.setBlankPolicy(QContactSortOrder::BlanksFirst); QContactSortOrder displayLabelGroupOrder; setDetailType(displayLabelGroupOrder, QContactDisplayLabel__FieldLabelGroup); m_sortOrder = firstNameFirst ? (QList() << displayLabelGroupOrder << firstNameOrder << lastNameOrder) : (QList() << displayLabelGroupOrder << lastNameOrder << firstNameOrder); m_onlineSortOrder = m_sortOrder; QContactSortOrder onlineOrder; setDetailType(onlineOrder, QContactGlobalPresence::FieldPresenceState); onlineOrder.setDirection(Qt::AscendingOrder); m_onlineSortOrder.prepend(onlineOrder); } void SeasideCache::displayLabelOrderChanged(CacheConfiguration::DisplayLabelOrder order) { // Update the display labels typedef QHash::iterator iterator; for (iterator it = m_people.begin(); it != m_people.end(); ++it) { // Regenerate the display label QString newLabel = generateDisplayLabel(it->contact, static_cast(order)); if (newLabel != it->displayLabel) { it->displayLabel = newLabel; contactDataChanged(it->iid); reportItemUpdated(&*it); } if (it->itemData) { it->itemData->displayLabelOrderChanged(static_cast(order)); } } for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (int j = 0; j < models.count(); ++j) { ListModel *model = models.at(j); model->updateDisplayLabelOrder(); model->sourceItemsChanged(); } } } void SeasideCache::displayLabelGroupsChanged(const QStringList &groups) { allContactDisplayLabelGroups = groups; contactDisplayLabelGroupCount = groups.count(); } void SeasideCache::sortPropertyChanged(const QString &sortProperty) { setSortOrder(sortProperty); for (int i = 0; i < FilterTypesCount; ++i) { const QList &models = m_models[i]; for (int j = 0; j < models.count(); ++j) { models.at(j)->updateSortProperty(); // No need for sourceItemsChanged, as the sorted list update will cause that } } // Update the sorted list order m_refreshRequired = true; requestUpdate(); } void SeasideCache::displayStatusChanged(const QString &status) { #ifdef HAS_MCE const bool off = (status == QLatin1String(MCE_DISPLAY_OFF_STRING)); if (m_displayOff != off) { m_displayOff = off; if (!m_displayOff) { // The display has been enabled; check for pending fetches requestUpdate(); } } #endif } int SeasideCache::importContacts(const QString &path) { QFile vcf(path); if (!vcf.open(QIODevice::ReadOnly)) { qWarning() << Q_FUNC_INFO << "Cannot open " << path; return 0; } // Ensure the cache has been instantiated instance(); // TODO: thread QVersitReader reader(&vcf); reader.startReading(); reader.waitForFinished(); QVersitContactImporter importer; importer.importDocuments(reader.results()); QList newContacts = importer.contacts(); instancePtr->m_contactsToCreate += newContacts; instancePtr->requestUpdate(); return newContacts.count(); } QString SeasideCache::exportContacts() { // Ensure the cache has been instantiated instance(); QVersitContactExporter exporter; QList contacts; contacts.reserve(instancePtr->m_people.count()); QList contactsToFetch; contactsToFetch.reserve(instancePtr->m_people.count()); const quint32 selfId = internalId(manager()->selfContactId()); typedef QHash::iterator iterator; for (iterator it = instancePtr->m_people.begin(); it != instancePtr->m_people.end(); ++it) { if (it.key() == selfId) { continue; } else if (it->contactState == ContactComplete) { contacts.append(it->contact); } else { contactsToFetch.append(apiId(it.key())); } } if (!contactsToFetch.isEmpty()) { QList fetchedContacts = manager()->contacts(contactsToFetch); contacts.append(fetchedContacts); } if (!exporter.exportContacts(contacts)) { qWarning() << Q_FUNC_INFO << "Failed to export contacts: " << exporter.errorMap(); return QString(); } QString baseDir; foreach (const QString &loc, QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation)) { baseDir = loc; break; } QFile vcard(baseDir + QDir::separator() + QLocale::c().toString(QDateTime::currentDateTime(), QStringLiteral("ss_mm_hh_dd_mm_yyyy")) + ".vcf"); if (!vcard.open(QIODevice::WriteOnly)) { qWarning() << "Cannot open " << vcard.fileName(); return QString(); } QVersitWriter writer(&vcard); if (!writer.startWriting(exporter.documents())) { qWarning() << Q_FUNC_INFO << "Can't start writing vcards " << writer.error(); return QString(); } // TODO: thread writer.waitForFinished(); return vcard.fileName(); } void SeasideCache::keepPopulated(quint32 requiredTypes, quint32 extraTypes) { bool updateRequired(false); // If these types are required, we will fetch them immediately quint32 unfetchedTypes = requiredTypes & ~m_fetchTypes & SeasideCache::FetchTypesMask; if (unfetchedTypes) { m_fetchTypes |= requiredTypes; updateRequired = true; } // Otherwise, we can fetch them when idle unfetchedTypes = extraTypes & ~m_extraFetchTypes & SeasideCache::FetchTypesMask; if (unfetchedTypes) { m_extraFetchTypes |= extraTypes; updateRequired = true; } if (((requiredTypes | extraTypes) & SeasideCache::FetchPhoneNumber) != 0) { // We won't need to check resolved numbers any further m_resolvedPhoneNumbers.clear(); } if (!m_keepPopulated) { m_keepPopulated = true; updateRequired = true; } if (updateRequired) { requestUpdate(); } } // Aggregates contact2 into contact1. Aggregate relationships will be created between the first // contact and the constituents of the second contact. void SeasideCache::aggregateContacts(const QContact &contact1, const QContact &contact2) { // Ensure the cache has been instantiated instance(); instancePtr->m_contactPairsToLink.append(qMakePair( ContactLinkRequest(apiId(contact1)), ContactLinkRequest(apiId(contact2)))); instancePtr->fetchConstituents(contact1); instancePtr->fetchConstituents(contact2); } // Disaggregates contact2 (a non-aggregate constituent) from contact1 (an aggregate). This removes // the existing aggregate relationships between the two contacts. void SeasideCache::disaggregateContacts(const QContact &contact1, const QContact &contact2) { // Ensure the cache has been instantiated instance(); instancePtr->m_relationshipsToRemove.append(makeRelationship(aggregateRelationshipType, contact1.id(), contact2.id())); instancePtr->m_relationshipsToSave.append(makeRelationship(isNotRelationshipType, contact1.id(), contact2.id())); instancePtr->requestUpdate(); } void SeasideCache::updateConstituentAggregations(const QContactId &contactId) { typedef QList >::iterator iterator; for (iterator it = m_contactPairsToLink.begin(); it != m_contactPairsToLink.end(); ) { QPair &pair = *it; if (pair.first.contactId == contactId) pair.first.constituentsFetched = true; if (pair.second.contactId == contactId) pair.second.constituentsFetched = true; if (pair.first.constituentsFetched && pair.second.constituentsFetched) { completeContactAggregation(pair.first.contactId, pair.second.contactId); it = m_contactPairsToLink.erase(it); } else { ++it; } } } // Called once constituents have been fetched for both persons. void SeasideCache::completeContactAggregation(const QContactId &contact1Id, const QContactId &contact2Id) { CacheItem *cacheItem1 = itemById(contact1Id); CacheItem *cacheItem2 = itemById(contact2Id); if (!cacheItem1 || !cacheItem2 || !cacheItem1->itemData || !cacheItem2->itemData) return; const QList &constituents2 = cacheItem2->itemData->constituents(); // For each constituent of contact2, add a relationship between it and contact1, and remove the // relationship between it and contact2. foreach (int id, constituents2) { const QContactId constituentId(apiId(id)); m_relationshipsToSave.append(makeRelationship(aggregateRelationshipType, contact1Id, constituentId)); m_relationshipsToRemove.append(makeRelationship(aggregateRelationshipType, contact2Id, constituentId)); // If there is an existing IsNot relationship, it would be better to remove it; // unfortunately, we can't be certain that it exists, and if it does not, the // relationship removal will fail - hence, better to ignore the possibility: //m_relationshipsToRemove.append(makeRelationship(isNotRelationshipType, contact1Id, constituentId)); } if (!m_relationshipsToSave.isEmpty() || !m_relationshipsToRemove.isEmpty()) requestUpdate(); } void SeasideCache::resolveAddress(ResolveListener *listener, const QString &first, const QString &second, bool requireComplete) { ResolveData data; data.first = first; data.second = second; data.requireComplete = requireComplete; data.listener = listener; // filter out duplicate requests if (m_pendingResolve.find(data) != m_pendingResolve.end()) return; // Is this address a known-unknown? bool knownUnknown = false; QList::const_iterator it = instancePtr->m_unknownAddresses.constBegin(), end = m_unknownAddresses.constEnd(); for ( ; it != end; ++it) { if (it->first == first && it->second == second) { knownUnknown = true; break; } } if (knownUnknown) { m_unknownResolveAddresses.append(data); requestUpdate(); } else { QContactFetchRequest *request = new QContactFetchRequest(this); request->setManager(manager()); if (first.isEmpty()) { // Search for phone number request->setFilter(QContactPhoneNumber::match(second)); } else if (second.isEmpty()) { // Search for email address QContactDetailFilter detailFilter; setDetailType(detailFilter, QContactEmailAddress::FieldEmailAddress); detailFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive detailFilter.setValue(first); request->setFilter(detailFilter); } else { // Search for online account QContactDetailFilter localFilter; setDetailType(localFilter, QContactOnlineAccount__FieldAccountPath); localFilter.setValue(first); QContactDetailFilter remoteFilter; setDetailType(remoteFilter, QContactOnlineAccount::FieldAccountUri); remoteFilter.setMatchFlags(QContactFilter::MatchExactly | QContactFilter::MatchFixedString); // allow case insensitive remoteFilter.setValue(second); request->setFilter(localFilter & remoteFilter); } // If completion is not required, at least include the contact endpoint details (since resolving is obviously being used) const quint32 detailFetchTypes(SeasideCache::FetchAccountUri | SeasideCache::FetchPhoneNumber | SeasideCache::FetchEmailAddress); request->setFetchHint(requireComplete ? basicFetchHint() : onlineFetchHint(m_fetchTypes | m_extraFetchTypes | detailFetchTypes)); connect(request, SIGNAL(stateChanged(QContactAbstractRequest::State)), this, SLOT(addressRequestStateChanged(QContactAbstractRequest::State))); m_resolveAddresses[request] = data; m_pendingResolve.insert(data); request->setFilter(request->filter() & aggregateFilter()); request->start(); } } SeasideCache::CacheItem *SeasideCache::itemMatchingPhoneNumber(const QString &number, const QString &normalized, bool requireComplete) { QMultiHash::const_iterator it = m_phoneNumberIds.find(number), end = m_phoneNumberIds.constEnd(); if (it == end) return 0; QHash possibleMatches; ::i18n::phonenumbers::PhoneNumberUtil *util = ::i18n::phonenumbers::PhoneNumberUtil::GetInstance(); ::std::string normalizedStdStr = normalized.toStdString(); for (QMultiHash::const_iterator matchingIt = it; matchingIt != end && matchingIt.key() == number; ++matchingIt) { const CachedPhoneNumber &cachedPhoneNumber = matchingIt.value(); // Bypass libphonenumber if the numbers match exactly if (matchingIt->normalizedNumber == normalized) return itemById(cachedPhoneNumber.iid, requireComplete); ::i18n::phonenumbers::PhoneNumberUtil::MatchType matchType = util->IsNumberMatchWithTwoStrings(normalizedStdStr, cachedPhoneNumber.normalizedNumber.toStdString()); switch (matchType) { case ::i18n::phonenumbers::PhoneNumberUtil::EXACT_MATCH: // This is the optimal outcome return itemById(cachedPhoneNumber.iid, requireComplete); case ::i18n::phonenumbers::PhoneNumberUtil::NSN_MATCH: case ::i18n::phonenumbers::PhoneNumberUtil::SHORT_NSN_MATCH: // Store numbers whose NSN (national significant number) might match // Example: if +36701234567 is calling, then 1234567 is an NSN match possibleMatches.insert(cachedPhoneNumber.normalizedNumber, cachedPhoneNumber.iid); break; default: // Either couldn't parse the number or it was NO_MATCH, ignore it break; } } // Choose the best match from these contacts int bestMatchLength = 0; CacheItem *matchItem = 0; for (QHash::const_iterator matchingIt = possibleMatches.begin(); matchingIt != possibleMatches.end(); ++matchingIt) { if (CacheItem *item = existingItem(*matchingIt)) { if (item->contact.collectionId() != aggregateCollectionId()) { continue; } int matchLength = bestPhoneNumberMatchLength(item->contact, normalized); if (matchLength > bestMatchLength) { bestMatchLength = matchLength; matchItem = item; if (bestMatchLength == ExactMatch) break; } } } if (matchItem != 0) { if (requireComplete) { ensureCompletion(matchItem); } return matchItem; } return 0; } int SeasideCache::contactIndex(quint32 iid, FilterType filterType) { const QList &cacheIds(m_contacts[filterType]); return cacheIds.indexOf(iid); } QContactFilter SeasideCache::aggregateFilter() const { QContactCollectionFilter filter; filter.setCollectionId(aggregateCollectionId()); return filter; } bool SeasideCache::ignoreContactForDisplayLabelGroups(const QContact &contact) const { // Don't include the self contact in name groups if (SeasideCache::apiId(contact) == SeasideCache::selfContactId()) { return true; } // Also ignore non-aggregate contacts return contact.collectionId() != aggregateCollectionId(); } QContactRelationship SeasideCache::makeRelationship(const QString &type, const QContactId &id1, const QContactId &id2) { QContactRelationship relationship; relationship.setRelationshipType(type); relationship.setFirst(id1); relationship.setSecond(id2); return relationship; } bool operator==(const SeasideCache::ResolveData &lhs, const SeasideCache::ResolveData &rhs) { // .listener and .requireComplete first because they are the cheapest comparisons // then .second before .first because .second is most likely to be unequal // .compare is derived from first and second so it does not need to be checked return lhs.listener == rhs.listener && lhs.requireComplete == rhs.requireComplete && lhs.second == rhs.second && lhs.first == rhs.first; } uint qHash(const SeasideCache::ResolveData &key, uint seed) { uint h1 = qHash(key.first, seed); uint h2 = qHash(key.second, seed); uint h3 = key.requireComplete; uint h4 = qHash(key.listener, seed); // Don't xor with seed here because h4 already did that return h1 ^ h2 ^ h3 ^ h4; } // Instantiate the contact ID functions for qtcontacts-sqlite #include nemo-qml-plugin-contacts-0.3.32/lib/seasidecache.h000066400000000000000000000533731475761757000220120ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 SEASIDECACHE_H #define SEASIDECACHE_H #include "contactcacheexport.h" #include "cacheconfiguration.h" // qtcontacts-sqlite-extensions #include #include #include // qtcontacts #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class CONTACTCACHE_EXPORT SeasideDisplayLabelGroupChangeListener { public: SeasideDisplayLabelGroupChangeListener() {} ~SeasideDisplayLabelGroupChangeListener() {} virtual void displayLabelGroupsUpdated(const QHash > &groups) = 0; }; class CONTACTCACHE_EXPORT SeasideCache : public QObject { Q_OBJECT public: enum FilterType { FilterNone, FilterAll, FilterFavorites, FilterTypesCount }; Q_ENUM(FilterType) enum FetchDataType { FetchNone = 0, FetchAccountUri = (1 << 0), FetchPhoneNumber = (1 << 1), FetchEmailAddress = (1 << 2), FetchOrganization = (1 << 3), FetchAvatar = (1 << 4), FetchFavorite = (1 << 5), FetchGender = (1 << 6), FetchTypesMask = (FetchAccountUri | FetchPhoneNumber | FetchEmailAddress | FetchOrganization | FetchAvatar | FetchFavorite | FetchGender) }; enum DisplayLabelOrder { FirstNameFirst = CacheConfiguration::FirstNameFirst, LastNameFirst = CacheConfiguration::LastNameFirst }; Q_ENUM(DisplayLabelOrder) enum ContactState { ContactAbsent, ContactPartial, ContactRequested, ContactComplete }; Q_ENUM(ContactState) enum { // Must be after the highest bit used in QContactStatusFlags::Flag HasValidOnlineAccount = (QContactStatusFlags::IsOnline << 1) }; struct ItemData { virtual ~ItemData() {} virtual void displayLabelOrderChanged(DisplayLabelOrder order) = 0; virtual void updateContact(const QContact &newContact, QContact *oldContact, ContactState state) = 0; virtual void constituentsFetched(const QList &ids) = 0; virtual void mergeCandidatesFetched(const QList &ids) = 0; virtual void aggregationOperationCompleted() = 0; virtual QList constituents() const = 0; }; struct CacheItem; struct ItemListener { ItemListener() : next(0), key(0) {} virtual ~ItemListener() {} virtual void itemUpdated(CacheItem *item) = 0; virtual void itemAboutToBeRemoved(CacheItem *item) = 0; ItemListener *next; void *key; }; struct CachedPhoneNumber { CachedPhoneNumber() {} CachedPhoneNumber(const QString &n, quint32 i) : normalizedNumber(n), iid(i) {} CachedPhoneNumber(const CachedPhoneNumber &other) : normalizedNumber(other.normalizedNumber), iid(other.iid) {} bool operator==(const CachedPhoneNumber &other) const { return other.normalizedNumber == normalizedNumber && other.iid == iid; } QString normalizedNumber; quint32 iid; }; struct CacheItem { CacheItem() : itemData(0), iid(0), statusFlags(0), contactState(ContactAbsent), listeners(0), filterMatchRole(-1) {} CacheItem(const QContact &contact) : contact(contact), itemData(0), iid(internalId(contact)), statusFlags(contact.detail().flagsValue()), contactState(ContactAbsent), listeners(0), filterMatchRole(-1) {} QContactId apiId() const { return SeasideCache::apiId(contact); } ItemListener *appendListener(ItemListener *listener, void *key) { if (listeners) { ItemListener *existing(listeners); while (existing->next) { existing = existing->next; } existing->next = listener; } else { listeners = listener; } listener->next = 0; listener->key = key; return listener; } bool removeListener(ItemListener *listener) { if (listeners) { ItemListener *existing(listeners); ItemListener **previous = &listeners; while (existing) { if (existing == listener) { *previous = listener->next; return true; } previous = &existing->next; existing = existing->next; } } return false; } ItemListener *listener(void *key) { ItemListener *existing(listeners); while (existing && (existing->key != key) && (existing->next)) { existing = existing->next; } return (existing && (existing->key == key)) ? existing : 0; } QContact contact; ItemData *itemData; quint32 iid; quint64 statusFlags; ContactState contactState; ItemListener *listeners; QString displayLabelGroup; QString displayLabel; int filterMatchRole; }; struct ContactLinkRequest { ContactLinkRequest(const QContactId &id) : contactId(id), constituentsFetched(false) {} ContactLinkRequest(const ContactLinkRequest &req) : contactId(req.contactId), constituentsFetched(req.constituentsFetched) {} QContactId contactId; bool constituentsFetched; }; class ListModel : public QAbstractListModel { public: ListModel(QObject *parent = 0) : QAbstractListModel(parent) {} virtual ~ListModel() {} virtual void sourceAboutToRemoveItems(int begin, int end) = 0; virtual void sourceItemsRemoved() = 0; virtual void sourceAboutToInsertItems(int begin, int end) = 0; virtual void sourceItemsInserted(int begin, int end) = 0; virtual void sourceDataChanged(int begin, int end) = 0; virtual void sourceItemsChanged() = 0; virtual void makePopulated() = 0; virtual void updateDisplayLabelOrder() = 0; virtual void updateSortProperty() = 0; virtual void updateGroupProperty() = 0; virtual void updateSectionBucketIndexCache() = 0; virtual void saveContactComplete(int localId, int aggregateId) = 0; }; struct ResolveListener { virtual ~ResolveListener() {} virtual void addressResolved(const QString &first, const QString &second, CacheItem *item) = 0; }; struct ChangeListener { virtual ~ChangeListener() {} virtual void itemUpdated(CacheItem *item) = 0; virtual void itemAboutToBeRemoved(CacheItem *item) = 0; }; static SeasideCache *instance(); static QContactManager *manager(); static QContactId apiId(const QContact &contact); static QContactId apiId(quint32 iid); static bool validId(const QContactId &id); static quint32 internalId(const QContact &contact); static quint32 internalId(const QContactId &id); static void registerModel(ListModel *model, FilterType type, FetchDataType requiredTypes = FetchNone, FetchDataType extraTypes = FetchNone); static void unregisterModel(ListModel *model); static void registerUser(QObject *user); static void unregisterUser(QObject *user); static void registerDisplayLabelGroupChangeListener(SeasideDisplayLabelGroupChangeListener *listener); static void unregisterDisplayLabelGroupChangeListener(SeasideDisplayLabelGroupChangeListener *listener); static void registerChangeListener(ChangeListener *listener, FetchDataType requiredTypes = FetchNone, FetchDataType extraTypes = FetchNone); static void unregisterChangeListener(ChangeListener *listener); static void unregisterResolveListener(ResolveListener *listener); static DisplayLabelOrder displayLabelOrder(); static QString sortProperty(); static QString groupProperty(); static int contactId(const QContact &contact); static int contactId(const QContactId &contactId); static CacheItem *existingItem(const QContactId &id); static CacheItem *existingItem(quint32 iid); static CacheItem *itemById(const QContactId &id, bool requireComplete = true); static CacheItem *itemById(int id, bool requireComplete = true); static QContactId selfContactId(); static QContact contactById(const QContactId &id); static void ensureCompletion(CacheItem *cacheItem); static void refreshContact(CacheItem *cacheItem); static QString displayLabelGroup(const CacheItem *cacheItem); static QStringList allDisplayLabelGroups(); static QHash > displayLabelGroupMembers(); static CacheItem *itemByPhoneNumber(const QString &number, bool requireComplete = true); static CacheItem *itemByEmailAddress(const QString &address, bool requireComplete = true); static CacheItem *itemByOnlineAccount(const QString &localUid, const QString &remoteUid, bool requireComplete = true); static CacheItem *resolvePhoneNumber(ResolveListener *listener, const QString &number, bool requireComplete = true); static CacheItem *resolveEmailAddress(ResolveListener *listener, const QString &address, bool requireComplete = true); static CacheItem *resolveOnlineAccount(ResolveListener *listener, const QString &localUid, const QString &remoteUid, bool requireComplete = true); static bool saveContact(const QContact &contact); static bool saveContacts(const QList &contacts); static bool removeContact(const QContact &contact); static bool removeContacts(const QList &contacts); static void aggregateContacts(const QContact &contact1, const QContact &contact2); static void disaggregateContacts(const QContact &contact1, const QContact &contact2); static bool fetchConstituents(const QContact &contact); static bool fetchMergeCandidates(const QContact &contact); static int importContacts(const QString &path); static QString exportContacts(); static const QList *contacts(FilterType filterType); static bool isPopulated(FilterType filterType); static QString getPrimaryName(const QContact &contact); static QString getSecondaryName(const QContact &contact); static QString primaryName(const QString &firstName, const QString &lastName); static QString secondaryName(const QString &firstName, const QString &lastName); static QString placeholderDisplayLabel(); static void decomposeDisplayLabel(const QString &formattedDisplayLabel, QContactName *nameDetail); static QString generateDisplayLabel(const QContact &contact, DisplayLabelOrder order = FirstNameFirst, bool fallbackToNonNameDetails = true); static QString generateDisplayLabelFromNonNameDetails(const QContact &contact); static QUrl filteredAvatarUrl(const QContact &contact, const QStringList &metadataFragments = QStringList()); static bool removeLocalAvatarFile(const QContact &contact, const QContactAvatar &avatar); static QString normalizePhoneNumber(const QString &input, bool validate = false); static QString minimizePhoneNumber(const QString &input, bool validate = false); static QContactCollection collectionFromId(const QContactCollectionId &collectionId); static QContactCollectionId aggregateCollectionId(); static QContactCollectionId localCollectionId(); bool event(QEvent *event); // For synchronizeLists() int insertRange(int index, int count, const QList &source, int sourceIndex) { return insertRange(m_syncFilter, index, count, source, sourceIndex); } int removeRange(int index, int count) { removeRange(m_syncFilter, index, count); return 0; } protected: void timerEvent(QTimerEvent *event); void setSortOrder(const QString &property); void startRequest(bool *idleProcessing); private slots: void contactsAvailable(); void contactIdsAvailable(); void relationshipsAvailable(); void requestStateChanged(QContactAbstractRequest::State state); void addressRequestStateChanged(QContactAbstractRequest::State state); void dataChanged(); void contactsAdded(const QList &contactIds); void contactsChanged(const QList &contactIds, const QList &typesChanged); void contactsPresenceChanged(const QList &contactIds); void contactsRemoved(const QList &contactIds); void displayLabelGroupsChanged(const QStringList &groups); void displayLabelOrderChanged(CacheConfiguration::DisplayLabelOrder order); void sortPropertyChanged(const QString &sortProperty); void displayStatusChanged(const QString &); private: enum PopulateProgress { Unpopulated, FetchFavorites, FetchMetadata, Populated }; SeasideCache(); ~SeasideCache(); static void checkForExpiry(); void keepPopulated(quint32 requiredTypes, quint32 extraTypes); void requestUpdate(); void appendContacts(const QList &contacts, FilterType filterType, bool partialFetch, const QSet &queryDetailTypes); void fetchContacts(); void updateContacts(const QList &contactIds, QList *updateList); void applyPendingContactUpdates(); void applyContactUpdates(const QList &contacts, const QSet &queryDetailTypes); void updateSectionBucketIndexCaches(); void resolveUnknownAddresses(const QString &first, const QString &second, CacheItem *item); bool updateContactIndexing(const QContact &oldContact, const QContact &contact, quint32 iid, const QSet &queryDetailTypes, CacheItem *item); void updateCache(CacheItem *item, const QContact &contact, bool partialFetch, bool initialInsert); void reportItemUpdated(CacheItem *item); void removeRange(FilterType filter, int index, int count); int insertRange(FilterType filter, int index, int count, const QList &queryIds, int queryIndex); void contactDataChanged(quint32 iid); void contactDataChanged(quint32 iid, FilterType filter); void removeContactData(quint32 iid, FilterType filter); void makePopulated(FilterType filter); void addToContactDisplayLabelGroup(quint32 iid, const QString &group, QSet *modifiedGroups = 0); void removeFromContactDisplayLabelGroup(quint32 iid, const QString &group, QSet *modifiedGroups = 0); void notifyDisplayLabelGroupsChanged(const QSet &groups); void updateConstituentAggregations(const QContactId &contactId); void completeContactAggregation(const QContactId &contact1Id, const QContactId &contact2Id); void resolveAddress(ResolveListener *listener, const QString &first, const QString &second, bool requireComplete); CacheItem *itemMatchingPhoneNumber(const QString &number, const QString &normalized, bool requireComplete); int contactIndex(quint32 iid, FilterType filter); QContactFilter filterForMergeCandidates(const QContact &contact) const; QContactFilter aggregateFilter() const; bool ignoreContactForDisplayLabelGroups(const QContact &contact) const; void notifySaveContactComplete(int constituentId, int aggregateId); static QContactRelationship makeRelationship(const QString &type, const QContactId &id1, const QContactId &id2); QList m_contacts[FilterTypesCount]; QBasicTimer m_expiryTimer; QBasicTimer m_fetchTimer; QHash m_people; QMultiHash m_phoneNumberIds; QHash m_emailAddressIds; QHash, quint32> m_onlineAccountIds; QMap > m_contactsToSave; QHash > m_contactDisplayLabelGroups; QList m_contactsToCreate; QHash, QList > > m_contactsToAppend; QList, QList > > m_contactsToUpdate; QMap > m_contactsToRemove; QList m_localContactsToRemove; QList m_changedContacts; QList m_presenceChangedContacts; QSet m_aggregatedContacts; QList m_contactsToFetchConstituents; QList m_contactsToFetchCandidates; QList m_contactsToLinkTo; QList > m_contactPairsToLink; QList m_relationshipsToSave; QList m_relationshipsToRemove; QList m_displayLabelGroupChangeListeners; QList m_changeListeners; QList m_models[FilterTypesCount]; QSet m_users; QHash m_expiredContacts; QContactFetchRequest m_fetchRequest; QContactFetchByIdRequest m_fetchByIdRequest; QContactIdFetchRequest m_contactIdRequest; QContactRelationshipFetchRequest m_relationshipsFetchRequest; QContactClearChangeFlagsRequest m_clearChangeFlagsRequest; QContactRemoveRequest m_removeRequest; QContactSaveRequest m_saveRequest; QContactRelationshipSaveRequest m_relationshipSaveRequest; QContactRelationshipRemoveRequest m_relationshipRemoveRequest; QList m_sortOrder; QList m_onlineSortOrder; FilterType m_syncFilter; int m_populated; int m_cacheIndex; int m_queryIndex; int m_fetchProcessedCount; int m_fetchByIdProcessedCount; DisplayLabelOrder m_displayLabelOrder; QString m_sortProperty; QString m_groupProperty; bool m_keepPopulated; PopulateProgress m_populateProgress; bool m_populating; // true if current m_fetchRequest makes progress quint32 m_fetchTypes; quint32 m_extraFetchTypes; quint32 m_dataTypesFetched; bool m_updatesPending; bool m_refreshRequired; bool m_contactsUpdated; bool m_displayOff; QSet m_constituentIds; QSet m_candidateIds; struct ResolveData { QString first; QString second; QString compare; // only used in m_unknownAddresses bool requireComplete; ResolveListener *listener; }; QHash m_resolveAddresses; QSet m_pendingResolve; // these have active requests already QList m_unknownResolveAddresses; QList m_unknownAddresses; QSet m_resolvedPhoneNumbers; QElapsedTimer m_timer; QElapsedTimer m_fetchPostponed; static SeasideCache *instancePtr; static int contactDisplayLabelGroupCount; static QStringList allContactDisplayLabelGroups; static QTranslator *engEnTranslator; static QTranslator *translator; friend bool operator==(const SeasideCache::ResolveData &lhs, const SeasideCache::ResolveData &rhs); friend uint qHash(const SeasideCache::ResolveData &key, uint seed); }; bool operator==(const SeasideCache::ResolveData &lhs, const SeasideCache::ResolveData &rhs); uint qHash(const SeasideCache::ResolveData &key, uint seed = 0); #endif nemo-qml-plugin-contacts-0.3.32/lib/seasidecontactbuilder.cpp000066400000000000000000000545071475761757000243040ustar00rootroot00000000000000/* * Copyright (c) 2015 - 2020 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 "seasidecontactbuilder.h" #include "seasidecache.h" #include "seasidepropertyhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { QContactFetchHint basicFetchHint() { QContactFetchHint fetchHint; fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships | QContactFetchHint::NoActionPreferences | QContactFetchHint::NoBinaryBlobs); return fetchHint; } QContactFilter localContactFilter() { QContactCollectionFilter filter; filter.setCollectionId(SeasideCache::localCollectionId()); return filter; } bool allCharactersMatchScript(const QString &s, QChar::Script script) { for (QString::const_iterator it = s.constBegin(), end = s.constEnd(); it != end; ++it) { if ((*it).script() != script) { return false; } } return true; } bool applyNameFixes(QContactName *nameDetail) { // Chinese names shouldn't have a middle name, so if it is present in a Han-script-only // name, it is probably wrong and it should be prepended to the first name instead. QString middleName = nameDetail->middleName(); if (middleName.isEmpty()) { return false; } QString firstName = nameDetail->firstName(); QString lastName = nameDetail->lastName(); if (!allCharactersMatchScript(middleName, QChar::Script_Han) || (!firstName.isEmpty() && !allCharactersMatchScript(firstName, QChar::Script_Han)) || (!lastName.isEmpty() && !allCharactersMatchScript(lastName, QChar::Script_Han))) { return false; } nameDetail->setFirstName(middleName + firstName); nameDetail->setMiddleName(QString()); return true; } bool nameIsEmpty(const QContactName &name) { if (name.isEmpty()) return true; return (name.prefix().isEmpty() && name.firstName().isEmpty() && name.middleName().isEmpty() && name.lastName().isEmpty() && name.suffix().isEmpty()); } QString contactNameString(const QContact &contact) { QStringList details; QContactName name(contact.detail()); if (nameIsEmpty(name)) return QString(); details.append(name.prefix()); details.append(name.firstName()); details.append(name.middleName()); details.append(name.lastName()); details.append(name.suffix()); return details.join(QChar::fromLatin1('|')); } void setNickname(QContact &contact, const QString &text) { foreach (const QContactNickname &nick, contact.details()) { if (nick.nickname() == text) { return; } } QContactNickname nick; nick.setNickname(text); contact.saveDetail(&nick); } template QVariant detailValue(const T &detail, F field) { return detail.value(field); } typedef QMap DetailMap; DetailMap detailValues(const QContactDetail &detail) { DetailMap rv(detail.values()); 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 detailValuesSuperset(const QContactDetail &lhs, const QContactDetail &rhs) { // True if all values in rhs are present in lhs const DetailMap lhsValues(detailValues(lhs)); const DetailMap rhsValues(detailValues(rhs)); if (lhsValues.count() < rhsValues.count()) { return false; } foreach (const DetailMap::key_type &key, rhsValues.keys()) { if (!variantEqual(lhsValues[key], rhsValues[key])) { return false; } } return true; } static void fixupDetail(QContactDetail &) { } // Fixup QContactUrl because importer produces incorrectly typed URL field static void fixupDetail(QContactUrl &url) { QVariant urlField = url.value(QContactUrl::FieldUrl); if (!urlField.isNull()) { QString urlString = urlField.toString(); if (!urlString.isEmpty()) { url.setValue(QContactUrl::FieldUrl, QUrl(urlString)); } else { url.setValue(QContactUrl::FieldUrl, QVariant()); } } } // Fixup QContactOrganization because importer produces invalid department static void fixupDetail(QContactOrganization &org) { QVariant deptField = org.value(QContactOrganization::FieldDepartment); if (!deptField.isNull()) { QStringList deptList = deptField.toStringList(); // Remove any empty elements from the list QStringList::iterator it = deptList.begin(); while (it != deptList.end()) { if ((*it).isEmpty()) { it = deptList.erase(it); } else { ++it; } } if (!deptList.isEmpty()) { org.setValue(QContactOrganization::FieldDepartment, deptList); } else { org.setValue(QContactOrganization::FieldDepartment, QVariant()); } } } template bool mergeContactDetails(QContact *mergeInto, const QContact &mergeFrom, bool singular = false) { bool rv = false; QList existingDetails(mergeInto->details()); if (singular && !existingDetails.isEmpty()) return rv; foreach (T detail, mergeFrom.details()) { // Make any corrections to the input fixupDetail(detail); // See if the contact already has a detail which is a superset of this one bool found = false; foreach (const T &existing, existingDetails) { if (detailValuesSuperset(existing, detail)) { found = true; break; } } if (!found) { mergeInto->saveDetail(&detail); rv = true; } } return rv; } bool mergeContacts(QContact *mergeInto, const QContact &mergeFrom) { bool rv = false; // Update the existing contact with any details in the new import rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom, true); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); rv |= mergeContactDetails(mergeInto, mergeFrom); return rv; } } SeasideContactBuilder::SeasideContactBuilder() : d(new SeasideContactBuilderPrivate) { // defaults. override in the ctor of your derived type. d->manager = 0; d->propertyHandler = 0; d->unimportableDetailTypes = (QSet() << QContactDetail::TypeGlobalPresence << QContactDetail::TypeVersion); } SeasideContactBuilder::~SeasideContactBuilder() { delete d->propertyHandler; delete d; } /* * Returns a pointer to a valid QContactManager. * The default implementation uses the SeasideCache manager. */ QContactManager *SeasideContactBuilder::manager() { if (!d->manager) { d->manager = SeasideCache::manager(); } return d->manager; } /* * Returns a filter which will return the subset of contacts * in the manager which are potential merge candidates for * the imported contacts (ie, come from the same sync target). * * The default implementation will return a filter which matches * any local / was_local / Bluetooth contact. */ QContactFilter SeasideContactBuilder::mergeSubsetFilter() const { return localContactFilter(); } /* * Returns a versit property handler which will be used during * conversion of versit documents into QContact instances. * * The default implementation will return a SeasidePropertyHandler. */ QVersitContactHandler *SeasideContactBuilder::propertyHandler() { if (!d->propertyHandler) { d->propertyHandler = new SeasidePropertyHandler; } return d->propertyHandler; } /* * Merge the given (matching) \a local contact into the given * \a import contact, so that the \a import contact could be * later saved in the manager. * * Returns \c true if the \a import contact differed significantly * from the \a local contact (that is, if saving the returned * \a import contact would result in the \a local contact being * updated). * * The \a erase value will be set to true if the given import * contact should be erased from the imported contacts list if * there are no significant differences between it and the local * contact (that is, if the imported contact should be omitted * from later possible store operations, due to the fact that such * a store operation would be a no-op for that contact). * * The default implementation performs a non-destructive merge * and will set \a erase to true, which is the required behaviour * for an "import" style sync. * * Sync implementations which require remote-detail-removal * semantics, for example, should implement this function * differently (eg, prefer-local or prefer-remote), and should * possibly set \a erase to false if they don't wish to prune * the import list prior to save. */ bool SeasideContactBuilder::mergeLocalIntoImport(QContact &import, const QContact &local, bool *erase) { // this implementation does a (mostly) non-destructive detail-addition-merge. // other implementations may choose to prefer-local or prefer-remote. *erase = true; QContact temp(import); import = local; return mergeContacts(&import, temp); } /* * Merge the given (matching) \a otherImport contact into the given * \a import contact. This function is set \a erase to true if * the \a otherImport contact should be erased from the import list. * The function will return \c true if the \a import contact * is modified as a result of the merge. * * Returns \c true if the \a otherImport contact * should be erased from the import list, otherwise \c false. * * The default implementation performs a non-destructive merge * into the \a import contact, and then sets \a erase to true, which * is the required behaviour for an "import" style sync. * * Sync implementations which require one-to-one mapping between * import contacts and device-stored contacts should set \a erase * to false in this function. */ bool SeasideContactBuilder::mergeImportIntoImport(QContact &import, QContact &otherImport, bool *erase) { *erase = true; return mergeContacts(&import, otherImport); } /* * Import the given Versit \a documents as QContacts and return them. * The default implementation uses a SeasidePropertyHandler during import */ QList SeasideContactBuilder::importContacts(const QList &documents) { QVersitContactHandler *handler = propertyHandler(); QVersitContactImporter importer; importer.setPropertyHandler(handler); importer.importDocuments(documents); return importer.contacts(); } /* * Preprocess the given import contact prior to duplicate detection, * merging, and subsequent storage. * * The default implementation performs some fixes for common issues * encountered in NAME field details, and removes various detail types * which are not supported by the qtcontacts-sqlite manager engine. */ void SeasideContactBuilder::preprocessContact(QContact &contact) { // Fix up name (field ordering) if required QContactName nameDetail = contact.detail(); if (applyNameFixes(&nameDetail)) { contact.saveDetail(&nameDetail); } // Remove any details that our backend can't store, or which // the client wishes stripped from the imported contacts. foreach (QContactDetail detail, contact.details()) { if (d->unimportableDetailTypes.contains(detail.type())) { qDebug() << " Removing unimportable detail:" << detail; contact.removeDetail(&detail); } } // Set nickname by default if the name is empty if (contactNameString(contact).isEmpty()) { QContactName nameDetail = contact.detail(); contact.removeDetail(&nameDetail); if (contact.details().isEmpty()) { QString label = contact.detail().label(); if (label.isEmpty()) { label = SeasideCache::generateDisplayLabelFromNonNameDetails(contact); } setNickname(contact, label); } } } /* * Returns the index into the \a importedContacts list at which a * duplicate (merge candidate) of the contact at the given * \a contactIndex may be found, or \c -1 if no match is found. * * The default implementation uses a combination of GUID and * name / label matching to determine if a contact is duplicated * within the import list. * * The result will be used to merge any duplicated contacts within * the import list, which is the required behaviour when performing * an "import" style sync. Any implementation which requires * a one-to-one mapping between import contacts and stored device * contacts should instead return -1 from this function. */ int SeasideContactBuilder::previousDuplicateIndex(QList &importedContacts, int contactIndex) { QContact &contact(importedContacts[contactIndex]); const QString guid = contact.detail().guid(); const QString name = contactNameString(contact); const bool emptyName = name.isEmpty(); const QString label = contact.detail().label().isEmpty() ?SeasideCache::generateDisplayLabelFromNonNameDetails(contact) : contact.detail().label(); int previousIndex = -1; QHash::const_iterator git = d->importGuids.find(guid); if (git != d->importGuids.end()) { previousIndex = git.value(); if (!emptyName) { // If we have a GUID match, but names differ, ignore the match const QContact &previous(importedContacts[previousIndex]); const QString previousName = contactNameString(previous); if (!previousName.isEmpty() && (previousName != name)) { previousIndex = -1; // Remove the conflicting GUID from this contact QContactGuid guidDetail = contact.detail(); contact.removeDetail(&guidDetail); } } } if (previousIndex == -1) { if (!emptyName) { QHash::const_iterator nit = d->importNames.find(name); if (nit != d->importNames.end()) { previousIndex = nit.value(); } } else if (!label.isEmpty()) { // Only if name is empty, use displayLabel - probably SIM import QHash::const_iterator lit = d->importLabels.find(label); if (lit != d->importLabels.end()) { previousIndex = lit.value(); } } } if (previousIndex == -1) { // No previous duplicate detected. This is a new contact. // Update our identification hashes with this contact's info. if (!guid.isEmpty()) { d->importGuids.insert(guid, contactIndex); } if (!emptyName) { d->importNames.insert(name, contactIndex); } else if (!label.isEmpty()) { d->importLabels.insert(label, contactIndex); } } return previousIndex; } /* * Build up any indexes of information required to later determine * whether a given import contact is already represented on the * device (ie, if the "new" contact is actually a new contact, * or if it constitutes an update to a previously imported contact). * * The default implementation builds up hashes of GUID, name and * label information to use later during match detection. */ void SeasideContactBuilder::buildLocalDeviceContactIndexes() { // Find all names and GUIDs for local contacts that might match these contacts QContactFetchHint fetchHint(basicFetchHint()); fetchHint.setDetailTypesHint(QList() << QContactName::Type << QContactNickname::Type << QContactGuid::Type); QContactManager *mgr(manager()); foreach (const QContact &contact, mgr->contacts(mergeSubsetFilter(), QList(), fetchHint)) { const QString guid = contact.detail().guid(); const QString name = contactNameString(contact); if (!guid.isEmpty()) { d->existingGuids.insert(guid, contact.id()); } if (!name.isEmpty()) { d->existingNames.insert(name, contact.id()); d->existingContactNames.insert(contact.id(), name); } foreach (const QContactNickname &nick, contact.details()) { d->existingNicknames.insert(nick.nickname(), contact.id()); } } } /* * Returns the id of an existing local device contact which matches * the given import \a contact. This local device contact is a * previously-imported version of the "new" import \a contact. * * The default implementation uses the previously cached GUID, * name and label information to perform the match detection. */ QContactId SeasideContactBuilder::matchingLocalContactId(QContact &contact) { const QString guid = contact.detail().guid(); const QString name = contactNameString(contact); const bool emptyName = name.isEmpty(); QContactId existingId; QHash::const_iterator git = d->existingGuids.find(guid); if (git != d->existingGuids.end()) { existingId = *git; if (!emptyName) { // If we have a GUID match, but names differ, ignore the match QMap::iterator nit = d->existingContactNames.find(existingId); if (nit != d->existingContactNames.end()) { const QString &existingName(*nit); if (!existingName.isEmpty() && (existingName != name)) { existingId = QContactId(); // Remove the conflicting GUID from this contact QContactGuid guidDetail = contact.detail(); contact.removeDetail(&guidDetail); } } } } if (existingId.isNull()) { if (!emptyName) { QHash::const_iterator nit = d->existingNames.find(name); if (nit != d->existingNames.end()) { existingId = *nit; } } else { foreach (const QContactNickname nick, contact.details()) { const QString nickname(nick.nickname()); if (!nickname.isEmpty()) { QHash::const_iterator nit = d->existingNicknames.find(nickname); if (nit != d->existingNicknames.end()) { existingId = *nit; break; } } } } } return existingId; } nemo-qml-plugin-contacts-0.3.32/lib/seasidecontactbuilder.h000066400000000000000000000065371475761757000237510ustar00rootroot00000000000000/* * Copyright (c) 2015 - 2020 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 SEASIDECONTACTBUILDER_H #define SEASIDECONTACTBUILDER_H #include "contactcacheexport.h" #include #include #include #include #include QTCONTACTS_USE_NAMESPACE QTVERSIT_USE_NAMESPACE class CONTACTCACHE_EXPORT SeasideContactBuilderPrivate { public: QContactManager *manager; QVersitContactHandler *propertyHandler; QSet unimportableDetailTypes; QHash importGuids; QHash importNames; QHash importLabels; QHash existingGuids; QHash existingNames; QMap existingContactNames; QHash existingNicknames; QVariantMap extraData; // anything the derived type wants to store. }; class CONTACTCACHE_EXPORT SeasideContactBuilder { public: SeasideContactBuilder(); virtual ~SeasideContactBuilder(); virtual QVersitContactHandler *propertyHandler(); virtual QContactManager *manager(); virtual QContactFilter mergeSubsetFilter() const; virtual bool mergeLocalIntoImport(QContact &import, const QContact &local, bool *erase); virtual bool mergeImportIntoImport(QContact &import, QContact &otherImport, bool *erase); virtual QList importContacts(const QList &documents); virtual void preprocessContact(QContact &contact); virtual int previousDuplicateIndex(QList &importedContacts, int contactIndex); virtual void buildLocalDeviceContactIndexes(); virtual QContactId matchingLocalContactId(QContact &contact); protected: SeasideContactBuilderPrivate *d; }; #endif nemo-qml-plugin-contacts-0.3.32/lib/seasideexport.cpp000066400000000000000000000040531475761757000226120ustar00rootroot00000000000000/* * Copyright (c) 2014 - 2020 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 "seasideexport.h" #include "seasidepropertyhandler.h" #include QList SeasideExport::buildExportContacts(const QList &contacts) { SeasidePropertyHandler propertyHandler; QVersitContactExporter exporter; exporter.setDetailHandler(&propertyHandler); exporter.exportContacts(contacts); return exporter.documents(); } nemo-qml-plugin-contacts-0.3.32/lib/seasideexport.h000066400000000000000000000040151475761757000222550ustar00rootroot00000000000000/* * Copyright (c) 2014 - 2020 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 SEASIDEEXPORT_H #define SEASIDEEXPORT_H #include "contactcacheexport.h" #include #include QTCONTACTS_USE_NAMESPACE QTVERSIT_USE_NAMESPACE class CONTACTCACHE_EXPORT SeasideExport { SeasideExport(); ~SeasideExport(); public: static QList buildExportContacts(const QList &contacts); }; #endif nemo-qml-plugin-contacts-0.3.32/lib/seasideimport.cpp000066400000000000000000000167341475761757000226140ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 "seasideimport.h" #include #include #include #include namespace { QContactFetchHint basicFetchHint() { QContactFetchHint fetchHint; fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships | QContactFetchHint::NoActionPreferences | QContactFetchHint::NoBinaryBlobs); return fetchHint; } } QList SeasideImport::buildImportContacts( const QList &details, int *newCount, int *updatedCount, int *ignoredCount, SeasideContactBuilder *contactBuilder, bool skipLocalDupDetection) { if (newCount) *newCount = 0; if (updatedCount) *updatedCount = 0; int existingCount = 0; bool eraseMatch = false; SeasideContactBuilder *builder = contactBuilder ? contactBuilder : new SeasideContactBuilder; QList importedContacts = builder->importContacts(details); // Preprocess the imported contacts and merge any duplicates in the import list QList::iterator it = importedContacts.begin(); while (it != importedContacts.end()) { builder->preprocessContact(*it); int previousIndex = builder->previousDuplicateIndex(importedContacts, it - importedContacts.begin()); if (previousIndex != -1) { // Combine these duplicate contacts QContact &previous(importedContacts[previousIndex]); builder->mergeImportIntoImport(previous, *it, &eraseMatch); if (eraseMatch) { it = importedContacts.erase(it); } else { ++it; } } else { ++it; } } if (!skipLocalDupDetection) { // Build up information about local device contacts, so we can detect matches // in order to correctly set the appropriate ContactId in the imported contacts // prior to save (thereby ensuring correct add vs update save semantics). builder->buildLocalDeviceContactIndexes(); // Find any imported contacts that match contacts we already have QMap existingIds; it = importedContacts.begin(); while (it != importedContacts.end()) { QContactId existingId = builder->matchingLocalContactId(*it); if (!existingId.isNull()) { QMap::iterator eit = existingIds.find(existingId); if (eit == existingIds.end()) { // this match hasn't been seen before. existingIds.insert(existingId, (it - importedContacts.begin())); ++it; } else { // another import contact which matches that local contact has // been seen already. Merge these both-matching import contacts. QContact &previous(importedContacts[*eit]); builder->mergeImportIntoImport(previous, *it, &eraseMatch); if (eraseMatch) { it = importedContacts.erase(it); } else { ++it; } } } else { ++it; } } existingCount = existingIds.count(); if (existingCount > 0) { // Retrieve all the contacts that we have matches for QContactIdFilter idFilter; idFilter.setIds(existingIds.keys()); QSet modifiedContacts; QSet unmodifiedContacts; QHash unmodifiedErase; foreach (const QContact &contact, builder->manager()->contacts(idFilter & builder->mergeSubsetFilter(), QList(), basicFetchHint())) { QMap::const_iterator it = existingIds.find(contact.id()); if (it != existingIds.end()) { // Update the existing version of the contact with any new details QContact &importContact(importedContacts[*it]); bool modified = builder->mergeLocalIntoImport(importContact, contact, &eraseMatch); if (modified) { modifiedContacts.insert(importContact.id()); } else { unmodifiedContacts.insert(importContact.id()); unmodifiedErase.insert(importContact.id(), eraseMatch); } } else { qWarning() << "unable to update existing contact:" << contact.id(); } } if (!unmodifiedContacts.isEmpty()) { QList::iterator it = importedContacts.begin(); while (it != importedContacts.end()) { const QContact &importContact(*it); const QContactId contactId(importContact.id()); if (!modifiedContacts.contains(contactId) && unmodifiedContacts.contains(contactId) && unmodifiedErase.value(contactId, false) == true) { // This contact was not modified by import and should be erased from the import list - don't update it it = importedContacts.erase(it); --existingCount; } else { ++it; } } } } } if (updatedCount) *updatedCount = existingCount; if (newCount) *newCount = importedContacts.count() - existingCount; if (ignoredCount) // duplicates or insignificant updates *ignoredCount = details.count() - importedContacts.count(); return importedContacts; } nemo-qml-plugin-contacts-0.3.32/lib/seasideimport.h000066400000000000000000000042701475761757000222510ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 SEASIDEIMPORT_H #define SEASIDEIMPORT_H #include "contactcacheexport.h" #include "seasidecontactbuilder.h" #include #include QTCONTACTS_USE_NAMESPACE QTVERSIT_USE_NAMESPACE class CONTACTCACHE_EXPORT SeasideImport { SeasideImport(); ~SeasideImport(); public: static QList buildImportContacts(const QList &details, int *newCount = 0, int *updatedCount = 0, int *ignoredCount = 0, SeasideContactBuilder *builder = 0, bool skipLocalDupDetection = false); }; #endif nemo-qml-plugin-contacts-0.3.32/lib/seasidepropertyhandler.cpp000066400000000000000000000234671475761757000245250ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 Jolla Ltd 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 "seasidepropertyhandler.h" #include #include #include #include #include #include #include #include #include namespace { QContactAvatar avatarFromPhotoProperty(const QVersitProperty &property) { // if the property is a PHOTO property, store the data to disk // and then create an avatar detail which points to it. // The data might be either a URL, a file path, or encoded image data // It's hard to tell what the content is, because versit removes the encoding // information in the process of decoding the data... // Try to interpret the data as a URL QString path(property.variantValue().toString()); QUrl url(path); if (url.isValid()) { // Treat remote URL as a true URL, and reference it in the avatar if (!url.scheme().isEmpty() && !url.isLocalFile()) { QContactAvatar newAvatar; newAvatar.setImageUrl(url); // we have successfully processed this PHOTO property. return newAvatar; } } if (!url.isValid()) { // See if we can resolve the data as a local file path url = QUrl::fromLocalFile(path); } QByteArray photoData; if (url.isValid()) { // Try to read the data from the referenced file const QString filePath(url.path()); if (QFile::exists(filePath)) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Unable to process photo data as file:" << path; return QContactAvatar(); } else { photoData = file.readAll(); } } } if (photoData.isEmpty()) { // Try to interpret the encoded property data as the image photoData = property.variantValue().toByteArray(); if (photoData.isEmpty()) { qWarning() << "Failed to extract avatar data from vCard PHOTO property"; return QContactAvatar(); } } QImage img; bool loaded = img.loadFromData(photoData); if (!loaded) { qWarning() << "Failed to load avatar image from vCard PHOTO data"; return QContactAvatar(); } // We will save the avatar image to disk in the system's data location // Since we're importing user data, it should not require privileged access const QString subdirectory(QString::fromLatin1(".local/share/system/Contacts/avatars")); const QString photoDirPath(QDir::home().filePath(subdirectory)); // create the photo file dir if it doesn't exist. QDir photoDir; if (!photoDir.mkpath(photoDirPath)) { qWarning() << "Failed to create avatar image directory when loading avatar image from vCard PHOTO data"; return QContactAvatar(); } // construct the filename of the new avatar image. QString photoFilePath = QString::fromLatin1(QCryptographicHash::hash(photoData, QCryptographicHash::Md5).toHex()); photoFilePath = photoDirPath + QDir::separator() + photoFilePath + QString::fromLatin1(".jpg"); // save the file to disk bool saved = img.save(photoFilePath); if (!saved) { qWarning() << "Failed to save avatar image from vCard PHOTO data to" << photoFilePath; return QContactAvatar(); } qWarning() << "Successfully saved avatar image from vCard PHOTO data to" << photoFilePath; // save the avatar detail - TODO: mark the avatar as "owned by the contact" (remove on delete) QContactAvatar newAvatar; newAvatar.setImageUrl(QUrl::fromLocalFile(photoFilePath)); // we have successfully processed this PHOTO property. return newAvatar; } void processPhoto(const QVersitProperty &property, bool *alreadyProcessed, QList * updatedDetails) { QContactAvatar newAvatar = avatarFromPhotoProperty(property); if (!newAvatar.isEmpty()) { updatedDetails->append(newAvatar); *alreadyProcessed = true; } } void processOnlineAccount(const QVersitProperty &property, bool *alreadyProcessed, QList * updatedDetails) { // Create an online account instance for demo purposes; it will not be connected // to a registered telepathy account, so it won't actually be able to converse // Try to interpret the data as a stringlist const QString detail(property.variantValue().toString()); // The format is: URI/path/display-name/icon-path/service-provider/service-provider-display-name #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const QStringList details(detail.split(QLatin1Char(';'), Qt::KeepEmptyParts)); #else const QStringList details(detail.split(QLatin1Char(';'), QString::KeepEmptyParts)); #endif if (details.count() == 6) { QContactOnlineAccount qcoa; qcoa.setValue(QContactOnlineAccount::FieldAccountUri, details.at(0)); qcoa.setValue(QContactOnlineAccount__FieldAccountPath, details.at(1)); qcoa.setValue(QContactOnlineAccount__FieldAccountDisplayName, details.at(2)); qcoa.setValue(QContactOnlineAccount__FieldAccountIconPath, details.at(3)); qcoa.setValue(QContactOnlineAccount::FieldServiceProvider, details.at(4)); qcoa.setValue(QContactOnlineAccount__FieldServiceProviderDisplayName, details.at(5)); qcoa.setDetailUri(QString::fromLatin1("%1:%2").arg(details.at(1)).arg(details.at(0))); updatedDetails->append(qcoa); // Since it is a demo account, give it a random presence state #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const int state = (rand() % 4); #else const int state = (qrand() % 4); #endif QContactPresence presence; presence.setPresenceState(state == 3 ? QContactPresence::PresenceBusy : (state == 2 ? QContactPresence::PresenceAway : QContactPresence::PresenceAvailable)); presence.setLinkedDetailUris(QStringList() << qcoa.detailUri()); updatedDetails->append(presence); *alreadyProcessed = true; } else { qWarning() << "Invalid online account details:" << details; } } void ignoreDetail(const QContactSyncTarget &detail, QSet * processedFields, QList * toBeRemoved, QList * toBeAdded) { Q_UNUSED(detail) Q_UNUSED(processedFields) toBeAdded->clear(); toBeRemoved->clear(); } } class SeasidePropertyHandlerPrivate { public: QSet m_nonexportableDetails; }; SeasidePropertyHandler::SeasidePropertyHandler(const QSet &nonexportableDetails) : QVersitContactHandler() , priv(new SeasidePropertyHandlerPrivate) { priv->m_nonexportableDetails = nonexportableDetails; } SeasidePropertyHandler::~SeasidePropertyHandler() { delete priv; } void SeasidePropertyHandler::documentProcessed(const QVersitDocument &, QContact *) { // do nothing, have no state to clean. } void SeasidePropertyHandler::propertyProcessed(const QVersitDocument &, const QVersitProperty &property, const QContact &, bool *alreadyProcessed, QList * updatedDetails) { const QString propertyName(property.name().toLower()); if (propertyName == QLatin1String("photo")) { processPhoto(property, alreadyProcessed, updatedDetails); } else if (propertyName == QLatin1String("x-nemomobile-onlineaccount-demo")) { processOnlineAccount(property, alreadyProcessed, updatedDetails); } } void SeasidePropertyHandler::contactProcessed(const QContact &, QVersitDocument *) { } void SeasidePropertyHandler::detailProcessed(const QContact &, const QContactDetail &detail, const QVersitDocument &, QSet * processedFields, QList * toBeRemoved, QList * toBeAdded) { const QContactDetail::DetailType detailType(detail.type()); if (priv->m_nonexportableDetails.contains(detailType)) { ignoreDetail(static_cast(detail), processedFields, toBeRemoved, toBeAdded); } } QContactAvatar SeasidePropertyHandler::avatarFromPhotoProperty(const QVersitProperty &property) { return ::avatarFromPhotoProperty(property); } nemo-qml-plugin-contacts-0.3.32/lib/seasidepropertyhandler.h000066400000000000000000000067251475761757000241700ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 Jolla Ltd 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 PROPERTYHANDLER_H #define PROPERTYHANDLER_H #include "contactcacheexport.h" #include #include #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE QTVERSIT_USE_NAMESPACE /* SeasidePropertyHandler Some backends don't support saving PHOTO data directly. Instead, the PHOTO data needs to be extracted, saved to a file, and then the path to the file needs to be saved to the backend as a contact avatar url detail. Also support the X-NEMOMOBILE-ONLINEACCOUNT-DEMO property for loading demo online account data. */ class SeasidePropertyHandlerPrivate; class CONTACTCACHE_EXPORT SeasidePropertyHandler : public QVersitContactHandler { public: SeasidePropertyHandler(const QSet &nonexportableDetails = QSet()); ~SeasidePropertyHandler(); // QVersitContactImporterPropertyHandlerV2 void documentProcessed(const QVersitDocument &, QContact *); void propertyProcessed(const QVersitDocument &, const QVersitProperty &property, const QContact &, bool *alreadyProcessed, QList * updatedDetails); // QVersitContactExporterDetailHandlerV2 void contactProcessed(const QContact &, QVersitDocument *); void detailProcessed(const QContact &, const QContactDetail &detail, const QVersitDocument &, QSet * processedFields, QList * toBeRemoved, QList * toBeAdded); static QContactAvatar avatarFromPhotoProperty(const QVersitProperty &property); private: SeasidePropertyHandlerPrivate *priv; }; #endif // PROPERTYHANDLER_H nemo-qml-plugin-contacts-0.3.32/lib/synchronizelists.h000066400000000000000000000231271475761757000230350ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 SYNCHRONIZELISTS_H #define SYNCHRONIZELISTS_H // Helper utility to synchronize a cached list with some reference list with correct // QAbstractItemModel signals and filtering. // If the reference list is populated incrementally synchronizeList can be called multiple times with the // same variables c and r to progressively synchronize the lists. After the final call completeSynchronizeList // can be called to remove or append any items which remain unsynchronized. // The Filtered variants allow the reference list to be filtered by a callback function to // exclude unwanted items from the synchronized list. template bool compareIdentity(const T &item, const T &reference) { return item == reference; } template int insertRange(Agent *agent, int index, int count, const ReferenceList &source, int sourceIndex) { agent->insertRange(index, count, source, sourceIndex); return count; } template int removeRange(Agent *agent, int index, int count) { agent->removeRange(index, count); return 0; } template int updateRange(Agent *agent, int index, int count, const ReferenceList &source, int sourceIndex) { Q_UNUSED(agent); Q_UNUSED(index); Q_UNUSED(source); Q_UNUSED(sourceIndex); return count; } template class SynchronizeList { typedef typename CacheList::const_reference CacheItem; typedef typename ReferenceList::const_reference ReferenceItem; public: SynchronizeList( Agent *agent, const CacheList &cache, int &c, const ReferenceList &reference, int &r) : agent(agent), cache(cache), c(c), reference(reference), r(r) { int lastEqualC = c; int lastEqualR = r; for (; c < cache.count() && r < reference.count(); ++c, ++r) { if (compareIdentity(cache.at(c), reference.at(r))) { continue; } if (c > lastEqualC) { lastEqualC += updateRange(agent, lastEqualC, c - lastEqualC, reference, lastEqualR); c = lastEqualC; lastEqualR = r; } bool match = false; // Iterate through both the reference and cache lists in parallel looking for first // point of commonality, when that is found resolve the differences and continue // looking. int count = 1; for (; !match && c + count < cache.count() && r + count < reference.count(); ++count) { CacheItem cacheItem = cache.at(c + count); ReferenceItem referenceItem = reference.at(r + count); for (int i = 0; i <= count; ++i) { if (cacheMatch(i, count, referenceItem) || referenceMatch(i, count, cacheItem)) { match = true; break; } } } // Continue scanning the reference list if the cache has been exhausted. for (int re = r + count; !match && re < reference.count(); ++re) { ReferenceItem referenceItem = reference.at(re); for (int i = 0; i < count; ++i) { if (cacheMatch(i, re - r, referenceItem)) { match = true; break; } } } // Continue scanning the cache if the reference list has been exhausted. for (int ce = c + count; !match && ce < cache.count(); ++ce) { CacheItem cacheItem = cache.at(ce); for (int i = 0; i < count; ++i) { if (referenceMatch(i, ce - c, cacheItem)) { match = true; break; } } } if (!match) return; lastEqualC = c; lastEqualR = r; } if (c > lastEqualC) { updateRange(agent, lastEqualC, c - lastEqualC, reference, lastEqualR); } } private: // Tests if the cached contact id at i matches a referenceId. // If there is a match removes all items traversed in the cache since the previous match // and inserts any items in the reference set found to to not be in the cache. bool cacheMatch(int i, int count, ReferenceItem referenceItem) { if (compareIdentity(cache.at(c + i), referenceItem)) { if (i > 0) c += removeRange(agent, c, i); c += insertRange(agent, c, count, reference, r); r += count; return true; } else { return false; } } // Tests if the reference contact id at i matches a cacheId. // If there is a match inserts all items traversed in the reference set since the // previous match and removes any items from the cache that were not found in the // reference list. bool referenceMatch(int i, int count, CacheItem cacheItem) { if (compareIdentity(reference.at(r + i), cacheItem)) { c += removeRange(agent, c, count); if (i > 0) c += insertRange(agent, c, i, reference, r); r += i; return true; } else { return false; } } Agent * const agent; const CacheList &cache; int &c; const ReferenceList &reference; int &r; }; template void completeSynchronizeList( Agent *agent, const CacheList &cache, int &cacheIndex, const ReferenceList &reference, int &referenceIndex) { if (cacheIndex < cache.count()) { agent->removeRange(cacheIndex, cache.count() - cacheIndex); } if (referenceIndex < reference.count()) { agent->insertRange(cache.count(), reference.count() - referenceIndex, reference, referenceIndex); } cacheIndex = 0; referenceIndex = 0; } template void synchronizeList( Agent *agent, const CacheList &cache, int &cacheIndex, const ReferenceList &reference, int &referenceIndex) { SynchronizeList( agent, cache, cacheIndex, reference, referenceIndex); } template void synchronizeList(Agent *agent, const CacheList &cache, const ReferenceList &reference) { int cacheIndex = 0; int referenceIndex = 0; synchronizeList(agent, cache, cacheIndex, reference, referenceIndex); completeSynchronizeList(agent, cache, cacheIndex, reference, referenceIndex); } template ReferenceList filterList( Agent *agent, const ReferenceList &reference) { ReferenceList filtered; filtered.reserve(reference.count()); foreach (const typename ReferenceList::value_type &value, reference) if (agent->filterValue(value)) filtered.append(value); return filtered; } template void synchronizeFilteredList( Agent *agent, const CacheList &cache, int &cacheIndex, const ReferenceList &reference, int &referenceIndex) { ReferenceList filtered = filterList(agent, reference); synchronizeList(agent, cache, cacheIndex, filtered, referenceIndex); } template void synchronizeFilteredList(Agent *agent, const CacheList &cache, const ReferenceList &reference) { int cacheIndex = 0; int referenceIndex = 0; ReferenceList filtered = filterList(agent, reference); synchronizeList(agent, cache, cacheIndex, filtered, referenceIndex); completeSynchronizeList(agent, cache, cacheIndex, filtered, referenceIndex); } #endif nemo-qml-plugin-contacts-0.3.32/rpm/000077500000000000000000000000001475761757000172555ustar00rootroot00000000000000nemo-qml-plugin-contacts-0.3.32/rpm/nemo-qml-plugin-contacts-qt5.spec000066400000000000000000000050461475761757000255020ustar00rootroot00000000000000Name: nemo-qml-plugin-contacts-qt5 Summary: Nemo QML contacts library Version: 0.3.24 Release: 1 License: BSD URL: https://github.com/sailfishos/nemo-qml-plugin-contacts Source0: %{name}-%{version}.tar.bz2 Requires: qtcontacts-sqlite-qt5 >= 0.1.37 BuildRequires: pkgconfig(Qt5Core) BuildRequires: pkgconfig(Qt5Qml) BuildRequires: pkgconfig(Qt5Contacts) BuildRequires: pkgconfig(Qt5Versit) BuildRequires: pkgconfig(Qt5Test) BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions) >= 0.3.0 BuildRequires: pkgconfig(mlocale5) BuildRequires: pkgconfig(mce) BuildRequires: pkgconfig(mlite5) BuildRequires: pkgconfig(accounts-qt5) BuildRequires: libphonenumber-devel BuildRequires: qt5-qttools-linguist BuildRequires: qt5-qttools BuildRequires: qt5-qttools-qthelp-devel BuildRequires: sailfish-qdoc-template %description %{summary}. %package devel Summary: Nemo QML contacts devel headers Requires: %{name} = %{version}-%{release} %description devel %{summary}. %package doc Summary: Nemo QML contacts documentation %description doc %{summary}. %package ts-devel Summary: Translation source for nemo-qml-plugin-contacts-qt5 %description ts-devel %{summary}. %package tools Summary: Development tools for qmlcontacts License: BSD %description tools %{summary}. %package tests Summary: QML contacts plugin tests Requires: %{name} = %{version}-%{release} Requires: blts-tools %description tests %{summary}. %prep %setup -q -n %{name}-%{version} %build %qmake5 VERSION=%{version} %make_build %install %qmake5_install install -m 644 doc/html/*.html %{buildroot}/%{_docdir}/nemo-qml-plugin-contacts/ install -m 644 doc/html/nemo-qml-plugin-contacts.index %{buildroot}/%{_docdir}/nemo-qml-plugin-contacts/ install -m 644 doc/nemo-qml-plugin-contacts.qch %{buildroot}/%{_docdir}/nemo-qml-plugin-contacts/ %post -p /sbin/ldconfig %postun -p /sbin/ldconfig %files %license LICENSE.BSD %{_libdir}/libcontactcache-qt5.so.* %{_libdir}/qt5/qml/org/nemomobile/contacts/libnemocontacts.so %{_libdir}/qt5/qml/org/nemomobile/contacts/plugins.qmltypes %{_libdir}/qt5/qml/org/nemomobile/contacts/qmldir %{_datadir}/translations/nemo-qml-plugin-contacts_eng_en.qm %files devel %{_libdir}/libcontactcache-qt5.so %{_includedir}/contactcache-qt5/* %{_libdir}/pkgconfig/contactcache-qt5.pc %files doc %{_docdir}/nemo-qml-plugin-contacts/* %files ts-devel %{_datadir}/translations/source/nemo-qml-plugin-contacts.ts %files tools %{_bindir}/vcardconverter %{_bindir}/contacts-tool %files tests /opt/tests/nemo-qml-plugin-contacts-qt5/* nemo-qml-plugin-contacts-0.3.32/src/000077500000000000000000000000001475761757000172465ustar00rootroot00000000000000nemo-qml-plugin-contacts-0.3.32/src/knowncontacts.cpp000066400000000000000000000145351475761757000226550ustar00rootroot00000000000000/* * 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 #include #include #include #include #include #include "knowncontacts.h" static const auto KnownContactsSyncFolder = QStringLiteral("system/privileged/Contacts/knowncontacts"); static const auto KnownContactsSyncProfile = QStringLiteral("knowncontacts.Contacts"); static const auto MsyncdService = QStringLiteral("com.meego.msyncd"); static const auto SynchronizerPath = QStringLiteral("/synchronizer"); static const auto MsyncdInterface = MsyncdService; static const auto GalIdKey = QStringLiteral("id"); static const auto AccountIdKey = QStringLiteral("accountId"); /*! \qmltype KnownContacts \inqmlmodule org.nemomobile.contacts */ KnownContacts::KnownContacts(QObject *parent) : QObject(parent) , m_msyncd(MsyncdService, SynchronizerPath, MsyncdInterface) { if (!m_msyncd.isValid()) qWarning() << "Could not connect to msyncd: contacts are not synchronized automatically"; } KnownContacts::~KnownContacts() { } /*! \qmlmethod bool KnownContacts::storeContact(object contact) */ bool KnownContacts::storeContact(const QVariantMap &contact) { return storeContacts({contact}); } /*! \qmlmethod bool KnownContacts::storeContacts(array contacts) */ bool KnownContacts::storeContacts(const QVariantList &contacts) { QMap > accountContacts; for (const auto variant : contacts) { const QVariantMap contact = variant.toMap(); if (contact.isEmpty()) { qWarning() << "Cannot store contacts: a contact is not a mapping"; continue; } const QString contactGalId = contact.value(GalIdKey).toString(); if (contactGalId.isEmpty()) { qWarning() << "Cannot store contact: missing value for key 'id'"; continue; } const int accountId = contact.value(AccountIdKey).toInt(); if (accountId <= 0) { qWarning() << "Cannot store contact: missing value for key 'accountId'"; continue; } accountContacts[accountId].append(contact); } for (auto it = accountContacts.constBegin(); it != accountContacts.constEnd(); ++it) { const int accountId = it.key(); QSettings file(getPath(accountId), QSettings::IniFormat, this); if (!file.isWritable()) { qWarning() << "Can not store contacts:" << file.fileName() << "is not writable"; continue; } const QList &contacts = it.value(); for (const QVariantMap &contact : contacts) { const QString contactGalId = contact.value(GalIdKey).toString(); file.beginGroup(contactGalId); QMapIterator iter(contact); while (iter.hasNext()) { iter.next(); if (iter.key() != GalIdKey) { file.setValue(iter.key(), iter.value()); } } file.endGroup(); } file.sync(); } return synchronize(); } quint32 KnownContacts::getRandomNumber() { static std::random_device random; static std::mt19937 generator(random()); static std::uniform_int_distribution distribution; return distribution(generator); } QString KnownContacts::getRandomPath(int accountId) { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + KnownContactsSyncFolder + QDir::separator() + QStringLiteral("%1-contacts-%2.ini").arg(accountId).arg(getRandomNumber()); } const QString & KnownContacts::getPath(int accountId) { if (m_currentPath.isEmpty()) { auto path = getRandomPath(accountId); while (QFile::exists(path)) path = getRandomPath(accountId); m_currentPath.swap(path); } return m_currentPath; } bool KnownContacts::synchronize() { if (m_msyncd.isValid()) { QDBusPendingCall call = m_msyncd.asyncCall(QStringLiteral("startSync"), KnownContactsSyncProfile); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, &KnownContacts::syncStarted); return true; } qWarning() << "Can not start synchronizing knowncontacts: can not connect to msyncd"; return false; } void KnownContacts::syncStarted(QDBusPendingCallWatcher *call) { QDBusPendingReply reply = *call; if (reply.isValid()) { if (reply.value()) { } else { qWarning() << "Starting knowncontacts sync failed"; } } else { qWarning() << "Starting knowncontacts sync failed:" << reply.error(); } call->deleteLater(); } nemo-qml-plugin-contacts-0.3.32/src/knowncontacts.h000066400000000000000000000045501475761757000223160ustar00rootroot00000000000000/* * 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 KNOWNCONTACTS_H #define KNOWNCONTACTS_H #include #include #include #include #include class QDBusPendingCallWatcher; class KnownContacts : public QObject { Q_OBJECT public: KnownContacts(QObject *parent = 0); ~KnownContacts(); Q_INVOKABLE bool storeContact(const QVariantMap &contact); Q_INVOKABLE bool storeContacts(const QVariantList &contacts); private: QString m_currentPath; QDBusInterface m_msyncd; static quint32 getRandomNumber(); static QString getRandomPath(int accountId); const QString &getPath(int accountId); bool synchronize(); private slots: void syncStarted(QDBusPendingCallWatcher *call); }; #endif // KNOWNCONTACTS_H nemo-qml-plugin-contacts-0.3.32/src/plugin.cpp000066400000000000000000000104271475761757000212540ustar00rootroot00000000000000/* * Copyright (c) 2012 Robin Burchell * Copyright (c) 2012 – 2020 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 #include #include #include #include "seasideaddressbook.h" #include "seasideaddressbookmodel.h" #include "seasideaddressbookutil.h" #include "seasideperson.h" #include "seasidefilteredmodel.h" #include "seasidedisplaylabelgroupmodel.h" #include "seasidevcardmodel.h" #include "seasideconstituentmodel.h" #include "seasidemergecandidatemodel.h" #include "knowncontacts.h" template static QObject *singletonApiCallback(QQmlEngine *engine, QJSEngine *) { return new T(engine); } class AppTranslator: public QTranslator { Q_OBJECT public: AppTranslator(QObject *parent) : QTranslator(parent) { qApp->installTranslator(this); } virtual ~AppTranslator() { qApp->removeTranslator(this); } }; class Q_DECL_EXPORT NemoContactsPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.nemomobile.contacts") public: virtual ~NemoContactsPlugin() { } void initializeEngine(QQmlEngine *engine, const char *uri) { Q_ASSERT(uri == QLatin1String("org.nemomobile.contacts")); AppTranslator *engineeringEnglish = new AppTranslator(engine); AppTranslator *translator = new AppTranslator(engine); engineeringEnglish->load("nemo-qml-plugin-contacts_eng_en", "/usr/share/translations"); translator->load(QLocale(), "nemo-qml-plugin-contacts", "-", "/usr/share/translations"); } void registerTypes(const char *uri) { Q_ASSERT(uri == QLatin1String("org.nemomobile.contacts")); qmlRegisterType(uri, 1, 0, "PeopleModel"); qmlRegisterType(uri, 1, 0, "AddressBookModel"); qmlRegisterType(uri, 1, 0, "PeopleDisplayLabelGroupModel"); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) qmlRegisterType(); #endif qmlRegisterType(uri, 1, 0, "Person"); qmlRegisterType(uri, 1, 0, "PeopleVCardModel"); qmlRegisterType(uri, 1, 0, "ConstituentModel"); qmlRegisterType(uri, 1, 0, "MergeCandidateModel"); qmlRegisterUncreatableType(uri, 1, 0, "AddressBook", ""); qmlRegisterSingletonType(uri, 1, 0, "KnownContacts", singletonApiCallback); qmlRegisterSingletonType(uri, 1, 0, "AddressBookUtil", singletonApiCallback); } }; #include "plugin.moc" nemo-qml-plugin-contacts-0.3.32/src/plugins.qmltypes000066400000000000000000000543121475761757000225340ustar00rootroot00000000000000import QtQuick.tooling 1.2 // This file describes the plugin-supplied types contained in the library. // It is used for QML tooling purposes only. // // This file was auto-generated by: // 'qmlplugindump -nonrelocatable org.nemomobile.contacts 1.0' Module { dependencies: ["QtQuick 2.0"] Component { name: "KnownContacts" prototype: "QObject" exports: ["org.nemomobile.contacts/KnownContacts 1.0"] isCreatable: false isSingleton: true exportMetaObjectRevisions: [0] Method { name: "storeContact" type: "bool" Parameter { name: "contact"; type: "QVariantMap" } } Method { name: "storeContacts" type: "bool" Parameter { name: "contacts"; type: "QVariantList" } } } Component { name: "SeasideAddressBook" exports: ["org.nemomobile.contacts/AddressBook 1.0"] isCreatable: false exportMetaObjectRevisions: [0] Property { name: "id"; type: "string"; isReadonly: true } Property { name: "name"; type: "string" } Property { name: "color"; type: "QColor" } Property { name: "secondaryColor"; type: "QColor" } Property { name: "image"; type: "string" } Property { name: "extendedMetaData"; type: "QVariantMap" } Property { name: "accountId"; type: "int" } Property { name: "isAggregate"; type: "bool" } Property { name: "isLocal"; type: "bool" } Property { name: "readOnly"; type: "bool" } } Component { name: "SeasideAddressBookModel" prototype: "QAbstractListModel" exports: ["org.nemomobile.contacts/AddressBookModel 1.0"] exportMetaObjectRevisions: [0] Enum { name: "Role" values: { "AddressBookRole": 256 } } Property { name: "count"; type: "int"; isReadonly: true } Property { name: "contactId"; type: "int" } Method { name: "addressBookAt" type: "QVariant" Parameter { name: "index"; type: "int" } } } Component { name: "SeasideAddressBookUtil" prototype: "QObject" exports: ["org.nemomobile.contacts/AddressBookUtil 1.0"] isCreatable: false isSingleton: true exportMetaObjectRevisions: [0] Property { name: "addressBooks"; type: "QVariantList"; isReadonly: true } } Component { name: "SeasideConstituentModel" prototype: "SeasideSimpleContactModel" exports: ["org.nemomobile.contacts/ConstituentModel 1.0"] exportMetaObjectRevisions: [0] Property { name: "person"; type: "SeasidePerson"; isPointer: true } } Component { name: "SeasideDisplayLabelGroupModel" prototype: "QAbstractListModel" exports: ["org.nemomobile.contacts/PeopleDisplayLabelGroupModel 1.0"] exportMetaObjectRevisions: [0] Enum { name: "Role" values: { "NameRole": 256, "CompressedRole": 257, "CompressedContentRole": 258 } } Enum { name: "RequiredPropertyType" values: { "NoPropertyRequired": 0, "AccountUriRequired": 1, "PhoneNumberRequired": 2, "EmailAddressRequired": 4 } } Property { name: "count"; type: "int"; isReadonly: true } Property { name: "minimumCount"; type: "int"; isReadonly: true } Property { name: "maximumCount"; type: "int" } Property { name: "requiredProperty"; type: "int" } Method { name: "indexOf" type: "int" Parameter { name: "name"; type: "string" } } Method { name: "get" type: "QVariantMap" Parameter { name: "row"; type: "int" } } Method { name: "get" type: "QVariant" Parameter { name: "row"; type: "int" } Parameter { name: "role"; type: "int" } } } Component { name: "SeasideFilteredModel" prototype: "QAbstractListModel" exports: ["org.nemomobile.contacts/PeopleModel 1.0"] exportMetaObjectRevisions: [0] Enum { name: "FilterType" values: { "FilterNone": 0, "FilterAll": 1, "FilterFavorites": 2, "FilterTypesCount": 3 } } Enum { name: "RequiredPropertyType" values: { "NoPropertyRequired": 0, "AccountUriRequired": 1, "PhoneNumberRequired": 2, "EmailAddressRequired": 4, "OrganizationRequired": 8 } } Enum { name: "SearchablePropertyType" values: { "NoPropertySearchable": 0, "AccountUriSearchable": 1, "PhoneNumberSearchable": 2, "EmailAddressSearchable": 4, "OrganizationSearchable": 8 } } Enum { name: "DisplayLabelOrder" values: { "FirstNameFirst": 0, "LastNameFirst": 1 } } Enum { name: "PeopleRoles" values: { "FirstNameRole": 256, "LastNameRole": 257, "FavoriteRole": 258, "AvatarRole": 259, "AvatarUrlRole": 260, "SectionBucketRole": 261, "GlobalPresenceStateRole": 262, "ContactIdRole": 263, "PhoneNumbersRole": 264, "EmailAddressesRole": 265, "AccountUrisRole": 266, "AccountPathsRole": 267, "PersonRole": 268, "PrimaryNameRole": 269, "SecondaryNameRole": 270, "NicknameDetailsRole": 271, "PhoneDetailsRole": 272, "EmailDetailsRole": 273, "AccountDetailsRole": 274, "NoteDetailsRole": 275, "CompanyNameRole": 276, "TitleRole": 277, "RoleRole": 278, "NameDetailsRole": 279, "FilterMatchDataRole": 280, "AddressBookRole": 281 } } Property { name: "populated"; type: "bool"; isReadonly: true } Property { name: "filterType"; type: "FilterType" } Property { name: "displayLabelOrder"; type: "DisplayLabelOrder" } Property { name: "sortProperty"; type: "string"; isReadonly: true } Property { name: "groupProperty"; type: "string"; isReadonly: true } Property { name: "filterPattern"; type: "string" } Property { name: "requiredProperty"; type: "int" } Property { name: "searchableProperty"; type: "int" } Property { name: "searchByFirstNameCharacter"; type: "bool" } Property { name: "count"; type: "int"; isReadonly: true } Property { name: "placeholderDisplayLabel"; type: "string"; isReadonly: true } Signal { name: "savePersonSucceeded" Parameter { name: "localId"; type: "int" } Parameter { name: "aggregateId"; type: "int" } } Signal { name: "savePersonFailed" } Method { name: "get" type: "QVariantMap" Parameter { name: "row"; type: "int" } } Method { name: "get" type: "QVariant" Parameter { name: "row"; type: "int" } Parameter { name: "role"; type: "int" } } Method { name: "savePerson" type: "bool" Parameter { name: "person"; type: "SeasidePerson"; isPointer: true } } Method { name: "savePeople" type: "bool" Parameter { name: "people"; type: "QVariantList" } } Method { name: "personByRow" type: "SeasidePerson*" Parameter { name: "row"; type: "int" } } Method { name: "personById" type: "SeasidePerson*" Parameter { name: "id"; type: "int" } } Method { name: "personByPhoneNumber" type: "SeasidePerson*" Parameter { name: "number"; type: "string" } Parameter { name: "requireComplete"; type: "bool" } } Method { name: "personByPhoneNumber" type: "SeasidePerson*" Parameter { name: "number"; type: "string" } } Method { name: "personByEmailAddress" type: "SeasidePerson*" Parameter { name: "email"; type: "string" } Parameter { name: "requireComplete"; type: "bool" } } Method { name: "personByEmailAddress" type: "SeasidePerson*" Parameter { name: "email"; type: "string" } } Method { name: "personByOnlineAccount" type: "SeasidePerson*" Parameter { name: "localUid"; type: "string" } Parameter { name: "remoteUid"; type: "string" } Parameter { name: "requireComplete"; type: "bool" } } Method { name: "personByOnlineAccount" type: "SeasidePerson*" Parameter { name: "localUid"; type: "string" } Parameter { name: "remoteUid"; type: "string" } } Method { name: "selfPerson"; type: "SeasidePerson*" } Method { name: "removePerson" Parameter { name: "person"; type: "SeasidePerson"; isPointer: true } } Method { name: "removePeople" Parameter { name: "people"; type: "QVariantList" } } Method { name: "importContacts" type: "int" Parameter { name: "path"; type: "string" } } Method { name: "exportContacts"; type: "string" } Method { name: "prepareSearchFilters" } Method { name: "firstIndexInGroup" type: "int" Parameter { name: "sectionBucket"; type: "string" } } Method { name: "setFilter" Parameter { name: "type"; type: "FilterType" } } Method { name: "search" Parameter { name: "pattern"; type: "string" } } } Component { name: "SeasideMergeCandidateModel" prototype: "SeasideSimpleContactModel" exports: ["org.nemomobile.contacts/MergeCandidateModel 1.0"] exportMetaObjectRevisions: [0] Property { name: "person"; type: "SeasidePerson"; isPointer: true } } Component { name: "SeasidePerson" prototype: "QObject" exports: ["org.nemomobile.contacts/Person 1.0"] exportMetaObjectRevisions: [0] attachedType: "SeasidePersonAttached" Enum { name: "DetailType" values: { "NoType": 0, "FirstNameType": 1, "LastNameType": 2, "MiddleNameType": 3, "PrefixType": 4, "SuffixType": 5, "CompanyType": 6, "TitleType": 7, "RoleType": 8, "DepartmentType": 9, "NicknameType": 10, "PhoneNumberType": 11, "EmailAddressType": 12, "OnlineAccountType": 13, "AddressType": 14, "WebsiteType": 15, "BirthdayType": 16, "AnniversaryType": 17, "GlobalPresenceStateType": 18, "NoteType": 19 } } Enum { name: "DetailSubType" values: { "NoSubType": 0, "PhoneSubTypeLandline": 1, "PhoneSubTypeMobile": 2, "PhoneSubTypeFax": 3, "PhoneSubTypePager": 4, "PhoneSubTypeVoice": 5, "PhoneSubTypeModem": 6, "PhoneSubTypeVideo": 7, "PhoneSubTypeCar": 8, "PhoneSubTypeBulletinBoardSystem": 9, "PhoneSubTypeMessagingCapable": 10, "PhoneSubTypeAssistant": 11, "PhoneSubTypeDtmfMenu": 12, "AddressSubTypeParcel": 13, "AddressSubTypePostal": 14, "AddressSubTypeDomestic": 15, "AddressSubTypeInternational": 16, "OnlineAccountSubTypeSip": 17, "OnlineAccountSubTypeSipVoip": 18, "OnlineAccountSubTypeImpp": 19, "OnlineAccountSubTypeVideoShare": 20, "WebsiteSubTypeHomePage": 21, "WebsiteSubTypeBlog": 22, "WebsiteSubTypeFavorite": 23, "AnniversarySubTypeWedding": 24, "AnniversarySubTypeEngagement": 25, "AnniversarySubTypeHouse": 26, "AnniversarySubTypeEmployment": 27, "AnniversarySubTypeMemorial": 28 } } Enum { name: "AddressField" values: { "AddressStreetField": 0, "AddressLocalityField": 1, "AddressRegionField": 2, "AddressPostcodeField": 3, "AddressCountryField": 4, "AddressPOBoxField": 5 } } Enum { name: "DetailLabel" values: { "NoLabel": 0, "HomeLabel": 1, "WorkLabel": 2, "OtherLabel": 3 } } Enum { name: "PresenceState" values: { "PresenceUnknown": 0, "PresenceAvailable": 1, "PresenceHidden": 2, "PresenceBusy": 3, "PresenceAway": 4, "PresenceExtendedAway": 5, "PresenceOffline": 6 } } Property { name: "id"; type: "int"; isReadonly: true } Property { name: "complete"; type: "bool"; isReadonly: true } Property { name: "firstName"; type: "string" } Property { name: "lastName"; type: "string" } Property { name: "middleName"; type: "string" } Property { name: "namePrefix"; type: "string" } Property { name: "nameSuffix"; type: "string" } Property { name: "sectionBucket"; type: "string"; isReadonly: true } Property { name: "displayLabel"; type: "string"; isReadonly: true } Property { name: "primaryName"; type: "string"; isReadonly: true } Property { name: "secondaryName"; type: "string"; isReadonly: true } Property { name: "companyName"; type: "string" } Property { name: "title"; type: "string" } Property { name: "role"; type: "string" } Property { name: "department"; type: "string" } Property { name: "favorite"; type: "bool" } Property { name: "avatarPath"; type: "QUrl" } Property { name: "avatarUrl"; type: "QUrl" } Property { name: "nicknameDetails"; type: "QVariantList" } Property { name: "phoneDetails"; type: "QVariantList" } Property { name: "emailDetails"; type: "QVariantList" } Property { name: "addressDetails"; type: "QVariantList" } Property { name: "websiteDetails"; type: "QVariantList" } Property { name: "birthday"; type: "QDateTime" } Property { name: "birthdayDetail"; type: "QVariantMap"; isReadonly: true } Property { name: "anniversaryDetails"; type: "QVariantList" } Property { name: "globalPresenceState"; type: "PresenceState"; isReadonly: true } Property { name: "accountDetails"; type: "QVariantList" } Property { name: "noteDetails"; type: "QVariantList" } Property { name: "syncTarget"; type: "string"; isReadonly: true } Property { name: "addressBook"; type: "SeasideAddressBook" } Property { name: "constituents"; type: "QList"; isReadonly: true } Property { name: "mergeCandidates"; type: "QList"; isReadonly: true } Property { name: "resolving"; type: "bool"; isReadonly: true } Signal { name: "contactChanged" } Signal { name: "contactRemoved" } Signal { name: "aggregationOperationFinished" } Signal { name: "addressResolved" } Signal { name: "dataChanged" } Method { name: "recalculateDisplayLabel" Parameter { name: "order"; type: "SeasideCache::DisplayLabelOrder" } } Method { name: "recalculateDisplayLabel" } Method { name: "filteredAvatarUrl" type: "QUrl" Parameter { name: "metadataFragments"; type: "QStringList" } } Method { name: "filteredAvatarUrl"; type: "QUrl" } Method { name: "ensureComplete" } Method { name: "contactData"; type: "QVariant" } Method { name: "setContactData" Parameter { name: "data"; type: "QVariant" } } Method { name: "resetContactData" } Method { name: "vCard"; type: "string" } Method { name: "avatarUrls"; type: "QStringList" } Method { name: "avatarUrlsExcluding" type: "QStringList" Parameter { name: "excludeMetadata"; type: "QStringList" } } Method { name: "hasValidPhoneNumber"; type: "bool" } Method { name: "aggregateInto" Parameter { name: "person"; type: "SeasidePerson"; isPointer: true } } Method { name: "disaggregateFrom" Parameter { name: "person"; type: "SeasidePerson"; isPointer: true } } Method { name: "fetchConstituents" } Method { name: "fetchMergeCandidates" } Method { name: "resolvePhoneNumber" Parameter { name: "number"; type: "string" } Parameter { name: "requireComplete"; type: "bool" } } Method { name: "resolvePhoneNumber" Parameter { name: "number"; type: "string" } } Method { name: "resolveEmailAddress" Parameter { name: "address"; type: "string" } Parameter { name: "requireComplete"; type: "bool" } } Method { name: "resolveEmailAddress" Parameter { name: "address"; type: "string" } } Method { name: "resolveOnlineAccount" Parameter { name: "localUid"; type: "string" } Parameter { name: "remoteUid"; type: "string" } Parameter { name: "requireComplete"; type: "bool" } } Method { name: "resolveOnlineAccount" Parameter { name: "localUid"; type: "string" } Parameter { name: "remoteUid"; type: "string" } } Method { name: "removeDuplicatePhoneNumbers" type: "QVariantList" Parameter { name: "phoneNumbers"; type: "QVariantList" } } Method { name: "removeDuplicateOnlineAccounts" type: "QVariantList" Parameter { name: "onlineAccounts"; type: "QVariantList" } } Method { name: "removeDuplicateEmailAddresses" type: "QVariantList" Parameter { name: "emailAddresses"; type: "QVariantList" } } Method { name: "decomposeName" type: "QVariantMap" Parameter { name: "name"; type: "string" } } } Component { name: "SeasidePersonAttached" prototype: "QObject" Property { name: "selfPerson"; type: "SeasidePerson"; isReadonly: true; isPointer: true } Method { name: "normalizePhoneNumber" type: "string" Parameter { name: "input"; type: "string" } } Method { name: "minimizePhoneNumber" type: "string" Parameter { name: "input"; type: "string" } } Method { name: "validatePhoneNumber" type: "string" Parameter { name: "input"; type: "string" } } Method { name: "removeDuplicatePhoneNumbers" type: "QVariantList" Parameter { name: "phoneNumbers"; type: "QVariantList" } } Method { name: "removeDuplicateOnlineAccounts" type: "QVariantList" Parameter { name: "onlineAccounts"; type: "QVariantList" } } Method { name: "removeDuplicateEmailAddresses" type: "QVariantList" Parameter { name: "emailAddresses"; type: "QVariantList" } } } Component { name: "SeasideSimpleContactModel" prototype: "QAbstractListModel" Enum { name: "Role" values: { "IdRole": 256, "PrimaryNameRole": 257, "SecondaryNameRole": 258, "DisplayLabelRole": 259, "AddressBookRole": 260 } } Property { name: "populated"; type: "bool"; isReadonly: true } Property { name: "count"; type: "int"; isReadonly: true } } Component { name: "SeasideVCardModel" prototype: "QAbstractListModel" exports: ["org.nemomobile.contacts/PeopleVCardModel 1.0"] exportMetaObjectRevisions: [0] Enum { name: "VCardRoles" values: { "PrimaryNameRole": 256, "SecondaryNameRole": 257, "AvatarRole": 258, "AvatarUrlRole": 259, "PhoneNumbersRole": 260, "EmailAddressesRole": 261, "NicknameDetailsRole": 262, "PhoneDetailsRole": 263, "EmailDetailsRole": 264, "PersonRole": 265 } } Property { name: "source"; type: "QUrl" } Property { name: "displayLabelOrder"; type: "DisplayLabelOrder"; isReadonly: true } Property { name: "count"; type: "int"; isReadonly: true } Property { name: "defaultCodec"; type: "string" } Method { name: "getPerson" type: "SeasidePerson*" Parameter { name: "index"; type: "int" } } } } nemo-qml-plugin-contacts-0.3.32/src/qmldir000066400000000000000000000001151475761757000204560ustar00rootroot00000000000000module org.nemomobile.contacts plugin nemocontacts typeinfo plugins.qmltypes nemo-qml-plugin-contacts-0.3.32/src/seasideaddressbook.cpp000066400000000000000000000072221475761757000236130ustar00rootroot00000000000000/* * 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 "seasideaddressbook.h" // Seaside #include // qtcontacts-sqlite #include #include /*! \qmltype AddressBook \inqmlmodule org.nemomobile.contacts */ /*! \qmlproperty string AddressBook::name \qmlproperty color AddressBook::color \qmlproperty color AddressBook::secondaryColor \qmlproperty string AddressBook::image \qmlproperty object AddressBook::extendedMetaData \qmlproperty int AddressBook::accountId \qmlproperty bool AddressBook::isAggregate \qmlproperty bool AddressBook::isLocal \qmlproperty bool AddressBook::readOnly */ SeasideAddressBook::SeasideAddressBook() { } SeasideAddressBook::~SeasideAddressBook() { } bool SeasideAddressBook::operator==(const SeasideAddressBook &other) { return collectionId == other.collectionId; } /*! \qmlproperty string AddressBook::id */ QString SeasideAddressBook::idString() const { return collectionId.toString(); } SeasideAddressBook SeasideAddressBook::fromCollectionId(const QContactCollectionId &collectionId) { const QContactCollection collection = SeasideCache::manager()->collection(collectionId); SeasideAddressBook addressBook; addressBook.collectionId = collectionId; addressBook.extendedMetaData = collection.extendedMetaData(); addressBook.name = collection.metaData(QContactCollection::KeyName).toString(); addressBook.color = collection.metaData(QContactCollection::KeyColor).value(); addressBook.secondaryColor = collection.metaData(QContactCollection::KeySecondaryColor).value(); addressBook.image = collection.metaData(QContactCollection::KeyImage).toString(); addressBook.accountId = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); addressBook.isAggregate = collection.id() == SeasideCache::aggregateCollectionId(); addressBook.isLocal = collection.id() == SeasideCache::localCollectionId(); addressBook.readOnly = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_READONLY).toBool(); return addressBook; } nemo-qml-plugin-contacts-0.3.32/src/seasideaddressbook.h000066400000000000000000000056761475761757000232730ustar00rootroot00000000000000/* * 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 SEASIDEADDRESSBOOK_H #define SEASIDEADDRESSBOOK_H // Qt #include #include // Mobility #include QTCONTACTS_USE_NAMESPACE class SeasideAddressBook { Q_GADGET Q_PROPERTY(QString id READ idString) Q_PROPERTY(QString name MEMBER name) Q_PROPERTY(QColor color MEMBER color) Q_PROPERTY(QColor secondaryColor MEMBER secondaryColor) Q_PROPERTY(QString image MEMBER image) Q_PROPERTY(QVariantMap extendedMetaData MEMBER extendedMetaData) Q_PROPERTY(int accountId MEMBER accountId) Q_PROPERTY(bool isAggregate MEMBER isAggregate) Q_PROPERTY(bool isLocal MEMBER isLocal) Q_PROPERTY(bool readOnly MEMBER readOnly) public: SeasideAddressBook(); ~SeasideAddressBook(); bool operator==(const SeasideAddressBook &other); inline bool operator!=(const SeasideAddressBook &other) { return !(operator==(other)); } QString idString() const; static SeasideAddressBook fromCollectionId(const QContactCollectionId &id); QContactCollectionId collectionId; QVariantMap extendedMetaData; QString name; QColor color; QColor secondaryColor; QString image; int accountId = 0; bool isAggregate = false; bool isLocal = false; bool readOnly = false; }; Q_DECLARE_METATYPE(SeasideAddressBook) #endif // SEASIDEADDRESSBOOK_H nemo-qml-plugin-contacts-0.3.32/src/seasideaddressbookmodel.cpp000066400000000000000000000267411475761757000246430ustar00rootroot00000000000000/* * 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 "seasideaddressbookmodel.h" #include #include #include #include #include #include #include #include #include namespace { bool accountIsEnabled(Accounts::Account *account) { Accounts::Service srv; const Accounts::ServiceList &services = account->services(); for (const Accounts::Service &s : services) { if (s.serviceType().toLower() == QStringLiteral("carddav") || s.name().toLower().contains(QStringLiteral("carddav")) || s.name().toLower().contains(QStringLiteral("contacts"))) { srv = s; break; } } Accounts::AccountService globalSrv(account, Accounts::Service()); if (srv.isValid()) { Accounts::AccountService accSrv(account, srv); return globalSrv.isEnabled() && accSrv.isEnabled(); } else { return globalSrv.isEnabled(); } } bool addressBookIsEnabled(const QContactCollection &col, Accounts::Manager *accountManager) { const Accounts::AccountId accountId = col.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); Accounts::Account *account = (accountId > 0) ? accountManager->account(accountId) : nullptr; return !account || accountIsEnabled(account); } } /*! \qmltype AddressBookModel \inqmlmodule org.nemomobile.contacts */ SeasideAddressBookModel::SeasideAddressBookModel(QObject *parent) : QAbstractListModel(parent) , m_accountManager(new Accounts::Manager(this)) { const QList collections = SeasideCache::manager()->collections(); for (const QContactCollection &collection : collections) { if (addressBookIsEnabled(collection, m_accountManager)) { m_addressBooks.append(SeasideAddressBook::fromCollectionId(collection.id())); } } connect(SeasideCache::manager(), &QContactManager::collectionsAdded, this, &SeasideAddressBookModel::collectionsAdded); connect(SeasideCache::manager(), &QContactManager::collectionsRemoved, this, &SeasideAddressBookModel::collectionsRemoved); connect(SeasideCache::manager(), &QContactManager::collectionsChanged, this, &SeasideAddressBookModel::collectionsChanged); refilter(); } SeasideAddressBookModel::~SeasideAddressBookModel() { } void SeasideAddressBookModel::setContactId(int contactId) { if (m_contactId != contactId) { m_contactId = contactId; refilter(); emit contactIdChanged(); } } /*! \qmlproperty int AddressBookModel::contactId */ int SeasideAddressBookModel::contactId() const { return m_contactId; } /*! \qmlmethod AddressBook AddressBookModel::addressBookAt(int index) */ QVariant SeasideAddressBookModel::addressBookAt(int index) const { return QVariant::fromValue(m_filteredAddressBooks.value(index)); } QHash SeasideAddressBookModel::roleNames() const { QHash roles; roles.insert(AddressBookRole, "addressBook"); return roles; } /*! \qmlproperty int AddressBookModel::count */ int SeasideAddressBookModel::rowCount(const QModelIndex &parent) const { return !parent.isValid() ? m_filteredAddressBooks.count() : 0; } QVariant SeasideAddressBookModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_filteredAddressBooks.count()) return QVariant(); switch (role) { case AddressBookRole: return QVariant::fromValue(m_filteredAddressBooks.at(index.row())); } return QVariant(); } void SeasideAddressBookModel::classBegin() { } void SeasideAddressBookModel::componentComplete() { m_complete = true; refilter(); } void SeasideAddressBookModel::collectionsAdded(const QList &collectionIds) { QList collectionsMatchingFilter; for (const QContactCollectionId &id : collectionIds) { if (matchesFilter(id)) { if (addressBookIsEnabled(SeasideCache::manager()->collection(id), m_accountManager)) { collectionsMatchingFilter.append(id); } } } if (collectionsMatchingFilter.count() > 0) { beginInsertRows(QModelIndex(), m_filteredAddressBooks.count(), m_filteredAddressBooks.count() + collectionsMatchingFilter.count() - 1); } for (const QContactCollectionId &id : collectionsMatchingFilter) { m_filteredAddressBooks.append(SeasideAddressBook::fromCollectionId(id)); } for (const QContactCollectionId &id : collectionIds) { if (addressBookIsEnabled(SeasideCache::manager()->collection(id), m_accountManager)) { m_addressBooks.append(SeasideAddressBook::fromCollectionId(id)); } } if (collectionsMatchingFilter.count() > 0) { endInsertRows(); emit countChanged(); } } void SeasideAddressBookModel::collectionsRemoved(const QList &collectionIds) { for (const QContactCollectionId &id : collectionIds) { const int i = findCollection(id); if (i >= 0) { const int filteredIndex = findFilteredCollection(id); if (filteredIndex >= 0) { beginRemoveRows(QModelIndex(), filteredIndex, filteredIndex); m_filteredAddressBooks.removeAt(filteredIndex); } m_addressBooks.removeAt(i); if (filteredIndex >= 0) { endRemoveRows(); emit countChanged(); } } } } void SeasideAddressBookModel::collectionsChanged(const QList &collectionIds) { int startRow = 0; int endRow = 0; bool emitDataChanged = false; for (const QContactCollectionId &id : collectionIds) { const int i = findCollection(id); if (i >= 0) { const int filteredIndex = findFilteredCollection(id); if (filteredIndex >= 0) { startRow = qMin(filteredIndex, startRow); endRow = qMax(filteredIndex, endRow); emitDataChanged = true; } m_addressBooks.replace(i, SeasideAddressBook::fromCollectionId(id)); } } if (emitDataChanged) { static const QVector roles = QVector() << AddressBookRole; emit dataChanged(createIndex(startRow, 0), createIndex(endRow, 0), roles); } } bool SeasideAddressBookModel::matchesFilter(const QContactCollectionId &id) const { if (m_contactId <= 0) { // No contact filter set, so add all available collections to the model. return true; } if (m_allowedCollections.isEmpty()) { // A filter has been set but the constituents have not yet been fetched. return true; } return m_allowedCollections.contains(id); } void SeasideAddressBookModel::refilter() { if (!m_complete) { return; } if (m_contactId <= 0) { // No filter set, so update immediately filteredCollectionsChanged(); } else { // Find the constituents of the contact if (!m_relationshipsFetch) { m_relationshipsFetch = new QContactRelationshipFetchRequest(this); m_relationshipsFetch->setManager(SeasideCache::manager()); m_relationshipsFetch->setRelationshipType(QContactRelationship::Aggregates()); connect(m_relationshipsFetch, &QContactAbstractRequest::stateChanged, this, &SeasideAddressBookModel::requestStateChanged); } if (m_relationshipsFetch->state() == QContactAbstractRequest::ActiveState && !m_relationshipsFetch->cancel()) { qmlInfo(this) << "Unable to filter address books, cannot cancel active relationship request"; return; } m_allowedCollections.clear(); m_relationshipsFetch->setFirst(SeasideCache::apiId(m_contactId)); m_relationshipsFetch->start(); } } void SeasideAddressBookModel::requestStateChanged(QContactAbstractRequest::State state) { if (state != QContactAbstractRequest::FinishedState) return; // For each constituent of the contact, add the constituent's collections to the list of // (unique) allowed collections. for (const QContactRelationship &rel : m_relationshipsFetch->relationships()) { if (rel.relationshipType() == QContactRelationship::Aggregates()) { const QContactId constituentId = rel.second(); const QContactCollectionId collectionId = SeasideCache::manager()->contact(constituentId).collectionId(); if (!m_allowedCollections.contains(collectionId)) { m_allowedCollections.append(collectionId); } } } filteredCollectionsChanged(); } void SeasideAddressBookModel::filteredCollectionsChanged() { const int prevCount = rowCount(); beginResetModel(); m_filteredAddressBooks.clear(); for (const SeasideAddressBook &addressBook : m_addressBooks) { if (matchesFilter(addressBook.collectionId)) { m_filteredAddressBooks.append(addressBook); } } endResetModel(); if (prevCount != rowCount()) { emit countChanged(); } } int SeasideAddressBookModel::findCollection(const QContactCollectionId &id) const { for (int i = 0; i < m_addressBooks.count(); ++i) { if (m_addressBooks.at(i).collectionId == id) { return i; } } return -1; } int SeasideAddressBookModel::findFilteredCollection(const QContactCollectionId &id) const { for (int i = 0; i < m_filteredAddressBooks.count(); ++i) { if (m_filteredAddressBooks.at(i).collectionId == id) { return i; } } return -1; } nemo-qml-plugin-contacts-0.3.32/src/seasideaddressbookmodel.h000066400000000000000000000072201475761757000242770ustar00rootroot00000000000000/* * 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 SEASIDEADDRESSBOOKMODEL_H #define SEASIDEADDRESSBOOKMODEL_H #include "seasideaddressbook.h" #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class SeasideAddressBookModel : public QAbstractListModel, public QQmlParserStatus { Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(int contactId READ contactId WRITE setContactId NOTIFY contactIdChanged) public: enum Role { AddressBookRole = Qt::UserRole, }; Q_ENUM(Role) SeasideAddressBookModel(QObject *parent = 0); ~SeasideAddressBookModel(); void setContactId(int contactId); int contactId() const; Q_INVOKABLE QVariant addressBookAt(int index) const; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; signals: void countChanged(); void contactIdChanged(); protected: void classBegin() override; void componentComplete() override; private: void collectionsAdded(const QList &collectionIds); void collectionsRemoved(const QList &collectionIds); void collectionsChanged(const QList &collectionIds); int findCollection(const QContactCollectionId &id) const; int findFilteredCollection(const QContactCollectionId &id) const; bool matchesFilter(const QContactCollectionId &id) const; void refilter(); void filteredCollectionsChanged(); void requestStateChanged(QContactAbstractRequest::State state); QList m_addressBooks; QList m_filteredAddressBooks; QList m_allowedCollections; QContactRelationshipFetchRequest *m_relationshipsFetch = nullptr; Accounts::Manager *m_accountManager = nullptr; int m_contactId = -1; bool m_complete = false; }; #endif nemo-qml-plugin-contacts-0.3.32/src/seasideaddressbookutil.cpp000066400000000000000000000100451475761757000245060ustar00rootroot00000000000000/* * 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 "seasideaddressbookutil.h" #include #include /*! \qmltype AddressBookUtil \inqmlmodule org.nemomobile.contacts */ SeasideAddressBookUtil::SeasideAddressBookUtil(QObject *parent) : QObject(parent) { const QList collections = SeasideCache::manager()->collections(); for (const QContactCollection &collection : collections) { m_addressBooks.append(QVariant::fromValue(SeasideAddressBook::fromCollectionId(collection.id()))); } connect(SeasideCache::manager(), &QContactManager::collectionsAdded, this, &SeasideAddressBookUtil::collectionsAdded); connect(SeasideCache::manager(), &QContactManager::collectionsRemoved, this, &SeasideAddressBookUtil::collectionsRemoved); connect(SeasideCache::manager(), &QContactManager::collectionsChanged, this, &SeasideAddressBookUtil::collectionsChanged); } SeasideAddressBookUtil::~SeasideAddressBookUtil() { } /*! \qmlproperty array AddressBookUtil::addressBooks */ QVariantList SeasideAddressBookUtil::addressBooks() const { return m_addressBooks; } void SeasideAddressBookUtil::collectionsAdded(const QList &collectionIds) { for (const QContactCollectionId &id : collectionIds) { m_addressBooks.append(QVariant::fromValue(SeasideAddressBook::fromCollectionId(id))); } emit addressBooksChanged(); } void SeasideAddressBookUtil::collectionsRemoved(const QList &collectionIds) { for (const QContactCollectionId &id : collectionIds) { const int i = findCollection(id); if (i >= 0) { m_addressBooks.removeAt(i); } } emit addressBooksChanged(); } void SeasideAddressBookUtil::collectionsChanged(const QList &collectionIds) { bool emitChanged = false; for (const QContactCollectionId &id : collectionIds) { const int i = findCollection(id); if (i >= 0) { emitChanged = true; m_addressBooks.replace(i, QVariant::fromValue(SeasideAddressBook::fromCollectionId(id))); } } if (emitChanged) { emit addressBooksChanged(); } } int SeasideAddressBookUtil::findCollection(const QContactCollectionId &id) const { for (int i = 0; i < m_addressBooks.count(); ++i) { if (m_addressBooks.at(i).value().collectionId == id) { return i; } } return -1; } nemo-qml-plugin-contacts-0.3.32/src/seasideaddressbookutil.h000066400000000000000000000046501475761757000241600ustar00rootroot00000000000000/* * 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 SEASIDEADDRESSBOOKUTIL_H #define SEASIDEADDRESSBOOKUTIL_H #include "seasideaddressbook.h" #include QTCONTACTS_USE_NAMESPACE class SeasideAddressBookUtil : public QObject { Q_OBJECT Q_PROPERTY(QVariantList addressBooks READ addressBooks NOTIFY addressBooksChanged) public: SeasideAddressBookUtil(QObject *parent = 0); ~SeasideAddressBookUtil(); QVariantList addressBooks() const; signals: void addressBooksChanged(); private: void collectionsAdded(const QList &collectionIds); void collectionsRemoved(const QList &collectionIds); void collectionsChanged(const QList &collectionIds); int findCollection(const QContactCollectionId &id) const; QVariantList m_addressBooks; }; #endif nemo-qml-plugin-contacts-0.3.32/src/seasideconstituentmodel.cpp000066400000000000000000000071351475761757000247160ustar00rootroot00000000000000/* * 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 "seasideconstituentmodel.h" #include "seasideperson.h" #include #include /*! \qmltype ConstituentModel \inqmlmodule org.nemomobile.contacts */ SeasideConstituentModel::SeasideConstituentModel(QObject *parent) : SeasideSimpleContactModel(parent) { } SeasideConstituentModel::~SeasideConstituentModel() { } /*! \qmlproperty Person ConstituentModel::person */ SeasidePerson* SeasideConstituentModel::person() const { return m_person; } void SeasideConstituentModel::setPerson(SeasidePerson *person) { if (m_person != person) { if (m_person) { m_person->disconnect(this); m_person = nullptr; } if (m_cacheItem) { m_cacheItem = nullptr; } m_person = person; reset(); emit personChanged(); } } void SeasideConstituentModel::reset() { if (!m_complete) { return; } if (m_person) { connect(m_person, &SeasidePerson::constituentsChanged, this, &SeasideConstituentModel::personConstituentsChanged); SeasideCache::CacheItem *cacheItem = SeasideCache::itemById(m_person->id()); if (cacheItem) { m_cacheItem = cacheItem; m_person->fetchConstituents(); } else { qmlInfo(this) << "Cannot find cache item for contact:" << m_person->id(); } } else if (m_contacts.count() > 0) { setContactIds(QList()); } } void SeasideConstituentModel::personConstituentsChanged() { if (m_person) { setContactIds(m_person->constituents()); } } void SeasideConstituentModel::itemUpdated(SeasideCache::CacheItem *item) { if (item == m_cacheItem) { m_person->fetchConstituents(); } SeasideSimpleContactModel::itemUpdated(item); } void SeasideConstituentModel::itemAboutToBeRemoved(SeasideCache::CacheItem *item) { if (item == m_cacheItem) { setPerson(nullptr); } SeasideSimpleContactModel::itemAboutToBeRemoved(item); } nemo-qml-plugin-contacts-0.3.32/src/seasideconstituentmodel.h000066400000000000000000000053231475761757000243600ustar00rootroot00000000000000/* * 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 SEASIDECONSTITUENTMODEL_H #define SEASIDECONSTITUENTMODEL_H #include "seasidesimplecontactmodel.h" class SeasidePerson; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #ifndef DECLARE_SEASIDE_PERSON #define DECLARE_SEASIDE_PERSON Q_DECLARE_OPAQUE_POINTER(SeasidePerson) #endif #endif QTCONTACTS_USE_NAMESPACE class SeasideConstituentModel : public SeasideSimpleContactModel { Q_OBJECT Q_PROPERTY(SeasidePerson* person READ person WRITE setPerson NOTIFY personChanged) public: SeasideConstituentModel(QObject *parent = 0); ~SeasideConstituentModel(); SeasidePerson* person() const; void setPerson(SeasidePerson *person); virtual void itemUpdated(SeasideCache::CacheItem *item) override; virtual void itemAboutToBeRemoved(SeasideCache::CacheItem *item) override; Q_SIGNALS: void personChanged(); protected: virtual void reset() override; private: void personConstituentsChanged(); SeasidePerson *m_person = nullptr; SeasideCache::CacheItem *m_cacheItem = nullptr; }; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) Q_MOC_INCLUDE("seasideperson.h") #endif #endif nemo-qml-plugin-contacts-0.3.32/src/seasidedisplaylabelgroupmodel.cpp000066400000000000000000000267761475761757000260750ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 "seasidedisplaylabelgroupmodel.h" #include "seasidestringlistcompressor.h" #include #include #include #include #include #include #include #include /*! \qmltype PeopleDisplayLabelGroupModel \inqmlmodule org.nemomobile.contacts */ SeasideDisplayLabelGroupModel::SeasideDisplayLabelGroupModel(QObject *parent) : QAbstractListModel(parent) , m_requiredProperty(NoPropertyRequired) , m_maximumCount(std::numeric_limits::max()) , m_complete(false) { SeasideCache::registerDisplayLabelGroupChangeListener(this); const QStringList &allGroups = SeasideCache::allDisplayLabelGroups(); QHash > existingGroups = SeasideCache::displayLabelGroupMembers(); if (!existingGroups.isEmpty()) { for (int i=0; i::iterator it = m_groups.begin(), end = m_groups.end(); for ( ; it != end; ++it) { SeasideDisplayLabelGroup &existing(*it); bool hasContacts = hasFilteredContacts(existing.contactIds); if (existing.hasContacts != hasContacts) { existing.hasContacts = hasContacts; needsRecompression = true; } } emit requiredPropertyChanged(); if (needsRecompression) { reloadCompressedGroups(); } } } /*! \qmlproperty int PeopleDisplayLabelGroupModel::minimumCount */ int SeasideDisplayLabelGroupModel::minimumCount() const { return SeasideStringListCompressor::minimumCompressionInputCount(); } /*! \qmlproperty int PeopleDisplayLabelGroupModel::maximumCount */ int SeasideDisplayLabelGroupModel::maximumCount() const { return m_maximumCount; } void SeasideDisplayLabelGroupModel::setMaximumCount(int maximumCount) { maximumCount = qMax(minimumCount(), maximumCount); if (maximumCount != m_maximumCount) { m_maximumCount = maximumCount; emit maximumCountChanged(); reloadCompressedGroups(); } } /*! \qmlmethod int PeopleDisplayLabelGroupModel::indexOf(string name) */ int SeasideDisplayLabelGroupModel::indexOf(const QString &name) const { return m_groupIndices.value(name); } /*! \qmlmethod object PeopleDisplayLabelGroupModel::get(int row) */ QVariantMap SeasideDisplayLabelGroupModel::get(int row) const { if (row < 0 || row > m_compressedGroups.count()) { return QVariantMap(); } QVariantMap m; QString group = m_compressedGroups.at(row); m.insert("name", group); m.insert("compressed", SeasideStringListCompressor::isCompressionMarker(group)); m.insert("compressedContent", m_compressedContent.value(row)); return m; } /*! \qmlmethod object PeopleDisplayLabelGroupModel::get(int row, int role) */ QVariant SeasideDisplayLabelGroupModel::get(int row, int role) const { if (row < 0 || row >= m_compressedGroups.count()) { return QVariant(); } const QString &group = m_compressedGroups.at(row); switch (role) { case NameRole: return group; case CompressedRole: return SeasideStringListCompressor::isCompressionMarker(group); case CompressedContentRole: return m_compressedContent.value(row); } return QVariant(); } QHash SeasideDisplayLabelGroupModel::roleNames() const { QHash roles; roles.insert(NameRole, "name"); roles.insert(CompressedRole, "compressed"); roles.insert(CompressedContentRole, "compressedContent"); return roles; } /*! \qmlproperty int PeopleDisplayLabelGroupModel::count */ int SeasideDisplayLabelGroupModel::rowCount(const QModelIndex &) const { return m_compressedGroups.count(); } void SeasideDisplayLabelGroupModel::classBegin() { } void SeasideDisplayLabelGroupModel::componentComplete() { m_complete = true; reloadCompressedGroups(); } QVariant SeasideDisplayLabelGroupModel::data(const QModelIndex &index, int role) const { return get(index.row(), role); } void SeasideDisplayLabelGroupModel::displayLabelGroupsUpdated(const QHash > &groups) { if (groups.isEmpty()) return; bool wasEmpty = m_groups.isEmpty(); bool needsRecompression = false; if (wasEmpty) { const QStringList &allGroups = SeasideCache::allDisplayLabelGroups(); if (!allGroups.isEmpty()) { for (int i=0; i >::const_iterator it = groups.constBegin(), end = groups.constEnd(); for ( ; it != end; ++it) { const QString group(it.key()); QList::iterator existingIt = m_groups.begin(), existingEnd = m_groups.end(); for ( ; existingIt != existingEnd; ++existingIt) { SeasideDisplayLabelGroup &existing(*existingIt); if (existing.name == group) { existing.contactIds = it.value(); bool hasContacts = hasFilteredContacts(existing.contactIds); if (existing.hasContacts != hasContacts) { existing.hasContacts = hasContacts; needsRecompression = true; } break; } } if (existingIt == existingEnd) { // Find the index of this group in the groups list const QStringList &allGroups = SeasideCache::allDisplayLabelGroups(); int allIndex = 0; int groupIndex = 0; for ( ; allIndex < allGroups.size() && allGroups.at(allIndex) != group; ++allIndex) { if (m_groups.at(groupIndex).name == allGroups.at(allIndex)) { ++groupIndex; } } if (allIndex < allGroups.count()) { // Insert this group m_groups.insert(groupIndex, SeasideDisplayLabelGroup(group, it.value())); needsRecompression = true; } else { qWarning() << "Could not find unknown group in allGroups!" << group; } } } if (needsRecompression) { reloadCompressedGroups(); } } bool SeasideDisplayLabelGroupModel::hasFilteredContacts(const QSet &contactIds) const { if (m_requiredProperty != NoPropertyRequired) { // Check if these contacts are included by the current filter foreach (quint32 iid, contactIds) { if (SeasideCache::CacheItem *item = SeasideCache::existingItem(iid)) { bool haveMatch = (m_requiredProperty & AccountUriRequired) && (item->statusFlags & QContactStatusFlags::HasOnlineAccount); haveMatch |= (m_requiredProperty & PhoneNumberRequired) && (item->statusFlags & QContactStatusFlags::HasPhoneNumber); haveMatch |= (m_requiredProperty & EmailAddressRequired) && (item->statusFlags & QContactStatusFlags::HasEmailAddress); if (haveMatch) { return true; } } else { qWarning() << "SeasideDisplayLabelGroupModel: obsolete contact" << iid; } } return false; } return contactIds.count() > 0; } void SeasideDisplayLabelGroupModel::reloadGroupIndices() { m_groupIndices.clear(); // Cache the index of each display label group for a fast lookup in indexOf(). If a group is // compressed, all of its contents are inserted into the hash with the same index. for (int i = 0; i < m_compressedGroups.count(); i++) { const QString &group = m_compressedGroups.at(i); if (SeasideStringListCompressor::isCompressionMarker(group)) { // An empty default must be passed in, otherwise if the key doesn't exist // the reference to the returned default-constructed value can become invalid const QStringList &groupContent = m_compressedContent.value(i, QStringList()); for (const QString &groupContentEntry : groupContent) { m_groupIndices.insert(groupContentEntry, i); } } else { m_groupIndices.insert(group, i); } } } void SeasideDisplayLabelGroupModel::reloadCompressedGroups() { if (!m_complete) { return; } QStringList labelGroups; for (const SeasideDisplayLabelGroup &group : m_groups) { if (group.hasContacts) { labelGroups.append(group.name); } } QMap compressedContent; QStringList compressedGroups = SeasideStringListCompressor::compress(labelGroups, m_maximumCount, &compressedContent); if (compressedGroups.count() < minimumCount()) { compressedGroups.clear(); } if (m_compressedGroups != compressedGroups) { const bool countChanging = m_compressedGroups.count() != compressedGroups.count(); beginResetModel(); m_compressedGroups = compressedGroups; m_compressedContent = compressedContent; reloadGroupIndices(); endResetModel(); if (countChanging) { emit countChanged(); } } } nemo-qml-plugin-contacts-0.3.32/src/seasidedisplaylabelgroupmodel.h000066400000000000000000000107731475761757000255300ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 SEASIDEPEOPLENAMEGROUPMODEL_H #define SEASIDEPEOPLENAMEGROUPMODEL_H #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class SeasideDisplayLabelGroup { public: SeasideDisplayLabelGroup() {} SeasideDisplayLabelGroup(const QString &n, const QSet &ids = QSet()) : name(n), contactIds(ids) { hasContacts = contactIds.count() > 0; } inline bool operator==(const SeasideDisplayLabelGroup &other) { return other.name == name; } QString name; bool hasContacts = false; QSet contactIds; }; class SeasideDisplayLabelGroupModel : public QAbstractListModel, public QQmlParserStatus, public SeasideDisplayLabelGroupChangeListener { Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(int minimumCount READ minimumCount CONSTANT) Q_PROPERTY(int maximumCount WRITE setMaximumCount READ maximumCount NOTIFY maximumCountChanged) Q_PROPERTY(int requiredProperty READ requiredProperty WRITE setRequiredProperty NOTIFY requiredPropertyChanged) public: enum Role { NameRole = Qt::UserRole, CompressedRole, CompressedContentRole }; Q_ENUM(Role) enum RequiredPropertyType { NoPropertyRequired = SeasideFilteredModel::NoPropertyRequired, AccountUriRequired = SeasideFilteredModel::AccountUriRequired, PhoneNumberRequired = SeasideFilteredModel::PhoneNumberRequired, EmailAddressRequired = SeasideFilteredModel::EmailAddressRequired }; Q_ENUM(RequiredPropertyType) SeasideDisplayLabelGroupModel(QObject *parent = 0); ~SeasideDisplayLabelGroupModel(); int requiredProperty() const; void setRequiredProperty(int type); int minimumCount() const; int maximumCount() const; void setMaximumCount(int maximumCount); Q_INVOKABLE int indexOf(const QString &name) const; Q_INVOKABLE QVariantMap get(int row) const; Q_INVOKABLE QVariant get(int row, int role) const; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; void displayLabelGroupsUpdated(const QHash > &groups); QHash roleNames() const override; void classBegin() override; void componentComplete() override; signals: void countChanged(); void maximumCountChanged(); void requiredPropertyChanged(); private: bool hasFilteredContacts(const QSet &contactIds) const; void reloadCompressedGroups(); void reloadGroupIndices(); QList m_groups; QStringList m_compressedGroups; QMap m_compressedContent; QHash m_groupIndices; int m_requiredProperty; int m_maximumCount; bool m_complete; }; #endif nemo-qml-plugin-contacts-0.3.32/src/seasidefilteredmodel.cpp000066400000000000000000001761361475761757000241450ustar00rootroot00000000000000/* * Copyright (c) 2013 - 2020 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 "seasidefilteredmodel.h" #include "seasideperson.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { const QByteArray displayLabelRole("displayLabel"); const QByteArray firstNameRole("firstName"); // To be deprecated const QByteArray lastNameRole("lastName"); // To be deprecated const QByteArray sectionBucketRole("sectionBucket"); const QByteArray favoriteRole("favorite"); const QByteArray avatarRole("avatar"); const QByteArray avatarUrlRole("avatarUrl"); const QByteArray globalPresenceStateRole("globalPresenceState"); const QByteArray contactIdRole("contactId"); const QByteArray phoneNumbersRole("phoneNumbers"); const QByteArray emailAddressesRole("emailAddresses"); const QByteArray accountUrisRole("accountUris"); const QByteArray accountPathsRole("accountPaths"); const QByteArray personRole("person"); const QByteArray primaryNameRole("primaryName"); const QByteArray secondaryNameRole("secondaryName"); const QByteArray nicknameDetailsRole("nicknameDetails"); const QByteArray phoneDetailsRole("phoneDetails"); const QByteArray emailDetailsRole("emailDetails"); const QByteArray accountDetailsRole("accountDetails"); const QByteArray noteDetailsRole("noteDetails"); const QByteArray companyNameRole("companyName"); const QByteArray titleRole("title"); const QByteArray roleRole("role"); const QByteArray nameDetailsRole("nameDetails"); const QByteArray filterMatchDataRole("filterMatchData"); const QByteArray addressBookRole("addressBook"); const ML10N::MLocale mLocale; template void insert(QList &dst, const QList &src) { for (const T &item : src) dst.append(item); } QSet alphabetCharacters() { QSet rv; for (const QString &c : mLocale.exemplarCharactersIndex()) { rv.insert(mLocale.toLower(c)); } return rv; } QMap decompositionMapping() { QMap rv; rv.insert(0x00df, QStringLiteral("ss")); // sharp-s ('sz' ligature) rv.insert(0x00e6, QStringLiteral("ae")); // 'ae' ligature rv.insert(0x00f0, QStringLiteral("d")); // eth rv.insert(0x00f8, QStringLiteral("o")); // o with stroke rv.insert(0x00fe, QStringLiteral("th")); // thorn rv.insert(0x0111, QStringLiteral("d")); // d with stroke rv.insert(0x0127, QStringLiteral("h")); // h with stroke rv.insert(0x0138, QStringLiteral("k")); // kra rv.insert(0x0142, QStringLiteral("l")); // l with stroke rv.insert(0x014b, QStringLiteral("n")); // eng rv.insert(0x0153, QStringLiteral("oe")); // 'oe' ligature rv.insert(0x0167, QStringLiteral("t")); // t with stroke rv.insert(0x017f, QStringLiteral("s")); // long s return rv; } QStringList tokenize(const QString &word) { static const QSet alphabet(alphabetCharacters()); static const QMap decompositions(decompositionMapping()); // Convert the word to canonical form, lowercase QString canonical(word.normalized(QString::NormalizationForm_C)); QStringList tokens; ML10N::MBreakIterator it(mLocale, canonical, ML10N::MBreakIterator::CharacterIterator); while (it.hasNext()) { const int position = it.next(); const int nextPosition = it.peekNext(); if (position < nextPosition) { const QString character(canonical.mid(position, (nextPosition - position))); QStringList matches; if (alphabet.contains(character)) { // This character is a member of the alphabet for this locale - do not decompose it matches.append(character); } else { // This character is not a member of the alphabet; decompose it to // assist with diacritic-insensitive matching QString normalized(character.normalized(QString::NormalizationForm_D)); matches.append(normalized); // For some characters, we want to match alternative spellings that do not correspond // to decomposition characters const uint codePoint(normalized.at(0).unicode()); QMap::const_iterator dit = decompositions.find(codePoint); if (dit != decompositions.end()) { matches.append(*dit); } } if (tokens.isEmpty()) { tokens.append(QString()); } int previousCount = tokens.count(); for (int i = 1; i < matches.count(); ++i) { // Make an additional copy of the existing tokens, for each new possible match for (int j = 0; j < previousCount; ++j) { tokens.append(tokens.at(j) + matches.at(i)); } } for (int j = 0; j < previousCount; ++j) { tokens[j].append(matches.at(0)); } } } return tokens; } QList makeSearchToken(const QString &word) { static QMap indexedTokens; static QHash > indexedWords; // Index all search text in lower case const QString lowered(mLocale.toLower(word)); QHash >::const_iterator wit = indexedWords.find(lowered); if (wit == indexedWords.end()) { QList indexed; // Index these tokens for later dereferencing for (const QString &token : tokenize(lowered)) { uint hashValue(qHash(token)); QMap::const_iterator tit = indexedTokens.find(hashValue); if (tit == indexedTokens.end()) { tit = indexedTokens.insert(hashValue, new QString(token)); } indexed.append(*tit); } wit = indexedWords.insert(lowered, indexed); } return *wit; } // Splits a string at word boundaries identified by MBreakIterator QList splitWords(const QString &string) { QList rv; if (!string.isEmpty()) { // Ignore any instances of '.' (frequently present in email addresses, but not useful) const QString dot(QStringLiteral(".")); ML10N::MBreakIterator it(mLocale, string, ML10N::MBreakIterator::WordIterator); while (it.hasNext()) { const int position = it.next(); const QString word(string.mid(position, (it.peekNext() - position)).trimmed()); if (!word.isEmpty() && word != dot) { for (const QString *alternative : makeSearchToken(word)) { rv.append(alternative); } } } } return rv; } QList extractSearchTerms(const QString &string) { QList rv; // Test all searches in lower case const QString lowered(mLocale.toLower(string)); ML10N::MBreakIterator it(mLocale, lowered, ML10N::MBreakIterator::WordIterator); while (it.hasNext()) { const int position = it.next(); const QString word(lowered.mid(position, (it.peekNext() - position)).trimmed()); if (!word.isEmpty()) { const bool apostrophe(word.length() == 1 && word.at(0) == QChar('\'')); if (apostrophe && !rv.isEmpty()) { // Special case - a trailing apostrophe is not counted as a component of the // previous word, although it is included in the word if there is a following character rv.last().last().append(word); } else { rv.append(tokenize(word)); } } } return rv; } QString stringPreceding(const QString &s, const QChar &c) { int index = s.indexOf(c); if (index != -1) { return s.left(index); } return s; } struct LessThanIndirect { template bool operator()(T lhs, T rhs) const { return *lhs < *rhs; } }; struct EqualIndirect { template bool operator()(T lhs, T rhs) const { return *lhs == *rhs; } }; struct FirstElementLessThanIndirect { template bool operator()(const Container *lhs, typename Container::const_iterator rhs) { return *lhs->cbegin() < *rhs; } template bool operator()(typename Container::const_iterator lhs, const Container *rhs) { return *lhs < *rhs->cbegin(); } }; template