Implementing support to request and receive original attachment file from the remote device.

This commit is contained in:
Aniket Kumar 2020-08-13 23:53:02 +05:30
parent cfc29fafa6
commit e368dd4ab5
10 changed files with 422 additions and 3 deletions

View file

@ -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<ConversationMessage> &messages)
{
QSet<qint64> 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);
}

View file

@ -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

View file

@ -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"
]
}

View file

@ -16,6 +16,7 @@
#include <core/device.h>
#include <core/daemon.h>
#include <core/filetransferjob.h>
#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<QString>(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<FileTransferJob*>(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");

View file

@ -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": <long> // Part id of the attachment
* "unique_identifier": <String> // 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
* <p>
* The following fields are available:
* "thread_id": <long> // Thread to which the attachment belongs
* "filename": <String> // 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;
};

View file

@ -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<QQmlApplicationEngine*>(QQmlEngine::contextForObject(this)->engine());
m_thumbnailsProvider = dynamic_cast<ThumbnailsProvider*>(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);
}

View file

@ -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);

View file

@ -0,0 +1,198 @@
/**
* 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.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()
}
}

View file

@ -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)
}
}

View file

@ -5,5 +5,6 @@
<file>qml/ConversationDisplay.qml</file>
<file>qml/ChatMessage.qml</file>
<file>qml/MessageAttachments.qml</file>
<file>qml/AttachmentViewer.qml</file>
</qresource>
</RCC>