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