diff --git a/.gitignore b/.gitignore
index 670a70d..25b3f73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,7 @@ Makefile
*.qm
*.moc
*.obj
+/RPMS/
+*.list
+*-client
+tests/tst_*/tst_*
diff --git a/rpm/sociald.spec b/rpm/sociald.spec
index bddc9e7..b792022 100644
--- a/rpm/sociald.spec
+++ b/rpm/sociald.spec
@@ -16,6 +16,7 @@ BuildRequires: pkgconfig(libsignon-qt5)
BuildRequires: pkgconfig(accounts-qt5) >= 1.13
BuildRequires: pkgconfig(socialcache) >= 0.0.48
BuildRequires: pkgconfig(libsailfishkeyprovider)
+BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions)
BuildRequires: qt5-qttools-linguist
BuildRequires: ssu-devel
Requires: buteo-syncfw-qt5-msyncd
@@ -44,7 +45,7 @@ BuildRequires: pkgconfig(libkcalcoren-qt5)
Requires: %{name} = %{version}-%{release}
%description facebook-calendars
-Provides calendar synchronisation with Facebook
+%{summary}.
%files facebook-calendars
#out-of-process-plugin form:
@@ -74,7 +75,7 @@ BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions)
Requires: %{name} = %{version}-%{release}
%description facebook-contacts
-Provides contact synchronisation with Facebook
+%{summary}
%files facebook-contacts
#out-of-process-plugin form:
@@ -101,7 +102,7 @@ Summary: Provides image synchronisation with Facebook
Requires: %{name} = %{version}-%{release}
%description facebook-images
-Provides image synchronisation with Facebook
+%{summary}.
%files facebook-images
#out-of-process-plugin form:
@@ -129,7 +130,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description facebook-signon
-Provides signon credentials refreshing with Facebook
+%{summary}.
%files facebook-signon
#out-of-process-plugin form:
@@ -159,7 +160,7 @@ BuildRequires: pkgconfig(libkcalcoren-qt5)
Requires: %{name} = %{version}-%{release}
%description google-calendars
-Provides calendar synchronisation with Google
+%{summary}.
%files google-calendars
#out-of-process-plugin form:
@@ -189,7 +190,7 @@ BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions) >= 0.1.58
Requires: %{name} = %{version}-%{release}
%description google-contacts
-Provides contact synchronisation with Google
+%{summary}.
%files google-contacts
#out-of-process-plugin form:
@@ -217,7 +218,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description google-signon
-Provides signon credentials refreshing with Google
+%{summary}.
%files google-signon
#out-of-process-plugin form:
@@ -249,7 +250,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description twitter-notifications
-Provides notification synchronisation with Twitter
+%{summary}.
%files twitter-notifications
#out-of-process-plugin form:
@@ -283,7 +284,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description twitter-posts
-Provides post synchronisation with Twitter
+%{summary}.
%files twitter-posts
#out-of-process-plugin form:
@@ -312,7 +313,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description onedrive-signon
-Provides signon credentials refreshing with OneDrive
+%{summary}.
%files onedrive-signon
#out-of-process-plugin form:
@@ -342,7 +343,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description vk-posts
-Provides post synchronisation with VK
+%{summary}.
%files vk-posts
%{_datadir}/lipstick/notificationcategories/x-nemo.social.vk.statuspost.conf
@@ -370,7 +371,7 @@ Summary: Provides image synchronisation with Dropbox
Requires: %{name} = %{version}-%{release}
%description dropbox-images
-Provides image synchronisation with Dropbox
+%{summary}.
%files dropbox-images
#out-of-process-plugin form:
@@ -396,7 +397,7 @@ Summary: Provides image synchronisation with OneDrive
Requires: %{name} = %{version}-%{release}
%description onedrive-images
-Provides image synchronisation with OneDrive
+%{summary}.
%files onedrive-images
#out-of-process-plugin form:
@@ -425,7 +426,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description onedrive-backup
-Provides backup-blob synchronization for OneDrive
+%{summary}.
%files onedrive-backup
#out-of-process-plugin form:
@@ -454,7 +455,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description dropbox-backup
-Provides backup-blob synchronization for Dropbox
+%{summary}.
%files dropbox-backup
#out-of-process-plugin form:
@@ -484,7 +485,7 @@ BuildRequires: qt5-qttools-linguist
Requires: %{name} = %{version}-%{release}
%description vk-notifications
-Provides notification synchronisation with VK
+%{summary}.
%files vk-notifications
#out-of-process-plugin form:
@@ -514,7 +515,7 @@ BuildRequires: pkgconfig(libkcalcoren-qt5)
Requires: %{name} = %{version}-%{release}
%description vk-calendars
-Provides calendar synchronisation with VK
+%{summary}.
%files vk-calendars
#out-of-proces-plugin form:
@@ -544,7 +545,7 @@ BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions)
Requires: %{name} = %{version}-%{release}
%description vk-contacts
-Provides contact synchronisation with VK
+%{summary}.
%files vk-contacts
#out-of-process-plugin form:
@@ -571,7 +572,7 @@ Summary: Provides image synchronisation with VK
Requires: %{name} = %{version}-%{release}
%description vk-images
-Provides image synchronisation with VK
+%{summary}.
%files vk-images
#out-of-process-plugin form:
@@ -593,12 +594,34 @@ done
systemctl-user restart msyncd.service || :
+
+%package knowncontacts
+Summary: Store locally created contacts
+
+%description knowncontacts
+Buteo sync plugin that stores locally created contacts, such as email
+recipients.
+
+%post knowncontacts
+systemctl-user try-restart msyncd.service || :
+
+%files knowncontacts
+%defattr(-,root,root,-)
+#out-of-process-plugin form:
+%{_libdir}/buteo-plugins-qt5/oopp/knowncontacts-client
+#in-process-plugin form:
+#/usr/lib/buteo-plugins-qt5/knowncontacts-client.so
+%{_sysconfdir}/buteo/profiles/client/knowncontacts.xml
+%{_sysconfdir}/buteo/profiles/sync/knowncontacts.Contacts.xml
+
+
+
%package ts-devel
Summary: Translation source for sociald
Group: System/Applications
%description ts-devel
-Translation source for sociald
+%{summary}.
%files ts-devel
%defattr(-,root,root,-)
@@ -612,7 +635,7 @@ Requires: qt5-qtdeclarative-devel-tools
Requires: qt5-qtdeclarative-import-qttest
%description tests
-Automatable tests for sociald
+%{summary}.
%files tests
%defattr(-,root,root,-)
diff --git a/src/knowncontacts/README.md b/src/knowncontacts/README.md
new file mode 100644
index 0000000..4f5e778
--- /dev/null
+++ b/src/knowncontacts/README.md
@@ -0,0 +1,30 @@
+Knowncontacts Sync Plugin
+=========================
+This is a sync plugin for Buteo framework. It stores locally created contacts,
+such as email recipients that are not to be synced elsewhere and only ever
+syncs to the device side.
+
+In this case the "server" means QSettings files containing contacts. On sync
+this plugin reads the files and creates local contacts from the information.
+
+Contact file format
+-------------------
+A contact in QSettings file must have an id as group name and contact
+information as key value pairs. There are no requirements for the id but it
+must be consistent between syncs to avoid duplicating contact information. All
+keys are optional. A file may have as many contacts as needed. The file must
+end with file extension `ini` and they are stored in
+`~/.local/share/system/privileged/Contacts/knowncontacts`.
+
+```ini
+[john.doe.example]
+FirstName=John
+LastName=Doe
+EmailAddress=john@example.com
+```
+
+Supported keys:
+- FirstName, LastName
+- EmailAddress
+- Phone, HomePhone, MobilePhone
+- Company, Title, Office
diff --git a/src/knowncontacts/knowncontacts.Contacts.xml b/src/knowncontacts/knowncontacts.Contacts.xml
new file mode 100644
index 0000000..a9a0bf7
--- /dev/null
+++ b/src/knowncontacts/knowncontacts.Contacts.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/knowncontacts/knowncontacts.pro b/src/knowncontacts/knowncontacts.pro
new file mode 100644
index 0000000..3ebfc2c
--- /dev/null
+++ b/src/knowncontacts/knowncontacts.pro
@@ -0,0 +1,49 @@
+TARGET = knowncontacts-client
+
+DEFINES += "CLASSNAME=KnownContactsPlugin"
+DEFINES += CLASSNAME_H=\\\"knowncontactsplugin.h\\\"
+knowncontacts_sync_profile.path = /etc/buteo/profiles/sync
+knowncontacts_sync_profile.files = knowncontacts.Contacts.xml
+knowncontacts_client_plugin_xml.path = /etc/buteo/profiles/client
+knowncontacts_client_plugin_xml.files = knowncontacts.xml
+
+HEADERS += \
+ knowncontactsplugin.h \
+ knowncontactssyncer.h
+
+SOURCES += \
+ knowncontactsplugin.cpp \
+ knowncontactssyncer.cpp
+
+OTHER_FILES = \
+ knowncontacts.Contacts.xml
+ knowncontacts.xml
+
+QT -= gui
+QT += dbus
+
+CONFIG += link_pkgconfig c++14
+PKGCONFIG += buteosyncfw5 Qt5Contacts qtcontacts-sqlite-qt5-extensions
+
+QMAKE_CXXFLAGS = -Wall \
+ -g \
+ -Wno-cast-align \
+ -O2 -finline-functions
+
+TEMPLATE = app
+target.path = /usr/lib/buteo-plugins-qt5/oopp
+DEFINES += CLIENT_PLUGIN
+INCLUDE_DIR = $$system(pkg-config --cflags buteosyncfw5|cut -f2 -d'I')
+
+HEADERS += $$INCLUDE_DIR/ButeoPluginIfaceAdaptor.h \
+ $$INCLUDE_DIR/PluginCbImpl.h \
+ $$INCLUDE_DIR/PluginServiceObj.h
+
+SOURCES += $$INCLUDE_DIR/ButeoPluginIfaceAdaptor.cpp \
+ $$INCLUDE_DIR/PluginCbImpl.cpp \
+ $$INCLUDE_DIR/PluginServiceObj.cpp \
+ $$INCLUDE_DIR/plugin_main.cpp
+
+INSTALLS += target \
+ knowncontacts_sync_profile \
+ knowncontacts_client_plugin_xml
diff --git a/src/knowncontacts/knowncontacts.xml b/src/knowncontacts/knowncontacts.xml
new file mode 100644
index 0000000..935fa57
--- /dev/null
+++ b/src/knowncontacts/knowncontacts.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/knowncontacts/knowncontactsplugin.cpp b/src/knowncontacts/knowncontactsplugin.cpp
new file mode 100644
index 0000000..84a4b00
--- /dev/null
+++ b/src/knowncontacts/knowncontactsplugin.cpp
@@ -0,0 +1,181 @@
+/*
+ * Buteo sync plugin that stores locally created contacts
+ * Copyright (C) 2020 Open Mobile Platform LLC.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1 as published by the Free Software Foundation
+ * and appearing in the file LICENSE.LGPL included in the packaging
+ * of this file.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include
+#include
+#include
+#include
+#include "knowncontactsplugin.h"
+#include "knowncontactssyncer.h"
+
+const auto KnownContactsSyncFolder = QStringLiteral("system/privileged/Contacts/knowncontacts");
+
+extern "C" KnownContactsPlugin* createPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *cbInterface)
+{
+ return new KnownContactsPlugin(pluginName, profile, cbInterface);
+}
+
+extern "C" void destroyPlugin(KnownContactsPlugin *client)
+{
+ delete client;
+}
+
+KnownContactsPlugin::KnownContactsPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *cbInterface)
+ : Buteo::ClientPlugin(pluginName, profile, cbInterface)
+ , m_syncer(nullptr)
+{
+ FUNCTION_CALL_TRACE;
+}
+
+KnownContactsPlugin::~KnownContactsPlugin()
+{
+ FUNCTION_CALL_TRACE;
+}
+
+/**
+ * \!brief Initialize the plugin for the actual sync to happen
+ * This method will be invoked by the framework
+ */
+bool KnownContactsPlugin::init()
+{
+ FUNCTION_CALL_TRACE;
+
+ if (!m_syncer) {
+ auto path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
+ QDir::separator() + KnownContactsSyncFolder;
+ m_syncer = new KnownContactsSyncer(path, this);
+ LOG_DEBUG("KnownContacts plugin initialized for path" << path);
+ }
+
+ return true;
+}
+
+/**
+ * \!brief Uninitializes the plugin
+ * This method will be invoked by the framework
+ */
+bool KnownContactsPlugin::uninit()
+{
+ FUNCTION_CALL_TRACE;
+
+ delete m_syncer;
+ m_syncer = nullptr;
+
+ LOG_DEBUG("KnownContacts plugin uninitialized");
+ return true;
+}
+
+/**
+ * \!brief Start the actual sync. This method will be invoked by the
+ * framework
+ */
+bool KnownContactsPlugin::startSync()
+{
+ FUNCTION_CALL_TRACE;
+
+ if (!m_syncer)
+ return false;
+
+ connect(m_syncer, &KnownContactsSyncer::syncSucceeded,
+ this, &KnownContactsPlugin::syncSucceeded);
+
+ connect(m_syncer, &KnownContactsSyncer::syncFailed,
+ this, &KnownContactsPlugin::syncFailed);
+
+ LOG_DEBUG("Starting sync");
+
+ // Start the actual sync
+ return m_syncer->startSync();
+}
+
+/**
+ * \!brief Aborts sync. An abort can happen due to protocol errors,
+ * connection failures or by the user (via a UI)
+ */
+void KnownContactsPlugin::abortSync(Sync::SyncStatus status)
+{
+ Q_UNUSED(status)
+ FUNCTION_CALL_TRACE;
+
+ LOG_DEBUG("Aborting is not supported");
+ // Not supported, syncing usually takes very little time
+ // and there is not much to abort
+}
+
+Buteo::SyncResults KnownContactsPlugin::getSyncResults() const
+{
+ FUNCTION_CALL_TRACE;
+
+ return m_results;
+}
+
+/**
+ * This method is required if a profile has been deleted. The plugin
+ * has to implement the necessary cleanup (like temporary data, anchors etc.)
+ */
+bool KnownContactsPlugin::cleanUp()
+{
+ FUNCTION_CALL_TRACE;
+
+ bool success;
+
+ init(); // Ensure that syncer exists
+ success = m_syncer->purgeData();
+ uninit(); // Destroy syncer
+
+ return success;
+}
+
+void KnownContactsPlugin::syncSucceeded()
+{
+ FUNCTION_CALL_TRACE;
+
+ LOG_DEBUG("Sync successful");
+ m_results = Buteo::SyncResults(QDateTime::currentDateTimeUtc(),
+ Buteo::SyncResults::SYNC_RESULT_SUCCESS,
+ Buteo::SyncResults::NO_ERROR);
+ emit success(getProfileName(), QStringLiteral("Success"));
+}
+
+void KnownContactsPlugin::syncFailed(int errorCode)
+{
+ FUNCTION_CALL_TRACE;
+
+ LOG_DEBUG("Sync failed: " << errorCode);
+ m_results = Buteo::SyncResults(iProfile.lastSuccessfulSyncTime(),
+ Buteo::SyncResults::SYNC_RESULT_FAILED, errorCode);
+ emit error(getProfileName(), QStringLiteral("Failure"), errorCode);
+}
+
+/**
+ * Signal from the protocol engine about connectivity state changes
+ */
+void KnownContactsPlugin::connectivityStateChanged(Sync::ConnectivityType type, bool state)
+{
+ Q_UNUSED(type)
+ Q_UNUSED(state)
+ FUNCTION_CALL_TRACE;
+ // Stub
+}
diff --git a/src/knowncontacts/knowncontactsplugin.h b/src/knowncontacts/knowncontactsplugin.h
new file mode 100644
index 0000000..00ef0a7
--- /dev/null
+++ b/src/knowncontacts/knowncontactsplugin.h
@@ -0,0 +1,104 @@
+/*
+ * Buteo sync plugin that stores locally created contacts
+ * Copyright (C) 2020 Open Mobile Platform LLC.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1 as published by the Free Software Foundation
+ * and appearing in the file LICENSE.LGPL included in the packaging
+ * of this file.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#ifndef KNOWNCONTACTSPLUGIN_H
+#define KNOWNCONTACTSPLUGIN_H
+
+#include
+#include
+
+class KnownContactsSyncer;
+
+/*! \brief Implementation for client plugin
+ *
+ */
+class KnownContactsPlugin : public Buteo::ClientPlugin
+{
+ Q_OBJECT;
+
+public:
+ /*! \brief Constructor
+ *
+ * @param pluginName Name of this client plugin
+ * @param profile Sync profile
+ * @param cbInterface Pointer to the callback interface
+ */
+ KnownContactsPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *cbInterface);
+
+ /*! \brief Destructor
+ *
+ * Call uninit before destroying the object.
+ */
+ virtual ~KnownContactsPlugin();
+
+ //! @see SyncPluginBase::init
+ virtual bool init();
+
+ //! @see SyncPluginBase::uninit
+ virtual bool uninit();
+
+ //! @see ClientPlugin::startSync
+ virtual bool startSync();
+
+ //! @see SyncPluginBase::abortSync
+ virtual void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED);
+
+ //! @see SyncPluginBase::getSyncResults
+ virtual Buteo::SyncResults getSyncResults() const;
+
+ //! @see SyncPluginBase::cleanUp
+ virtual bool cleanUp();
+
+public slots:
+ //! @see SyncPluginBase::connectivityStateChanged
+ virtual void connectivityStateChanged(Sync::ConnectivityType type,
+ bool state);
+
+protected slots:
+ void syncSucceeded();
+
+ void syncFailed(int errorCode);
+
+private:
+ Buteo::SyncResults m_results;
+ KnownContactsSyncer *m_syncer;
+};
+
+/*! \brief Creates KnownContactsPlugin client plugin
+ *
+ * @param pluginName Name of this client plugin
+ * @param profile Profile to use
+ * @param cbInterface Pointer to the callback interface
+ * @return Client plugin on success, otherwise NULL
+ */
+extern "C" KnownContactsPlugin* createPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *cbInterface);
+
+/*! \brief Destroys KnownContactsPlugin client plugin
+ *
+ * @param client KnownContactsPlugin client plugin instance to destroy
+ */
+extern "C" void destroyPlugin(KnownContactsPlugin *client);
+
+#endif // KNOWNCONTACTSPLUGIN_H
diff --git a/src/knowncontacts/knowncontactssyncer.cpp b/src/knowncontacts/knowncontactssyncer.cpp
new file mode 100644
index 0000000..2980ca0
--- /dev/null
+++ b/src/knowncontacts/knowncontactssyncer.cpp
@@ -0,0 +1,279 @@
+/*
+ * Buteo sync plugin that stores locally created contacts
+ * Copyright (C) 2020 Open Mobile Platform LLC.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1 as published by the Free Software Foundation
+ * and appearing in the file LICENSE.LGPL included in the packaging
+ * of this file.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "knowncontactssyncer.h"
+
+const auto KnownContactsSyncTarget = QLatin1String("knowncontacts");
+const auto AccountId = QLatin1String("0");
+
+static void setGuid(QContact *contact, const QString &id);
+static void setNames(QContact *contact, const QSettings &file);
+static void setPhoneNumbers(QContact *contact, const QSettings &file);
+static void setEmailAddress(QContact *contact, const QSettings &file);
+static void setCompanyInfo(QContact *contact, const QSettings &file);
+
+KnownContactsSyncer::KnownContactsSyncer(QString path, QObject *parent)
+ : QObject(parent)
+ , QtContactsSqliteExtensions::TwoWayContactSyncAdapter(KnownContactsSyncTarget)
+ , m_syncFolder(path)
+{
+ FUNCTION_CALL_TRACE;
+}
+
+KnownContactsSyncer::~KnownContactsSyncer()
+{
+ FUNCTION_CALL_TRACE;
+}
+
+bool KnownContactsSyncer::startSync()
+{
+ FUNCTION_CALL_TRACE;
+
+ QDateTime remoteSince;
+ if (!initSyncAdapter(AccountId, KnownContactsSyncTarget)
+ || !readSyncStateData(&remoteSince, AccountId)) {
+ handleSyncFailure(InitFailure);
+ LOG_WARNING("Can not sync knowncontacts");
+ return false;
+ }
+
+ determineRemoteChanges(remoteSince);
+ return true;
+}
+
+void KnownContactsSyncer::determineRemoteChanges(const QDateTime &remoteSince)
+{
+ FUNCTION_CALL_TRACE;
+
+ QDir syncDir(m_syncFolder);
+ QStringList files;
+ for (const auto &file : syncDir.entryList(QStringList() << "*.ini", QDir::Files)) {
+ QFileInfo info(syncDir, file);
+ if (info.lastModified() >= remoteSince)
+ files.append(info.absoluteFilePath());
+ }
+ LOG_DEBUG(files.size() << "files to sync");
+ if (!QMetaObject::invokeMethod(this, "asyncDeterminedRemoteChanges",
+ Qt::QueuedConnection,
+ Q_ARG(const QStringList, files))) {
+ handleSyncFailure(CallFailure);
+ }
+}
+
+void KnownContactsSyncer::asyncDeterminedRemoteChanges(const QStringList files)
+{
+ FUNCTION_CALL_TRACE;
+
+ QList contacts;
+ for (const auto &path : files) {
+ QSettings file(path, QSettings::IniFormat);
+ contacts.append(readContacts(file));
+ }
+ LOG_DEBUG(contacts.size() << "contacts to store");
+ if (!storeRemoteChanges(QList(), &contacts, AccountId)) {
+ handleSyncFailure(StoreChangesFailure);
+ return;
+ }
+
+ QDateTime localSince;
+ QList locallyAdded, locallyModified, locallyDeleted;
+ determineLocalChanges(&localSince, &locallyAdded, &locallyModified, &locallyDeleted, AccountId);
+ if (!storeSyncStateData(AccountId)) {
+ handleSyncFailure(StoreStateFailure);
+ return;
+ }
+
+ for (const auto &path : files) {
+ if (!QLockFile(path + QStringLiteral(".lock")).tryLock()) {
+ LOG_DEBUG("File in use, not removing" << path);
+ } else if (!QFile::remove(path)) {
+ LOG_WARNING("Could not remove" << path);
+ }
+ }
+
+ LOG_DEBUG("knowncontacts sync finished successfully");
+ emit syncSucceeded();
+}
+
+template
+static inline T findDetail(QContact &contact, int field, const QString &value)
+{
+ T result;
+ QList details = contact.details();
+ for (T &detail : details) {
+ if (detail.value(field) == value) {
+ result = detail;
+ break;
+ }
+ }
+ return result;
+}
+
+static void setGuid(QContact *contact, const QString &id)
+{
+ Q_ASSERT(contact);
+ auto detail = contact->detail();
+ auto guid = QStringLiteral("%1:%2").arg(KnownContactsSyncTarget).arg(id);
+ detail.setGuid(guid);
+ contact->saveDetail(&detail);
+}
+
+static void setNames(QContact *contact, const QSettings &file)
+{
+ Q_ASSERT(contact);
+ const auto firstName = file.value("FirstName").toString();
+ const auto lastName = file.value("LastName").toString();
+ if (!firstName.isEmpty() || !lastName.isEmpty()) {
+ auto detail = contact->detail();
+ if (!firstName.isEmpty())
+ detail.setFirstName(firstName);
+ if (!lastName.isEmpty())
+ detail.setLastName(lastName);
+ contact->saveDetail(&detail);
+ }
+}
+
+// Using QVariant as optional (aka 'maybe') type
+static inline void addPhoneNumberDetail(QContact *contact, const QString &value,
+ const QVariant subType, const QVariant context)
+{
+ Q_ASSERT(contact);
+ if (!value.isEmpty()) {
+ auto detail = findDetail(*contact, QContactPhoneNumber::FieldNumber, value);
+ detail.setValue(QContactPhoneNumber::FieldNumber, value);
+ if (subType.isValid())
+ detail.setSubTypes({subType.value()});
+ if (context.isValid())
+ detail.setContexts({context.value()});
+ contact->saveDetail(&detail);
+ }
+}
+
+static void setPhoneNumbers(QContact *contact, const QSettings &file)
+{
+ Q_ASSERT(contact);
+ addPhoneNumberDetail(contact, file.value("Phone").toString(),
+ QContactPhoneNumber::SubTypeLandline, QVariant());
+ addPhoneNumberDetail(contact, file.value("HomePhone").toString(),
+ QContactPhoneNumber::SubTypeLandline, QContactDetail::ContextHome);
+ addPhoneNumberDetail(contact, file.value("MobilePhone").toString(),
+ QContactPhoneNumber::SubTypeMobile, QVariant());
+}
+
+static void setEmailAddress(QContact *contact, const QSettings &file)
+{
+ Q_ASSERT(contact);
+ const auto emailAddress = file.value("EmailAddress").toString();
+ if (!emailAddress.isEmpty()) {
+ auto detail = findDetail(
+ *contact, QContactEmailAddress::FieldEmailAddress, emailAddress);
+ detail.setValue(QContactEmailAddress::FieldEmailAddress, emailAddress);
+ contact->saveDetail(&detail);
+ }
+}
+
+static void setCompanyInfo(QContact *contact, const QSettings &file)
+{
+ Q_ASSERT(contact);
+ const auto company = file.value("Company").toString();
+ const auto title = file.value("Title").toString();
+ const auto office = file.value("Office").toString();
+ if (!title.isEmpty() || !office.isEmpty()) {
+ auto detail = contact->detail();
+ if (!company.isEmpty())
+ detail.setName(company);
+ if (!title.isEmpty())
+ detail.setTitle(title);
+ if (!office.isEmpty())
+ detail.setLocation(office);
+ contact->saveDetail(&detail);
+ }
+}
+
+QContact KnownContactsSyncer::getContact(const QString &id)
+{
+ QContactDetailFilter syncTargetFilter;
+ syncTargetFilter.setDetailType(QContactDetail::TypeSyncTarget, QContactSyncTarget::FieldSyncTarget);
+ syncTargetFilter.setValue(KnownContactsSyncTarget);
+ QContactDetailFilter guidFilter;
+ guidFilter.setDetailType(QContactDetail::TypeGuid, QContactGuid::FieldGuid);
+ guidFilter.setValue(QStringLiteral("%1:%2").arg(KnownContactsSyncTarget).arg(id));
+ guidFilter.setMatchFlags(QContactDetailFilter::MatchExactly);
+ QList candidates = contactManager().contactIds(syncTargetFilter & guidFilter);
+ if (candidates.size()) {
+ if (candidates.size() > 1)
+ LOG_WARNING("More than one plausible contact found");
+ return contactManager().contact(candidates.at(0));
+ }
+ return QContact();
+}
+
+QList KnownContactsSyncer::readContacts(QSettings &file)
+{
+ FUNCTION_CALL_TRACE;
+
+ /*
+ * This was implemented to support certain subset of contact fields
+ * but can be extended to support more as long as the fields are
+ * kept optional.
+ */
+ QList contacts;
+ for (const auto &id : file.childGroups()) {
+ file.beginGroup(id);
+ auto contact = getContact(id);
+ setGuid(&contact, id);
+ setNames(&contact, file);
+ setPhoneNumbers(&contact, file);
+ setEmailAddress(&contact, file);
+ setCompanyInfo(&contact, file);
+
+ contacts.append(contact);
+ file.endGroup();
+ }
+ return contacts;
+}
+
+void KnownContactsSyncer::handleSyncFailure(Failure error)
+{
+ LOG_WARNING("knowncontacts sync failed, emitting error code" << error);
+ purgeSyncStateData(AccountId);
+ emit syncFailed(error);
+}
+
+bool KnownContactsSyncer::purgeData()
+{
+ // Remove stale contacts
+ bool removed = removeAllContacts();
+ // Remove OOB data
+ removed &= purgeSyncStateData(AccountId, true);
+ // Return true if all data got removed
+ return removed;
+}
diff --git a/src/knowncontacts/knowncontactssyncer.h b/src/knowncontacts/knowncontactssyncer.h
new file mode 100644
index 0000000..a775d37
--- /dev/null
+++ b/src/knowncontacts/knowncontactssyncer.h
@@ -0,0 +1,76 @@
+/*
+ * Buteo sync plugin that stores locally created contacts
+ * Copyright (C) 2020 Open Mobile Platform LLC.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1 as published by the Free Software Foundation
+ * and appearing in the file LICENSE.LGPL included in the packaging
+ * of this file.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#ifndef KNOWNCONTACTS_SYNCER_H
+#define KNOWNCONTACTS_SYNCER_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+QTCONTACTS_USE_NAMESPACE
+
+class KnownContactsSyncer : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdapter
+{
+ Q_OBJECT
+
+public:
+ KnownContactsSyncer(QString path, QObject *parent = nullptr);
+
+ ~KnownContactsSyncer();
+
+ bool startSync();
+
+ bool purgeData();
+
+ enum Failure : int {
+ NoFailure = 0, // Reserved as success value
+ InitFailure,
+ CallFailure,
+ StoreChangesFailure,
+ StoreStateFailure
+ };
+ Q_ENUM(Failure)
+
+signals:
+ void syncSucceeded();
+ void syncFailed(Failure errorCode);
+
+protected:
+ void determineRemoteChanges(const QDateTime &remoteSince);
+
+private slots:
+ void asyncDeterminedRemoteChanges(const QStringList files);
+
+private:
+ QContact getContact(const QString &id);
+ void handleSyncFailure(Failure error);
+ QList readContacts(QSettings &file);
+
+ QString m_syncFolder;
+};
+
+#endif // KNOWNCONTACTS_SYNCER_H
diff --git a/src/src.pro b/src/src.pro
index bfa5a0a..bb4385a 100644
--- a/src/src.pro
+++ b/src/src.pro
@@ -6,4 +6,5 @@ SUBDIRS = \
twitter \
onedrive \
dropbox \
- vk
+ vk \
+ knowncontacts