Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Commit 4078f1db authored by Linus Jahn's avatar Linus Jahn

Add DownloadManager for loading received media files

parent 27b4f258
Pipeline #3297 passed with stages
in 4 minutes and 56 seconds
......@@ -20,6 +20,7 @@ set(KAIDAN_SOURCES
${CURDIR}/UploadManager.cpp
${CURDIR}/EmojiModel.cpp
${CURDIR}/TransferCache.cpp
${CURDIR}/DownloadManager.cpp
# needed to trigger moc generation
${CURDIR}/Enums.h
......
......@@ -47,6 +47,7 @@
#include "DiscoveryManager.h"
#include "VCardManager.h"
#include "UploadManager.h"
#include "DownloadManager.h"
ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QGuiApplication *app,
QObject* parent)
......@@ -62,6 +63,8 @@ ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, Q
discoManager = new DiscoveryManager(client, this);
uploadManager = new UploadManager(kaidan, client, caches->msgModel, rosterManager,
caches->transferCache, this);
downloadManager = new DownloadManager(kaidan, caches->transferCache,
caches->msgModel, this);
connect(client, &QXmppClient::presenceReceived,
caches->presCache, &PresenceCache::updatePresenceRequested);
......@@ -88,6 +91,7 @@ ClientWorker::~ClientWorker()
delete discoManager;
delete vCardManager;
delete uploadManager;
delete downloadManager;
}
void ClientWorker::main()
......
......@@ -56,6 +56,7 @@ class MessageHandler;
class DiscoveryManager;
class VCardManager;
class UploadManager;
class DownloadManager;
using namespace Enums;
......@@ -83,7 +84,6 @@ protected:
class ClientWorker : public QObject
{
Q_OBJECT
Q_PROPERTY(UploadManager* uploadManager READ getUploadManager)
public:
struct Caches {
......@@ -135,11 +135,6 @@ public:
~ClientWorker();
UploadManager* getUploadManager()
{
return uploadManager;
}
public slots:
/**
* Main function of the client thread
......@@ -210,6 +205,7 @@ private:
DiscoveryManager *discoManager;
VCardManager *vCardManager;
UploadManager *uploadManager;
DownloadManager *downloadManager;
};
#endif // CLIENTWORKER_H
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2019 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan 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 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
// Kaidan
#include "DownloadManager.h"
#include "Kaidan.h"
#include "TransferCache.h"
#include "MessageModel.h"
#include "Globals.h"
// Qt
#include "QDir"
#include "QStandardPaths"
#include "QNetworkRequest"
#include "QNetworkReply"
#include "QNetworkAccessManager"
DownloadManager::DownloadManager(Kaidan *kaidan, TransferCache *transferCache,
MessageModel *model, QObject *parent)
: QObject(parent), thread(new DownloadThread()),
netMngr(new QNetworkAccessManager), kaidan(kaidan),
transferCache(transferCache), model(model)
{
connect(this, &DownloadManager::startDownloadRequested,
this, &DownloadManager::startDownload);
connect(this, &DownloadManager::abortDownloadRequested,
this, &DownloadManager::abortDownload);
connect(kaidan, &Kaidan::downloadMedia, this, &DownloadManager::startDownload);
netMngr->moveToThread(thread);
thread->start();
}
DownloadManager::~DownloadManager()
{
delete netMngr;
delete thread;
}
void DownloadManager::startDownload(const QString msgId, const QString url)
{
// don't download the same file twice and in parallel
if (downloads.keys().contains(msgId)) {
qWarning() << "Tried to download a file that is currently being "
"downloaded.";
return;
}
// we want to save files to 'Downloads/Kaidan/'
QString filePath = QStandardPaths::writableLocation(
QStandardPaths::DownloadLocation);
filePath += "/";
filePath += APPLICATION_DISPLAY_NAME;
filePath += "/";
DownloadJob *dl = new DownloadJob(msgId, QUrl(url), filePath, netMngr,
transferCache, kaidan);
dl->moveToThread(thread);
downloads[msgId] = dl;
connect(dl, &DownloadJob::finished, this, [this, dl, msgId]() {
MessageModel::Message msgUpdate;
msgUpdate.mediaLocation = dl->downloadLocation();
emit model->updateMessageRequested(msgId, msgUpdate);
abortDownload(msgId);
});
connect(dl, &DownloadJob::failed, this, [this, msgId]() {
abortDownload(msgId);
});
emit dl->startDownloadRequested();
}
void DownloadManager::abortDownload(const QString msgId)
{
DownloadJob *job = downloads.value(msgId);
if (job != nullptr)
delete job;
downloads.remove(msgId);
emit transferCache->removeJobRequested(msgId);
}
DownloadJob::DownloadJob(QString msgId, QUrl source, QString filePath,
QNetworkAccessManager *netMngr,
TransferCache *transferCache, Kaidan *kaidan)
: QObject(nullptr), msgId(msgId), source(source), filePath(filePath),
netMngr(netMngr), transferCache(transferCache), kaidan(kaidan), file()
{
connect(this, &DownloadJob::startDownloadRequested,
this, &DownloadJob::startDownload);
}
void DownloadJob::startDownload()
{
QDir dlDir(filePath);
if (!dlDir.exists())
dlDir.mkpath(".");
// don't override other files
file.setFileName(filePath + source.fileName());
int counter = 1;
while (file.exists()) {
QString newName = filePath + source.fileName() + "-"
+ QString::number(counter);
file.setFileName(newName);
counter++;
}
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "Could not open file for writing";
emit kaidan->passiveNotificationRequested("Could not open file for "
"writing");
emit failed();
return;
}
QNetworkRequest request(source);
QNetworkReply *reply = netMngr->get(request);
emit transferCache->addJobRequested(msgId, 0);
connect(reply, &QNetworkReply::downloadProgress,
this, [this] (qint64 bytesReceived, qint64 bytesTotal) {
emit transferCache->setJobProgressRequested(msgId, bytesReceived, bytesTotal);
});
connect(reply, &QNetworkReply::finished, this, [this] () {
emit transferCache->removeJobRequested(msgId);
emit finished();
});
connect(reply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error),
[this] () {
emit transferCache->removeJobRequested(msgId);
emit kaidan->passiveNotificationRequested(tr("Download failed."));
emit finished();
});
connect(reply, &QNetworkReply::readyRead, this, [this, reply](){
file.write(reply->readAll());
});
}
QString DownloadJob::downloadLocation() const
{
return file.fileName();
}
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2019 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan 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 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DOWNLOADMANAGER_H
#define DOWNLOADMANAGER_H
#include <QObject>
#include <QFile>
#include <QThread>
#include <QUrl>
#include <QMap>
class Kaidan;
class TransferCache;
class MessageModel;
class QNetworkAccessManager;
class DownloadJob : public QObject
{
Q_OBJECT
public:
DownloadJob(QString msgId, QUrl source, QString filePath,
QNetworkAccessManager *netMngr, TransferCache *transferCache,
Kaidan *kaidan);
QString downloadLocation() const;
signals:
void startDownloadRequested();
void finished();
void failed();
private slots:
void startDownload();
private:
QString msgId;
QUrl source;
QString filePath;
QNetworkAccessManager *netMngr;
TransferCache *transferCache;
Kaidan *kaidan;
QFile file;
};
class DownloadThread : public QThread
{
Q_OBJECT
public:
DownloadThread()
{
setObjectName("DownloadManager");
}
protected:
void run() override
{
exec();
}
};
class DownloadManager : public QObject
{
Q_OBJECT
public:
DownloadManager(Kaidan *kaidan, TransferCache *transferCache,
MessageModel *model, QObject *parent = nullptr);
~DownloadManager();
signals:
void startDownloadRequested(const QString msgId, const QString url);
void abortDownloadRequested(const QString msgId);
public slots:
void startDownload(const QString msgId, const QString url);
void abortDownload(const QString msgId);
private:
DownloadThread *thread;
QNetworkAccessManager *netMngr;
Kaidan *kaidan;
TransferCache *transferCache;
MessageModel *model;
QMap<QString, DownloadJob*> downloads;
};
#endif // DOWNLOADMANAGER_H
......@@ -389,6 +389,14 @@ signals:
*/
void removeContact(QString jid);
/**
* @brief Downloads an attached media file of a message
*
* @param msgId The message
* @param url the media url from the message
*/
void downloadMedia(QString msgId, QString url);
public slots:
/**
* Set current connection state
......
......@@ -91,7 +91,6 @@ MessageHandler::~MessageHandler()
void MessageHandler::handleMessage(const QXmppMessage &msg)
{
// TODO: Enable carbons (currently needs QXmpp master)
bool isCarbonMessage = false;
if (msg.body().isEmpty())
......@@ -124,7 +123,9 @@ void MessageHandler::handleMessage(const QXmppMessage &msg)
MessageType mType = MessageModel::messageTypeFromMimeType(type);
if (mType == MessageType::MessageImage ||
mType == MessageType::MessageAudio ||
mType == MessageType::MessageVideo) {
mType == MessageType::MessageVideo ||
mType == MessageType::MessageDocument ||
mType == MessageType::MessageFile) {
entry.type = mType;
entry.mediaContentType = type.name();
entry.mediaUrl = url.toEncoded();
......
......@@ -66,11 +66,13 @@ void TransferJob::setBytesTotal(qint64 bytesTotal)
TransferCache::TransferCache(QObject *parent)
: QObject(parent)
{
connect(this, &TransferCache::addUploadRequested, this, &TransferCache::addUpload);
connect(this, &TransferCache::removeUploadRequested,
this, &TransferCache::removeUpload);
connect(this, &TransferCache::setUploadBytesSentRequested,
this, &TransferCache::setUploadBytesSent);
connect(this, &TransferCache::addJobRequested, this, &TransferCache::addJob);
connect(this, &TransferCache::removeJobRequested,
this, &TransferCache::removeJob);
connect(this, &TransferCache::setJobBytesSentRequested,
this, &TransferCache::setJobBytesSent);
connect(this, &TransferCache::setJobProgressRequested,
this, &TransferCache::setJobBytesSent);
}
TransferCache::~TransferCache()
......@@ -79,7 +81,7 @@ TransferCache::~TransferCache()
QMutexLocker locker(&mutex);
}
void TransferCache::addUpload(const QString& msgId, qint64 bytesTotal)
void TransferCache::addJob(const QString& msgId, qint64 bytesTotal)
{
QMutexLocker locker(&mutex);
uploads.insert(msgId, new TransferJob(bytesTotal));
......@@ -88,7 +90,7 @@ void TransferCache::addUpload(const QString& msgId, qint64 bytesTotal)
emit jobsChanged();
}
void TransferCache::removeUpload(const QString& msgId)
void TransferCache::removeJob(const QString& msgId)
{
QMutexLocker locker(&mutex);
delete uploads[msgId];
......@@ -104,7 +106,7 @@ bool TransferCache::hasUpload(QString msgId) const
return uploads.contains(msgId);
}
TransferJob* TransferCache::uploadByMessageId(QString msgId) const
TransferJob* TransferCache::jobByMessageId(QString msgId) const
{
QMutexLocker locker(&mutex);
TransferJob* job = uploads.value(msgId);
......@@ -113,7 +115,19 @@ TransferJob* TransferCache::uploadByMessageId(QString msgId) const
return job;
}
void TransferCache::setUploadBytesSent(const QString& msgId, qint64 bytesSent)
void TransferCache::setJobProgress(const QString &msgId, qint64 bytesSent, qint64 bytesTotal)
{
uploadByMessageId(msgId)->setBytesSent(bytesSent);
TransferJob* job = jobByMessageId(msgId);
QMutexLocker locker(&mutex);
job->setBytesTotal(bytesTotal);
job->setBytesSent(bytesSent);
}
void TransferCache::setJobBytesSent(const QString &msgId, qint64 bytesSent)
{
TransferJob* job = jobByMessageId(msgId);
QMutexLocker locker(&mutex);
job->setBytesSent(bytesSent);
}
......@@ -84,12 +84,14 @@ public:
/**
* Returns the upload associated with the message id (used for progress)
*/
Q_INVOKABLE TransferJob* uploadByMessageId(QString msgId) const;
Q_INVOKABLE TransferJob* jobByMessageId(QString msgId) const;
public slots:
Q_INVOKABLE void addUpload(const QString &msgId, qint64 bytesTotal);
Q_INVOKABLE void removeUpload(const QString& msgId);
Q_INVOKABLE void setUploadBytesSent(const QString& msgId, qint64 bytesSent);
Q_INVOKABLE void addJob(const QString &msgId, qint64 bytesTotal);
Q_INVOKABLE void removeJob(const QString& msgId);
Q_INVOKABLE void setJobBytesSent(const QString& msgId, qint64 bytesSent);
Q_INVOKABLE void setJobProgress(const QString& msgId, qint64 bytesSent,
qint64 bytesTotal);
signals:
/**
......@@ -97,9 +99,11 @@ signals:
* about changes of hasUpload().
*/
void jobsChanged();
void addUploadRequested(const QString& msgId, qint64 bytesTotal);
void removeUploadRequested(const QString& msgId);
void setUploadBytesSentRequested(const QString& msgId, qint64 bytesSent);
void addJobRequested(const QString& msgId, qint64 bytesTotal);
void removeJobRequested(const QString& msgId);
void setJobBytesSentRequested(const QString& msgId, qint64 bytesSent);
void setJobProgressRequested(const QString& msgId, qint64 bytesSent,
qint64 bytesTotal);
private:
QMap<QString, TransferJob*> uploads;
......
......@@ -100,7 +100,7 @@ void UploadManager::sendFile(QString jid, QString fileUrl, QString body)
msg->mediaLocation = file.filePath();
// cache message and upload
emit transfers->addUploadRequested(msgId, upload->bytesTotal());
emit transfers->addJobRequested(msgId, upload->bytesTotal());
messages.insert(upload->id(), msg);
emit msgModel->addMessageRequested(*msg);
......@@ -113,7 +113,7 @@ void UploadManager::sendFile(QString jid, QString fileUrl, QString body)
rosterManager->handleSendMessage(jid, lastMessage);
connect(upload, &QXmppHttpUpload::bytesSentChanged, this, [upload, this, msgId] () {
emit transfers->setUploadBytesSentRequested(msgId, upload->bytesSent());
emit transfers->setJobBytesSentRequested(msgId, upload->bytesSent());
});
}
......@@ -145,7 +145,7 @@ void UploadManager::handleUploadSucceeded(const QXmppHttpUpload *upload)
// TODO: handle error
messages.remove(upload->id());
emit transfers->removeUploadRequested(originalMsg->id);
emit transfers->removeJobRequested(originalMsg->id);
}
void UploadManager::handleUploadFailed(const QXmppHttpUpload *upload)
......@@ -153,5 +153,5 @@ void UploadManager::handleUploadFailed(const QXmppHttpUpload *upload)
qDebug() << "[client] [UploadManager] A file upload has failed.";
const QString &msgId = messages.value(upload->id())->id;
messages.remove(upload->id());
emit transfers->removeUploadRequested(msgId);
emit transfers->removeJobRequested(msgId);
}
......@@ -50,10 +50,11 @@ RowLayout {
property var textEdit
property bool isLastMessage
property bool edited
property bool isLoading: kaidan.transferCache.hasUpload(msgId)
property var upload: {
if (mediaType !== Enums.MessageText && mediaGetUrl === ""
&& kaidan.transferCache.hasUpload(msgId)) {
kaidan.transferCache.uploadByMessageId(model.id)
if (mediaType !== Enums.MessageText &&
kaidan.transferCache.hasUpload(msgId)) {
kaidan.transferCache.jobByMessageId(model.id)
}
}
......@@ -140,18 +141,30 @@ RowLayout {
anchors.centerIn: parent
anchors.margins: 4
Controls.ToolButton {
visible: {
mediaType !== Enums.MessageText && !isLoading && mediaLocation === ""
}
text: qsTr("Download")
onClicked: {
print("Donwload")
kaidan.downloadMedia(msgId, mediaGetUrl)
}
}
// media loader
Loader {
id: media
source: mediaType === Enums.MessageImage ? "ChatMessageImage.qml"
: ""
property string sourceUrl: {
mediaLocation === "" ? mediaGetUrl
: "file://" + mediaLocation
source: {
if (mediaType == Enums.MessageImage &&
mediaLocation !== "")
"ChatMessageImage.qml"
else
""
}
property string sourceUrl: "file://" + mediaLocation
Layout.maximumWidth: root.width - Kirigami.Units.gridUnit * 6
Layout.preferredHeight: mediaType === Enums.MessageImage && loaded ?
item.paintedHeight : 0
Layout.preferredHeight: loaded ? item.paintedHeight : 0
}
// message body
......@@ -171,21 +184,10 @@ RowLayout {
// message meta: date, isRead
RowLayout {
// progress bar for upload/download status
Controls.ProgressBar {
id: progressBar
visible: isLoading
value: upload.progress
visible: kaidan.transferCache.hasUpload(msgId)
function updateVisibility() {
progressBar.visible = kaidan.transferCache.hasUpload(msgId)
}
Component.onCompleted: {
kaidan.transferCache.jobsChanged.connect(updateVisibility)
}
Component.onDestruction: {
kaidan.transferCache.jobsChanged.disconnect(updateVisibility)
}
}
Controls.Label {
......@@ -219,4 +221,15 @@ RowLayout {
Item {
Layout.fillWidth: true
}
function updateIsLoading() {
isLoading = kaidan.transferCache.hasUpload(msgId)
}
Component.onCompleted: {
kaidan.transferCache.jobsChanged.connect(updateIsLoading)
}
Component.onDestruction: {
kaidan.transferCache.jobsChanged.disconnect(updateIsLoading)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment