From 6341f32618c221f9d4f7009c9c5876a1037d08cf Mon Sep 17 00:00:00 2001
From: Xavier Jouslin de Noray <xavier.jouslindenoray@savoirfairelinux.com>
Date: Wed, 28 Jun 2023 10:45:04 -0400
Subject: [PATCH] updatemanager: refactor windows update and macos update
 manager

To be able to use an update manager for the plugins store, a refactor of windows update manager is done. The windows and macos update manager is used for updating jami. The plugins store update manager is to update plugins to the newest version.

Gitlab: #1229
Change-Id: I0541b6191401f2aa2c6d6034722796455e9c18d2
---
 CMakeLists.txt                                |   8 +-
 src/app/MainApplicationWindow.qml             |   8 +-
 ...pdatemanager.cpp => appversionmanager.cpp} | 177 +++---------------
 .../{updatemanager.h => appversionmanager.h}  |  47 ++---
 src/app/constant/JamiStrings.qml              |   2 +-
 src/app/imagedownloader.cpp                   |   2 +-
 src/app/lrcinstance.cpp                       |   6 +-
 src/app/lrcinstance.h                         |   6 +-
 src/app/networkmanager.cpp                    | 128 ++++++++++++-
 src/app/networkmanager.h                      |  19 +-
 ...{updatemanager.mm => appversionmanager.mm} |  29 ++-
 src/app/pluginadapter.cpp                     |   2 +
 src/app/qmlregister.cpp                       |   6 +-
 src/app/settingsview/SettingsSidePanel.qml    |   2 +-
 .../components/PluginItemDelegate.qml         |  16 +-
 .../components/PluginSettingsPage.qml         |  37 ++--
 .../components/UpdateDownloadDialog.qml       |  11 +-
 .../components/UpdateSettingsPage.qml         |  12 +-
 src/app/wizardview/WizardView.qml             |   2 +-
 .../ConnectToAccountManagerPage.qml           |   2 +-
 .../components/ImportFromBackupPage.qml       |   2 +-
 .../components/ImportFromDevicePage.qml       |   2 +-
 22 files changed, 260 insertions(+), 266 deletions(-)
 rename src/app/{updatemanager.cpp => appversionmanager.cpp} (52%)
 rename src/app/{updatemanager.h => appversionmanager.h} (59%)
 rename src/app/os/macos/{updatemanager.mm => appversionmanager.mm} (83%)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4f936b48d..2f25d2db6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -245,7 +245,7 @@ set(COMMON_HEADERS
   ${APP_SRC_DIR}/avatarimageprovider.h
   ${APP_SRC_DIR}/networkmanager.h
   ${APP_SRC_DIR}/smartlistmodel.h
-  ${APP_SRC_DIR}/updatemanager.h
+  ${APP_SRC_DIR}/appversionmanager.h
   ${APP_SRC_DIR}/utils.h
   ${APP_SRC_DIR}/bannedlistmodel.h
   ${APP_SRC_DIR}/version.h
@@ -336,7 +336,7 @@ if(MSVC)
 
   list(APPEND COMMON_SOURCES
     ${APP_SRC_DIR}/connectivitymonitor.cpp
-    ${APP_SRC_DIR}/updatemanager.cpp)
+    ${APP_SRC_DIR}/appversionmanager.cpp)
   # preprocessor defines
   add_definitions(-DUNICODE -DQT_NO_DEBUG -DNDEBUG)
 
@@ -388,7 +388,7 @@ elseif (NOT APPLE)
     ${APP_SRC_DIR}/xrectsel.c
     ${APP_SRC_DIR}/connectivitymonitor.cpp
     ${APP_SRC_DIR}/dbuserrorhandler.cpp
-    ${APP_SRC_DIR}/updatemanager.cpp)
+    ${APP_SRC_DIR}/appversionmanager.cpp)
   list(APPEND COMMON_HEADERS
     ${APP_SRC_DIR}/xrectsel.h
     ${APP_SRC_DIR}/dbuserrorhandler.h)
@@ -440,7 +440,7 @@ elseif (NOT APPLE)
   find_library(X11 X11)
 else() # APPLE
   list(APPEND COMMON_SOURCES
-    ${APP_SRC_DIR}/os/macos/updatemanager.mm
+    ${APP_SRC_DIR}/os/macos/appversionmanager.mm
     ${APP_SRC_DIR}/os/macos/connectivitymonitor.mm
     ${APP_SRC_DIR}/os/macos/macutils.mm)
   list(APPEND COMMON_HEADERS
diff --git a/src/app/MainApplicationWindow.qml b/src/app/MainApplicationWindow.qml
index 6410f6a54..f75732477 100644
--- a/src/app/MainApplicationWindow.qml
+++ b/src/app/MainApplicationWindow.qml
@@ -205,8 +205,8 @@ ApplicationWindow {
             // Quiet check for updates on start if set to.
             if (Qt.platform.os.toString() === "windows") {
                 if (UtilsAdapter.getAppValue(Settings.AutoUpdate)) {
-                    UpdateManager.checkForUpdates(true);
-                    UpdateManager.setAutoUpdateCheck(true);
+                    AppVersionManager.checkForUpdates(true);
+                    AppVersionManager.setAutoUpdateCheck(true);
                 }
             }
 
@@ -268,7 +268,7 @@ ApplicationWindow {
     }
 
     Connections {
-        target: UpdateManager
+        target: AppVersionManager
 
         function onDownloadStarted() {
             viewCoordinator.presentDialog(appWindow, "settingsview/components/UpdateDownloadDialog.qml", {
@@ -290,7 +290,7 @@ ApplicationWindow {
                         "buttonTitles": [JamiStrings.optionUpgrade, JamiStrings.optionLater],
                         "buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue, SimpleMessageDialog.ButtonStyle.TintedBlue],
                         "buttonCallBacks": [function () {
-                                UpdateManager.applyUpdates();
+                                AppVersionManager.applyUpdates();
                             }]
                     });
             }
diff --git a/src/app/updatemanager.cpp b/src/app/appversionmanager.cpp
similarity index 52%
rename from src/app/updatemanager.cpp
rename to src/app/appversionmanager.cpp
index ca300d336..c3ac06c9c 100644
--- a/src/app/updatemanager.cpp
+++ b/src/app/appversionmanager.cpp
@@ -15,7 +15,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include "updatemanager.h"
+#include "appversionmanager.h"
 
 #include "lrcinstance.h"
 #include "version.h"
@@ -37,9 +37,9 @@ static constexpr char betaVersionSubUrl[] = "/beta/version";
 static constexpr char msiSubUrl[] = "/jami.release.x64.msi";
 static constexpr char betaMsiSubUrl[] = "/beta/jami.beta.x64.msi";
 
-struct UpdateManager::Impl : public QObject
+struct AppVersionManager::Impl : public QObject
 {
-    Impl(const QString& url, LRCInstance* instance, UpdateManager& parent)
+    Impl(const QString& url, LRCInstance* instance, AppVersionManager& parent)
         : QObject(nullptr)
         , parent_(parent)
         , lrcInstance_(instance)
@@ -60,9 +60,9 @@ struct UpdateManager::Impl : public QObject
         // Fail without UI if this is a programmatic check.
         if (!quiet)
             connect(&parent_,
-                    &NetworkManager::errorOccured,
+                    &NetworkManager::errorOccurred,
                     &parent_,
-                    &UpdateManager::updateErrorOccurred);
+                    &AppVersionManager::updateErrorOccurred);
 
         cleanUpdateFiles();
         QUrl versionUrl {isBeta ? QUrl::fromUserInput(baseUrlString_ + betaVersionSubUrl)
@@ -92,31 +92,21 @@ struct UpdateManager::Impl : public QObject
     {
         parent_.disconnect();
         connect(&parent_,
-                &NetworkManager::errorOccured,
+                &NetworkManager::errorOccurred,
                 &parent_,
-                &UpdateManager::updateErrorOccurred);
-        connect(&parent_, &UpdateManager::statusChanged, this, [this](Status status) {
-            switch (status) {
-            case Status::STARTED:
-                Q_EMIT parent_.updateDownloadStarted();
-                break;
-            case Status::FINISHED:
-                Q_EMIT parent_.updateDownloadFinished();
-                break;
-            default:
-                break;
-            }
-        });
+                &AppVersionManager::updateErrorOccurred);
 
-        QUrl downloadUrl {(beta || isBeta) ? QUrl::fromUserInput(baseUrlString_ + betaMsiSubUrl)
-                                           : QUrl::fromUserInput(baseUrlString_ + msiSubUrl)};
+        const QUrl downloadUrl {(beta || isBeta)
+                                    ? QUrl::fromUserInput(baseUrlString_ + betaMsiSubUrl)
+                                    : QUrl::fromUserInput(baseUrlString_ + msiSubUrl)};
 
-        parent_.downloadFile(
+        int uuid = parent_.downloadFile(
             downloadUrl,
+            *(parent_.replyId_),
             [this, downloadUrl](bool success, const QString& errorMessage) {
                 Q_UNUSED(success)
                 Q_UNUSED(errorMessage)
-                QProcess process;
+                const QProcess process;
                 auto basePath = tempPath_ + QDir::separator();
                 auto msiPath = QDir::toNativeSeparators(basePath + downloadUrl.fileName());
                 auto logPath = QDir::toNativeSeparators(basePath + "jami_x64_install.log");
@@ -127,11 +117,12 @@ struct UpdateManager::Impl : public QObject
                                                     << "/L*V" << logPath);
             },
             tempPath_);
+        parent_.replyId_.reset(&uuid);
     };
 
     void cancelUpdate()
     {
-        parent_.cancelDownload();
+        parent_.cancelDownload(*(parent_.replyId_));
     };
 
     void setAutoUpdateCheck(bool state)
@@ -162,7 +153,7 @@ struct UpdateManager::Impl : public QObject
         }
     };
 
-    UpdateManager& parent_;
+    AppVersionManager& parent_;
 
     LRCInstance* lrcInstance_ {nullptr};
     QString baseUrlString_;
@@ -170,51 +161,52 @@ struct UpdateManager::Impl : public QObject
     QTimer* updateTimer_;
 };
 
-UpdateManager::UpdateManager(const QString& url,
-                             ConnectivityMonitor* cm,
-                             LRCInstance* instance,
-                             QObject* parent)
+AppVersionManager::AppVersionManager(const QString& url,
+                                     ConnectivityMonitor* cm,
+                                     LRCInstance* instance,
+                                     QObject* parent)
     : NetworkManager(cm, parent)
     , pimpl_(std::make_unique<Impl>(url, instance, *this))
+    , replyId_(new int(0))
 {}
 
-UpdateManager::~UpdateManager()
+AppVersionManager::~AppVersionManager()
 {
-    cancelDownload();
+    cancelDownload(*replyId_);
 }
 
 void
-UpdateManager::checkForUpdates(bool quiet)
+AppVersionManager::checkForUpdates(bool quiet)
 {
     pimpl_->checkForUpdates(quiet);
 }
 
 void
-UpdateManager::applyUpdates(bool beta)
+AppVersionManager::applyUpdates(bool beta)
 {
     pimpl_->applyUpdates(beta);
 }
 
 void
-UpdateManager::cancelUpdate()
+AppVersionManager::cancelUpdate()
 {
     pimpl_->cancelUpdate();
 }
 
 void
-UpdateManager::setAutoUpdateCheck(bool state)
+AppVersionManager::setAutoUpdateCheck(bool state)
 {
     pimpl_->setAutoUpdateCheck(state);
 }
 
 bool
-UpdateManager::isCurrentVersionBeta()
+AppVersionManager::isCurrentVersionBeta()
 {
     return isBeta;
 }
 
 bool
-UpdateManager::isUpdaterEnabled()
+AppVersionManager::isUpdaterEnabled()
 {
 #ifdef Q_OS_WIN
     return true;
@@ -223,116 +215,7 @@ UpdateManager::isUpdaterEnabled()
 }
 
 bool
-UpdateManager::isAutoUpdaterEnabled()
+AppVersionManager::isAutoUpdaterEnabled()
 {
     return false;
 }
-
-void
-UpdateManager::cancelDownload()
-{
-    if (downloadReply_) {
-        Q_EMIT errorOccured(GetError::CANCELED);
-        downloadReply_->abort();
-        resetDownload();
-    }
-}
-
-void
-UpdateManager::downloadFile(const QUrl& url,
-                            std::function<void(bool, const QString&)> onDoneCallback,
-                            const QString& filePath)
-{
-    // If there is already a download in progress, return.
-    if (downloadReply_ && downloadReply_->isRunning()) {
-        qWarning() << Q_FUNC_INFO << "Download already in progress";
-        return;
-    }
-
-    // Clean up any previous download.
-    resetDownload();
-
-    // If the url is invalid, return.
-    if (!url.isValid()) {
-        Q_EMIT errorOccured(GetError::NETWORK_ERROR, "Invalid url");
-        return;
-    }
-
-    // If the file path is empty, return.
-    if (filePath.isEmpty()) {
-        Q_EMIT errorOccured(GetError::NETWORK_ERROR, "Invalid file path");
-        return;
-    }
-
-    // Create the file. Return if it cannot be created.
-    QFileInfo fileInfo(url.path());
-    QString fileName = fileInfo.fileName();
-    file_.reset(new QFile(filePath + "/" + fileName));
-    if (!file_->open(QIODevice::WriteOnly)) {
-        Q_EMIT errorOccured(GetError::ACCESS_DENIED);
-        file_.reset();
-        qWarning() << Q_FUNC_INFO << "Could not open file for writing";
-        return;
-    }
-
-    // Start the download.
-    QNetworkRequest request(url);
-    downloadReply_ = manager_->get(request);
-
-    connect(downloadReply_, &QNetworkReply::readyRead, this, [=]() {
-        if (file_ && file_->isOpen()) {
-            file_->write(downloadReply_->readAll());
-        }
-    });
-
-    connect(downloadReply_,
-            &QNetworkReply::downloadProgress,
-            this,
-            [=](qint64 bytesReceived, qint64 bytesTotal) {
-                Q_EMIT downloadProgressChanged(bytesReceived, bytesTotal);
-            });
-
-    connect(downloadReply_,
-            QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred),
-            this,
-            [this](QNetworkReply::NetworkError error) {
-                downloadReply_->disconnect();
-                resetDownload();
-                qWarning() << Q_FUNC_INFO
-                           << QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error);
-                Q_EMIT errorOccured(GetError::NETWORK_ERROR);
-            });
-
-    connect(downloadReply_, &QNetworkReply::finished, this, [this, onDoneCallback]() {
-        bool success = false;
-        QString errorMessage;
-        if (downloadReply_->error() == QNetworkReply::NoError) {
-            resetDownload();
-            success = true;
-        } else {
-            errorMessage = downloadReply_->errorString();
-            resetDownload();
-        }
-        onDoneCallback(success, errorMessage);
-        Q_EMIT statusChanged(Status::FINISHED);
-    });
-
-    Q_EMIT statusChanged(Status::STARTED);
-}
-
-void
-UpdateManager::resetDownload()
-{
-    if (downloadReply_) {
-        downloadReply_->deleteLater();
-        downloadReply_ = nullptr;
-    }
-    if (file_) {
-        if (file_->isOpen()) {
-            file_->flush();
-            file_->close();
-        }
-        file_->deleteLater();
-        file_.reset();
-    }
-}
diff --git a/src/app/updatemanager.h b/src/app/appversionmanager.h
similarity index 59%
rename from src/app/updatemanager.h
rename to src/app/appversionmanager.h
index 373c97fce..55fa94c5f 100644
--- a/src/app/updatemanager.h
+++ b/src/app/appversionmanager.h
@@ -1,6 +1,5 @@
 /*
  * Copyright (C) 2020-2023 Savoir-faire Linux Inc.
- * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.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
@@ -25,51 +24,35 @@
 class LRCInstance;
 class ConnectivityMonitor;
 
-class UpdateManager final : public NetworkManager
+class AppVersionManager final : public NetworkManager
 {
     Q_OBJECT
-    Q_DISABLE_COPY(UpdateManager)
+    Q_DISABLE_COPY(AppVersionManager)
 public:
-    explicit UpdateManager(const QString& url,
-                           ConnectivityMonitor* cm,
-                           LRCInstance* instance = nullptr,
-                           QObject* parent = nullptr);
-    ~UpdateManager();
-
-    enum Status { STARTED, FINISHED };
-    Q_ENUM(Status)
+    explicit AppVersionManager(const QString& url,
+                               ConnectivityMonitor* cm,
+                               LRCInstance* instance = nullptr,
+                               QObject* parent = nullptr);
+    ~AppVersionManager();
 
     Q_INVOKABLE void checkForUpdates(bool quiet = false);
     Q_INVOKABLE void applyUpdates(bool beta = false);
-    Q_INVOKABLE void cancelUpdate();
-    Q_INVOKABLE void setAutoUpdateCheck(bool state);
-    Q_INVOKABLE bool isCurrentVersionBeta();
     Q_INVOKABLE bool isUpdaterEnabled();
     Q_INVOKABLE bool isAutoUpdaterEnabled();
-    Q_INVOKABLE void cancelDownload();
-
-    void downloadFile(const QUrl& url,
-                      std::function<void(bool, const QString&)> onDoneCallback,
-                      const QString& filePath);
+    Q_INVOKABLE void setAutoUpdateCheck(bool state);
+    Q_INVOKABLE void cancelUpdate();
+    Q_INVOKABLE bool isCurrentVersionBeta();
 
 Q_SIGNALS:
-    void statusChanged(UpdateManager::Status status);
-    void downloadProgressChanged(qint64 bytesRead, qint64 totalBytes);
-
+    void appCloseRequested();
     void updateCheckReplyReceived(bool ok, bool found = false);
-    void updateErrorOccurred(const NetworkManager::GetError& error);
-    void updateDownloadStarted();
     void updateDownloadProgressChanged(qint64 bytesRead, qint64 totalBytes);
-    void updateDownloadFinished();
-    void appCloseRequested();
-
-private:
-    void resetDownload();
-    QNetworkReply* downloadReply_ {nullptr};
-    QScopedPointer<QFile> file_;
+    void updateErrorOccurred(const NetworkManager::GetError& error);
 
 private:
+    QScopedPointer<int> replyId_;
     struct Impl;
+    friend struct Impl;
     std::unique_ptr<Impl> pimpl_;
 };
-Q_DECLARE_METATYPE(UpdateManager*)
+Q_DECLARE_METATYPE(AppVersionManager*)
diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml
index c04dc8030..c62fbd884 100644
--- a/src/app/constant/JamiStrings.qml
+++ b/src/app/constant/JamiStrings.qml
@@ -46,7 +46,7 @@ Item {
     property string reconnectTry: qsTr("Trying to reconnect to the Jami daemon (jamid)…")
 
     // AboutPopUp
-    property string version: qsTr("Version") + (UpdateManager.isCurrentVersionBeta() ? " (Beta)" : "")
+    property string version: qsTr("Version") + (AppVersionManager.isCurrentVersionBeta() ? " (Beta)" : "")
     property string companyDeclarationYear: declarationYear + " " + companyName
     property string declarationYear: "© 2015-2023"
     property string companyName: "Savoir-faire Linux Inc."
diff --git a/src/app/imagedownloader.cpp b/src/app/imagedownloader.cpp
index 945a4e607..9eb0784d5 100644
--- a/src/app/imagedownloader.cpp
+++ b/src/app/imagedownloader.cpp
@@ -26,7 +26,7 @@ ImageDownloader::ImageDownloader(ConnectivityMonitor* cm, QObject* parent)
 void
 ImageDownloader::downloadImage(const QUrl& url, const QString& localPath)
 {
-    Utils::oneShotConnect(this, &NetworkManager::errorOccured, this, [this, localPath]() {
+    Utils::oneShotConnect(this, &NetworkManager::errorOccurred, this, [this, localPath]() {
         onDownloadImageFinished({}, localPath);
     });
 
diff --git a/src/app/lrcinstance.cpp b/src/app/lrcinstance.cpp
index 7e7162d8e..323cab0b5 100644
--- a/src/app/lrcinstance.cpp
+++ b/src/app/lrcinstance.cpp
@@ -34,7 +34,7 @@ LRCInstance::LRCInstance(migrateCallback willMigrateCb,
                          bool debugMode,
                          bool muteDaemon)
     : lrc_(std::make_unique<Lrc>(willMigrateCb, didMigrateCb, !debugMode || muteDaemon))
-    , updateManager_(std::make_unique<UpdateManager>(updateUrl, connectivityMonitor, this))
+    , updateManager_(std::make_unique<AppVersionManager>(updateUrl, connectivityMonitor, this))
     , threadPool_(new QThreadPool(this))
 {
     debugMode_ = debugMode;
@@ -75,8 +75,8 @@ LRCInstance::LRCInstance(migrateCallback willMigrateCb,
     }
 };
 
-UpdateManager*
-LRCInstance::getUpdateManager()
+AppVersionManager*
+LRCInstance::getAppVersionManager()
 {
     return updateManager_.get();
 }
diff --git a/src/app/lrcinstance.h b/src/app/lrcinstance.h
index 4acf94884..0da5abee1 100644
--- a/src/app/lrcinstance.h
+++ b/src/app/lrcinstance.h
@@ -24,7 +24,7 @@
 #undef ERROR
 #endif
 
-#include "updatemanager.h"
+#include "appversionmanager.h"
 #include "qtutils.h"
 #include "utils.h"
 
@@ -71,7 +71,7 @@ public:
 
     void finish();
 
-    UpdateManager* getUpdateManager();
+    AppVersionManager* getAppVersionManager();
 
     AccountModel& accountModel();
     ConversationModel* getCurrentConversationModel();
@@ -145,7 +145,7 @@ Q_SIGNALS:
 
 private:
     std::unique_ptr<Lrc> lrc_;
-    std::unique_ptr<UpdateManager> updateManager_;
+    std::unique_ptr<AppVersionManager> updateManager_;
 
     QString selectedConvUid_;
     MapStringString contentDrafts_;
diff --git a/src/app/networkmanager.cpp b/src/app/networkmanager.cpp
index 15cec11ca..0af5f216e 100644
--- a/src/app/networkmanager.cpp
+++ b/src/app/networkmanager.cpp
@@ -19,14 +19,18 @@
 
 #include "connectivitymonitor.h"
 
+#include <QMap>
+#include <QDir>
 #include <QMetaEnum>
 #include <QtNetwork>
+#include <QScopedPointer>
 
 NetworkManager::NetworkManager(ConnectivityMonitor* cm, QObject* parent)
     : QObject(parent)
     , manager_(new QNetworkAccessManager(this))
     , connectivityMonitor_(cm)
     , lastConnectionState_(cm->isOnline())
+    , rng_(std::random_device {}())
 {
 #if QT_CONFIG(ssl)
     connect(manager_,
@@ -36,7 +40,7 @@ NetworkManager::NetworkManager(ConnectivityMonitor* cm, QObject* parent)
                 Q_UNUSED(reply);
                 Q_FOREACH (const QSslError& error, errors) {
                     qWarning() << Q_FUNC_INFO << error.errorString();
-                    Q_EMIT errorOccured(GetError::SSL_ERROR, error.errorString());
+                    Q_EMIT errorOccurred(GetError::SSL_ERROR, error.errorString());
                 }
             });
 #endif
@@ -53,15 +57,127 @@ NetworkManager::NetworkManager(ConnectivityMonitor* cm, QObject* parent)
 
 void
 NetworkManager::sendGetRequest(const QUrl& url,
-                               std::function<void(const QByteArray&)> onDoneCallback)
+                               std::function<void(const QByteArray&)>&& onDoneCallback)
 {
-    auto reply = manager_->get(QNetworkRequest(url));
+    auto* const reply = manager_->get(QNetworkRequest(url));
     QObject::connect(reply, &QNetworkReply::finished, this, [reply, onDoneCallback, this]() {
         if (reply->error() == QNetworkReply::NoError) {
             onDoneCallback(reply->readAll());
-        } else{
-            Q_EMIT errorOccured(GetError::NETWORK_ERROR, reply->errorString());
-        }      
+        } else {
+            Q_EMIT errorOccurred(GetError::NETWORK_ERROR, reply->errorString());
+        }
         reply->deleteLater();
     });
 }
+
+int
+NetworkManager::downloadFile(const QUrl& url,
+                             unsigned int replyId,
+                             std::function<void(bool, const QString&)>&& onDoneCallback,
+                             const QString& filePath)
+{
+    // If there is already a download in progress, return.
+    if ((downloadReplies_.value(replyId) != NULL || !(replyId == 0))
+        && downloadReplies_[replyId]->isRunning()) {
+        qWarning() << Q_FUNC_INFO << "Download already in progress";
+        return replyId;
+    }
+
+    // Clean up any previous download.
+    resetDownload(replyId);
+
+    // If the url is invalid, return.
+    if (!url.isValid()) {
+        Q_EMIT errorOccurred(GetError::NETWORK_ERROR, "Invalid url");
+        return 0;
+    }
+
+    // If the file path is empty, return.
+    if (filePath.isEmpty()) {
+        Q_EMIT errorOccurred(GetError::NETWORK_ERROR, "Invalid file path");
+        return 0;
+    }
+
+    // set the id for the request
+    std::uniform_int_distribution<int> dist(1, std::numeric_limits<int>::max());
+    auto uuid = dist(rng_);
+
+    const QDir dir;
+    if (!dir.exists(filePath)) {
+        dir.mkpath(filePath);
+    }
+
+    // Create the file. Return if it cannot be created.
+    const QFileInfo fileInfo(url.path());
+    const QString fileName = fileInfo.fileName();
+    auto& file = files_[uuid];
+    file = new QFile(filePath + fileName + ".jpl");
+    if (!file->open(QIODevice::WriteOnly)) {
+        Q_EMIT errorOccurred(GetError::ACCESS_DENIED);
+        files_.remove(uuid);
+        qWarning() << Q_FUNC_INFO << "Could not open file for writing";
+        return 0;
+    }
+
+    // Start the download.
+    const QNetworkRequest request(url);
+
+    downloadReplies_[uuid] = manager_->get(request);
+    auto* const reply = downloadReplies_[uuid];
+    connect(reply, &QNetworkReply::readyRead, this, [file, reply]() {
+        if (file && file->isOpen()) {
+            file->write(reply->readAll());
+        }
+    });
+
+    connect(reply,
+            &QNetworkReply::downloadProgress,
+            this,
+            [this](qint64 bytesReceived, qint64 bytesTotal) {
+                Q_EMIT downloadProgressChanged(bytesReceived, bytesTotal);
+            });
+
+    connect(reply,
+            QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred),
+            this,
+            [this, uuid, reply](QNetworkReply::NetworkError error) {
+                reply->disconnect();
+                resetDownload(uuid);
+                qWarning() << Q_FUNC_INFO
+                           << QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error);
+                Q_EMIT errorOccurred(GetError::NETWORK_ERROR);
+            });
+
+    connect(reply, &QNetworkReply::finished, this, [this, uuid, onDoneCallback, reply]() {
+        bool success = false;
+        QString errorMessage;
+        if (reply->error() == QNetworkReply::NoError) {
+            resetDownload(uuid);
+            success = true;
+        } else {
+            errorMessage = reply->errorString();
+            resetDownload(uuid);
+        }
+        onDoneCallback(success, errorMessage);
+        Q_EMIT downloadFinished(uuid);
+    });
+    Q_EMIT downloadStarted(uuid);
+    return uuid;
+}
+
+void
+NetworkManager::cancelDownload(int replyId)
+{
+    if (downloadReplies_.value(replyId) != NULL) {
+        Q_EMIT errorOccurred(GetError::CANCELED);
+        downloadReplies_[replyId]->abort();
+        resetDownload(replyId);
+    }
+}
+
+void
+NetworkManager::resetDownload(int replyId)
+{
+    files_.remove(replyId);
+    downloadReplies_.remove(replyId);
+}
diff --git a/src/app/networkmanager.h b/src/app/networkmanager.h
index bf80b0d95..67dacb237 100644
--- a/src/app/networkmanager.h
+++ b/src/app/networkmanager.h
@@ -20,7 +20,10 @@
 #include <QObject>
 #include <QFile>
 #include <QSslError>
+#include <QMap>
+#include <QString>
 #include <QNetworkReply>
+#include <random>
 
 class QNetworkAccessManager;
 class ConnectivityMonitor;
@@ -35,10 +38,19 @@ public:
     enum GetError { DISCONNECTED, NETWORK_ERROR, ACCESS_DENIED, SSL_ERROR, CANCELED };
     Q_ENUM(GetError)
 
-    void sendGetRequest(const QUrl& url, std::function<void(const QByteArray&)> onDoneCallback);
+    void sendGetRequest(const QUrl& url, std::function<void(const QByteArray&)>&& onDoneCallback);
 
+    int downloadFile(const QUrl& url,
+                     unsigned int replyId,
+                     std::function<void(bool, const QString&)>&& onDoneCallback,
+                     const QString& filePath);
+    void resetDownload(int replyId);
+    void cancelDownload(int replyId);
 Q_SIGNALS:
-    void errorOccured(GetError error, const QString& msg = {});
+    void errorOccurred(GetError error, const QString& msg = {});
+    void downloadProgressChanged(qint64 bytesRead, qint64 totalBytes);
+    void downloadFinished(int replyId);
+    void downloadStarted(int replyId);
 
 protected:
     QNetworkAccessManager* manager_;
@@ -46,5 +58,8 @@ protected:
 private:
     ConnectivityMonitor* connectivityMonitor_;
     bool lastConnectionState_;
+    QMap<int, QNetworkReply*> downloadReplies_ {};
+    QMap<int, QFile*> files_ {};
+    std::mt19937 rng_;
 };
 Q_DECLARE_METATYPE(NetworkManager*)
diff --git a/src/app/os/macos/updatemanager.mm b/src/app/os/macos/appversionmanager.mm
similarity index 83%
rename from src/app/os/macos/updatemanager.mm
rename to src/app/os/macos/appversionmanager.mm
index ecbac0152..1933ab842 100644
--- a/src/app/os/macos/updatemanager.mm
+++ b/src/app/os/macos/appversionmanager.mm
@@ -16,7 +16,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include "updatemanager.h"
+#include "appversionmanager.h"
 
 #ifdef ENABLE_SPARKLE
 #include <Sparkle/Sparkle.h>
@@ -29,7 +29,7 @@ static constexpr bool isBeta = false;
 #endif
 
 #ifdef ENABLE_SPARKLE
- struct UpdateManager::Impl
+ struct AppVersionManager::Impl
 {
 
     Impl()
@@ -63,7 +63,7 @@ static constexpr bool isBeta = false;
 
 };
 #else
-struct UpdateManager::Impl
+struct AppVersionManager::Impl
 {
     void checkForUpdates() {};
 
@@ -80,7 +80,7 @@ struct UpdateManager::Impl
 };
 #endif
 
-UpdateManager::UpdateManager(const QString& url,
+AppVersionManager::AppVersionManager(const QString& url,
                              ConnectivityMonitor* cm,
                              LRCInstance* instance,
                              QObject* parent)
@@ -88,51 +88,46 @@ UpdateManager::UpdateManager(const QString& url,
     , pimpl_(std::make_unique<Impl>())
 {}
 
-UpdateManager::~UpdateManager()
+AppVersionManager::~AppVersionManager()
 {}
 
 void
-UpdateManager::checkForUpdates(bool quiet)
+AppVersionManager::checkForUpdates(bool quiet)
 {
     Q_UNUSED(quiet)
     pimpl_->checkForUpdates();
 }
 
 void
-UpdateManager::applyUpdates(bool beta)
+AppVersionManager::applyUpdates(bool beta)
 {
     Q_UNUSED(beta)
 }
 
 void
-UpdateManager::cancelUpdate()
-{}
-
-
-void
-UpdateManager::cancelDownload()
+AppVersionManager::cancelUpdate()
 {}
 
 void
-UpdateManager::setAutoUpdateCheck(bool state)
+AppVersionManager::setAutoUpdateCheck(bool state)
 {
     pimpl_->setAutoUpdateCheck(state);
 }
 
 bool
-UpdateManager::isCurrentVersionBeta()
+AppVersionManager::isCurrentVersionBeta()
 {
     return isBeta;
 }
 
 bool
-UpdateManager::isUpdaterEnabled()
+AppVersionManager::isUpdaterEnabled()
 {
     return pimpl_->isUpdaterEnabled();
 }
 
 bool
-UpdateManager::isAutoUpdaterEnabled()
+AppVersionManager::isAutoUpdaterEnabled()
 {
     return pimpl_->isAutoUpdaterEnabled();
 }
diff --git a/src/app/pluginadapter.cpp b/src/app/pluginadapter.cpp
index bde8370a4..6c5a97cf0 100644
--- a/src/app/pluginadapter.cpp
+++ b/src/app/pluginadapter.cpp
@@ -18,10 +18,12 @@
 
 #include "pluginadapter.h"
 
+#include "networkmanager.h"
 #include "lrcinstance.h"
 
 PluginAdapter::PluginAdapter(LRCInstance* instance, QObject* parent)
     : QmlAdapterBase(instance, parent)
+
 {
     set_isEnabled(lrcInstance_->pluginModel().getPluginsEnabled());
     updateHandlersListCount();
diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp
index b19901264..5c5d677b1 100644
--- a/src/app/qmlregister.cpp
+++ b/src/app/qmlregister.cpp
@@ -55,8 +55,8 @@
 #include "appsettingsmanager.h"
 #include "mainapplication.h"
 #include "namedirectory.h"
-#include "updatemanager.h"
 #include "pluginlistmodel.h"
+#include "appversionmanager.h"
 #include "pluginlistpreferencemodel.h"
 #include "preferenceitemlistmodel.h"
 #include "wizardviewstepmodel.h"
@@ -156,7 +156,7 @@ registerTypes(QQmlEngine* engine,
     // TODO: remove these
     QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
     QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, PluginModel, &lrcInstance->pluginModel())
-    QML_REGISTERSINGLETONTYPE_CUSTOM(NS_HELPERS, UpdateManager, lrcInstance->getUpdateManager())
+    QML_REGISTERSINGLETONTYPE_CUSTOM(NS_HELPERS, AppVersionManager, lrcInstance->getAppVersionManager())
 
     // Hack for QtCreator autocomplete (part 2)
     // https://bugreports.qt.io/browse/QTCREATORBUG-20569
@@ -244,7 +244,7 @@ registerTypes(QQmlEngine* engine,
 
     engine->setObjectOwnership(&lrcInstance->avModel(), QQmlEngine::CppOwnership);
     engine->setObjectOwnership(&lrcInstance->pluginModel(), QQmlEngine::CppOwnership);
-    engine->setObjectOwnership(lrcInstance->getUpdateManager(), QQmlEngine::CppOwnership);
+    engine->setObjectOwnership(lrcInstance->getAppVersionManager(), QQmlEngine::CppOwnership);
     engine->setObjectOwnership(&NameDirectory::instance(), QQmlEngine::CppOwnership);
 }
 // clang-format on
diff --git a/src/app/settingsview/SettingsSidePanel.qml b/src/app/settingsview/SettingsSidePanel.qml
index 887a3dfb3..92511de41 100644
--- a/src/app/settingsview/SettingsSidePanel.qml
+++ b/src/app/settingsview/SettingsSidePanel.qml
@@ -83,7 +83,7 @@ SidePanelBase {
                     }, {
                         "id": 11,
                         "title": JamiStrings.updatesTitle,
-                        "visible": UpdateManager.isUpdaterEnabled()
+                        "visible": AppVersionManager.isUpdaterEnabled()
                     }]
             }, {
                 "title": JamiStrings.audioVideoSettingsTitle,
diff --git a/src/app/settingsview/components/PluginItemDelegate.qml b/src/app/settingsview/components/PluginItemDelegate.qml
index e273d8772..578808bb8 100644
--- a/src/app/settingsview/components/PluginItemDelegate.qml
+++ b/src/app/settingsview/components/PluginItemDelegate.qml
@@ -77,7 +77,21 @@ ItemDelegate {
                 text: pluginName === "" ? pluginId : pluginName
                 verticalAlignment: Text.AlignVCenter
             }
-
+            MaterialButton {
+                id: update
+                TextMetrics {
+                    id: updateTextSize
+                    font.weight: Font.Bold
+                    font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
+                    font.capitalization: Font.AllUppercase
+                    text: JamiStrings.updatePlugin
+                }
+                visible: false
+                secondary: true
+                preferredWidth: updateTextSize.width
+                text: JamiStrings.updatePlugin
+                fontSize: 15
+            }
             ToggleSwitch {
                 id: loadSwitch
                 Layout.fillHeight: true
diff --git a/src/app/settingsview/components/PluginSettingsPage.qml b/src/app/settingsview/components/PluginSettingsPage.qml
index f1350b315..6b177e94b 100644
--- a/src/app/settingsview/components/PluginSettingsPage.qml
+++ b/src/app/settingsview/components/PluginSettingsPage.qml
@@ -25,7 +25,7 @@ import "../../commoncomponents"
 
 SettingsPageBase {
     id: root
-
+    contentFlickableWidth: Math.min(root.width, root.width - 2 * JamiTheme.preferredSettingsMarginSize)
     title: JamiStrings.pluginSettingsTitle
 
     flickableContent: ColumnLayout {
@@ -38,34 +38,19 @@ SettingsPageBase {
 
         ColumnLayout {
             id: generalSettings
-
-            width: parent.width
+            Layout.preferredWidth: root.width
             spacing: JamiTheme.settingsCategorySpacing
+        }
+        // View of installed plugins
+        PluginListView {
+            id: pluginListView
 
-            ToggleSwitch {
-                id: enabledplugin
-
-                checked: PluginAdapter.isEnabled
-                Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
-                Layout.fillWidth: true
-                labelText: JamiStrings.enable
-
-                onSwitchToggled: {
-                    PluginModel.setPluginsEnabled(checked);
-                    PluginAdapter.isEnabled = checked;
-                }
-            }
-
-            PluginListView {
-                id: pluginListView
-
-                visible: PluginAdapter.isEnabled
+            visible: PluginAdapter.isEnabled
 
-                Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
-                Layout.preferredWidth: parent.width
-                Layout.minimumHeight: 0
-                Layout.preferredHeight: childrenRect.height
-            }
+            Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
+            Layout.preferredWidth: parent.width
+            Layout.minimumHeight: 0
+            Layout.preferredHeight: childrenRect.height
         }
     }
 }
diff --git a/src/app/settingsview/components/UpdateDownloadDialog.qml b/src/app/settingsview/components/UpdateDownloadDialog.qml
index d4827c223..f183bd845 100644
--- a/src/app/settingsview/components/UpdateDownloadDialog.qml
+++ b/src/app/settingsview/components/UpdateDownloadDialog.qml
@@ -34,9 +34,10 @@ SimpleMessageDialog {
     property alias progressBarValue: progressBar.value
 
     Connections {
-        target: UpdateManager
+        target: AppVersionManager
 
-        function onUpdateErrorOccurred(error) {
+        function onErrorOccurred(error, msg) {
+            console.warn("Error while downloading update: " + error + " - " + msg);
             downloadDialog.close();
         }
 
@@ -44,7 +45,7 @@ SimpleMessageDialog {
             downloadDialog.setDownloadProgress(bytesRead, totalBytes);
         }
 
-        function onUpdateDownloadFinished() {
+        function onDownloadFinished() {
             downloadDialog.close();
         }
     }
@@ -98,10 +99,10 @@ SimpleMessageDialog {
     buttonTitles: [JamiStrings.optionCancel]
     buttonStyles: [SimpleMessageDialog.ButtonStyle.TintedBlue]
     buttonCallBacks: [function () {
-            UpdateManager.cancelDownload();
+            AppVersionManager.cancelUpdate();
         }]
     onVisibleChanged: {
         if (!visible)
-            UpdateManager.cancelDownload();
+            AppVersionManager.cancelUpdate();
     }
 }
diff --git a/src/app/settingsview/components/UpdateSettingsPage.qml b/src/app/settingsview/components/UpdateSettingsPage.qml
index 186cf0c46..e2a02e546 100644
--- a/src/app/settingsview/components/UpdateSettingsPage.qml
+++ b/src/app/settingsview/components/UpdateSettingsPage.qml
@@ -49,7 +49,7 @@ SettingsPageBase {
                 "buttonTitles": [JamiStrings.optionUpgrade, JamiStrings.optionLater],
                 "buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue, SimpleMessageDialog.ButtonStyle.TintedBlue],
                 "buttonCallBacks": [function () {
-                        UpdateManager.applyUpdates(beta);
+                        AppVersionManager.applyUpdates(beta);
                     }]
             });
     }
@@ -66,14 +66,14 @@ SettingsPageBase {
 
             Layout.fillWidth: true
 
-            checked: Qt.platform.os.toString() === "windows" ? UtilsAdapter.getAppValue(Settings.Key.AutoUpdate) : UpdateManager.isAutoUpdaterEnabled()
+            checked: Qt.platform.os.toString() === "windows" ? UtilsAdapter.getAppValue(Settings.Key.AutoUpdate) : AppVersionManager.isAutoUpdaterEnabled()
 
             labelText: JamiStrings.update
             tooltipText: JamiStrings.enableAutoUpdates
 
             onSwitchToggled: {
                 UtilsAdapter.setAppValue(Settings.Key.AutoUpdate, checked);
-                UpdateManager.setAutoUpdateCheck(checked);
+                AppVersionManager.setAutoUpdateCheck(checked);
             }
         }
 
@@ -98,13 +98,13 @@ SettingsPageBase {
             toolTipText: JamiStrings.checkForUpdates
             text: JamiStrings.checkForUpdates
 
-            onClicked: UpdateManager.checkForUpdates()
+            onClicked: AppVersionManager.checkForUpdates()
         }
 
         MaterialButton {
             id: installBetaButton
 
-            visible: !UpdateManager.isCurrentVersionBeta() && Qt.platform.os.toString() === "windows"
+            visible: !AppVersionManager.isCurrentVersionBeta() && Qt.platform.os.toString() === "windows"
 
             Layout.alignment: Qt.AlignHCenter
 
@@ -125,7 +125,7 @@ SettingsPageBase {
                     "buttonTitles": [JamiStrings.optionUpgrade, JamiStrings.optionLater],
                     "buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue, SimpleMessageDialog.ButtonStyle.TintedBlue],
                     "buttonCallBacks": [function () {
-                            UpdateManager.applyUpdates(true);
+                            AppVersionManager.applyUpdates(true);
                         }]
                 })
         }
diff --git a/src/app/wizardview/WizardView.qml b/src/app/wizardview/WizardView.qml
index 461feb471..036e695e8 100644
--- a/src/app/wizardview/WizardView.qml
+++ b/src/app/wizardview/WizardView.qml
@@ -43,7 +43,7 @@ BaseView {
             var errorMessage = JamiStrings.errorCreateAccount;
             for (var i = 0; i < controlPanelStackView.children.length; i++) {
                 if (i === controlPanelStackView.currentIndex) {
-                    controlPanelStackView.children[i].errorOccured(errorMessage);
+                    controlPanelStackView.children[i].errorOccurred(errorMessage);
                     return;
                 }
             }
diff --git a/src/app/wizardview/components/ConnectToAccountManagerPage.qml b/src/app/wizardview/components/ConnectToAccountManagerPage.qml
index 0f8848376..2564779a4 100644
--- a/src/app/wizardview/components/ConnectToAccountManagerPage.qml
+++ b/src/app/wizardview/components/ConnectToAccountManagerPage.qml
@@ -36,7 +36,7 @@ Rectangle {
         errorText = "";
     }
 
-    function errorOccured(errorMessage) {
+    function errorOccurred(errorMessage) {
         connectBtn.spinnerTriggered = false;
         errorText = errorMessage;
     }
diff --git a/src/app/wizardview/components/ImportFromBackupPage.qml b/src/app/wizardview/components/ImportFromBackupPage.qml
index b4fed5433..9cd94783b 100644
--- a/src/app/wizardview/components/ImportFromBackupPage.qml
+++ b/src/app/wizardview/components/ImportFromBackupPage.qml
@@ -43,7 +43,7 @@ Rectangle {
         fileImportBtnText = JamiStrings.selectArchiveFile;
     }
 
-    function errorOccured(errorMessage) {
+    function errorOccurred(errorMessage) {
         errorText = errorMessage;
         connectBtn.spinnerTriggered = false;
     }
diff --git a/src/app/wizardview/components/ImportFromDevicePage.qml b/src/app/wizardview/components/ImportFromDevicePage.qml
index e12295ffc..d377cfff6 100644
--- a/src/app/wizardview/components/ImportFromDevicePage.qml
+++ b/src/app/wizardview/components/ImportFromDevicePage.qml
@@ -39,7 +39,7 @@ Rectangle {
         connectBtn.spinnerTriggered = false;
     }
 
-    function errorOccured(errorMessage) {
+    function errorOccurred(errorMessage) {
         errorText = errorMessage;
         connectBtn.spinnerTriggered = false;
     }
-- 
GitLab