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 +) 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,190 @@ +/** + * 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 + +#include + +/** + * Request the device send us the entire contacts book + * + * This package type is soon to be depreciated and deleted + */ +#define PACKAGE_TYPE_CONTACTS_REQUEST_ALL QStringLiteral("kdeconnect.contacts.request_all") + +/** + * Used to request the device send the unique ID of every contact + */ +#define PACKAGE_TYPE_CONTACTS_REQUEST_ALL_UIDS QStringLiteral("kdeconnect.contacts.request_all_uids") + +/** + * Response from the device containing a list of zero or more pairings of names and phone numbers + * + * This package type is soon to be depreciated and deleted + */ +#define PACKAGE_TYPE_CONTACTS_RESPONSE QStringLiteral("kdeconnect.contacts.response") + +/** + * Response indicating the package contains a list of contact uIDs + * + * It shall contain the key "uids", which will mark a list of uIDs (long int) + * The returned IDs can be used in future requests for more information about the contact + */ +#define PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS QStringLiteral("kdeconnect.contacts.response_uids") + +/** + * Amount of time we are willing to wait before deciding the device is not going to reply + * + * This is a random number picked by me, and might need to be adjusted based on real-world testing + */ +#define CONTACTS_TIMEOUT_MS 10000 + +/** + * Type definition for a single contact database entry + * + * A contact is identified by: + * a name, paired to a phone number and a phone number category + * or + * a phone number, paired to a name and a phone number category + */ +typedef QPair> ContactsEntry; + +/** + * Type definition for a contacts database + * + * A contacts database pairs either names or numbers to a list of corresponding contacts + */ +typedef QHash> ContactsCache; + +typedef QSet UIDCache_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 receivePackage(const NetworkPackage& np) override; + void connected() override {} + + QString dbusPath() const override; + +public Q_SLOTS: + + /** + * Get all the contacts known from the phone + * + * @return Map of names to pairs of phone number categories and phone numbers + * e.g. > + */ + Q_SCRIPTABLE QStringList getAllContacts(); + + /** + * Enumerate a uID for every contact on the phone + * + * These uIDs can be used in future dbus calls to get more information about the contact + */ + Q_SCRIPTABLE QStringList getAllContactUIDs(); + +protected: + /** + * Store locally-known list of contacts keyed by name, e.g. > + */ + ContactsCache cachedContactsByName; + + /** + * Store locally-known list of contacts keyed by number, e.g. <+12025550101, > + */ + ContactsCache cachedContactsByNumber; + + /** + * Store list of locally-known contacts' uIDs + */ + UIDCache_t uIDCache; + + /** + * Enforce mutual exclusion when accessing the cached contacts + */ + QMutex cacheLock; + + /** + * Enforce mutual exclusion when accessing the cached uIDs + */ + QMutex uIDCacheLock; + + /** + * Handle a packet of type PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS + */ + bool handleResponseUIDs(const NetworkPackage&); + + /** + * Get the locally-known collection of contacts + * + * If the cache has not yet been populated, populate it first + * + * @return Locally-cached contacts buffers + */ + QPair getCachedContacts(); + + /** + * Get the locally-known collection of uIDs + * + * If the cache has not yet been populated, populate it first + * + * @return Locally-cached contacts' uIDs + */ + UIDCache_t getCachedUIDs(); + + /** + * Query the remote device for its contacts book, bypassing and populating the local cache + */ + void sendAllContactsRequest(); + + /** + * Send a request-type packet, which contains no body + * + * @return True if the send was successful, false otherwise + */ + bool sendRequest(QString packageType); + + +public: Q_SIGNALS: + /** + * Emitted to indicate that we have received some contacts from the device + */ + Q_SCRIPTABLE void cachedContactsAvailable(); + + /** + * Emitted to indicate we have received some contacts' uIDs from the device + */ + Q_SCRIPTABLE void cachedUIDsAvailable(); +}; + +#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,269 @@ +/** + * 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 + +K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_contacts.json", registerPlugin< ContactsPlugin >(); ) + +Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_CONTACTS, "kdeconnect.plugin.contacts") + +ContactsPlugin::ContactsPlugin(QObject* parent, const QVariantList& args) + : KdeConnectPlugin(parent, args) +{ + // Initialize the dbus interface + // TODO: Error checking like https://doc.qt.io/qt-5/qtdbus-pingpong-pong-cpp.html + QDBusConnection::sessionBus().registerService(this->dbusPath()); + QDBusConnection::sessionBus().registerObject(this->dbusPath(), this, QDBusConnection::ExportAllSlots); + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Contacts constructor for device " << device()->name(); +} + +ContactsPlugin::~ContactsPlugin() +{ +// qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Contacts plugin destructor for device" << device()->name(); +} + +bool ContactsPlugin::receivePackage(const NetworkPackage& np) +{ + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Package Received for device " << device()->name(); + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << np.body(); + + if (np.type() == PACKAGE_TYPE_CONTACTS_RESPONSE) + { + int index = 0; + + cacheLock.lock(); + while(true) + { + auto contact = np.get(QString::number(index)); + if (contact.length() == 0) + { + // If nothing was returned, assume we have processed all contacts + break; + } + QString contactName = contact[0]; + QString contactNumber = contact[2]; + QString contactNumberCategory = contact[1]; + + ContactsEntry newContact = + ContactsEntry(contactName, + QPair(contactNumberCategory, contactNumber)); + + cachedContactsByName[contactName].insert(newContact); + cachedContactsByNumber[contactNumber].insert(newContact); + + index ++; + } + cacheLock.unlock(); + + // Now that we have processed an incoming packet, there (should be) contacts available + Q_EMIT cachedContactsAvailable(); + } else if (np.type() == PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS) + { + this->handleResponseUIDs(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; + } + + return true; +} + +void ContactsPlugin::sendAllContactsRequest() +{ + NetworkPackage np(PACKAGE_TYPE_CONTACTS_REQUEST_ALL); + bool success = sendPackage(np); + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "sendAllContactsRequest:" << success; +} + +bool ContactsPlugin::sendRequest(QString packageType) +{ + NetworkPackage np(packageType); + bool success = sendPackage(np); + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "sendRequest: Sending " << packageType << success; + + return success; +} + +UIDCache_t ContactsPlugin::getCachedUIDs() +{ + UIDCache_t toReturn; + + // I assume the remote device has at least one contact, so if there is nothing in the cache + // it needs to be populated + bool cachePopulated = uIDCache.size() > 0; + + if (!cachePopulated) + { + this->sendRequest(PACKAGE_TYPE_CONTACTS_REQUEST_ALL_UIDS); + + // Wait to receive result from phone or timeout + QTimer timer; + timer.setSingleShot(true); + timer.setInterval(CONTACTS_TIMEOUT_MS); + QEventLoop waitForReplyLoop; + // Allow timeout + connect(&timer, SIGNAL(timeout()), &waitForReplyLoop, SLOT(quit())); + // Also allow a reply + connect(this, SIGNAL(cachedUIDsAvailable()), &waitForReplyLoop, SLOT(quit())); + + // Wait + waitForReplyLoop.exec(); + + if (!(timer.isActive())) + { + // The device did not reply before we timed out + // Note that it still might reply eventually, and receivePackage(..) will import the + // contacts to our local cache at that point + qCDebug(KDECONNECT_PLUGIN_CONTACTS)<< "getCachedContacts:" << "Timeout waiting for device reply"; + } + } + + uIDCacheLock.lock(); + toReturn = uIDCache; + uIDCacheLock.unlock(); + + return toReturn; +} + +QPair ContactsPlugin::getCachedContacts() +{ + // I assume the remote device has at least one contact, so if there is nothing in the cache + // it needs to be populated + bool cachePopulated = cachedContactsByName.size() > 0; + + QPair toReturn; + + if (cachePopulated) + { + // Do nothing. Fall through to return code. + } + else + { + // Otherwise we need to request the contacts book from the remote device + this->sendAllContactsRequest(); + + // Wait to receive result from phone or timeout + QTimer timer; + timer.setSingleShot(true); + timer.setInterval(CONTACTS_TIMEOUT_MS); + QEventLoop waitForReplyLoop; + // Allow timeout + connect(&timer, SIGNAL(timeout()), &waitForReplyLoop, SLOT(quit())); + // Also allow a reply + connect(this, SIGNAL(cachedContactsAvailable()), &waitForReplyLoop, SLOT(quit())); + + // Wait + waitForReplyLoop.exec(); + + if (!(timer.isActive())) + { + // The device did not reply before we timed out + // Note that it still might reply eventually, and receivePackage(..) will import the + // contacts to our local cache at that point + qCDebug(KDECONNECT_PLUGIN_CONTACTS)<< "getCachedContacts:" << "Timeout waiting for device reply"; + } + } + + cacheLock.lock(); + toReturn = QPair(cachedContactsByName, cachedContactsByNumber); + cacheLock.unlock(); + return toReturn; +} + +bool ContactsPlugin::handleResponseUIDs(const NetworkPackage& np) +{ + + if (!np.has("uids")) + { + qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseUIDs:" << "Malformed packet does not have uids key"; + return false; + } + + QStringList uIDs = np.get("uids"); + + uIDCacheLock.lock(); + for (const QString& uID : uIDs) + { + uIDCache.insert(uID.toLong()); + } + uIDCacheLock.unlock(); + + Q_EMIT cachedUIDsAvailable(); + return true; +} + +QStringList ContactsPlugin::getAllContacts() +{ + QPair contactsCaches = this->getCachedContacts(); + + // Test code: Iterate through the list of contacts and reply to the DBus request with their names + QStringList toReturn; + for ( auto contactSet : contactsCaches.first) + { + for (ContactsEntry contact : contactSet) + { + toReturn.append(contact.first); // Name + toReturn.append(contact.second.second); // Number + } + } + + toReturn.append(QString::number(contactsCaches.first.size())); + + return toReturn; +} + +QStringList ContactsPlugin::getAllContactUIDs() +{ + QSet uIDs = this->getCachedUIDs(); + QStringList toReturn; + + for (long uID : uIDs) + { + toReturn.append(QString::number(uID)); + } + + toReturn.append(QString::number(uIDCache.size())); + + return toReturn; +} + +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-OutgoingPackageType": [ + "kdeconnect.contacts.request_all", + "kdeconnect.contacts.request_all_uids" + ], + "X-KdeConnect-SupportedPackageType": [ + "kdeconnect.contacts.response", + "kdeconnect.contacts.response_uids" + ] +}