diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(ping) add_subdirectory(clipboard) +add_subdirectory(contacts) add_subdirectory(telephony) add_subdirectory(share) add_subdirectory(notifications) diff --git a/plugins/contacts/CMakeLists.txt b/plugins/contacts/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/contacts/CMakeLists.txt @@ -0,0 +1,11 @@ +set(kdeconnect_contacts_SRCS + contactsplugin.cpp +) + +kdeconnect_add_plugin(kdeconnect_contacts JSON kdeconnect_contacts.json SOURCES ${kdeconnect_contacts_SRCS}) + +target_link_libraries(kdeconnect_contacts + kdeconnectcore + Qt5::DBus + KF5::I18n +) \ No newline at end of file diff --git a/plugins/contacts/README b/plugins/contacts/README new file mode 100644 --- /dev/null +++ b/plugins/contacts/README @@ -0,0 +1,3 @@ +This plugin allows communicating with the paired device to access its contacts +book, either by downloading the entire list of contacts or by requesting a +specific contact \ No newline at end of file diff --git a/plugins/contacts/contactsplugin.h b/plugins/contacts/contactsplugin.h new file mode 100644 --- /dev/null +++ b/plugins/contacts/contactsplugin.h @@ -0,0 +1,164 @@ +/** + * Copyright 2018 Simon Redman + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CONTACTSPLUGIN_H +#define CONTACTSPLUGIN_H + +#include +#include + +#include + +/** + * Used to request the device send the unique ID and last-changed timestamp of every contact + */ +#define PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMP QStringLiteral("kdeconnect.contacts.request_all_uids_timestamps") + +/** + * Used to request the vcards for the contacts corresponding to a list of UIDs + * + * It shall contain the key "uids", which will have a list of uIDs (long int, as string) + */ +#define PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS QStringLiteral("kdeconnect.contacts.request_vcards_by_uid") + +/** + * Response indicating the package contains a list of all contact uIDs and last-changed timestamps + * + * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) + * then, for each UID, there shall be a field with the key of that UID and the value of the timestamp (int, as string) + * + * For example: + * ( 'uids' : ['1', '3', '15'], + * '1' : '973486597', + * '3' : '973485443', + * '15' : '973492390' ) + * + * The returned IDs can be used in future requests for more information about the contact + */ +#define PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS QStringLiteral("kdeconnect.contacts.response_uids_timestamps") + +/** + * Response indicating the package contains a list of contact vcards + * + * It shall contain the key "uids", which will mark a list of uIDs (long int, as string) + * then, for each UID, there shall be a field with the key of that UID and the value of the remote's vcard for that contact + * + * For example: + * ( 'uids' : ['1', '3', '15'], + * '1' : 'BEGIN:VCARD\n....\nEND:VCARD', + * '3' : 'BEGIN:VCARD\n....\nEND:VCARD', + * '15' : 'BEGIN:VCARD\n....\nEND:VCARD' ) + */ +#define PACKET_TYPE_CONTACTS_RESPONSE_VCARDS QStringLiteral("kdeconnect.contacts.response_vcards") + +/** + * Where the synchronizer will write vcards and other metadata + * TODO: Per-device folders since each device *will* have different uIDs + */ +Q_GLOBAL_STATIC_WITH_ARGS( + QString, vcardsLocation, + (QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + ("/kpeoplevcard"))) + +#define VCARD_EXTENSION QStringLiteral(".vcf") +#define METADATA_EXTENSION QStringLiteral(".meta") + +typedef QString uID; +Q_DECLARE_METATYPE(uID) + +typedef QStringList uIDList_t; +Q_DECLARE_METATYPE(uIDList_t) + +class Q_DECL_EXPORT ContactsPlugin : public KdeConnectPlugin { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.contacts") + +public: + explicit ContactsPlugin (QObject *parent, const QVariantList &args); + ~ContactsPlugin () override; + + bool receivePacket (const NetworkPacket& np) override; + void connected () override { + } + + QString dbusPath () const override; + +protected: + /** + * Path where this instance of the plugin stores its synchronized contacts + */ + QString vcardsPath; + +public Q_SLOTS: + + /** + * Query the remote device for all its uIDs and last-changed timestamps, then: + * Delete any contacts which are known locally but not reported by the remote + * Update any contacts which are known locally but have an older timestamp + * Add any contacts which are not known locally but are reported by the remote + */ + Q_SCRIPTABLE + void synchronizeRemoteWithLocal (); + +public: +Q_SIGNALS: + /** + * Emitted to indicate that we have locally cached all remote contacts + * + * @param newContacts The list of just-synchronized contacts + */ + Q_SCRIPTABLE + void localCacheSynchronized (const uIDList_t& newContacts); + +protected: + + /** + * Handle a packet of type PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS + * + * For every uID in the reply: + * Delete any from local storage if it does not appear in the reply + * Compare the modified timestamp for each in the reply and update any which should have changed + * Request the details any IDs which were not locally cached + */ + bool handleResponseUIDsTimestamps (const NetworkPacket&); + + /** + * Handle a packet of type PACKET_TYPE_CONTACTS_RESPONSE_VCARDS + */ + bool handleResponseVCards (const NetworkPacket&); + + /** + * Send a request-type packet which contains no body + * + * @return True if the send was successful, false otherwise + */ + bool sendRequest (const QString& packetType); + + /** + * Send a request-type packet which has a body with the key 'uids' and the value the list of + * specified uIDs + * + * @param packageType Type of package to send + * @param uIDs List of uIDs to request + * @return True if the send was successful, false otherwise + */ + bool sendRequestWithIDs (const QString& packetType, const uIDList_t& uIDs); +}; + +#endif // CONTACTSPLUGIN_H diff --git a/plugins/contacts/contactsplugin.cpp b/plugins/contacts/contactsplugin.cpp new file mode 100644 --- /dev/null +++ b/plugins/contacts/contactsplugin.cpp @@ -0,0 +1,211 @@ +/** + * Copyright 2018 Simon Redman + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(KdeConnectPluginFactory, "kdeconnect_contacts.json", + registerPlugin(); ) + +Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_CONTACTS, "kdeconnect.plugin.contacts") + +ContactsPlugin::ContactsPlugin (QObject* parent, const QVariantList& args) : + KdeConnectPlugin(parent, args) { + vcardsPath = QString(*vcardsLocation).append("/kdeconnect-").append(device()->id()); + + // Register custom types with dbus + qRegisterMetaType("uID"); + qDBusRegisterMetaType(); + + qRegisterMetaType("uIDList_t"); + qDBusRegisterMetaType(); + + // Create the storage directory if it doesn't exist + if (!QDir().mkpath(vcardsPath)) { + qCWarning(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseVCards:" << "Unable to create VCard directory"; + } + + this->synchronizeRemoteWithLocal(); + + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Contacts constructor for device " << device()->name(); +} + +ContactsPlugin::~ContactsPlugin () { + QDBusConnection::sessionBus().unregisterObject(dbusPath(), QDBusConnection::UnregisterTree); +// qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Contacts plugin destructor for device" << device()->name(); +} + +bool ContactsPlugin::receivePacket (const NetworkPacket& np) { + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Packet Received for device " << device()->name(); + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << np.body(); + + if (np.type() == PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS) { + return this->handleResponseUIDsTimestamps(np); + } else if (np.type() == PACKET_TYPE_CONTACTS_RESPONSE_VCARDS) { + return this->handleResponseVCards(np); + } else { + // Is this check necessary? + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Unknown package type received from device: " + << device()->name() << ". Maybe you need to upgrade KDE Connect?"; + return false; + } +} + +void ContactsPlugin::synchronizeRemoteWithLocal () { + this->sendRequest(PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMP); +} + +bool ContactsPlugin::handleResponseUIDsTimestamps (const NetworkPacket& np) { + if (!np.has("uids")) { + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseUIDsTimestamps:" + << "Malformed packet does not have uids key"; + return false; + } + uIDList_t uIDsToUpdate; + QDir vcardsDir(vcardsPath); + + // Get a list of all file info in this directory + // Clean out IDs returned from the remote. Anything leftover should be deleted + QFileInfoList localVCards = vcardsDir.entryInfoList( { "*.vcard", "*.vcf" }); + + const QStringList& uIDs = np.get("uids"); + + // Check local storage for the contacts: + // If the contact is not found in local storage, request its vcard be sent + // If the contact is in local storage but not reported, delete it + // If the contact is in local storage, compare its timestamp. If different, request the contact + for (const QString& ID : uIDs) { + QString filename = vcardsDir.filePath(ID + VCARD_EXTENSION); + QFile vcardFile(filename); + + if (!QFile().exists(filename)) { + // We do not have a vcard for this contact. Request it. + uIDsToUpdate.push_back(ID); + continue; + } + + // Remove this file from the list of known files + QFileInfo fileInfo(vcardFile); + bool success = localVCards.removeOne(fileInfo); + Q_ASSERT(success); // We should have always been able to remove the existing file from our listing + + // Check if the vcard needs to be updated + if (!vcardFile.open(QIODevice::ReadOnly)) { + qCWarning(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseUIDsTimestamps:" + << "Unable to open" << filename << "to read even though it was reported to exist"; + continue; + } + + QTextStream fileReadStream(&vcardFile); + QString line; + while (!fileReadStream.atEnd()) { + fileReadStream >> line; + // TODO: Check that the saved ID is the same as the one we were expecting. This requires parsing the VCard + if (!line.startsWith("X-KDECONNECT-TIMESTAMP:")) { + continue; + } + QStringList parts = line.split(":"); + QString timestamp = parts[1]; + + qint32 remoteTimestamp = np.get(ID); + qint32 localTimestamp = timestamp.toInt(); + + if (!(localTimestamp == remoteTimestamp)) { + uIDsToUpdate.push_back(ID); + } + } + } + + // Delete all locally-known files which were not reported by the remote device + for (const QFileInfo& unknownFile : localVCards) { + QFile toDelete(unknownFile.filePath()); + toDelete.remove(); + } + + this->sendRequestWithIDs(PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS, uIDsToUpdate); + + return true; +} + +bool ContactsPlugin::handleResponseVCards (const NetworkPacket& np) { + if (!np.has("uids")) { + qCDebug(KDECONNECT_PLUGIN_CONTACTS) + << "handleResponseVCards:" << "Malformed packet does not have uids key"; + return false; + } + + QDir vcardsDir(vcardsPath); + const QStringList& uIDs = np.get("uids"); + + // Loop over all IDs, extract the VCard from the packet and write the file + for (const auto& ID : uIDs) { + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Got VCard:" << np.get(ID); + QString filename = vcardsDir.filePath(ID + VCARD_EXTENSION); + QFile vcardFile(filename); + bool vcardFileOpened = vcardFile.open(QIODevice::WriteOnly); // Want to smash anything that might have already been there + if (!vcardFileOpened) { + qCWarning(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseVCards:" << "Unable to open" << filename; + continue; + } + + QTextStream fileWriteStream(&vcardFile); + const QString& vcard = np.get(ID); + fileWriteStream << vcard; + } + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseVCards:" << "Got" << uIDs.size() << "VCards"; + Q_EMIT localCacheSynchronized(uIDs); + return true; +} + +bool ContactsPlugin::sendRequest (const QString& packetType) { + NetworkPacket np(packetType); + bool success = sendPacket(np); + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "sendRequest: Sending " << packetType << success; + + return success; +} + +bool ContactsPlugin::sendRequestWithIDs (const QString& packetType, const uIDList_t& uIDs) { + NetworkPacket np(packetType); + + np.set("uids", uIDs); + bool success = sendPacket(np); + return success; +} + +QString ContactsPlugin::dbusPath () const { + return "/modules/kdeconnect/devices/" + device()->id() + "/contacts"; +} + +#include "contactsplugin.moc" + diff --git a/plugins/contacts/kdeconnect_contacts.json b/plugins/contacts/kdeconnect_contacts.json new file mode 100644 --- /dev/null +++ b/plugins/contacts/kdeconnect_contacts.json @@ -0,0 +1,33 @@ +{ + "Encoding": "UTF-8", + "KPlugin": { + "Authors": [ + { + "Email": "simon@ergotech.com", + "Name": "Simon Redman", + "Name[x-test]": "xxSimon Redmanxx" + } + ], + "Description": "Synchronize Contacts Between the Desktop and the Connected Device", + "Description[x-test]": "xxSynchronize Contacts Between the Desktop and the Connected Devicexx", + "EnabledByDefault": true, + "Icon": "dialog-ok", + "Id": "kdeconnect_contacts", + "License": "GPL", + "Name": "Contacts", + "Name[x-test]": "xxContactsxx", + "ServiceTypes": [ + "KdeConnect/Plugin" + ], + "Version": "0.1", + "Website": "http://albertvaka.wordpress.com" + }, + "X-KdeConnect-OutgoingPacketType": [ + "kdeconnect.contacts.request_all_uids_timestamps", + "kdeconnect.contacts.request_vcards_by_uid" + ], + "X-KdeConnect-SupportedPacketType": [ + "kdeconnect.contacts.response_uids_timestamps", + "kdeconnect.contacts.response_vcards" + ] +}