pax_global_header 0000666 0000000 0000000 00000000064 14757617570 0014535 g ustar 00root root 0000000 0000000 52 comment=cb04d167c87d86b6cdc34dd4c3282c7b8be254ee
nemo-qml-plugin-contacts-0.3.32/ 0000775 0000000 0000000 00000000000 14757617570 0016457 5 ustar 00root root 0000000 0000000 nemo-qml-plugin-contacts-0.3.32/.gitignore 0000664 0000000 0000000 00000000224 14757617570 0020445 0 ustar 00root root 0000000 0000000 Makefile
/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.BSD 0000664 0000000 0000000 00000002653 14757617570 0020101 0 ustar 00root root 0000000 0000000 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 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.pri 0000664 0000000 0000000 00000000227 14757617570 0020441 0 ustar 00root root 0000000 0000000 CONFIG += 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.pro 0000664 0000000 0000000 00000000263 14757617570 0021020 0 ustar 00root root 0000000 0000000 TEMPLATE = 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/ 0000775 0000000 0000000 00000000000 14757617570 0017224 5 ustar 00root root 0000000 0000000 nemo-qml-plugin-contacts-0.3.32/doc/doc.pro 0000664 0000000 0000000 00000000511 14757617570 0020510 0 ustar 00root root 0000000 0000000 TEMPLATE = 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.qdoc 0000664 0000000 0000000 00000000441 14757617570 0021202 0 ustar 00root root 0000000 0000000 /*!
\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.qdocconf 0000664 0000000 0000000 00000003541 14757617570 0025602 0 ustar 00root root 0000000 0000000 project = 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"
navigation.homepage = "Nemo QML Plugin Contacts"
nemo-qml-plugin-contacts-0.3.32/lib/ 0000775 0000000 0000000 00000000000 14757617570 0017225 5 ustar 00root root 0000000 0000000 nemo-qml-plugin-contacts-0.3.32/lib/cacheconfiguration.cpp 0000664 0000000 0000000 00000021346 14757617570 0023572 0 ustar 00root root 0000000 0000000 /*
* 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.h 0000664 0000000 0000000 00000004737 14757617570 0023244 0 ustar 00root root 0000000 0000000 /*
* 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.h 0000664 0000000 0000000 00000003554 14757617570 0023266 0 ustar 00root root 0000000 0000000 /*
* 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.pri 0000664 0000000 0000000 00000003053 14757617570 0020510 0 ustar 00root root 0000000 0000000 include(../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.pro 0000664 0000000 0000000 00000001500 14757617570 0020511 0 ustar 00root root 0000000 0000000 include(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.cpp 0000664 0000000 0000000 00000402126 14757617570 0022337 0 ustar 00root root 0000000 0000000 /*
* 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