diff --git a/plugins/sms/conversationsdbusinterface.cpp b/plugins/sms/conversationsdbusinterface.cpp index a315c8535..cd43e6511 100644 --- a/plugins/sms/conversationsdbusinterface.cpp +++ b/plugins/sms/conversationsdbusinterface.cpp @@ -94,6 +94,11 @@ void ConversationsDbusInterface::requestConversation(const qint64& conversationI worker->work(); } +void ConversationsDbusInterface::requestAttachmentFile(const qint64& partID, const QString& uniqueIdentifier) +{ + m_smsInterface.getAttachment(partID, uniqueIdentifier); +} + void ConversationsDbusInterface::addMessages(const QList &messages) { QSet updatedConversationIDs; @@ -204,3 +209,7 @@ QString ConversationsDbusInterface::newId() { return QString::number(++m_lastId); } + +void ConversationsDbusInterface::attachmentDownloaded(const QString& filePath, const QString& fileName) { + Q_EMIT attachmentReceived(filePath, fileName); +} diff --git a/plugins/sms/conversationsdbusinterface.h b/plugins/sms/conversationsdbusinterface.h index 76107354b..9a5be8e9e 100644 --- a/plugins/sms/conversationsdbusinterface.h +++ b/plugins/sms/conversationsdbusinterface.h @@ -49,6 +49,12 @@ public: */ void updateConversation(const qint64& conversationID); + /** + * Gets the path of the succesfully downloaded attachment file and send + * update to the conversationModel + */ + void attachmentDownloaded(const QString &filePath, const QString &fileName); + public Q_SLOTS: /** * Return a list of the first message in every conversation @@ -83,6 +89,11 @@ public Q_SLOTS: */ void requestAllConversationThreads(); + /** + * Send the request to SMS plugin to fetch original attachment file path + */ + void requestAttachmentFile(const qint64& partID, const QString& uniqueIdentifier); + Q_SIGNALS: /** * Emitted whenever a conversation with no cached messages is added, either because the cache @@ -107,6 +118,11 @@ Q_SIGNALS: */ Q_SCRIPTABLE void conversationLoaded(qint64 conversationID, quint64 messageCount); + /** + * Emitted whenever we have succesfully download a requested attachment file from the phone + */ + Q_SCRIPTABLE void attachmentReceived(QString filePath, QString fileName); + private /*methods*/: QString newId(); //Generates successive identifiers to use as public ids diff --git a/plugins/sms/kdeconnect_sms.json b/plugins/sms/kdeconnect_sms.json index 7336b3b01..ed9d75e03 100644 --- a/plugins/sms/kdeconnect_sms.json +++ b/plugins/sms/kdeconnect_sms.json @@ -113,9 +113,11 @@ "X-KdeConnect-OutgoingPacketType": [ "kdeconnect.sms.request", "kdeconnect.sms.request_conversations", - "kdeconnect.sms.request_conversation" + "kdeconnect.sms.request_conversation", + "kdeconnect.sms.request_attachment" ], "X-KdeConnect-SupportedPacketType": [ - "kdeconnect.sms.messages" + "kdeconnect.sms.messages", + "kdeconnect.sms.attachment_file" ] } diff --git a/plugins/sms/smsplugin.cpp b/plugins/sms/smsplugin.cpp index a61becda4..9bf842180 100644 --- a/plugins/sms/smsplugin.cpp +++ b/plugins/sms/smsplugin.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "plugin_sms_debug.h" @@ -39,6 +40,10 @@ bool SmsPlugin::receivePacket(const NetworkPacket& np) return handleBatchMessages(np); } + if (np.type() == PACKET_TYPE_SMS_ATTACHMENT_FILE && np.hasPayload()) { + return handleSmsAttachmentFile(np); + } + return true; } @@ -78,6 +83,18 @@ void SmsPlugin::requestConversation (const qint64& conversationID) const sendPacket(np); } +void SmsPlugin::requestAttachment(const qint64& partID, const QString& uniqueIdentifier) +{ + const QVariantMap packetMap({ + {QStringLiteral("part_id"), partID}, + {QStringLiteral("unique_identifier"), uniqueIdentifier} + }); + + NetworkPacket np(PACKET_TYPE_SMS_REQUEST_ATTACHMENT, packetMap); + + sendPacket(np); +} + void SmsPlugin::forwardToTelepathy(const ConversationMessage& message) { // If we don't have a valid Telepathy interface, bail out @@ -110,6 +127,65 @@ bool SmsPlugin::handleBatchMessages(const NetworkPacket& np) return true; } +bool SmsPlugin::handleSmsAttachmentFile(const NetworkPacket& np) { + const QString fileName = np.get(QStringLiteral("filename")); + + QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + cacheDir.append(QStringLiteral("/") + device()->name() + QStringLiteral("/")); + QDir attachmentsCacheDir(cacheDir); + + if (!attachmentsCacheDir.exists()) { + qDebug() << attachmentsCacheDir.absolutePath() << " directory doesn't exist."; + return false; + } + + QUrl fileUrl = QUrl::fromLocalFile(attachmentsCacheDir.absolutePath()); + fileUrl = fileUrl.adjusted(QUrl::StripTrailingSlash); + fileUrl.setPath(fileUrl.path() + QStringLiteral("/") + fileName, QUrl::DecodedMode); + + + FileTransferJob* job = np.createPayloadTransferJob(fileUrl); + connect(job, &FileTransferJob::result, this, [this, fileName] (KJob* job) -> void { + FileTransferJob* ftjob = qobject_cast(job); + if (ftjob && !job->error()) { + // Notify SMS app about the newly downloaded attachment + m_conversationInterface->attachmentDownloaded(ftjob->destination().path(), fileName); + } else { + qCDebug(KDECONNECT_PLUGIN_SMS) << ftjob->errorString() << (ftjob ? ftjob->destination() : QUrl()); + } + }); + job->start(); + + return true; +} + +void SmsPlugin::getAttachment(const qint64& partID, const QString& uniqueIdentifier) +{ + QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + cacheDir.append(QStringLiteral("/") + device()->name() + QStringLiteral("/")); + QDir fileDirectory(cacheDir); + + bool fileFound = false; + if (fileDirectory.exists()) { + // Search for the attachment file locally before sending request to remote device + fileFound = fileDirectory.exists(uniqueIdentifier); + } else { + bool ret = fileDirectory.mkpath(QStringLiteral(".")); + if (!ret) { + qWarning() << "couldn't create directorty " << fileDirectory.absolutePath(); + } + } + + if (!fileFound) { + // If the file is not present in the local dir request the remote device for the file + requestAttachment(partID, uniqueIdentifier); + } else { + const QString fileDestination = fileDirectory.absoluteFilePath(uniqueIdentifier); + m_conversationInterface->attachmentDownloaded(fileDestination, uniqueIdentifier); + } +} + + QString SmsPlugin::dbusPath() const { return QStringLiteral("/modules/kdeconnect/devices/") + device()->id() + QStringLiteral("/sms"); diff --git a/plugins/sms/smsplugin.h b/plugins/sms/smsplugin.h index 415086fca..531b559b6 100644 --- a/plugins/sms/smsplugin.h +++ b/plugins/sms/smsplugin.h @@ -103,6 +103,24 @@ */ #define PACKET_TYPE_SMS_REQUEST_CONVERSATION QStringLiteral("kdeconnect.sms.request_conversation") +/** + * Packet sent to request an attachment file in a particular message of a conversation + * + * The body should look like so: + * "part_id": // Part id of the attachment + * "unique_identifier": // It can be any hash code or unique name of the file + */ +#define PACKET_TYPE_SMS_REQUEST_ATTACHMENT QStringLiteral("kdeconnect.sms.request_attachment") + +/** + * Packet used to send original attachment file from mms database to desktop + *

+ * The following fields are available: + * "thread_id": // Thread to which the attachment belongs + * "filename": // Name of the attachment file in the database + */ +#define PACKET_TYPE_SMS_ATTACHMENT_FILE QStringLiteral("kdeconnect.sms.attachment_file") + Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_SMS) class Q_DECL_EXPORT SmsPlugin @@ -137,6 +155,17 @@ public Q_SLOTS: Q_SCRIPTABLE void launchApp(); + /** + * Send a request to the remote device for a particulr attachment file + */ + Q_SCRIPTABLE void requestAttachment(const qint64& partID, const QString& uniqueIdentifier); + + /** + * Searches the requested file in the application's cache directory, + * if not found then sends the request to remote device + */ + Q_SCRIPTABLE void getAttachment(const qint64& partID, const QString& uniqueIdentifier); + private: /** @@ -149,6 +178,11 @@ private: */ bool handleBatchMessages(const NetworkPacket& np); + /** + * Handle a packet of type PACKET_TYPE_SMS_ATTACHMENT_FILE which contains an attachment file + */ + bool handleSmsAttachmentFile(const NetworkPacket& np); + QDBusInterface m_telepathyInterface; ConversationsDbusInterface* m_conversationInterface; }; diff --git a/smsapp/conversationmodel.cpp b/smsapp/conversationmodel.cpp index 9c4fc87a2..7bf74ceae 100644 --- a/smsapp/conversationmodel.cpp +++ b/smsapp/conversationmodel.cpp @@ -73,6 +73,8 @@ void ConversationModel::setDeviceId(const QString& deviceId) connect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64))); connect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant))); + connect(m_conversationsInterface, SIGNAL(attachmentReceived(QString, QString)), this, SIGNAL(filePathReceived(QString, QString))); + QQmlApplicationEngine* engine = qobject_cast(QQmlEngine::contextForObject(this)->engine()); m_thumbnailsProvider = dynamic_cast(engine->imageProvider(QStringLiteral("thumbnailsProvider"))); @@ -214,3 +216,8 @@ QString ConversationModel::getCharCountInfo(const QString& message) const return QString(); } } + +void ConversationModel::requestAttachmentPath(const qint64& partID, const QString& uniqueIdentifier) +{ + m_conversationsInterface->requestAttachmentFile(partID, uniqueIdentifier); +} diff --git a/smsapp/conversationmodel.h b/smsapp/conversationmodel.h index d75a0fc0e..07469fe1c 100644 --- a/smsapp/conversationmodel.h +++ b/smsapp/conversationmodel.h @@ -54,8 +54,11 @@ public: Q_INVOKABLE QString getCharCountInfo(const QString& message) const; + Q_INVOKABLE void requestAttachmentPath(const qint64& partID, const QString& UniqueIdentifier); + Q_SIGNALS: void loadingFinished(); + void filePathReceived(QString filePath, QString fileName); private Q_SLOTS: void handleConversationUpdate(const QDBusVariant &message); diff --git a/smsapp/qml/AttachmentViewer.qml b/smsapp/qml/AttachmentViewer.qml new file mode 100644 index 000000000..10d14f5d7 --- /dev/null +++ b/smsapp/qml/AttachmentViewer.qml @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2020 Aniket Kumar + * + * 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 . + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import org.kde.kirigami 2.13 as Kirigami +import QtMultimedia 5.12 + +Kirigami.Page { + id: root + property string filePath + property string mimeType + + contextualActions: [ + Kirigami.Action { + text: i18nd("kdeconnect-sms", "Open with default") + icon.name: "window-new" + onTriggered: { + Qt.openUrlExternally(filePath); + } + } + ] + + contentItem: Rectangle { + anchors.fill: parent + + Rectangle { + id: imageViewer + visible: mimeType.match("image") + anchors.horizontalCenter: parent.horizontalCenter + width: image.width + height: parent.height - y + y: root.implicitHeaderHeight + color: parent.color + + Image { + id: image + source: parent.visible ? filePath : "" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: sourceSize.width + height: parent.height + fillMode: Image.PreserveAspectFit + } + } + + MediaPlayer { + id: mediaPlayer + source: filePath + + onPositionChanged: { + if (mediaPlayer.position > 1000 && mediaPlayer.duration - mediaPlayer.position < 1000) { + playAndPauseButton.icon.name = "media-playback-start" + mediaPlayer.pause() + mediaPlayer.seek(0) + } + } + } + + Item { + width: parent.width + height: parent.height - mediaControls.height + anchors.topMargin: root.implicitHeaderHeight + + VideoOutput { + anchors.fill: parent + source: mediaPlayer + fillMode: VideoOutput.PreserveAspectFit + + // By default QML's videoOutput element rotates the vdeeo files by 90 degrees in clockwise direction + orientation: -90 + } + } + + Rectangle { + id: mediaControls + visible: mimeType.match("video") + width: parent.width + height: 50 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + color: Kirigami.Theme.backgroundColor + + Rectangle { + anchors.top: parent.top + width: parent.width + height: 1 + color: "lightGray" + } + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + + Rectangle { + id: progressBar + Layout.fillWidth: parent + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.topMargin: Kirigami.Units.smallSpacing + radius: 5 + height: 5 + + color: "gray" + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + radius: 5 + + width: mediaPlayer.duration > 0 ? parent.width*mediaPlayer.position/mediaPlayer.duration : 0 + + color: { + Kirigami.Theme.colorSet = Kirigami.Theme.View + var accentColor = Kirigami.Theme.highlightColor + return Qt.tint(Kirigami.Theme.backgroundColor, Qt.rgba(accentColor.r, accentColor.g, accentColor.b, 1)) + } + } + + MouseArea { + anchors.fill: parent + + onClicked: { + if (mediaPlayer.seekable) { + mediaPlayer.seek(mediaPlayer.duration * mouse.x/width); + } + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Kirigami.Units.largeSpacing + + Button { + id: backwardButton + icon.name: "media-seek-backward" + + onClicked: { + if (mediaPlayer.seekable) { + mediaPlayer.seek(mediaPlayer.position - 2000) + } + } + } + + Button { + id: playAndPauseButton + icon.name: "media-playback-pause" + + onClicked: { + if (icon.name == "media-playback-start") { + mediaPlayer.play() + icon.name = "media-playback-pause" + } else { + mediaPlayer.pause() + icon.name = "media-playback-start" + } + } + } + + Button { + id: forwardButton + icon.name: "media-seek-forward" + + onClicked: { + if (mediaPlayer.seekable) { + mediaPlayer.seek(mediaPlayer.position + 2000) + } + } + } + } + } + } + } + + Component.onCompleted: { + mediaPlayer.play() + } +} diff --git a/smsapp/qml/MessageAttachments.qml b/smsapp/qml/MessageAttachments.qml index 8d1f0c42e..95112b595 100644 --- a/smsapp/qml/MessageAttachments.qml +++ b/smsapp/qml/MessageAttachments.qml @@ -16,7 +16,7 @@ Item { property int partID property string mimeType property string uniqueIdentifier - property string sourcePath + property string sourcePath: "" readonly property int elementWidth: 100 readonly property int elementHeight: 100 @@ -24,6 +24,16 @@ Item { width: thumbnailElement.visible ? thumbnailElement.width : elementWidth height: thumbnailElement.visible ? thumbnailElement.height : elementHeight + Component { + id: attachmentViewer + + AttachmentViewer { + filePath: root.sourcePath + mimeType: root.mimeType + title: uniqueIdentifier + } + } + Image { id: thumbnailElement visible: mimeType.match("image") || mimeType.match("video") @@ -48,11 +58,29 @@ Item { } } + MouseArea { + anchors.fill: parent + onClicked: { + if (root.sourcePath == "") { + conversationModel.requestAttachmentPath(root.partID, root.uniqueIdentifier) + } else { + openMedia(); + } + } + } + Button { icon.name: "media-playback-start" visible: root.mimeType.match("video") anchors.horizontalCenter: thumbnailElement.horizontalCenter anchors.verticalCenter: thumbnailElement.verticalCenter + onClicked: { + if (root.sourcePath == "") { + conversationModel.requestAttachmentPath(root.partID, root.uniqueIdentifier) + } else { + openMedia(); + } + } } } @@ -63,6 +91,19 @@ Item { radius: messageBox.radius color: "lightgrey" + Audio { + id: audioPlayer + source: root.sourcePath + + onStopped: { + audioPlayButton.icon.name = "media-playback-start" + } + + onPlaying: { + audioPlayButton.icon.name = "media-playback-stop" + } + } + ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter @@ -72,6 +113,18 @@ Item { id : audioPlayButton icon.name: "media-playback-start" Layout.alignment: Qt.AlignCenter + + onClicked: { + if (root.sourcePath != "") { + if (icon.name == "media-playback-start") { + audioPlayer.play() + } else { + audioPlayer.stop() + } + } else { + conversationModel.requestAttachmentPath(root.partID, root.uniqueIdentifier) + } + } } Label { @@ -79,4 +132,24 @@ Item { } } } + + Connections { + target: conversationModel + onFilePathReceived: { + if (root.uniqueIdentifier == fileName && root.sourcePath == "") { + root.sourcePath = "file:" + filePath + + if (root.mimeType.match("audio")) { + audioPlayer.source = root.sourcePath + audioPlayer.play() + } else if (root.mimeType.match("image") || root.mimeType.match("video")) { + openMedia(); + } + } + } + } + + function openMedia() { + applicationWindow().pageStack.layers.push(attachmentViewer) + } } diff --git a/smsapp/resources.qrc b/smsapp/resources.qrc index 3c977e18d..751b80abd 100644 --- a/smsapp/resources.qrc +++ b/smsapp/resources.qrc @@ -5,5 +5,6 @@ qml/ConversationDisplay.qml qml/ChatMessage.qml qml/MessageAttachments.qml + qml/AttachmentViewer.qml