diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt index db8a12e0..6010bc62 100644 --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -1,82 +1,85 @@ project(KDEConnectInterfaces) function(geninterface source_h output_h) set(xml_file ${CMAKE_CURRENT_BINARY_DIR}/${output_h}.xml) qt5_generate_dbus_interface( ${source_h} ${xml_file}) list(APPEND libkdeconnect_HEADERS ${CMAKE_CURRENT_BINARY_DIR}/${output_h}) set_source_files_properties(${xml_file} PROPERTIES NO_NAMESPACE true) qt5_add_dbus_interface(libkdeconnect_SRC ${xml_file} ${output_h}) set(libkdeconnect_SRC ${libkdeconnect_SRC} PARENT_SCOPE) set(libkdeconnect_HEADERS ${libkdeconnect_HEADERS} PARENT_SCOPE) endfunction() set(libkdeconnect_SRC dbusinterfaces.cpp devicesmodel.cpp notificationsmodel.cpp devicessortproxymodel.cpp + conversationmessage.cpp # modeltest.cpp ) set(libkdeconnect_public_HEADERS KDEConnect/DevicesModel KDEConnect/NotificationsModel ) set(libkdeconnect_HEADERS devicesmodel.h notificationsmodel.h + conversationmessage.h dbusinterfaces.h ${CMAKE_CURRENT_BINARY_DIR}/kdeconnectinterfaces_export.h ) geninterface(${CMAKE_SOURCE_DIR}/core/daemon.h daemoninterface) geninterface(${CMAKE_SOURCE_DIR}/core/device.h deviceinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/battery/batterydbusinterface.h devicebatteryinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/sftp/sftpplugin.h devicesftpinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/notifications/notificationsdbusinterface.h devicenotificationsinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/findmyphone/findmyphoneplugin.h devicefindmyphoneinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/notifications/notification.h notificationinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/mprisremote/mprisremoteplugin.h mprisremoteinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/remotecontrol/remotecontrolplugin.h remotecontrolinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/lockdevice/lockdeviceplugin.h lockdeviceinterface) 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}) set_target_properties(kdeconnectinterfaces PROPERTIES VERSION ${KDECONNECT_VERSION} SOVERSION ${KDECONNECT_VERSION_MAJOR} ) generate_export_header(kdeconnectinterfaces EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/kdeconnectinterfaces_export.h BASE_NAME KDEConnectInterfaces) target_link_libraries(kdeconnectinterfaces LINK_PUBLIC Qt5::Gui Qt5::DBus LINK_PRIVATE KF5::ConfigCore KF5::I18n ) configure_file(KDEConnectConfig.cmake.in ${CMAKE_BINARY_DIR}/interfaces/KDEConnectConfig.cmake @ONLY) ecm_setup_version( "${KDECONNECT_VERSION_MAJOR}.${KDECONNECT_VERSION_MINOR}.${KDECONNECT_VERSION_PATCH}" VARIABLE_PREFIX KDECONNECTINTERFACES VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kdeconnectinterfaces_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KDEConnectConfigVersion.cmake" SOVERSION ${KDECONNECT_VERSION_MAJOR}) install(TARGETS kdeconnectinterfaces EXPORT kdeconnectLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) ## Don't install header files until API/ABI policy is defined # # install(FILES ${libkdeconnect_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/kdeconnect COMPONENT Devel) # install(FILES ${libkdeconnect_public_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/KDEConnect COMPONENT Devel) # install(FILES ${CMAKE_BINARY_DIR}/interfaces/KDEConnectConfig.cmake # ${CMAKE_BINARY_DIR}/interfaces/KDEConnectConfigVersion.cmake # DESTINATION ${LIB_INSTALL_DIR}/cmake/KDEConnect) diff --git a/interfaces/conversationmessage.cpp b/interfaces/conversationmessage.cpp new file mode 100644 index 00000000..8cb40f8a --- /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/conversationmessage.h b/interfaces/conversationmessage.h new file mode 100644 index 00000000..70b36250 --- /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/dbusinterfaces.cpp b/interfaces/dbusinterfaces.cpp index 53b17537..0f0fdfdd 100644 --- a/interfaces/dbusinterfaces.cpp +++ b/interfaces/dbusinterfaces.cpp @@ -1,173 +1,184 @@ /** * 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 . */ #include "dbusinterfaces.h" QString DaemonDbusInterface::activatedService() { static const QString service = QStringLiteral("org.kde.kdeconnect"); auto reply = QDBusConnection::sessionBus().interface()->startService(service); if (!reply.isValid()) { qWarning() << "error activating kdeconnectd:" << QDBusConnection::sessionBus().interface()->lastError(); } return service; } DaemonDbusInterface::DaemonDbusInterface(QObject* parent) : OrgKdeKdeconnectDaemonInterface(DaemonDbusInterface::activatedService(), QStringLiteral("/modules/kdeconnect"), QDBusConnection::sessionBus(), parent) { connect(this, &OrgKdeKdeconnectDaemonInterface::pairingRequestsChanged, this, &DaemonDbusInterface::pairingRequestsChangedProxy); } DaemonDbusInterface::~DaemonDbusInterface() { } DeviceDbusInterface::DeviceDbusInterface(const QString& id, QObject* parent) : OrgKdeKdeconnectDeviceInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/"+id, QDBusConnection::sessionBus(), parent) , m_id(id) { connect(this, &OrgKdeKdeconnectDeviceInterface::trustedChanged, this, &DeviceDbusInterface::trustedChangedProxy); connect(this, &OrgKdeKdeconnectDeviceInterface::reachableChanged, this, &DeviceDbusInterface::reachableChangedProxy); connect(this, &OrgKdeKdeconnectDeviceInterface::nameChanged, this, &DeviceDbusInterface::nameChangedProxy); connect(this, &OrgKdeKdeconnectDeviceInterface::hasPairingRequestsChanged, this, &DeviceDbusInterface::hasPairingRequestsChangedProxy); } DeviceDbusInterface::~DeviceDbusInterface() { } QString DeviceDbusInterface::id() const { return m_id; } void DeviceDbusInterface::pluginCall(const QString& plugin, const QString& method) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+id()+'/'+plugin, "org.kde.kdeconnect.device."+plugin, method); QDBusConnection::sessionBus().asyncCall(msg); } DeviceBatteryDbusInterface::DeviceBatteryDbusInterface(const QString& id, QObject* parent) : OrgKdeKdeconnectDeviceBatteryInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/"+id, QDBusConnection::sessionBus(), parent) { } DeviceBatteryDbusInterface::~DeviceBatteryDbusInterface() { } DeviceNotificationsDbusInterface::DeviceNotificationsDbusInterface(const QString& id, QObject* parent) : OrgKdeKdeconnectDeviceNotificationsInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/"+id, QDBusConnection::sessionBus(), parent) { } DeviceNotificationsDbusInterface::~DeviceNotificationsDbusInterface() { } NotificationDbusInterface::NotificationDbusInterface(const QString& deviceId, const QString& notificationId, QObject* parent) : OrgKdeKdeconnectDeviceNotificationsNotificationInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/"+deviceId+"/notifications/"+notificationId, QDBusConnection::sessionBus(), parent) , id(notificationId) { } NotificationDbusInterface::~NotificationDbusInterface() { } +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) { } SftpDbusInterface::~SftpDbusInterface() { } MprisDbusInterface::MprisDbusInterface(const QString& id, QObject* parent) : OrgKdeKdeconnectDeviceMprisremoteInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + id + "/mprisremote", QDBusConnection::sessionBus(), parent) { connect(this, &OrgKdeKdeconnectDeviceMprisremoteInterface::propertiesChanged, this, &MprisDbusInterface::propertiesChangedProxy); } MprisDbusInterface::~MprisDbusInterface() { } RemoteControlDbusInterface::RemoteControlDbusInterface(const QString& id, QObject* parent) : OrgKdeKdeconnectDeviceRemotecontrolInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + id + "/remotecontrol", QDBusConnection::sessionBus(), parent) { } RemoteControlDbusInterface::~RemoteControlDbusInterface() { } LockDeviceDbusInterface::LockDeviceDbusInterface(const QString& id, QObject* parent) : OrgKdeKdeconnectDeviceLockdeviceInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + id + "/lockdevice", QDBusConnection::sessionBus(), parent) { connect(this, &OrgKdeKdeconnectDeviceLockdeviceInterface::lockedChanged, this, &LockDeviceDbusInterface::lockedChangedProxy); Q_ASSERT(isValid()); } LockDeviceDbusInterface::~LockDeviceDbusInterface() { } FindMyPhoneDeviceDbusInterface::FindMyPhoneDeviceDbusInterface(const QString& deviceId, QObject* parent): OrgKdeKdeconnectDeviceFindmyphoneInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/findmyphone", QDBusConnection::sessionBus(), parent) { } FindMyPhoneDeviceDbusInterface::~FindMyPhoneDeviceDbusInterface() { } RemoteCommandsDbusInterface::RemoteCommandsDbusInterface(const QString& deviceId, QObject* parent): OrgKdeKdeconnectDeviceRemotecommandsInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/remotecommands", QDBusConnection::sessionBus(), parent) { } RemoteCommandsDbusInterface::~RemoteCommandsDbusInterface() = default; RemoteKeyboardDbusInterface::RemoteKeyboardDbusInterface(const QString& deviceId, QObject* parent): OrgKdeKdeconnectDeviceRemotekeyboardInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/remotekeyboard", QDBusConnection::sessionBus(), parent) { connect(this, &OrgKdeKdeconnectDeviceRemotekeyboardInterface::remoteStateChanged, this, &RemoteKeyboardDbusInterface::remoteStateChanged); } RemoteKeyboardDbusInterface::~RemoteKeyboardDbusInterface() = default; TelephonyDbusInterface::TelephonyDbusInterface(const QString& deviceId, QObject* parent): OrgKdeKdeconnectDeviceTelephonyInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/telephony", QDBusConnection::sessionBus(), parent) { } TelephonyDbusInterface::~TelephonyDbusInterface() = default; diff --git a/interfaces/dbusinterfaces.h b/interfaces/dbusinterfaces.h index 434e5981..348159d5 100644 --- a/interfaces/dbusinterfaces.h +++ b/interfaces/dbusinterfaces.h @@ -1,225 +1,234 @@ /** * 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 DBUSINTERFACES_H #define DBUSINTERFACES_H #include "interfaces/kdeconnectinterfaces_export.h" #include "interfaces/daemoninterface.h" #include "interfaces/deviceinterface.h" #include "interfaces/devicebatteryinterface.h" #include "interfaces/devicesftpinterface.h" #include "interfaces/devicefindmyphoneinterface.h" #include "interfaces/devicenotificationsinterface.h" #include "interfaces/notificationinterface.h" #include "interfaces/mprisremoteinterface.h" #include "interfaces/remotecontrolinterface.h" #include "interfaces/lockdeviceinterface.h" #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 * interface, so we can change the class name in a single place. */ class KDECONNECTINTERFACES_EXPORT DaemonDbusInterface : public OrgKdeKdeconnectDaemonInterface { Q_OBJECT public: explicit DaemonDbusInterface(QObject* parent = nullptr); ~DaemonDbusInterface() override; static QString activatedService(); Q_SIGNALS: void deviceAdded(const QString& id); void pairingRequestsChangedProxy(); }; class KDECONNECTINTERFACES_EXPORT DeviceDbusInterface : public OrgKdeKdeconnectDeviceInterface { Q_OBJECT // TODO: Workaround because OrgKdeKdeconnectDeviceInterface is not generating // the signals for the properties Q_PROPERTY(bool isReachable READ isReachable NOTIFY reachableChangedProxy) Q_PROPERTY(bool isTrusted READ isTrusted NOTIFY trustedChangedProxy) Q_PROPERTY(QString name READ name NOTIFY nameChangedProxy) Q_PROPERTY(bool hasPairingRequests READ hasPairingRequests NOTIFY hasPairingRequestsChangedProxy) public: explicit DeviceDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~DeviceDbusInterface() override; Q_SCRIPTABLE QString id() const; Q_SCRIPTABLE void pluginCall(const QString& plugin, const QString& method); Q_SIGNALS: void nameChangedProxy(const QString& name); void trustedChangedProxy(bool paired); void reachableChangedProxy(bool reachable); void hasPairingRequestsChangedProxy(bool); private: const QString m_id; }; class KDECONNECTINTERFACES_EXPORT DeviceBatteryDbusInterface : public OrgKdeKdeconnectDeviceBatteryInterface { Q_OBJECT public: explicit DeviceBatteryDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~DeviceBatteryDbusInterface() override; }; class KDECONNECTINTERFACES_EXPORT DeviceNotificationsDbusInterface : public OrgKdeKdeconnectDeviceNotificationsInterface { Q_OBJECT public: explicit DeviceNotificationsDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~DeviceNotificationsDbusInterface() override; }; class KDECONNECTINTERFACES_EXPORT NotificationDbusInterface : public OrgKdeKdeconnectDeviceNotificationsNotificationInterface { Q_OBJECT public: NotificationDbusInterface(const QString& deviceId, const QString& notificationId, QObject* parent = nullptr); ~NotificationDbusInterface() override; QString notificationId() { return id; } private: 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 { Q_OBJECT public: explicit SftpDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~SftpDbusInterface() override; }; class KDECONNECTINTERFACES_EXPORT MprisDbusInterface : public OrgKdeKdeconnectDeviceMprisremoteInterface { Q_OBJECT // TODO: Workaround because qdbusxml2cpp is not generating // the signals for the properties Q_PROPERTY(bool isPlaying READ isPlaying NOTIFY propertiesChangedProxy) Q_PROPERTY(int length READ length NOTIFY propertiesChangedProxy) Q_PROPERTY(QString nowPlaying READ nowPlaying NOTIFY propertiesChangedProxy) Q_PROPERTY(QString title READ title NOTIFY propertiesChangedProxy) Q_PROPERTY(QString artist READ artist NOTIFY propertiesChangedProxy) Q_PROPERTY(QString album READ album NOTIFY propertiesChangedProxy) Q_PROPERTY(QStringList playerList READ playerList NOTIFY propertiesChangedProxy) Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY propertiesChangedProxy) Q_PROPERTY(int position READ position WRITE setPosition NOTIFY propertiesChangedProxy) public: explicit MprisDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~MprisDbusInterface() override; Q_SIGNALS: void propertiesChangedProxy(); }; class KDECONNECTINTERFACES_EXPORT RemoteControlDbusInterface : public OrgKdeKdeconnectDeviceRemotecontrolInterface { Q_OBJECT public: explicit RemoteControlDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~RemoteControlDbusInterface() override; }; class KDECONNECTINTERFACES_EXPORT LockDeviceDbusInterface : public OrgKdeKdeconnectDeviceLockdeviceInterface { Q_OBJECT Q_PROPERTY(bool isLocked READ isLocked WRITE setIsLocked NOTIFY lockedChangedProxy) public: explicit LockDeviceDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~LockDeviceDbusInterface() override; Q_SIGNALS: void lockedChangedProxy(bool isLocked); }; class KDECONNECTINTERFACES_EXPORT FindMyPhoneDeviceDbusInterface : public OrgKdeKdeconnectDeviceFindmyphoneInterface { Q_OBJECT public: explicit FindMyPhoneDeviceDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~FindMyPhoneDeviceDbusInterface() override; }; class KDECONNECTINTERFACES_EXPORT RemoteCommandsDbusInterface : public OrgKdeKdeconnectDeviceRemotecommandsInterface { Q_OBJECT public: explicit RemoteCommandsDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~RemoteCommandsDbusInterface() override; }; class KDECONNECTINTERFACES_EXPORT RemoteKeyboardDbusInterface : public OrgKdeKdeconnectDeviceRemotekeyboardInterface { Q_OBJECT Q_PROPERTY(bool remoteState READ remoteState NOTIFY remoteStateChanged) public: explicit RemoteKeyboardDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~RemoteKeyboardDbusInterface() override; Q_SIGNALS: void remoteStateChanged(bool state); }; class KDECONNECTINTERFACES_EXPORT TelephonyDbusInterface : public OrgKdeKdeconnectDeviceTelephonyInterface { Q_OBJECT public: explicit TelephonyDbusInterface(const QString& deviceId, QObject* parent = nullptr); ~TelephonyDbusInterface() override; }; template static void setWhenAvailable(const QDBusPendingReply& pending, W func, QObject* parent) { QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(pending, parent); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, parent, [func](QDBusPendingCallWatcher* watcher) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; func(reply.value()); }); } #endif diff --git a/plugins/contacts/contactsplugin.cpp b/plugins/contacts/contactsplugin.cpp index 8306d724..1473f5b5 100644 --- a/plugins/contacts/contactsplugin.cpp +++ b/plugins/contacts/contactsplugin.cpp @@ -1,211 +1,212 @@ /** * Copyright 2018 Simon Redman * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KdeConnectPluginFactory, "kdeconnect_contacts.json", registerPlugin(); ) Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_CONTACTS, "kdeconnect.plugin.contacts") ContactsPlugin::ContactsPlugin (QObject* parent, const QVariantList& args) : - KdeConnectPlugin(parent, args) { + KdeConnectPlugin(parent, args) +{ vcardsPath = QString(*vcardsLocation).append("/kdeconnect-").append(device()->id()); // Register custom types with dbus qRegisterMetaType("uID"); qDBusRegisterMetaType(); qRegisterMetaType("uIDList_t"); qDBusRegisterMetaType(); // Create the storage directory if it doesn't exist if (!QDir().mkpath(vcardsPath)) { qCWarning(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseVCards:" << "Unable to create VCard directory"; } this->synchronizeRemoteWithLocal(); qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Contacts constructor for device " << device()->name(); } ContactsPlugin::~ContactsPlugin () { QDBusConnection::sessionBus().unregisterObject(dbusPath(), QDBusConnection::UnregisterTree); // qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Contacts plugin destructor for device" << device()->name(); } bool ContactsPlugin::receivePacket (const NetworkPacket& np) { qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Packet Received for device " << device()->name(); qCDebug(KDECONNECT_PLUGIN_CONTACTS) << np.body(); if (np.type() == PACKAGE_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS) { return this->handleResponseUIDsTimestamps(np); } else if (np.type() == PACKET_TYPE_CONTACTS_RESPONSE_VCARDS) { return this->handleResponseVCards(np); } else { // Is this check necessary? qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Unknown package type received from device: " << device()->name() << ". Maybe you need to upgrade KDE Connect?"; return false; } } void ContactsPlugin::synchronizeRemoteWithLocal () { this->sendRequest(PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMP); } bool ContactsPlugin::handleResponseUIDsTimestamps (const NetworkPacket& np) { if (!np.has("uids")) { qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseUIDsTimestamps:" << "Malformed packet does not have uids key"; return false; } uIDList_t uIDsToUpdate; QDir vcardsDir(vcardsPath); // Get a list of all file info in this directory // Clean out IDs returned from the remote. Anything leftover should be deleted QFileInfoList localVCards = vcardsDir.entryInfoList( { "*.vcard", "*.vcf" }); const QStringList& uIDs = np.get("uids"); // Check local storage for the contacts: // If the contact is not found in local storage, request its vcard be sent // If the contact is in local storage but not reported, delete it // If the contact is in local storage, compare its timestamp. If different, request the contact for (const QString& ID : uIDs) { QString filename = vcardsDir.filePath(ID + VCARD_EXTENSION); QFile vcardFile(filename); if (!QFile().exists(filename)) { // We do not have a vcard for this contact. Request it. uIDsToUpdate.push_back(ID); continue; } // Remove this file from the list of known files QFileInfo fileInfo(vcardFile); bool success = localVCards.removeOne(fileInfo); Q_ASSERT(success); // We should have always been able to remove the existing file from our listing // Check if the vcard needs to be updated if (!vcardFile.open(QIODevice::ReadOnly)) { qCWarning(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseUIDsTimestamps:" << "Unable to open" << filename << "to read even though it was reported to exist"; continue; } QTextStream fileReadStream(&vcardFile); QString line; while (!fileReadStream.atEnd()) { fileReadStream >> line; // TODO: Check that the saved ID is the same as the one we were expecting. This requires parsing the VCard if (!line.startsWith("X-KDECONNECT-TIMESTAMP:")) { continue; } QStringList parts = line.split(":"); QString timestamp = parts[1]; qint32 remoteTimestamp = np.get(ID); qint32 localTimestamp = timestamp.toInt(); if (!(localTimestamp == remoteTimestamp)) { uIDsToUpdate.push_back(ID); } } } // Delete all locally-known files which were not reported by the remote device for (const QFileInfo& unknownFile : localVCards) { QFile toDelete(unknownFile.filePath()); toDelete.remove(); } this->sendRequestWithIDs(PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS, uIDsToUpdate); return true; } bool ContactsPlugin::handleResponseVCards (const NetworkPacket& np) { if (!np.has("uids")) { qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseVCards:" << "Malformed packet does not have uids key"; return false; } QDir vcardsDir(vcardsPath); const QStringList& uIDs = np.get("uids"); // Loop over all IDs, extract the VCard from the packet and write the file for (const auto& ID : uIDs) { qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "Got VCard:" << np.get(ID); QString filename = vcardsDir.filePath(ID + VCARD_EXTENSION); QFile vcardFile(filename); bool vcardFileOpened = vcardFile.open(QIODevice::WriteOnly); // Want to smash anything that might have already been there if (!vcardFileOpened) { qCWarning(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseVCards:" << "Unable to open" << filename; continue; } QTextStream fileWriteStream(&vcardFile); const QString& vcard = np.get(ID); fileWriteStream << vcard; } qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "handleResponseVCards:" << "Got" << uIDs.size() << "VCards"; Q_EMIT localCacheSynchronized(uIDs); return true; } bool ContactsPlugin::sendRequest (const QString& packetType) { NetworkPacket np(packetType); bool success = sendPacket(np); qCDebug(KDECONNECT_PLUGIN_CONTACTS) << "sendRequest: Sending " << packetType << success; return success; } bool ContactsPlugin::sendRequestWithIDs (const QString& packetType, const uIDList_t& uIDs) { NetworkPacket np(packetType); np.set("uids", uIDs); bool success = sendPacket(np); return success; } QString ContactsPlugin::dbusPath () const { return "/modules/kdeconnect/devices/" + device()->id() + "/contacts"; } #include "contactsplugin.moc" diff --git a/plugins/telephony/CMakeLists.txt b/plugins/telephony/CMakeLists.txt index 933c4591..4c2ec9ed 100644 --- a/plugins/telephony/CMakeLists.txt +++ b/plugins/telephony/CMakeLists.txt @@ -1,13 +1,19 @@ +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 Qt5::Widgets ) diff --git a/plugins/telephony/conversationsdbusinterface.cpp b/plugins/telephony/conversationsdbusinterface.cpp new file mode 100644 index 00000000..976ec72f --- /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 + * + * 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/kdeconnect_telephony.json b/plugins/telephony/kdeconnect_telephony.json index 2ea6e687..0467828c 100644 --- a/plugins/telephony/kdeconnect_telephony.json +++ b/plugins/telephony/kdeconnect_telephony.json @@ -1,97 +1,100 @@ { "KPlugin": { "Authors": [ { "Email": "albertvaka@gmail.com", "Name": "Albert Vaca", "Name[sr@ijekavian]": "Алберт Вака Синтора", "Name[sr@ijekavianlatin]": "Albert Vaka Sintora", "Name[sr@latin]": "Albert Vaka Sintora", "Name[sr]": "Алберт Вака Синтора", "Name[x-test]": "xxAlbert Vacaxx" } ], "Description": "Show notifications for calls and SMS", "Description[ar]": "أظهر إخطارات المكالمات والرّسائل", "Description[ca@valencia]": "Mostra les notificacions de les trucades i els SMS", "Description[ca]": "Mostra les notificacions de les trucades i els SMS", "Description[cs]": "Zobrazit upozornění pro telefonáty a SMS", "Description[da]": "Vis bekendtgørelser for opkald og sms", "Description[de]": "Benachrichtigungen für Anrufe und SMS anzeigen", "Description[el]": "Εμφάνιση ειδοποιήσεων για κλήσεις και SMS", "Description[es]": "Mostrar notificaciones de llamadas y SMS", "Description[et]": "Kõnede ja SMS-ide märguannete näitamine", "Description[eu]": "Erakutsi deien eta SMS mezuen jakinarazpenak", "Description[fi]": "Näytä puhelujen ja tekstiviestien ilmoitukset", "Description[fr]": "Affichez les notifications pour les appels et les SMS", "Description[gl]": "Mostrar notificacións de chamadas e mensaxes SMS.", "Description[it]": "Mostra notifiche per le chiamate e per gli SMS", "Description[ko]": "통화와 SMS 알림 표시", "Description[nl]": "Meldingen tonen van oproepen en SMSjes", "Description[nn]": "Vis varslingar for oppringingar og SMS", "Description[pl]": "Powiadamiaj o dzwonieniu i esemesach", "Description[pt]": "Mostrar notificações para as chamadas e SMS", "Description[pt_BR]": "Mostra notificações para chamadas e SMS", "Description[ru]": "Показ уведомлений для звонков и SMS", "Description[sr@ijekavian]": "Приказује обавештења за позиве и СМС", "Description[sr@ijekavianlatin]": "Prikazuje obaveštenja za pozive i SMS", "Description[sr@latin]": "Prikazuje obaveštenja za pozive i SMS", "Description[sr]": "Приказује обавештења за позиве и СМС", "Description[sv]": "Visa underrättelser om samtal och SMS", "Description[tr]": "Çağrı ve SMS'ler için bildirim göster", "Description[uk]": "Показ сповіщень щодо дзвінків і SMS", "Description[x-test]": "xxShow notifications for calls and SMSxx", "Description[zh_CN]": "显示来电和短信通知", "Description[zh_TW]": "顯示來電與短訊通知", "EnabledByDefault": true, "Icon": "call-start", "Id": "kdeconnect_telephony", "License": "GPL", "Name": "Telephony integration", "Name[ar]": "تكامل الهاتف", "Name[ca@valencia]": "Integració amb la telefonia", "Name[ca]": "Integració amb la telefonia", "Name[cs]": "Integrace telefonu", "Name[da]": "Integration af telefoni", "Name[de]": "Telefon-Integration", "Name[el]": "Ενσωμάτωση τηλεφωνίας", "Name[es]": "Integración con el teléfono", "Name[et]": "Telefoniga lõimimine", "Name[eu]": "Telefoniarekin integrazioa", "Name[fi]": "Puhelinintegrointi", "Name[fr]": "Intégration de la téléphonie", "Name[gl]": "Integración telefónica", "Name[hu]": "Telefonintegráció", "Name[it]": "Integrazione telefonica", "Name[ko]": "전화 통합", "Name[nl]": "Telefoonintegratie", "Name[nn]": "Telefonintegrering", "Name[pl]": "Integracja z telefonem", "Name[pt]": "Integração telefónica", "Name[pt_BR]": "Integração telefônica", "Name[ru]": "Интеграция телефонии", "Name[sk]": "Integrácia telefónie", "Name[sr@ijekavian]": "Интегрисана телефонија", "Name[sr@ijekavianlatin]": "Integrisana telefonija", "Name[sr@latin]": "Integrisana telefonija", "Name[sr]": "Интегрисана телефонија", "Name[sv]": "Integrering av telefoni", "Name[tr]": "Telefon tümleştirmesi", "Name[uk]": "Інтеграція з системою телефонії", "Name[x-test]": "xxTelephony integrationxx", "Name[zh_CN]": "Telephony 集成", "Name[zh_TW]": "集成電話", "ServiceTypes": [ "KdeConnect/Plugin" ], "Version": "0.1", "Website": "http://albertvaka.wordpress.com" }, "X-KdeConnect-OutgoingPacketType": [ "kdeconnect.telephony.request", - "kdeconnect.sms.request" + "kdeconnect.sms.request", + "kdeconnect.telephony.request_conversations", + "kdeconnect.telephony.request_conversation" ], "X-KdeConnect-SupportedPacketType": [ - "kdeconnect.telephony" + "kdeconnect.telephony", + "kdeconnect.telephony.message" ] } diff --git a/plugins/telephony/telephonyplugin.cpp b/plugins/telephony/telephonyplugin.cpp index 5c50efa2..2a5ba418 100644 --- a/plugins/telephony/telephonyplugin.cpp +++ b/plugins/telephony/telephonyplugin.cpp @@ -1,171 +1,240 @@ /** * 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 * 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 "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; KNotification::NotificationFlags flags = KNotification::CloseOnTimeout; const QString title = device()->name(); if (event == QLatin1String("ringing")) { type = QStringLiteral("callReceived"); icon = QStringLiteral("call-start"); content = i18n("Incoming call from %1", contactName); } else if (event == QLatin1String("missedCall")) { type = QStringLiteral("missedCall"); icon = QStringLiteral("call-start"); content = i18n("Missed call from %1", contactName); flags |= KNotification::Persistent; //Note that in Unity this generates a message box! } else if (event == QLatin1String("sms")) { type = QStringLiteral("smsReceived"); icon = QStringLiteral("mail-receive"); QString messageBody = np.get(QStringLiteral("messageBody"), QLatin1String("")); content = i18n("SMS from %1
%2", contactName, messageBody); flags |= KNotification::Persistent; //Note that in Unity this generates a message box! } else if (event == QLatin1String("talking")) { return nullptr; } else { #ifndef NDEBUG return nullptr; #else type = QStringLiteral("callReceived"); icon = QStringLiteral("phone"); content = i18n("Unknown telephony event: %1", event); #endif } qCDebug(KDECONNECT_PLUGIN_TELEPHONY) << "Creating notification with type:" << type; KNotification* notification = new KNotification(type, flags, this); if (!phoneThumbnail.isEmpty()) { QPixmap photo; photo.loadFromData(phoneThumbnail, "JPEG"); notification->setPixmap(photo); } else { notification->setIconName(icon); } notification->setComponentName(QStringLiteral("kdeconnect")); notification->setTitle(title); notification->setText(content); if (event == QLatin1String("ringing")) { 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); notification->setProperty("originalMessage", messageBody); connect(notification, &KNotification::action1Activated, this, &TelephonyPlugin::showSendSmsDialog); } return notification; } bool TelephonyPlugin::receivePacket(const NetworkPacket& np) { if (np.get(QStringLiteral("isCancel"))) { //TODO: Clear the old notification 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; } void TelephonyPlugin::sendMutePacket() { NetworkPacket packet(PACKET_TYPE_TELEPHONY_REQUEST, {{"action", "mute"}}); sendPacket(packet); } void TelephonyPlugin::sendSms(const QString& phoneNumber, const QString& messageBody) { NetworkPacket np(PACKET_TYPE_SMS_REQUEST, { {"sendSms", true}, {"phoneNumber", phoneNumber}, {"messageBody", messageBody} }); qDebug() << "sending sms!"; sendPacket(np); } void TelephonyPlugin::showSendSmsDialog() { QString phoneNumber = sender()->property("phoneNumber").toString(); QString contactName = sender()->property("contactName").toString(); QString originalMessage = sender()->property("originalMessage").toString(); SendReplyDialog* dialog = new SendReplyDialog(originalMessage, phoneNumber, contactName); connect(dialog, &SendReplyDialog::sendReply, this, &TelephonyPlugin::sendSms); dialog->show(); 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"; } #include "telephonyplugin.moc" diff --git a/plugins/telephony/telephonyplugin.h b/plugins/telephony/telephonyplugin.h index acfdb5d6..e5912fbd 100644 --- a/plugins/telephony/telephonyplugin.h +++ b/plugins/telephony/telephonyplugin.h @@ -1,62 +1,151 @@ /** * 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 * 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 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 : public KdeConnectPlugin { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.telephony") 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/smsapp/CMakeLists.txt b/smsapp/CMakeLists.txt index a3d9e877..d63e9c73 100644 --- 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.cpp b/smsapp/conversationlistmodel.cpp new file mode 100644 index 00000000..ea8c9ebe --- /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/conversationlistmodel.h b/smsapp/conversationlistmodel.h new file mode 100644 index 00000000..833b9299 --- /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/conversationmodel.cpp b/smsapp/conversationmodel.cpp index 0aac7251..19aa3c50 100644 --- a/smsapp/conversationmodel.cpp +++ b/smsapp/conversationmodel.cpp @@ -1,43 +1,86 @@ /* * 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 "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/conversationmodel.h b/smsapp/conversationmodel.h index d504b566..85e25a60 100644 --- a/smsapp/conversationmodel.h +++ b/smsapp/conversationmodel.h @@ -1,44 +1,66 @@ /* * 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 CONVERSATIONMODEL_H #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/main.cpp b/smsapp/main.cpp index 77072586..4ed669be 100644 --- a/smsapp/main.cpp +++ b/smsapp/main.cpp @@ -1,58 +1,62 @@ /* * This file is part of KDE Telepathy Chat * * Copyright (C) 2015 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 "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[]) { QApplication app(argc, argv); KAboutData aboutData("org.kde.kdeconnect.sms", i18n("SMS Instant Messaging"), QStringLiteral(KDECONNECT_VERSION_STRING), i18n("KDE Connect SMS"), KAboutLicense::GPL, i18n("(c) 2018, Aleix Pol Gonzalez")); aboutData.addAuthor(i18n("Aleix Pol Gonzalez"), {}, "aleixpol@kde.org"); KAboutData::setApplicationData(aboutData); { QCommandLineParser parser; aboutData.setupCommandLine(&parser); parser.addVersionOption(); parser.addHelpOption(); parser.process(app); aboutData.processCommandLine(&parser); } 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)); engine.load(QUrl("qrc:/qml/main.qml")); return app.exec(); } diff --git a/smsapp/qml/ContactList.qml b/smsapp/qml/ContactList.qml index 7d24c37d..12623268 100644 --- a/smsapp/qml/ContactList.qml +++ b/smsapp/qml/ContactList.qml @@ -1,97 +1,97 @@ /* * This file is part of KDE Telepathy Chat * * Copyright (C) 2015 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 */ import QtQuick 2.5 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 import org.kde.people 1.0 import org.kde.plasma.core 2.0 as Core import org.kde.kirigami 2.2 as Kirigami import org.kde.kdeconnect 1.0 Kirigami.ScrollablePage { Component { id: chatView ConversationDisplay {} } ListView { id: view currentIndex: 0 model: PersonsSortFilterProxyModel { requiredProperties: ["phoneNumber"] sortRole: Qt.DisplayRole sortCaseSensitivity: Qt.CaseInsensitive sourceModel: PersonsModel {} } header: TextField { id: filter placeholderText: i18n("Filter...") width: parent.width onTextChanged: { view.model.filterRegExp = new RegExp(filter.text) view.currentIndex = 0 } Keys.onUpPressed: view.currentIndex = Math.max(view.currentIndex-1, 0) Keys.onDownPressed: view.currentIndex = Math.min(view.currentIndex+1, view.count-1) onAccepted: { view.currentItem.startChat() } Shortcut { sequence: "Ctrl+F" onActivated: filter.forceActiveFocus() } } delegate: Kirigami.BasicListItem { hoverEnabled: true readonly property var person: PersonData { personUri: model.personUri } label: display icon: decoration function startChat() { applicationWindow().pageStack.push(chatView, { person: person.person, device: Qt.binding(function() {return devicesCombo.device })}) } onClicked: { startChat(); } } } 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 { //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/ConversationDisplay.qml b/smsapp/qml/ConversationDisplay.qml index 00825fb0..74aa5a22 100644 --- a/smsapp/qml/ConversationDisplay.qml +++ b/smsapp/qml/ConversationDisplay.qml @@ -1,69 +1,73 @@ /* * This file is part of KDE Telepathy Chat * * Copyright (C) 2015 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 */ 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 { readonly property real margin: 100 x: fromMe ? Kirigami.Units.gridUnit : margin width: parent.width - margin - Kirigami.Units.gridUnit contentItem: Label { text: model.display } } } footer: RowLayout { enabled: page.device TextField { id: message Layout.fillWidth: true placeholderText: i18n("Say hi...") onAccepted: { console.log("sending sms", page.phoneNumber) - page.telephony.sendSms(page.phoneNumber, message.text) + model.sendReplyToConversation(message.text) } } Button { text: "Send" onClicked: { message.accepted() } } } } diff --git a/smsapp/qml/ContactList.qml b/smsapp/qml/ConversationList.qml similarity index 74% copy from smsapp/qml/ContactList.qml copy to smsapp/qml/ConversationList.qml index 7d24c37d..25964f10 100644 --- a/smsapp/qml/ContactList.qml +++ b/smsapp/qml/ConversationList.qml @@ -1,97 +1,102 @@ /* * This file is part of KDE Telepathy Chat * * Copyright (C) 2015 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 */ import QtQuick 2.5 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 import org.kde.people 1.0 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 {} } ListView { 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 { id: filter placeholderText: i18n("Filter...") width: parent.width onTextChanged: { view.model.filterRegExp = new RegExp(filter.text) view.currentIndex = 0 } Keys.onUpPressed: view.currentIndex = Math.max(view.currentIndex-1, 0) Keys.onDownPressed: view.currentIndex = Math.min(view.currentIndex+1, view.count-1) onAccepted: { view.currentItem.startChat() } Shortcut { sequence: "Ctrl+F" onActivated: filter.forceActiveFocus() } } delegate: Kirigami.BasicListItem { 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 index 2645b0f8..9bf36a46 100644 --- a/smsapp/qml/main.qml +++ b/smsapp/qml/main.qml @@ -1,36 +1,37 @@ /* * This file is part of KDE Telepathy Chat * * Copyright (C) 2014 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 */ import QtQuick 2.1 import org.kde.kirigami 2.2 as Kirigami +import org.kde.kdeconnect 1.0 Kirigami.ApplicationWindow { id: root visible: true width: 800 height: 600 header: Kirigami.ToolBarApplicationHeader {} - pageStack.initialPage: ContactList { + pageStack.initialPage: ConversationList { title: i18n("KDE Connect SMS") } } diff --git a/smsapp/resources.qrc b/smsapp/resources.qrc index a53e8c51..dd63e4fe 100644 --- a/smsapp/resources.qrc +++ b/smsapp/resources.qrc @@ -1,7 +1,7 @@ qml/main.qml - qml/ContactList.qml + qml/ConversationList.qml qml/ConversationDisplay.qml