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,108 @@ +/** + * 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) + +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 + * + * @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 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; } + +public: + /** + * 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; +}; + +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,85 @@ +/** + * 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()) + { +} + +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) +{ + +} + +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; + return *this; +} + +QDBusArgument &operator<<(QDBusArgument &argument, const ConversationMessage &message) +{ + argument.beginStructure(); + argument << message.m_body << message.m_address << message.m_date << message.m_type + << message.m_read << message.m_threadID; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, ConversationMessage &message) +{ + argument.beginStructure(); + argument >> message.m_body >> message.m_address >> message.m_date >> message.m_type + >> message.m_read >> message.m_threadID; + argument.endStructure(); + 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/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,94 @@ +/** + * 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(ConversationMessage* message); + void removeMessage(const QString& internalId); + +public Q_SLOTS: + /** + * Return a list of the threadID for all valid conversations + */ + QStringList activeConversations(); + + /** + * Get the first message in the requested conversation + */ + ConversationMessage getFirstFromConversation(const QString& conversationId); + + /** + * 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); + +private /*methods*/: + QString newId(); //Generates successive identifitiers to use as public ids + void notificationReady(); + +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; + 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,115 @@ +/** + * 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() +{ + qCDebug(KDECONNECT_PLUGIN_TELEPHONY) << "Destroying ConversationsDbusInterface"; + // TODO: Clean up m_conversations's contained Message objects, otherwise massive memory leaks will occur! +} + +QStringList ConversationsDbusInterface::activeConversations() +{ + return m_conversations.keys(); +} + +ConversationMessage ConversationsDbusInterface::getFirstFromConversation(const QString& conversationId) +{ + const QList> messagesList = m_conversations[conversationId]; + + if (messagesList.isEmpty()) + { + // Since there are no messages in the conversation, we can't do anything sensible + qCWarning(KDECONNECT_PLUGIN_TELEPHONY) << "Got a conversationID for a conversation with no messages!"; + return ConversationMessage(); + } + + return *messagesList.first().data(); +} + +void ConversationsDbusInterface::addMessage(ConversationMessage* message) +{ + // Dump the Message on DBus. I am not convinced this is the right or even a sane way to handle messages. + const QString& publicId = newId(); + QDBusConnection::sessionBus().registerObject(m_device->dbusPath()+"/messages/"+publicId, message, QDBusConnection::ExportScriptableContents); + + // Store the Message in the list corresponding to its thread + const QString& threadId = QString::number(message->threadID()); + bool newConversation = m_conversations.contains(threadId); + m_conversations[threadId].append(message); + + // Tell the world about what just happened + if (newConversation) + { + Q_EMIT conversationCreated(threadId); + } else + { + Q_EMIT conversationUpdated(threadId); + } +} + +void ConversationsDbusInterface::removeMessage(const QString& internalId) +{ + // TODO: Delete the specified message from our internal structures +} + +void ConversationsDbusInterface::replyToConversation(const QString& conversationID, const QString& message) +{ + const QList> messagesList = m_conversations[conversationID]; + if (messagesList.isEmpty()) + { + // Since there are no messages in the conversation, we can't do anything sensible + qCWarning(KDECONNECT_PLUGIN_TELEPHONY) << "Got a conversationID for a conversation with no messages!"; + return; + } + const QString& address = m_conversations[conversationID].front().data()->address(); + m_telephonyInterface.sendSms(address, message); +} + +void ConversationsDbusInterface::requestAllConversationThreads() +{ + // Prepare the list of conversations by requesting the first in every thread + m_telephonyInterface.requestAllConversations(); +} + +QString ConversationsDbusInterface::newId() +{ + return QString::number(++m_lastId); +} diff --git a/plugins/telephony/kdeconnect_telephony.json b/plugins/telephony/kdeconnect_telephony.json --- a/plugins/telephony/kdeconnect_telephony.json +++ b/plugins/telephony/kdeconnect_telephony.json @@ -89,9 +89,11 @@ }, "X-KdeConnect-OutgoingPacketType": [ "kdeconnect.telephony.request", - "kdeconnect.sms.request" + "kdeconnect.sms.request", + "kdeconnect.telephony.request_conversations" ], "X-KdeConnect-SupportedPacketType": [ - "kdeconnect.telephony" + "kdeconnect.telephony", + "kdeconnect.telephony.message" ] } diff --git a/plugins/telephony/telephonyplugin.h b/plugins/telephony/telephonyplugin.h --- a/plugins/telephony/telephonyplugin.h +++ b/plugins/telephony/telephonyplugin.h @@ -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,16 +22,61 @@ #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") + Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_TELEPHONY) class TelephonyPlugin @@ -49,14 +95,42 @@ 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(); + +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,21 +22,24 @@ #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(this) { } @@ -45,18 +49,14 @@ 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 +109,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 +128,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 +181,40 @@ dialog->raise(); } +void TelephonyPlugin::requestAllConversations() +{ + NetworkPacket np(PACKET_TYPE_TELEPHONY_REQUEST_CONVERSATIONS); + + 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 = new ConversationMessage(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,82 @@ +/* + * 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 "interfaces/conversationmessage.h" +#include "interfaces/dbusinterfaces.h" + +#include "interfaces/kdeconnectinterfaces_export.h" + +Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) + +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, + ConversationIdRole, + DateRole, + }; + + QString deviceId() const { return m_deviceId; } + void setDeviceId(const QString &/*deviceId*/); + +public Q_SLOTS: + void handleCreatedConversation(const QString& conversationId); + void createRowFromMessage(const ConversationMessage& message); + 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); + + 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,156 @@ +/* + * 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(PersonUriRole, "personUri"); + roles.insert(ConversationIdRole, "conversationId"); + roles.insert(DateRole, "date"); + setItemRoleNames(roles); + + ConversationMessage::registerDbusType(); +} + +ConversationListModel::~ConversationListModel() +{ + if (m_conversationsInterface) delete m_conversationsInterface; +} + +void ConversationListModel::setDeviceId(const QString& deviceId) +{ + qCCritical(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "setDeviceId" << "of" << this; + if (m_conversationsInterface) delete m_conversationsInterface; + + m_deviceId = deviceId; + + m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId); + + connect(m_conversationsInterface, SIGNAL(conversationCreated(const QString&)), + this, SLOT(handleCreatedConversation(const QString&)), Qt::UniqueConnection); + + prepareConversationsList(); + + m_conversationsInterface->requestAllConversationThreads(); +} + +void ConversationListModel::prepareConversationsList() +{ + clear(); + + QDBusPendingReply validThreadIDsReply = m_conversationsInterface->activeConversations(); + validThreadIDsReply.waitForFinished(); + + for (const QString& conversationId : validThreadIDsReply.value()) + { + handleCreatedConversation(conversationId); + } + +} + +void ConversationListModel::handleCreatedConversation(const QString& conversationId) +{ + QVariantList args; + args.append(conversationId); + + m_conversationsInterface->callWithCallback("getFirstFromConversation", args, + this, SLOT(createRowFromMessage(ConversationMessage)), SLOT(printDBusError(QDBusError))); +} + +void ConversationListModel::printDBusError(const QDBusError& error) +{ + qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << error; +} + +void ConversationListModel::createRowFromMessage(const ConversationMessage& message) +{ + 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; + } + + QStandardItem* item = new QStandardItem(); + + KPeople::PersonData* personData = lookupPersonByAddress(message.address()); + if (personData) + { + item->setText(personData->name()); + item->setIcon(QIcon(personData->photo())); + item->setData(personData->personUri(), PersonUriRole); + } + else + { + item->setText(message.address()); + } + + item->setData(message.threadID(), ConversationIdRole); + item->setData(message.type() == ConversationMessage::MessageTypeSent, FromMeRole); + item->setData(message.date(), DateRole); + + appendRow(item); + delete personData; +} + +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,39 @@ #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 }; 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: + 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,53 @@ */ #include "conversationmodel.h" +#include + +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"); setItemRoleNames(roles); } +ConversationModel::~ConversationModel() +{ + if (m_conversationsInterface) delete m_conversationsInterface; +} + QString ConversationModel::threadId() const { + qCCritical(KDECONNECT_SMS_CONVERSATION_MODEL) << "Hi"; return {}; } void ConversationModel::setThreadId(const QString &threadId) { + qCCritical(KDECONNECT_SMS_CONVERSATION_MODEL) << "Setting threadId of" << this << "to" << threadId; + 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"))); } + +void ConversationModel::setDeviceId(const QString& deviceId) +{ + qCCritical(KDECONNECT_SMS_CONVERSATION_MODEL) << "setDeviceId" << "of" << this; + if (m_conversationsInterface) delete m_conversationsInterface; + + m_deviceId = deviceId; + + m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId); +} + +void ConversationModel::sendReplyToConversation(const QString& message) +{ + qCCritical(KDECONNECT_SMS_CONVERSATION_MODEL) << "Should have sent " << message; + m_conversationsInterface->replyToConversation(m_threadId, message); +} 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,9 @@ } KDBusService service(KDBusService::Unique); - + 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 @@ -29,16 +29,16 @@ id: page property QtObject person property QtObject device + property QtObject 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) ListView { model: ConversationModel { id: model deviceId: device.id() - threadId: "xxxx" + threadId: conversationId } delegate: Kirigami.BasicListItem { @@ -56,7 +56,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,8 @@ id: view currentIndex: 0 - model: PersonsSortFilterProxyModel { - requiredProperties: ["phoneNumber"] - sortRole: Qt.DisplayRole - sortCaseSensitivity: Qt.CaseInsensitive - sourceModel: PersonsModel {} + model: ConversationListModel { + deviceId: device.id() } header: TextField { @@ -74,24 +89,13 @@ label: display icon: decoration function startChat() { - applicationWindow().pageStack.push(chatView, { person: person.person, device: Qt.binding(function() {return devicesCombo.device })}) + applicationWindow().pageStack.push(chatView, { + person: person.person, + 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