Commit 706fc314 authored by Aniket Kumar's avatar Aniket Kumar 🤵
Browse files

Adding support to send attachments to the remote device.

parent 60a1adf2
Pipeline #32585 passed with stage
in 14 minutes and 44 seconds
......@@ -53,6 +53,7 @@ int main(int argc, char** argv)
parser.addOption(QCommandLineOption(QStringLiteral("lock"), i18n("Lock the specified device")));
parser.addOption(QCommandLineOption(QStringLiteral("send-sms"), i18n("Sends an SMS. Requires destination"), i18n("message")));
parser.addOption(QCommandLineOption(QStringLiteral("destination"), i18n("Phone number to send the message"), i18n("phone number")));
parser.addOption(QCommandLineOption(QStringLiteral("attachment"), i18n("File urls to send attachments with the message"), i18n("file urls")));
parser.addOption(QCommandLineOption(QStringList(QStringLiteral("device")) << QStringLiteral("d"), i18n("Device ID"), QStringLiteral("dev")));
parser.addOption(QCommandLineOption(QStringList(QStringLiteral("name")) << QStringLiteral("n"), i18n("Device Name"), QStringLiteral("name")));
parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device")));
......@@ -258,9 +259,11 @@ int main(int argc, char** argv)
addresses << QVariant::fromValue(address);
}
const QStringList urlList = parser.value(QStringLiteral("attachment")).split(QRegularExpression(QStringLiteral("\\s+")));
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/sms"), QStringLiteral("org.kde.kdeconnect.device.sms"), QStringLiteral("sendSms"));
const QString text = parser.value(QStringLiteral("send-sms"));
msg.setArguments(QVariantList() << QVariant::fromValue(addresses) << text);
msg.setArguments(QVariantList() << QVariant::fromValue(addresses) << text << QVariant(urlList));
blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
} else {
QTextStream(stderr) << i18n("error: should specify the SMS's recipient by passing --destination <phone number>");
......
......@@ -72,6 +72,15 @@ ConversationAddress::ConversationAddress(QString address)
: m_address(address)
{}
bool ConversationMessage::isOutgoing() const
{
return type() == MessageTypeSent
|| type() == MessageTypeOutbox
|| type() == MessageTypeDraft
|| type() == MessageTypeFailed
|| type() == MessageTypeQueued;
}
Attachment::Attachment(qint64 partID, QString mimeType, QString base64EncodedFile, QString uniqueIdentifier)
: m_partID(partID)
, m_mimeType(mimeType)
......
......@@ -69,7 +69,7 @@ public:
bool isMultitarget() const { return (eventField() & ConversationMessage::EventMultiTarget); }
bool isIncoming() const { return type() == MessageTypeInbox; }
bool isOutgoing() const { return type() == MessageTypeSent; }
bool isOutgoing() const;
bool containsAttachment() const { return !attachments().isEmpty(); }
/**
......
......@@ -177,7 +177,7 @@ void ConversationsDbusInterface::updateConversation(const qint64& conversationID
waitingForMessagesLock.unlock();
}
void ConversationsDbusInterface::replyToConversation(const qint64& conversationID, const QString& message)
void ConversationsDbusInterface::replyToConversation(const qint64& conversationID, const QString& message, const QVariantList& attachmentUrls)
{
const auto messagesList = m_conversations[conversationID];
if (messagesList.isEmpty()) {
......@@ -192,11 +192,11 @@ void ConversationsDbusInterface::replyToConversation(const qint64& conversationI
addresses << QVariant::fromValue(address);
}
m_smsInterface.sendSms(addresses, message, messagesList.first().subID());
m_smsInterface.sendSms(addresses, message, attachmentUrls, messagesList.first().subID());
}
void ConversationsDbusInterface::sendWithoutConversation(const QVariantList& addresses, const QString& message) {
m_smsInterface.sendSms(addresses, message);
void ConversationsDbusInterface::sendWithoutConversation(const QVariantList& addresses, const QString& message, const QVariantList& attachmentUrls) {
m_smsInterface.sendSms(addresses, message, attachmentUrls);
}
void ConversationsDbusInterface::requestAllConversationThreads()
......
......@@ -77,12 +77,12 @@ public Q_SLOTS:
/**
* Send a new message to this conversation
*/
void replyToConversation(const qint64& conversationID, const QString& message);
void replyToConversation(const qint64& conversationID, const QString& message, const QVariantList& attachmentUrls);
/**
* Send a new message to the contact having no previous coversation with
*/
void sendWithoutConversation(const QVariantList& addressList, const QString& message);
void sendWithoutConversation(const QVariantList& addressList, const QString& message, const QVariantList& attachmentUrls);
/**
* Send the request to the Telephony plugin to update the list of conversation threads
......
......@@ -13,6 +13,10 @@
#include <QDebug>
#include <QDBusConnection>
#include <QProcess>
#include <QFile>
#include <QFileInfo>
#include <QMimeDatabase>
#include <QTextCodec>
#include <core/device.h>
#include <core/daemon.h>
......@@ -27,6 +31,7 @@ SmsPlugin::SmsPlugin(QObject* parent, const QVariantList& args)
, m_telepathyInterface(QStringLiteral("org.freedesktop.Telepathy.ConnectionManager.kdeconnect"), QStringLiteral("/kdeconnect"))
, m_conversationInterface(new ConversationsDbusInterface(this))
{
m_codec = QTextCodec::codecForName(CODEC_NAME);
}
SmsPlugin::~SmsPlugin()
......@@ -47,7 +52,7 @@ bool SmsPlugin::receivePacket(const NetworkPacket& np)
return true;
}
void SmsPlugin::sendSms(const QVariantList& addresses, const QString& messageBody, const qint64 subID)
void SmsPlugin::sendSms(const QVariantList& addresses, const QString& textMessage, const QVariantList& attachmentUrls, const qint64 subID)
{
QVariantList addressMapList;
for (const QVariant& address : addresses) {
......@@ -56,13 +61,35 @@ void SmsPlugin::sendSms(const QVariantList& addresses, const QString& messageBod
}
QVariantMap packetMap({
{QStringLiteral("sendSms"), true},
{QStringLiteral("addresses"), addressMapList},
{QStringLiteral("messageBody"), messageBody}
{QStringLiteral("version"), 2},
{QStringLiteral("addresses"), addressMapList}
});
// If there is any text message add it to the network packet
if (textMessage != QStringLiteral("")) {
packetMap[QStringLiteral("textMessage")] = textMessage;
}
if (subID != -1) {
packetMap[QStringLiteral("subID")] = subID;
}
QVariantList attachmentMapList;
for (const QVariant& attachmentUrl : attachmentUrls) {
const Attachment attachment = createAttachmentFromUrl(attachmentUrl.toString());
QVariantMap attachmentMap({
{QStringLiteral("fileName"), attachment.uniqueIdentifier()},
{QStringLiteral("base64EncodedFile"), attachment.base64EncodedFile()},
{QStringLiteral("mimeType"), attachment.mimeType()}
});
attachmentMapList.append(attachmentMap);
}
// If there is any attachment add it to the network packet
if (!attachmentMapList.isEmpty()) {
packetMap[QStringLiteral("attachments")] = attachmentMapList;
}
NetworkPacket np(PACKET_TYPE_SMS_REQUEST, packetMap);
qCDebug(KDECONNECT_PLUGIN_SMS) << "Dispatching SMS send request to remote";
sendPacket(np);
......@@ -185,6 +212,30 @@ void SmsPlugin::getAttachment(const qint64& partID, const QString& uniqueIdentif
}
}
Attachment SmsPlugin::createAttachmentFromUrl(const QString& url)
{
QFile file(url);
file.open(QIODevice::ReadOnly);
if (!file.exists()) {
return Attachment();
}
QFileInfo fileInfo(file);
QString fileName(fileInfo.fileName());
QByteArray byteArray = file.readAll().toBase64();
file.close();
QString base64EncodedFile = m_codec->toUnicode(byteArray);
QMimeDatabase mimeDatabase;
QString mimeType = mimeDatabase.mimeTypeForFile(url).name();
Attachment attachment(-1, mimeType, base64EncodedFile, fileName);
return attachment;
}
QString SmsPlugin::dbusPath() const
{
......
......@@ -75,15 +75,21 @@
/**
* Packet sent to request a message be sent
*
* This will almost certainly need to be replaced or augmented to support MMS,
* but be sure the Android side remains compatible with old desktop apps!
*
* The body should look like so:
* { "sendSms": true,
* { "version": 2,
* "addresses": <List of Addresses>
* "messageBody": "Hi mom!",
* "textMessage": "Hi mom!",
* "attachments": <List of Attached files>
* "sub_id": "3859358340534"
* }
*
* An AttachmentContainer object looks like:
* {
* "fileName": <String> // Name of the file
* "base64EncodedFile": <String> // Base64 encoded file
* "mimeType": <String> // File type (eg: image/jpg, video/mp4 etc.)
* }
*
*/
#define PACKET_TYPE_SMS_REQUEST QStringLiteral("kdeconnect.sms.request")
......@@ -123,6 +129,10 @@
Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_SMS)
#define CODEC_NAME "CP1251"
class QTextCodec;
class Q_DECL_EXPORT SmsPlugin
: public KdeConnectPlugin
{
......@@ -139,7 +149,7 @@ public:
QString dbusPath() const override;
public Q_SLOTS:
Q_SCRIPTABLE void sendSms(const QVariantList& addresses, const QString& messageBody, const qint64 subID = -1);
Q_SCRIPTABLE void sendSms(const QVariantList& addresses, const QString& textMessage, const QVariantList& attachmentUrls, const qint64 subID = -1);
/**
* Send a request to the remote for all of its conversations
......@@ -183,8 +193,14 @@ private:
*/
bool handleSmsAttachmentFile(const NetworkPacket& np);
/**
* Encode a local file so it can be sent to the remote device as part of an MMS message.
*/
Attachment createAttachmentFromUrl(const QString& url);
QDBusInterface m_telepathyInterface;
ConversationsDbusInterface* m_conversationInterface;
QTextCodec *m_codec;
};
#endif
......@@ -86,13 +86,18 @@ void ConversationModel::setAddressList(const QList<ConversationAddress>& address
m_addressList = addressList;
}
void ConversationModel::sendReplyToConversation(const QString& message)
bool ConversationModel::sendReplyToConversation(const QString& textMessage, QList<QUrl> attachmentUrls)
{
//qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Trying to send" << message << "to conversation with ID" << m_threadId;
m_conversationsInterface->replyToConversation(m_threadId, message);
QVariantList fileUrls;
for (const auto& url : attachmentUrls) {
fileUrls << QVariant::fromValue(url.toLocalFile());
}
m_conversationsInterface->replyToConversation(m_threadId, textMessage, fileUrls);
return true;
}
void ConversationModel::startNewConversation(const QString& message, const QList<ConversationAddress>& addressList)
bool ConversationModel::startNewConversation(const QString& textMessage, const QList<ConversationAddress>& addressList, QList<QUrl> attachmentUrls)
{
QVariantList addresses;
......@@ -100,7 +105,13 @@ void ConversationModel::startNewConversation(const QString& message, const QList
addresses << QVariant::fromValue(address);
}
m_conversationsInterface->sendWithoutConversation(addresses, message);
QVariantList fileUrls;
for (const auto& url : attachmentUrls) {
fileUrls << QVariant::fromValue(url.toLocalFile());
}
m_conversationsInterface->sendWithoutConversation(addresses, textMessage, fileUrls);
return true;
}
void ConversationModel::requestMoreMessages(const quint32& howMany)
......
......@@ -48,8 +48,8 @@ public:
QList<ConversationAddress> addressList() const { return m_addressList; }
void setAddressList(const QList<ConversationAddress>& addressList);
Q_INVOKABLE void sendReplyToConversation(const QString& message);
Q_INVOKABLE void startNewConversation(const QString& message, const QList<ConversationAddress>& addressList);
Q_INVOKABLE bool sendReplyToConversation(const QString& textMessage, QList<QUrl> attachmentUrls);
Q_INVOKABLE bool startNewConversation(const QString& textMessage, const QList<ConversationAddress>& addressList, QList<QUrl> attachmentUrls);
Q_INVOKABLE void requestMoreMessages(const quint32& howMany = 10);
Q_INVOKABLE QString getCharCountInfo(const QString& message) const;
......
......@@ -168,102 +168,8 @@ Kirigami.ScrollablePage
}
}
footer: Controls.Pane {
id: sendingArea
enabled: page.deviceConnected
layer.enabled: sendingArea.enabled
layer.effect: DropShadow {
verticalOffset: 1
color: Kirigami.Theme.disabledTextColor
samples: 20
spread: 0.3
}
Layout.fillWidth: true
padding: 0
wheelEnabled: true
background: Rectangle {
color: Kirigami.Theme.viewBackgroundColor
}
RowLayout {
anchors.fill: parent
Controls.ScrollView {
Layout.fillWidth: true
Layout.maximumHeight: page.height > 300 ? page.height / 3 : 2 * page.height / 3
contentWidth: page.width - sendButtonArea.width
clip: true
Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff
Controls.TextArea {
anchors.fill: parent
id: messageField
placeholderText: i18nd("kdeconnect-sms", "Compose message")
wrapMode: TextEdit.Wrap
topPadding: Kirigami.Units.gridUnit * 0.5
bottomPadding: topPadding
selectByMouse: true
topInset: height * 2 // This removes background (frame) of the TextArea. Setting `background: Item {}` would cause segfault.
Keys.onReturnPressed: {
if (event.key === Qt.Key_Return) {
if (event.modifiers & Qt.ShiftModifier) {
messageField.append("")
} else {
sendButton.onClicked()
event.accepted = true
}
}
}
}
}
ColumnLayout {
id: sendButtonArea
Controls.ToolButton {
id: sendButton
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
padding: 0
Kirigami.Icon {
source: "document-send"
enabled: sendButton.enabled
isMask: true
smooth: true
anchors.centerIn: parent
width: Kirigami.Units.gridUnit * 1.5
height: width
}
onClicked: {
// don't send empty messages
if (!messageField.text.length) {
return
}
// disable the button to prevent sending
// the same message several times
sendButton.enabled = false
// send the message
if (page.conversationId == page.invalidId) {
conversationModel.startNewConversation(messageField.text, addresses)
} else {
conversationModel.sendReplyToConversation(messageField.text)
}
messageField.text = ""
// re-enable the button
sendButton.enabled = true
}
}
Controls.Label {
id: "charCount"
text: conversationModel.getCharCountInfo(messageField.text)
visible: text.length > 0
Layout.minimumWidth: Math.max(Layout.minimumWidth, width) // Make this label only grow, never shrink
}
}
}
footer: SendingArea {
width: parent.width
addresses: page.addresses
}
}
/**
* Copyright (C) 2020 Aniket Kumar <anikketkumar786@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.1
import QtQuick.Controls 2.2 as Controls
import QtQuick.Layouts 1.1
import org.kde.kirigami 2.4 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Dialogs 1.1
import org.kde.kdeconnect.sms 1.0
ColumnLayout {
id: root
property var addresses
property var selectedFileUrls: []
readonly property int maxMessageSize: 600000
MessageDialog {
id: messageDialog
title: i18nd("kdeconnect-sms", "Failed to send")
text: i18nd("kdeconnect-sms", "Max message size limit exceeded.")
onAccepted: {
messageDialog.close()
}
}
FileDialog {
id: fileDialog
folder: shortcuts.home
selectMultiple: true
onAccepted: {
root.selectedFileUrls = fileDialog.fileUrls
fileDialog.close()
}
}
Controls.Pane {
id: sendingArea
enabled: page.deviceConnected
layer.enabled: sendingArea.enabled
layer.effect: DropShadow {
verticalOffset: 1
color: Kirigami.Theme.disabledTextColor
samples: 20
spread: 0.3
}
Layout.fillWidth: true
padding: 0
wheelEnabled: true
background: Rectangle {
color: Kirigami.Theme.viewBackgroundColor
}
RowLayout {
anchors.fill: parent
Controls.ScrollView {
Layout.fillWidth: true
Layout.maximumHeight: page.height > 300 ? page.height / 3 : 2 * page.height / 3
contentWidth: page.width - sendButtonArea.width
clip: true
Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff
Controls.TextArea {
anchors.fill: parent
id: messageField
placeholderText: i18nd("kdeconnect-sms", "Compose message")
wrapMode: TextEdit.Wrap
topPadding: Kirigami.Units.gridUnit * 0.5
bottomPadding: topPadding
selectByMouse: true
topInset: height * 2 // This removes background (frame) of the TextArea. Setting `background: Item {}` would cause segfault.
Keys.onReturnPressed: {
if (event.key === Qt.Key_Return) {
if (event.modifiers & Qt.ShiftModifier) {
messageField.append("")
} else {
sendButton.onClicked()
event.accepted = true
}
}
}
}
}
ColumnLayout {
id: sendButtonArea
RowLayout {
Controls.ToolButton {
id: sendButton
enabled: messageField.text.length || selectedFileUrls.length
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
padding: 0
Kirigami.Icon {
source: "document-send"
enabled: sendButton.enabled
isMask: true
smooth: true
anchors.centerIn: parent
width: Kirigami.Units.gridUnit * 1.5
height: width
}
property bool messageSent: false
onClicked: {
// disable the button to prevent sending
// the same message several times
sendButton.enabled = false
if (SmsHelper.totalMessageSize(selectedFileUrls, messageField.text) > maxMessageSize) {
messageDialog.visible = true
} else if (page.conversationId === page.invalidId) {
messageSent = conversationModel.startNewConversation(messageField.text, addresses, selectedFileUrls)
} else {
messageSent = conversationModel.sendReplyToConversation(messageField.text, selectedFileUrls)
}
if (messageSent) {
messageField.text = ""
selectedFileUrls = []
sendButton.enabled = false
}
}
}
Controls.ToolButton {
id: attachFilesButton
enabled: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
padding: 0
Text {
id: attachedFilesCount
text: selectedFileUrls.length
color: "red"
visible: selectedFileUrls.length > 0
}
Kirigami.Icon {
source: "insert-image"
isMask: true
smooth: true
anchors.centerIn: parent
width: Kirigami.Units.gridUnit * 1.5
height: width
}
onClicked: {
fileDialog.open()
}
}
Controls.ToolButton {
id: clearAttachmentButton
visible: selectedFileUrls.length > 0
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
padding: 0
Kirigami.Icon {
id: cancelIcon
source: "edit-clear"
isMask: true
smooth: true
anchors.centerIn: parent
width: Kirigami.Units.gridUnit * 1.5
height: width
}
onClicked: {
selectedFileUrls = []
if (messageField.text == "") {
sendButton.enabled = false
}
}
}
}
Controls.Label {
id: "charCount"
text: conversationModel.getCharCountInfo(messageField.text)
visible: text.length > 0
Layout.minimumWidth: Math