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

Add DownloadManager for loading received media files

parent 27b4f258
Pipeline #345 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