diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -17,6 +17,7 @@ devicesmodel.cpp notificationsmodel.cpp devicessortproxymodel.cpp + conversationmessage.cpp # modeltest.cpp ) @@ -28,6 +29,7 @@ set(libkdeconnect_HEADERS devicesmodel.h notificationsmodel.h + conversationmessage.h dbusinterfaces.h ${CMAKE_CURRENT_BINARY_DIR}/kdeconnectinterfaces_export.h ) @@ -45,6 +47,7 @@ geninterface(${CMAKE_SOURCE_DIR}/plugins/remotecommands/remotecommandsplugin.h remotecommandsinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/remotekeyboard/remotekeyboardplugin.h remotekeyboardinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/telephony/telephonyplugin.h telephonyinterface) +geninterface(${CMAKE_SOURCE_DIR}/plugins/telephony/conversationsdbusinterface.h conversationsinterface) add_library(kdeconnectinterfaces SHARED ${libkdeconnect_SRC}) diff --git a/interfaces/conversationmessage.h b/interfaces/conversationmessage.h new file mode 100644 --- /dev/null +++ b/interfaces/conversationmessage.h @@ -0,0 +1,123 @@ +/** + * 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 PLUGINS_TELEPHONY_CONVERSATIONMESSAGE_H_ +#define PLUGINS_TELEPHONY_CONVERSATIONMESSAGE_H_ + +#include +#include +#include +#include + +#include "interfaces/kdeconnectinterfaces_export.h" + +class KDECONNECTINTERFACES_EXPORT ConversationMessage + : public QObject { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.telephony.messages") + Q_PROPERTY(QString body READ body) + Q_PROPERTY(QString address READ address) + Q_PROPERTY(qint64 date READ date) + Q_PROPERTY(qint32 type READ type) + Q_PROPERTY(qint32 read READ read) + Q_PROPERTY(qint32 threadID READ threadID) + Q_PROPERTY(qint32 uID READ uID) + +public: + // TYPE field values from Android + enum Types + { + MessageTypeAll = 0, + MessageTypeInbox = 1, + MessageTypeSent = 2, + MessageTypeDraft = 3, + MessageTypeOutbox = 4, + MessageTypeFailed = 5, + MessageTypeQueued = 6, + }; + Q_ENUM(Types); + + /** + * Build a new message from a keyword argument dictionary + * + * @param args mapping of field names to values as might be contained in a network packet containing a message + */ + ConversationMessage(const QVariantMap& args = QVariantMap(), QObject* parent = Q_NULLPTR); + + ConversationMessage(const QString& body, const QString& address, const qint64& date, + const qint32& type, const qint32& read, const qint32& threadID, + const qint32& uID, + QObject* parent = Q_NULLPTR); + + ConversationMessage(const ConversationMessage& other, QObject* parent = Q_NULLPTR); + ~ConversationMessage(); + ConversationMessage& operator=(const ConversationMessage& other); + static void registerDbusType(); + + QString body() const { return m_body; } + QString address() const { return m_address; } + qint64 date() const { return m_date; } + qint32 type() const { return m_type; } + qint32 read() const { return m_read; } + qint32 threadID() const { return m_threadID; } + qint32 uID() const { return m_uID; } + + QVariantMap toVariant() const; + +protected: + /** + * Body of the message + */ + QString m_body; + + /** + * Remote-side address of the message. Most likely a phone number, but may be an email address + */ + QString m_address; + + /** + * Date stamp (Unix epoch millis) associated with the message + */ + qint64 m_date; + + /** + * Type of the message. See the message.type enum + */ + qint32 m_type; + + /** + * Whether we have a read report for this message + */ + qint32 m_read; + + /** + * Tag which binds individual messages into a thread + */ + qint32 m_threadID; + + /** + * Value which uniquely identifies a message + */ + qint32 m_uID; +}; + +Q_DECLARE_METATYPE(ConversationMessage); + +#endif /* PLUGINS_TELEPHONY_CONVERSATIONMESSAGE_H_ */ diff --git a/interfaces/conversationmessage.cpp b/interfaces/conversationmessage.cpp new file mode 100644 --- /dev/null +++ b/interfaces/conversationmessage.cpp @@ -0,0 +1,133 @@ +/** + * 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 "conversationmessage.h" + +#include +#include + + +ConversationMessage::ConversationMessage(const QVariantMap& args, QObject* parent) + : QObject(parent), + m_body(args["body"].toString()), + m_address(args["address"].toString()), + m_date(args["date"].toLongLong()), + m_type(args["type"].toInt()), + m_read(args["read"].toInt()), + m_threadID(args["thread_id"].toInt()), + m_uID(args["_id"].toInt()) + { +} + +ConversationMessage::ConversationMessage (const QString& body, const QString& address, const qint64& date, + const qint32& type, const qint32& read, const qint32& threadID, + const qint32& uID, + QObject* parent) + : QObject(parent) + , m_body(body) + , m_address(address) + , m_date(date) + , m_type(type) + , m_read(read) + , m_threadID(threadID) + , m_uID(uID) +{ + +} + +ConversationMessage::ConversationMessage(const ConversationMessage& other, QObject* parent) + : QObject(parent) + , m_body(other.m_body) + , m_address(other.m_address) + , m_date(other.m_date) + , m_type(other.m_type) + , m_read(other.m_read) + , m_threadID(other.m_threadID) + , m_uID(other.m_uID) +{ + +} + +ConversationMessage::~ConversationMessage() { } + +ConversationMessage& ConversationMessage::operator=(const ConversationMessage& other) +{ + this->m_body = other.m_body; + this->m_address = other.m_address; + this->m_date = other.m_date; + this->m_type = other.m_type; + this->m_read = other.m_read; + this->m_threadID = other.m_threadID; + this->m_uID = other.m_uID; + return *this; +} + +QVariantMap ConversationMessage::toVariant() const +{ + return { + {"body", m_body}, + {"address", m_address}, + {"date", m_date}, + {"type", m_type}, + {"read", m_read}, + {"thread_id", m_threadID}, + {"_id", m_uID}, + }; +} + +QDBusArgument &operator<<(QDBusArgument &argument, const ConversationMessage &message) +{ + argument.beginStructure(); + argument << message.body() << message.address() << message.date() << message.type() + << message.read() << message.threadID() << message.uID(); + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, ConversationMessage &message) +{ + QString body; + QString address; + qint64 date; + qint32 type; + qint32 read; + qint32 threadID; + qint32 uID; + + argument.beginStructure(); + argument >> body; + argument >> address; + argument >> date; + argument >> type; + argument >> read; + argument >> threadID; + argument >> uID; + argument.endStructure(); + + message = ConversationMessage(body, address, date, type, read, threadID, uID); + + return argument; +} + +void ConversationMessage::registerDbusType() +{ + qDBusRegisterMetaType(); + qRegisterMetaType(); +} diff --git a/interfaces/dbusinterfaces.h b/interfaces/dbusinterfaces.h --- a/interfaces/dbusinterfaces.h +++ b/interfaces/dbusinterfaces.h @@ -36,6 +36,7 @@ #include "interfaces/remotecommandsinterface.h" #include "interfaces/remotekeyboardinterface.h" #include "interfaces/telephonyinterface.h" +#include "interfaces/conversationsinterface.h" /** * Using these "proxy" classes just in case we need to rename the @@ -115,6 +116,14 @@ const QString id; }; +class KDECONNECTINTERFACES_EXPORT DeviceConversationsDbusInterface + : public OrgKdeKdeconnectDeviceConversationsInterface +{ + Q_OBJECT +public: + explicit DeviceConversationsDbusInterface(const QString& deviceId, QObject* parent = nullptr); + ~DeviceConversationsDbusInterface() override; +}; class KDECONNECTINTERFACES_EXPORT SftpDbusInterface : public OrgKdeKdeconnectDeviceSftpInterface diff --git a/interfaces/dbusinterfaces.cpp b/interfaces/dbusinterfaces.cpp --- a/interfaces/dbusinterfaces.cpp +++ b/interfaces/dbusinterfaces.cpp @@ -100,6 +100,17 @@ } +DeviceConversationsDbusInterface::DeviceConversationsDbusInterface(const QString& deviceId, QObject* parent) + : OrgKdeKdeconnectDeviceConversationsInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/"+deviceId, QDBusConnection::sessionBus(), parent) +{ + +} + +DeviceConversationsDbusInterface::~DeviceConversationsDbusInterface() +{ + +} + SftpDbusInterface::SftpDbusInterface(const QString& id, QObject* parent) : OrgKdeKdeconnectDeviceSftpInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + id + "/sftp", QDBusConnection::sessionBus(), parent) { diff --git a/plugins/contacts/contactsplugin.cpp b/plugins/contacts/contactsplugin.cpp --- a/plugins/contacts/contactsplugin.cpp +++ b/plugins/contacts/contactsplugin.cpp @@ -40,7 +40,8 @@ Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_CONTACTS, "kdeconnect.plugin.contacts") ContactsPlugin::ContactsPlugin (QObject* parent, const QVariantList& args) : - KdeConnectPlugin(parent, args) { + KdeConnectPlugin(parent, args) +{ vcardsPath = QString(*vcardsLocation).append("/kdeconnect-").append(device()->id()); // Register custom types with dbus diff --git a/plugins/telephony/CMakeLists.txt b/plugins/telephony/CMakeLists.txt --- a/plugins/telephony/CMakeLists.txt +++ b/plugins/telephony/CMakeLists.txt @@ -1,11 +1,17 @@ +set(kdeconnect_telephony_SRCS + telephonyplugin.cpp + conversationsdbusinterface.cpp +) + include_directories(${CMAKE_BINARY_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../notifications/) # needed for the sendreplydialog ki18n_wrap_ui(kdeconnect_telephony_SRCS ../notifications/sendreplydialog.ui) -kdeconnect_add_plugin(kdeconnect_telephony JSON kdeconnect_telephony.json SOURCES telephonyplugin.cpp ../notifications/sendreplydialog.cpp ${kdeconnect_telephony_SRCS}) +kdeconnect_add_plugin(kdeconnect_telephony JSON kdeconnect_telephony.json SOURCES ../notifications/sendreplydialog.cpp ${kdeconnect_telephony_SRCS}) target_link_libraries(kdeconnect_telephony kdeconnectcore + kdeconnectinterfaces KF5::I18n KF5::Notifications Qt5::DBus diff --git a/plugins/telephony/conversationsdbusinterface.h b/plugins/telephony/conversationsdbusinterface.h new file mode 100644 --- /dev/null +++ b/plugins/telephony/conversationsdbusinterface.h @@ -0,0 +1,97 @@ +/** + * Copyright 2013 Albert Vaca + * + * 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 CONVERSATIONSDBUSINTERFACE_H +#define CONVERSATIONSDBUSINTERFACE_H + +#include +#include +#include +#include +#include +#include +#include + +#include "interfaces/conversationmessage.h" +#include "interfaces/dbusinterfaces.h" + +class KdeConnectPlugin; +class Device; + +class ConversationsDbusInterface + : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.conversations") + +public: + explicit ConversationsDbusInterface(KdeConnectPlugin* plugin); + ~ConversationsDbusInterface() override; + + void addMessage(const ConversationMessage &message); + void removeMessage(const QString& internalId); + +public Q_SLOTS: + /** + * Return a list of the threadID for all valid conversations + */ + QStringList activeConversations(); + + void requestConversation(const QString &conversationID, int start, int end); + + /** + * Send a new message to this conversation + */ + void replyToConversation(const QString& conversationID, const QString& message); + + /** + * Send the request to the Telephony plugin to update the list of conversation threads + */ + void requestAllConversationThreads(); + +Q_SIGNALS: + Q_SCRIPTABLE void conversationCreated(const QString& threadID); + Q_SCRIPTABLE void conversationRemoved(const QString& threadID); + Q_SCRIPTABLE void conversationUpdated(const QString& threadID); + Q_SCRIPTABLE void conversationMessageReceived(const QVariantMap & msg, int pos) const; + +private /*methods*/: + QString newId(); //Generates successive identifitiers to use as public ids + +private /*attributes*/: + const Device* m_device; + KdeConnectPlugin* m_plugin; + + /** + * Mapping of threadID to the list of messages which make up that thread + */ + QHash> m_conversations; + + /** + * Mapping of threadID to the set of uIDs known in the corresponding conversation + */ + QHash> m_known_messages; + + int m_lastId; + + TelephonyDbusInterface m_telephonyInterface; +}; + +#endif // CONVERSATIONSDBUSINTERFACE_H diff --git a/plugins/telephony/conversationsdbusinterface.cpp b/plugins/telephony/conversationsdbusinterface.cpp new file mode 100644 --- /dev/null +++ b/plugins/telephony/conversationsdbusinterface.cpp @@ -0,0 +1,122 @@ +/** + * 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 "conversationsdbusinterface.h" +#include "interfaces/dbusinterfaces.h" +#include "interfaces/conversationmessage.h" + +#include + +#include +#include + +#include "telephonyplugin.h" + +ConversationsDbusInterface::ConversationsDbusInterface(KdeConnectPlugin* plugin) + : QDBusAbstractAdaptor(const_cast(plugin->device())) + , m_device(plugin->device()) + , m_plugin(plugin) + , m_lastId(0) + , m_telephonyInterface(m_device->id()) +{ + ConversationMessage::registerDbusType(); +} + +ConversationsDbusInterface::~ConversationsDbusInterface() +{ +} + +QStringList ConversationsDbusInterface::activeConversations() +{ + return m_conversations.keys(); +} + +void ConversationsDbusInterface::requestConversation(const QString& conversationID, int start, int end) +{ + const auto messagesList = m_conversations[conversationID]; + + if (messagesList.isEmpty()) + { + // Since there are no messages in the conversation, it's likely that it is a junk ID, but go ahead anyway + qCWarning(KDECONNECT_PLUGIN_TELEPHONY) << "Got a conversationID for a conversation with no messages!" << conversationID; + } + + m_telephonyInterface.requestConversation(conversationID); + + for(int i=start; i + * 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 @@ -21,16 +22,70 @@ #ifndef TELEPHONYPLUGIN_H #define TELEPHONYPLUGIN_H +#include "conversationsdbusinterface.h" +#include "interfaces/conversationmessage.h" + #include #include #include #include +/** + * Packet used to indicate a batch of messages has been pushed from the remote device + * + * The body should contain the key "messages" mapping to an array of messages + * + * For example: + * { "messages" : [ + * { "event" : "sms", + * "messageBody" : "Hello", + * "phoneNumber" : "2021234567", + * "messageDate" : "1518846484880", + * "messageType" : "2", + * "threadID" : "132" + * }, + * { ... }, + * ... + * ] + * } + */ +#define PACKET_TYPE_TELEPHONY_MESSAGE QStringLiteral("kdeconnect.telephony.message") + +/** + * Packet used for simple telephony events + * + * It contains the key "event" which maps to a string indicating the type of event: + * - "ringing" - A phone call is incoming + * - "missedCall" - An incoming call was not answered + * - "sms" - An incoming SMS message + * - Note: As of this writing (15 May 2018) the SMS interface is being improved and this type of event + * is no longer the preferred way of retrieving SMS. Use PACKET_TYPE_TELEPHONY_MESSAGE instead. + * + * Depending on the event, other fields may be defined + */ +#define PACKET_TYPE_TELEPHONY QStringLiteral("kdeconnect.telephony") + #define PACKET_TYPE_TELEPHONY_REQUEST QStringLiteral("kdeconnect.telephony.request") #define PACKET_TYPE_SMS_REQUEST QStringLiteral("kdeconnect.sms.request") +/** + * Packet sent to request all the most-recent messages in all conversations on the device + * + * The request packet shall contain no body + */ +#define PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATIONS QStringLiteral("kdeconnect.telephony.request_conversations") + +/** + * Packet sent to request all the messages in a particular conversation + * + * The body should contain the key "threadID" mapping to the threadID (as a string) being requested + * For example: + * { "threadID": 203 } + */ +#define PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATION QStringLiteral("kdeconnect.telephony.request_conversation") + Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_TELEPHONY) class TelephonyPlugin @@ -41,22 +96,56 @@ public: explicit TelephonyPlugin(QObject* parent, const QVariantList& args); + ~TelephonyPlugin() override; bool receivePacket(const NetworkPacket& np) override; void connected() override {} QString dbusPath() const override; public Q_SLOTS: Q_SCRIPTABLE void sendSms(const QString& phoneNumber, const QString& messageBody); + /** + * Send a request to the remote for all of its conversations + */ + Q_SCRIPTABLE void requestAllConversations(); + + /** + * Send a request to the remote for a particular conversation + */ + Q_SCRIPTABLE void requestConversation(const QString& conversationID) const; + +public: +Q_SIGNALS: + private Q_SLOTS: void sendMutePacket(); void showSendSmsDialog(); +protected: + /** + * Send to the telepathy plugin if it is available + */ + void forwardToTelepathy(const ConversationMessage& message); + + /** + * Handle a packet which contains many messages, such as PACKET_TYPE_TELEPHONY_MESSAGE + */ + bool handleBatchMessages(const NetworkPacket& np); + private: + /** + * Create a notification for: + * - Incoming call (with the option to mute the ringing) + * - Missed call + * - Incoming SMS (with the option to reply) + * - This comment is being written while SMS handling is in the process of being improved. + * As such, any code in createNotification which deals with SMS is legacy support + */ KNotification* createNotification(const NetworkPacket& np); QDBusInterface m_telepathyInterface; + ConversationsDbusInterface* m_conversationInterface; }; #endif diff --git a/plugins/telephony/telephonyplugin.cpp b/plugins/telephony/telephonyplugin.cpp --- a/plugins/telephony/telephonyplugin.cpp +++ b/plugins/telephony/telephonyplugin.cpp @@ -1,5 +1,6 @@ /** * Copyright 2013 Albert Vaca + * 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 @@ -21,42 +22,50 @@ #include "telephonyplugin.h" #include "sendreplydialog.h" +#include "conversationsdbusinterface.h" +#include "interfaces/conversationmessage.h" #include -#include #include #include #include +#include K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_telephony.json", registerPlugin< TelephonyPlugin >(); ) Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_TELEPHONY, "kdeconnect.plugin.telephony") TelephonyPlugin::TelephonyPlugin(QObject* parent, const QVariantList& args) : KdeConnectPlugin(parent, args) , m_telepathyInterface(QStringLiteral("org.freedesktop.Telepathy.ConnectionManager.kdeconnect"), QStringLiteral("/kdeconnect")) + , m_conversationInterface(new ConversationsDbusInterface(this)) { } +TelephonyPlugin::~TelephonyPlugin() +{ + // FIXME: Same problem as discussed in the BatteryPlugin destructor and for the same reason: + // QtDbus does not allow us to delete m_conversationInterface. If we do so, we get a crash in the + // next DBus access to the parent + + //m_conversationInterface->deleteLater(); +} + KNotification* TelephonyPlugin::createNotification(const NetworkPacket& np) { const QString event = np.get(QStringLiteral("event")); const QString phoneNumber = np.get(QStringLiteral("phoneNumber"), i18n("unknown number")); const QString contactName = np.get(QStringLiteral("contactName"), phoneNumber); const QByteArray phoneThumbnail = QByteArray::fromBase64(np.get(QStringLiteral("phoneThumbnail"), "")); + const QString messageBody = np.get(QStringLiteral("messageBody"),{}); // In case telepathy can handle the message, don't do anything else if (event == QLatin1String("sms") && m_telepathyInterface.isValid()) { - qCDebug(KDECONNECT_PLUGIN_TELEPHONY) << "Passing a text message to the telepathy interface"; - connect(&m_telepathyInterface, SIGNAL(messageReceived(QString,QString)), SLOT(sendSms(QString,QString)), Qt::UniqueConnection); - const QString messageBody = np.get(QStringLiteral("messageBody"),QLatin1String("")); - QDBusReply reply = m_telepathyInterface.call(QStringLiteral("sendMessage"), phoneNumber, contactName, messageBody); - if (reply) { - return nullptr; - } else { - qCDebug(KDECONNECT_PLUGIN_TELEPHONY) << "Telepathy failed, falling back to the default handling"; - } + // Telepathy has already been tried (in receivePacket) + // There is a chance that it somehow failed, but since nobody uses Telepathy anyway... + // TODO: When upgrading telepathy, handle failure case (in case m_telepathyInterface.call returns false) + return nullptr; } QString content, type, icon; @@ -109,7 +118,6 @@ notification->setActions( QStringList(i18n("Mute Call")) ); connect(notification, &KNotification::action1Activated, this, &TelephonyPlugin::sendMutePacket); } else if (event == QLatin1String("sms")) { - const QString messageBody = np.get(QStringLiteral("messageBody"),QLatin1String("")); notification->setActions( QStringList(i18n("Reply")) ); notification->setProperty("phoneNumber", phoneNumber); notification->setProperty("contactName", contactName); @@ -129,8 +137,27 @@ return true; } - KNotification* n = createNotification(np); - if (n != nullptr) n->sendEvent(); + const QString& event = np.get(QStringLiteral("event"), QStringLiteral("unknown")); + + // Handle old-style packets + if (np.type() == PACKET_TYPE_TELEPHONY) + { + if (event == QLatin1String("sms")) + { + // New-style packets should be a PACKET_TYPE_TELEPHONY_MESSAGE (15 May 2018) + qCDebug(KDECONNECT_PLUGIN_TELEPHONY) << "Handled an old-style Telephony sms packet. You should update your Android app to get the latest features!"; + ConversationMessage message(np.body()); + forwardToTelepathy(message); + } + KNotification* n = createNotification(np); + if (n != nullptr) n->sendEvent(); + return true; + } + + if (np.type() == PACKET_TYPE_TELEPHONY_MESSAGE) + { + return handleBatchMessages(np); + } return true; } @@ -163,6 +190,48 @@ dialog->raise(); } +void TelephonyPlugin::requestAllConversations() +{ + NetworkPacket np(PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATIONS); + + sendPacket(np); +} + +void TelephonyPlugin::requestConversation (const QString& conversationID) const +{ + NetworkPacket np(PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATION); + np.set("threadID", conversationID.toInt()); + + sendPacket(np); +} + +void TelephonyPlugin::forwardToTelepathy(const ConversationMessage& message) +{ + // In case telepathy can handle the message, don't do anything else + if (m_telepathyInterface.isValid()) { + qCDebug(KDECONNECT_PLUGIN_TELEPHONY) << "Passing a text message to the telepathy interface"; + connect(&m_telepathyInterface, SIGNAL(messageReceived(QString,QString)), SLOT(sendSms(QString,QString)), Qt::UniqueConnection); + const QString messageBody = message.body(); + const QString contactName; // TODO: When telepathy support is improved, look up the contact with KPeople + const QString phoneNumber = message.address(); + m_telepathyInterface.call(QDBus::NoBlock, QStringLiteral("sendMessage"), phoneNumber, contactName, messageBody); + } +} + +bool TelephonyPlugin::handleBatchMessages(const NetworkPacket& np) +{ + const auto messages = np.get("messages"); + + for (const QVariant& body : messages) + { + ConversationMessage message(body.toMap()); + forwardToTelepathy(message); + m_conversationInterface->addMessage(message); + } + + return true; +} + QString TelephonyPlugin::dbusPath() const { return "/modules/kdeconnect/devices/" + device()->id() + "/telephony"; diff --git a/smsapp/CMakeLists.txt b/smsapp/CMakeLists.txt --- a/smsapp/CMakeLists.txt +++ b/smsapp/CMakeLists.txt @@ -1,7 +1,23 @@ qt5_add_resources(KCSMS_SRCS resources.qrc) -add_executable(kdeconnect-sms main.cpp conversationmodel.cpp ${KCSMS_SRCS}) -target_link_libraries(kdeconnect-sms Qt5::Quick Qt5::Widgets KF5::DBusAddons KF5::CoreAddons KF5::I18n) +find_package(KF5People) + + +add_executable(kdeconnect-sms + main.cpp + conversationmodel.cpp + conversationlistmodel.cpp + ${KCSMS_SRCS}) + +target_link_libraries(kdeconnect-sms + kdeconnectinterfaces + Qt5::Quick + Qt5::Widgets + KF5::CoreAddons + KF5::DBusAddons + KF5::I18n + KF5::People + ) install(TARGETS kdeconnect-sms ${INSTALL_TARGETS_DEFAULT_ARGS}) install(PROGRAMS org.kde.kdeconnect.sms.desktop DESTINATION ${KDE_INSTALL_APPDIR}) diff --git a/smsapp/conversationlistmodel.h b/smsapp/conversationlistmodel.h new file mode 100644 --- /dev/null +++ b/smsapp/conversationlistmodel.h @@ -0,0 +1,118 @@ +/* + * This file is part of KDE Telepathy Chat + * + * Copyright (C) 2018 Aleix Pol Gonzalez + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * 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 St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef CONVERSATIONLISTMODEL_H +#define CONVERSATIONLISTMODEL_H + +#include +#include +#include +#include +#include + +#include "interfaces/conversationmessage.h" +#include "interfaces/dbusinterfaces.h" + +#include "interfaces/kdeconnectinterfaces_export.h" + +Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) + + +class OurSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder) +public: + + Qt::SortOrder sortOrder() const { return m_sortOrder; } + void setSortOrder(Qt::SortOrder sortOrder) { + if (m_sortOrder != sortOrder) { + m_sortOrder = sortOrder; + sortNow(); + } + } + void classBegin() override {} + void componentComplete() override { + m_completed = true; + sortNow(); + } + +private: + void sortNow() { + if (m_completed && dynamicSortFilter()) + sort(0, m_sortOrder); + } + + bool m_completed = false; + Qt::SortOrder m_sortOrder = Qt::AscendingOrder; +}; + +class KDECONNECTINTERFACES_EXPORT ConversationListModel + : public QStandardItemModel +{ + Q_OBJECT + Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId) + +public: + ConversationListModel(QObject* parent = nullptr); + ~ConversationListModel(); + + enum Roles { + FromMeRole = Qt::UserRole, + PersonUriRole, + AddressRole, + ConversationIdRole, + DateRole, + }; + Q_ENUM(Roles) + + QString deviceId() const { return m_deviceId; } + void setDeviceId(const QString &/*deviceId*/); + +public Q_SLOTS: + void handleCreatedConversation(const QString& conversationId); + void createRowFromMessage(const QVariantMap& message, int row); + void printDBusError(const QDBusError& error); + +private: + /** + * Get all conversations currently known by the conversationsInterface, if any + */ + void prepareConversationsList(); + + /** + * Get the data for a particular person given their contact address + */ + KPeople::PersonData* lookupPersonByAddress(const QString& address); + + /** + * Simplify a phone number to a known form + */ + QString canonicalizePhoneNumber(const QString& phoneNumber); + + QStandardItem* conversationForThreadId(qint32 threadId); + + DeviceConversationsDbusInterface* m_conversationsInterface; + QString m_deviceId; + KPeople::PersonsModel m_people; +}; + +#endif // CONVERSATIONLISTMODEL_H diff --git a/smsapp/conversationlistmodel.cpp b/smsapp/conversationlistmodel.cpp new file mode 100644 --- /dev/null +++ b/smsapp/conversationlistmodel.cpp @@ -0,0 +1,174 @@ +/* + * This file is part of KDE Telepathy Chat + * + * Copyright (C) 2018 Aleix Pol Gonzalez + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * 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 St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "conversationlistmodel.h" + +#include +#include "interfaces/conversationmessage.h" + +Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL, "kdeconnect.sms.conversations_list") + +ConversationListModel::ConversationListModel(QObject* parent) + : QStandardItemModel(parent) + , m_conversationsInterface(nullptr) +{ + qCCritical(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Constructing" << this; + auto roles = roleNames(); + roles.insert(FromMeRole, "fromMe"); + roles.insert(AddressRole, "address"); + roles.insert(PersonUriRole, "personUri"); + roles.insert(ConversationIdRole, "conversationId"); + roles.insert(DateRole, "date"); + setItemRoleNames(roles); + + ConversationMessage::registerDbusType(); +} + +ConversationListModel::~ConversationListModel() +{ +} + +void ConversationListModel::setDeviceId(const QString& deviceId) +{ + qCCritical(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "setDeviceId" << deviceId << "of" << this; + if (deviceId == m_deviceId) + return; + + if (m_conversationsInterface) { + disconnect(m_conversationsInterface, SIGNAL(conversationCreated(QString)), this, SLOT(handleCreatedConversation(QString))); + delete m_conversationsInterface; + } + + m_deviceId = deviceId; + m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); + connect(m_conversationsInterface, SIGNAL(conversationCreated(QString)), this, SLOT(handleCreatedConversation(QString))); + connect(m_conversationsInterface, SIGNAL(conversationMessageReceived(QVariantMap, int)), this, SLOT(createRowFromMessage(QVariantMap, int))); + prepareConversationsList(); + + m_conversationsInterface->requestAllConversationThreads(); +} + +void ConversationListModel::prepareConversationsList() +{ + + QDBusPendingReply validThreadIDsReply = m_conversationsInterface->activeConversations(); + + setWhenAvailable(validThreadIDsReply, [this](const QStringList& convs) { + clear(); + for (const QString& conversationId : convs) { + handleCreatedConversation(conversationId); + } + }, this); +} + +void ConversationListModel::handleCreatedConversation(const QString& conversationId) +{ + m_conversationsInterface->requestConversation(conversationId, 0, 1); +} + +void ConversationListModel::printDBusError(const QDBusError& error) +{ + qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << error; +} + +QStandardItem * ConversationListModel::conversationForThreadId(qint32 threadId) +{ + for(int i=0, c=rowCount(); idata(ConversationIdRole) == threadId) + return it; + } + return nullptr; +} + +void ConversationListModel::createRowFromMessage(const QVariantMap& msg, int row) +{ + if (row != 0) + return; + + const ConversationMessage message(msg); + if (message.type() == -1) + { + // The Android side currently hacks in -1 if something weird comes up + // TODO: Remove this hack when MMS support is implemented + return; + } + + bool toadd = false; + QStandardItem* item = conversationForThreadId(message.threadID()); + if (!item) { + toadd = true; + item = new QStandardItem(); + QScopedPointer personData(lookupPersonByAddress(message.address())); + if (personData) + { + item->setText(personData->name()); + item->setIcon(QIcon(personData->photo())); + item->setData(personData->personUri(), PersonUriRole); + } + else + { + item->setData(QString(), PersonUriRole); + item->setText(message.address()); + } + item->setData(message.threadID(), ConversationIdRole); + } + item->setData(message.address(), AddressRole); + item->setData(message.type() == ConversationMessage::MessageTypeSent, FromMeRole); + item->setData(message.body(), Qt::ToolTipRole); + item->setData(message.date(), DateRole); + + if (toadd) + appendRow(item); +} + +KPeople::PersonData* ConversationListModel::lookupPersonByAddress(const QString& address) +{ + int rowIndex = 0; + for (rowIndex = 0; rowIndex < m_people.rowCount(); rowIndex++) + { + const QString& uri = m_people.get(rowIndex, KPeople::PersonsModel::PersonUriRole).toString(); + KPeople::PersonData* person = new KPeople::PersonData(uri); + + const QString& email = person->email(); + const QString& phoneNumber = canonicalizePhoneNumber(person->contactCustomProperty("phoneNumber").toString()); + + if (address == email || canonicalizePhoneNumber(address) == phoneNumber) + { + qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Matched" << address << "to" << person->name(); + return person; + } + + delete person; + } + + return nullptr; +} + +QString ConversationListModel::canonicalizePhoneNumber(const QString& phoneNumber) +{ + QString toReturn(phoneNumber); + toReturn = toReturn.remove(' '); + toReturn = toReturn.remove('-'); + toReturn = toReturn.remove('('); + toReturn = toReturn.remove(')'); + toReturn = toReturn.remove('+'); + return toReturn; +} diff --git a/smsapp/conversationmodel.h b/smsapp/conversationmodel.h --- a/smsapp/conversationmodel.h +++ b/smsapp/conversationmodel.h @@ -22,23 +22,45 @@ #define CONVERSATIONMODEL_H #include +#include -class ConversationModel : public QStandardItemModel +#include "interfaces/dbusinterfaces.h" + +#include "interfaces/kdeconnectinterfaces_export.h" + +Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATION_MODEL) + +class KDECONNECTINTERFACES_EXPORT ConversationModel + : public QStandardItemModel { Q_OBJECT Q_PROPERTY(QString threadId READ threadId WRITE setThreadId) Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId) public: ConversationModel(QObject* parent = nullptr); + ~ConversationModel(); - enum Roles { FromMeRole = Qt::UserRole }; + enum Roles { + FromMeRole = Qt::UserRole, + DateRole, + }; QString threadId() const; void setThreadId(const QString &threadId); - QString deviceId() const { return {}; } - void setDeviceId(const QString &/*deviceId*/) {} + QString deviceId() const { return m_deviceId; } + void setDeviceId(const QString &/*deviceId*/); + + Q_INVOKABLE void sendReplyToConversation(const QString& message); + +private Q_SLOTS: + void createRowFromMessage(const QVariantMap &msg, int pos); + +private: + DeviceConversationsDbusInterface* m_conversationsInterface; + QString m_deviceId; + QString m_threadId; }; #endif // CONVERSATIONMODEL_H diff --git a/smsapp/conversationmodel.cpp b/smsapp/conversationmodel.cpp --- a/smsapp/conversationmodel.cpp +++ b/smsapp/conversationmodel.cpp @@ -19,25 +19,68 @@ */ #include "conversationmodel.h" +#include +#include "interfaces/conversationmessage.h" + +Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATION_MODEL, "kdeconnect.sms.conversation") ConversationModel::ConversationModel(QObject* parent) : QStandardItemModel(parent) + , m_conversationsInterface(nullptr) { auto roles = roleNames(); roles.insert(FromMeRole, "fromMe"); + roles.insert(DateRole, "date"); setItemRoleNames(roles); } +ConversationModel::~ConversationModel() +{ +} + QString ConversationModel::threadId() const { - return {}; + return m_threadId; } void ConversationModel::setThreadId(const QString &threadId) { + if (m_threadId == threadId) + return; + + m_threadId = threadId; clear(); - appendRow(new QStandardItem(threadId + QStringLiteral(" - A"))); - appendRow(new QStandardItem(threadId + QStringLiteral(" - A1"))); - appendRow(new QStandardItem(threadId + QStringLiteral(" - A2"))); - appendRow(new QStandardItem(threadId + QStringLiteral(" - A3"))); + if (!threadId.isEmpty()) { + m_conversationsInterface->requestConversation(threadId, 0, 10); + } +} + +void ConversationModel::setDeviceId(const QString& deviceId) +{ + if (deviceId == m_deviceId) + return; + + qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "setDeviceId" << "of" << this; + if (m_conversationsInterface) delete m_conversationsInterface; + + m_deviceId = deviceId; + + m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); + connect(m_conversationsInterface, SIGNAL(conversationMessageReceived(QVariantMap, int)), this, SLOT(createRowFromMessage(QVariantMap, int))); +} + +void ConversationModel::sendReplyToConversation(const QString& message) +{ + qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Trying to send" << message << "to conversation with ID" << m_threadId; + m_conversationsInterface->replyToConversation(m_threadId, message); +} + +void ConversationModel::createRowFromMessage(const QVariantMap& msg, int pos) +{ + const ConversationMessage message(msg); + auto item = new QStandardItem; + item->setText(message.body()); + item->setData(message.type() == ConversationMessage::MessageTypeSent, FromMeRole); + item->setData(message.date(), DateRole); + insertRow(pos, item); } diff --git a/smsapp/main.cpp b/smsapp/main.cpp --- a/smsapp/main.cpp +++ b/smsapp/main.cpp @@ -18,16 +18,18 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ +#include "conversationmodel.h" +#include "conversationlistmodel.h" +#include "kdeconnect-version.h" + #include #include #include #include #include #include #include #include -#include "conversationmodel.h" -#include "kdeconnect-version.h" #include int main(int argc, char *argv[]) @@ -47,8 +49,10 @@ } KDBusService service(KDBusService::Unique); - + + qmlRegisterType("org.kde.kdeconnect.sms", 1, 0, "QSortFilterProxyModel"); qmlRegisterType("org.kde.kdeconnect.sms", 1, 0, "ConversationModel"); + qmlRegisterType("org.kde.kdeconnect.sms", 1, 0, "ConversationListModel"); QQmlApplicationEngine engine; engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); diff --git a/smsapp/qml/ContactList.qml b/smsapp/qml/ContactList.qml --- a/smsapp/qml/ContactList.qml +++ b/smsapp/qml/ContactList.qml @@ -82,7 +82,7 @@ } footer: ComboBox { id: devicesCombo - readonly property QtObject device: currentIndex>0 ? model.data(model.index(currentIndex, 0), DevicesModel.DeviceRole) : null + readonly property QtObject device: currentIndex>=0 ? model.data(model.index(currentIndex, 0), DevicesModel.DeviceRole) : null enabled: count > 0 displayText: enabled ? undefined : i18n("No devices available") model: DevicesSortProxyModel { diff --git a/smsapp/qml/ConversationDisplay.qml b/smsapp/qml/ConversationDisplay.qml --- a/smsapp/qml/ConversationDisplay.qml +++ b/smsapp/qml/ConversationDisplay.qml @@ -21,24 +21,28 @@ import QtQuick 2.1 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 +import org.kde.people 1.0 import org.kde.kirigami 2.2 as Kirigami import org.kde.kdeconnect.sms 1.0 Kirigami.ScrollablePage { id: page - property QtObject person + property alias personUri: person.personUri + readonly property QtObject person: PersonData { + id: person + } property QtObject device + property string conversationId - readonly property string phoneNumber: person.contactCustomProperty("phoneNumber") - readonly property QtObject telephony: device ? TelephonyDbusInterfaceFactory.create(device.id()) : null - title: i18n("%1: %2", person.name, phoneNumber) + property string phoneNumber + title: person && person.name ? i18n("%1: %2", person.name, phoneNumber) : phoneNumber ListView { model: ConversationModel { id: model deviceId: device.id() - threadId: "xxxx" + threadId: page.conversationId } delegate: Kirigami.BasicListItem { @@ -56,7 +60,7 @@ placeholderText: i18n("Say hi...") onAccepted: { console.log("sending sms", page.phoneNumber) - page.telephony.sendSms(page.phoneNumber, message.text) + model.sendReplyToConversation(message.text) } } Button { diff --git a/smsapp/qml/ContactList.qml b/smsapp/qml/ConversationList.qml copy from smsapp/qml/ContactList.qml copy to smsapp/qml/ConversationList.qml --- a/smsapp/qml/ContactList.qml +++ b/smsapp/qml/ConversationList.qml @@ -25,9 +25,27 @@ import org.kde.plasma.core 2.0 as Core import org.kde.kirigami 2.2 as Kirigami import org.kde.kdeconnect 1.0 +import org.kde.kdeconnect.sms 1.0 Kirigami.ScrollablePage { + footer: ComboBox { + id: devicesCombo + enabled: count > 0 + displayText: enabled ? undefined : i18n("No devices available") + model: DevicesSortProxyModel { + id: devicesModel + //TODO: make it possible to sort only if they can do sms + sourceModel: DevicesModel { displayFilter: DevicesModel.Paired | DevicesModel.Reachable } + onRowsInserted: if (devicesCombo.currentIndex < 0) { + devicesCombo.currentIndex = 0 + } + } + textRole: "display" + } + + readonly property QtObject device: devicesCombo.currentIndex >= 0 ? devicesModel.data(devicesModel.index(devicesCombo.currentIndex, 0), DevicesModel.DeviceRole) : null + Component { id: chatView ConversationDisplay {} @@ -37,11 +55,12 @@ id: view currentIndex: 0 - model: PersonsSortFilterProxyModel { - requiredProperties: ["phoneNumber"] - sortRole: Qt.DisplayRole - sortCaseSensitivity: Qt.CaseInsensitive - sourceModel: PersonsModel {} + model: QSortFilterProxyModel { + sortOrder: Qt.DescendingOrder + sortRole: ConversationListModel.DateRole + sourceModel: ConversationListModel { + deviceId: device ? device.id() : "" + } } header: TextField { @@ -67,31 +86,17 @@ { hoverEnabled: true - readonly property var person: PersonData { - personUri: model.personUri - } - - label: display + label: i18n("%1 - %2", display, toolTip) icon: decoration function startChat() { - applicationWindow().pageStack.push(chatView, { person: person.person, device: Qt.binding(function() {return devicesCombo.device })}) + applicationWindow().pageStack.push(chatView, { + personUri: model.personUri, + phoneNumber: address, + conversationId: model.conversationId, + device: device}) } onClicked: { startChat(); } } } - footer: ComboBox { - id: devicesCombo - readonly property QtObject device: currentIndex>0 ? model.data(model.index(currentIndex, 0), DevicesModel.DeviceRole) : null - enabled: count > 0 - displayText: enabled ? undefined : i18n("No devices available") - model: DevicesSortProxyModel { - //TODO: make it possible to sort only if they can do sms - sourceModel: DevicesModel { displayFilter: DevicesModel.Paired | DevicesModel.Reachable } - onRowsInserted: if (devicesCombo.currentIndex < 0) { - devicesCombo.currentIndex = 0 - } - } - textRole: "display" - } } diff --git a/smsapp/qml/main.qml b/smsapp/qml/main.qml --- a/smsapp/qml/main.qml +++ b/smsapp/qml/main.qml @@ -20,6 +20,7 @@ import QtQuick 2.1 import org.kde.kirigami 2.2 as Kirigami +import org.kde.kdeconnect 1.0 Kirigami.ApplicationWindow { @@ -30,7 +31,7 @@ header: Kirigami.ToolBarApplicationHeader {} - pageStack.initialPage: ContactList { + pageStack.initialPage: ConversationList { title: i18n("KDE Connect SMS") } } diff --git a/smsapp/resources.qrc b/smsapp/resources.qrc --- a/smsapp/resources.qrc +++ b/smsapp/resources.qrc @@ -1,7 +1,7 @@ qml/main.qml - qml/ContactList.qml + qml/ConversationList.qml qml/ConversationDisplay.qml