diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 1b4c93c5b..25f98ca58 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -433,8 +433,11 @@ void App::initCore() { QMetaObject::invokeMethod( mLinphoneThread->getThreadId(), [this]() mutable { + lInfo() << log().arg("Updating downloaded codec files"); + Utils::updateCodecs(); // removing codec updates suffic (.in) before the core is created. lInfo() << log().arg("Starting Core"); CoreModel::getInstance()->start(); + Utils::loadDownloadedCodecs(); auto coreStarted = CoreModel::getInstance()->getCore()->getGlobalState() == linphone::GlobalState::On; lDebug() << log().arg("Creating SettingsModel"); SettingsModel::create(); @@ -541,6 +544,8 @@ void App::initCore() { }, Qt::QueuedConnection); + Utils::checkDownloadedCodecsUpdates(); + mEngine->load(url); }); }, @@ -631,6 +636,7 @@ void App::initCppInterfaces() { qmlRegisterType(Constants::MainQmlUri, 1, 0, "PayloadTypeGui"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "PayloadTypeProxy"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "PayloadTypeCore"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "DownloadablePayloadTypeCore"); LinphoneEnums::registerMetaTypes(); } diff --git a/Linphone/core/CMakeLists.txt b/Linphone/core/CMakeLists.txt index 92c8a0df4..8614b3b84 100644 --- a/Linphone/core/CMakeLists.txt +++ b/Linphone/core/CMakeLists.txt @@ -80,6 +80,7 @@ list(APPEND _LINPHONEAPP_SOURCES core/address-books/carddav/CarddavList.cpp core/payload-type/PayloadTypeCore.cpp + core/payload-type/DownloadablePayloadTypeCore.cpp core/payload-type/PayloadTypeGui.cpp core/payload-type/PayloadTypeProxy.cpp core/payload-type/PayloadTypeList.cpp diff --git a/Linphone/core/payload-type/DownloadablePayloadTypeCore.cpp b/Linphone/core/payload-type/DownloadablePayloadTypeCore.cpp new file mode 100644 index 000000000..bd3019bcb --- /dev/null +++ b/Linphone/core/payload-type/DownloadablePayloadTypeCore.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#include "DownloadablePayloadTypeCore.hpp" +#include "core/App.hpp" +#include "core/path/Paths.hpp" +#include "tool/file/FileDownloader.hpp" +#include "tool/file/FileExtractor.hpp" + +DEFINE_ABSTRACT_OBJECT(DownloadablePayloadTypeCore) + +QSharedPointer DownloadablePayloadTypeCore::create(PayloadTypeCore::Family family, + const QString &mimeType, + const QString &encoderDescription, + const QString &downloadUrl, + const QString &installName, + const QString &checkSum) { + auto sharedPointer = QSharedPointer( + new DownloadablePayloadTypeCore(family, mimeType, encoderDescription, downloadUrl, installName, checkSum), + &QObject::deleteLater); + sharedPointer->moveToThread(App::getInstance()->thread()); + return sharedPointer; +} + +DownloadablePayloadTypeCore::DownloadablePayloadTypeCore(PayloadTypeCore::Family family, + const QString &mimeType, + const QString &encoderDescription, + const QString &downloadUrl, + const QString &installName, + const QString &checkSum) + : PayloadTypeCore() { + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); + + mFamily = family; + mMimeType = mimeType; + mEnabled = false; + mDownloadable = true; + + mEncoderDescription = encoderDescription; + mDownloadUrl = downloadUrl; + mInstallName = installName; + mCheckSum = checkSum; +} + +DownloadablePayloadTypeCore::~DownloadablePayloadTypeCore() { + mustBeInMainThread(log().arg(Q_FUNC_INFO)); +} + +void DownloadablePayloadTypeCore::downloadAndExtract(bool isUpdate) { + lInfo() << log().arg("Downloading `%1` codec...").arg(mMimeType); + auto codecsFolder = Paths::getCodecsDirPath(); + QString versionFilePath = codecsFolder + mMimeType + ".txt"; + QFile versionFile(versionFilePath); + + FileDownloader *fileDownloader = new FileDownloader(this); + fileDownloader->setUrl(QUrl(mDownloadUrl)); + fileDownloader->setDownloadFolder(codecsFolder); + + FileExtractor *fileExtractor = new FileExtractor(fileDownloader); + fileExtractor->setExtractFolder(codecsFolder); + fileExtractor->setExtractName(mInstallName + (isUpdate ? ".in" : "")); + + QObject::connect(fileDownloader, &FileDownloader::downloadFinished, + [this, fileDownloader, fileExtractor, checksum = mCheckSum](const QString &filePath) { + fileExtractor->setFile(filePath); + QString fileChecksum = Utils::getFileChecksum(filePath); + if (checksum.isEmpty() || fileChecksum == checksum) fileExtractor->extract(); + else { + lWarning() << log().arg("File cannot be downloaded : Bad checksum : ") << fileChecksum; + fileDownloader->remove(); + fileDownloader->deleteLater(); + emit downloadError(); + } + }); + + QObject::connect(fileDownloader, &FileDownloader::downloadFailed, [this, fileDownloader]() { + fileDownloader->deleteLater(); + emit downloadError(); + }); + + QObject::connect(fileExtractor, &FileExtractor::extractFinished, + [this, fileDownloader, fileExtractor, versionFilePath, downloadUrl = mDownloadUrl]() { + QFile versionFile(versionFilePath); + if (!versionFile.open(QIODevice::WriteOnly)) { + lWarning() << log().arg("Unable to write codec version in: `%1`.").arg(versionFilePath); + emit extractError(); + } else if (versionFile.write(Utils::appStringToCoreString(downloadUrl).c_str(), + downloadUrl.length()) == -1) { + fileExtractor->remove(); + versionFile.close(); + versionFile.remove(); + emit extractError(); + } else emit success(); + fileDownloader->remove(); + fileDownloader->deleteLater(); + }); + + QObject::connect(fileExtractor, &FileExtractor::extractFailed, [this, fileDownloader]() { + fileDownloader->remove(); + fileDownloader->deleteLater(); + emit extractError(); + }); + + fileDownloader->download(); +} + +bool DownloadablePayloadTypeCore::shouldDownloadUpdate() { + auto codecsFolder = Paths::getCodecsDirPath(); + QString versionFilePath = codecsFolder + mMimeType + ".txt"; + QFile versionFile(versionFilePath); + + if (!versionFile.exists() && !QFileInfo::exists(codecsFolder + mInstallName)) { + lWarning() << log().arg("Codec `%1` is not installed.").arg(versionFilePath); + return false; + } + if (!versionFile.open(QIODevice::ReadOnly)) { + lWarning() << log().arg("Codec `%1` : unable to read codec version, attempting download.").arg(versionFilePath); + return true; + } else if (!QString::compare(QTextStream(&versionFile).readAll(), mDownloadUrl, Qt::CaseInsensitive)) { + lInfo() << log().arg("Codec `%1` is installed and up to date.").arg(versionFilePath); + return false; + } else { + return true; + } +} diff --git a/Linphone/core/payload-type/DownloadablePayloadTypeCore.hpp b/Linphone/core/payload-type/DownloadablePayloadTypeCore.hpp new file mode 100644 index 000000000..62210ced4 --- /dev/null +++ b/Linphone/core/payload-type/DownloadablePayloadTypeCore.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef DOWNLOADABLE_PAYLOAD_TYPE_CORE_H_ +#define DOWNLOADABLE_PAYLOAD_TYPE_CORE_H_ + +#include "PayloadTypeCore.hpp" +#include "tool/AbstractObject.hpp" +#include +#include +#include + +class DownloadablePayloadTypeCore : public PayloadTypeCore { + Q_OBJECT + +public: + Q_INVOKABLE void downloadAndExtract(bool isUpdate = false); + bool shouldDownloadUpdate(); + + static QSharedPointer create(PayloadTypeCore::Family family, + const QString &mime, + const QString &encoderDescription, + const QString &downloadUrl, + const QString &installName, + const QString &checkSum); + + DownloadablePayloadTypeCore(PayloadTypeCore::Family family, + const QString &mimeType, + const QString &encoderDescription, + const QString &downloadUrl, + const QString &installName, + const QString &checkSum); + + ~DownloadablePayloadTypeCore(); + void setSelf(QSharedPointer me); + +signals: + void success(); + void downloadError(); + void extractError(); + void installedChanged(); + void versionChanged(); + +private: + QString mDownloadUrl; + QString mInstallName; + QString mCheckSum; + bool mInstalled; + QString mVersion; + + DECLARE_ABSTRACT_OBJECT +}; +Q_DECLARE_METATYPE(DownloadablePayloadTypeCore *) +#endif diff --git a/Linphone/core/payload-type/PayloadTypeCore.cpp b/Linphone/core/payload-type/PayloadTypeCore.cpp index 6d0040452..5d764d51b 100644 --- a/Linphone/core/payload-type/PayloadTypeCore.cpp +++ b/Linphone/core/payload-type/PayloadTypeCore.cpp @@ -23,16 +23,16 @@ DEFINE_ABSTRACT_OBJECT(PayloadTypeCore) -QSharedPointer PayloadTypeCore::create(const std::shared_ptr &payloadType, - Family family) { +QSharedPointer PayloadTypeCore::create(Family family, + const std::shared_ptr &payloadType) { auto sharedPointer = - QSharedPointer(new PayloadTypeCore(payloadType, family), &QObject::deleteLater); + QSharedPointer(new PayloadTypeCore(family, payloadType), &QObject::deleteLater); sharedPointer->setSelf(sharedPointer); sharedPointer->moveToThread(App::getInstance()->thread()); return sharedPointer; } -PayloadTypeCore::PayloadTypeCore(const std::shared_ptr &payloadType, Family family) +PayloadTypeCore::PayloadTypeCore(Family family, const std::shared_ptr &payloadType) : QObject(nullptr) { App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); @@ -42,6 +42,7 @@ PayloadTypeCore::PayloadTypeCore(const std::shared_ptr &p INIT_CORE_MEMBER(ClockRate, mPayloadTypeModel) INIT_CORE_MEMBER(MimeType, mPayloadTypeModel) INIT_CORE_MEMBER(RecvFmtp, mPayloadTypeModel) + INIT_CORE_MEMBER(EncoderDescription, mPayloadTypeModel) } PayloadTypeCore::~PayloadTypeCore() { @@ -62,3 +63,7 @@ PayloadTypeCore::Family PayloadTypeCore::getFamily() { QString PayloadTypeCore::getMimeType() { return mMimeType; } + +bool PayloadTypeCore::getDownloadable() { + return mDownloadable; +} diff --git a/Linphone/core/payload-type/PayloadTypeCore.hpp b/Linphone/core/payload-type/PayloadTypeCore.hpp index 91af030eb..2d7ae3bea 100644 --- a/Linphone/core/payload-type/PayloadTypeCore.hpp +++ b/Linphone/core/payload-type/PayloadTypeCore.hpp @@ -33,25 +33,32 @@ class PayloadTypeCore : public QObject, public AbstractObject { Q_ENUMS(Family) Q_PROPERTY(Family family MEMBER mFamily CONSTANT) - DECLARE_CORE_GETSET_MEMBER(bool, enabled, Enabled) DECLARE_CORE_MEMBER(int, clockRate, ClockRate) - DECLARE_CORE_MEMBER(QString, mimeType, MimeType) DECLARE_CORE_MEMBER(QString, recvFmtp, RecvFmtp) public: - enum Family { None, Audio, Video, Text }; - static QSharedPointer create(const std::shared_ptr &payloadType, - Family family); - PayloadTypeCore(const std::shared_ptr &payloadType, Family family); + static QSharedPointer create(Family family, + const std::shared_ptr &payloadType); + + PayloadTypeCore(Family family, const std::shared_ptr &payloadType); + PayloadTypeCore() {}; ~PayloadTypeCore(); + void setSelf(QSharedPointer me); Family getFamily(); + bool getDownloadable(); QString getMimeType(); -private: +protected: Family mFamily; + bool mDownloadable = false; + DECLARE_CORE_GETSET_MEMBER(bool, enabled, Enabled) + DECLARE_CORE_MEMBER(QString, mimeType, MimeType) + DECLARE_CORE_MEMBER(QString, encoderDescription, EncoderDescription) + +private: std::shared_ptr mPayloadTypeModel; QSharedPointer> mPayloadTypeModelConnection; diff --git a/Linphone/core/payload-type/PayloadTypeList.cpp b/Linphone/core/payload-type/PayloadTypeList.cpp index 3e09e7475..123e8d442 100644 --- a/Linphone/core/payload-type/PayloadTypeList.cpp +++ b/Linphone/core/payload-type/PayloadTypeList.cpp @@ -19,8 +19,10 @@ */ #include "PayloadTypeList.hpp" +#include "DownloadablePayloadTypeCore.hpp" #include "PayloadTypeGui.hpp" #include "core/App.hpp" +#include "core/path/Paths.hpp" #include "model/object/VariantObject.hpp" #include #include @@ -37,12 +39,12 @@ QSharedPointer PayloadTypeList::create() { } PayloadTypeList::PayloadTypeList(QObject *parent) : ListProxy(parent) { - mustBeInMainThread(getClassName()); + mustBeInMainThread(log().arg(Q_FUNC_INFO)); App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); } PayloadTypeList::~PayloadTypeList() { - mustBeInMainThread("~" + getClassName()); + mustBeInMainThread(log().arg(Q_FUNC_INFO)); mModelConnection = nullptr; } @@ -52,21 +54,40 @@ void PayloadTypeList::setSelf(QSharedPointer me) { mModelConnection->makeConnectToCore(&PayloadTypeList::lUpdate, [this]() { mModelConnection->invokeToModel([this]() { QList> *payloadTypes = new QList>(); - mustBeInLinphoneThread(getClassName()); + mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); + + Utils::loadDownloadedCodecs(); + + // Audio for (auto payloadType : CoreModel::getInstance()->getCore()->getAudioPayloadTypes()) { - auto model = PayloadTypeCore::create(payloadType, PayloadTypeCore::Family::Audio); - payloadTypes->push_back(model); + auto core = PayloadTypeCore::create(PayloadTypeCore::Family::Audio, payloadType); + payloadTypes->push_back(core); } - for (auto payloadType : CoreModel::getInstance()->getCore()->getVideoPayloadTypes()) { - auto model = PayloadTypeCore::create(payloadType, PayloadTypeCore::Family::Video); - payloadTypes->push_back(model); + + // Video + auto videoCodecs = CoreModel::getInstance()->getCore()->getVideoPayloadTypes(); + for (auto payloadType : videoCodecs) { + auto core = PayloadTypeCore::create(PayloadTypeCore::Family::Video, payloadType); + payloadTypes->push_back(core); } + + // Downloadable Video + for (auto downloadableVideoCodec : Utils::getDownloadableVideoPayloadTypes()) { + if (find_if(videoCodecs.begin(), videoCodecs.end(), + [downloadableVideoCodec](const std::shared_ptr &codec) { + return Utils::coreStringToAppString(codec->getMimeType()) == + downloadableVideoCodec->getMimeType(); + }) == videoCodecs.end()) + payloadTypes->append(downloadableVideoCodec.dynamicCast()); + } + + // Text for (auto payloadType : CoreModel::getInstance()->getCore()->getTextPayloadTypes()) { - auto model = PayloadTypeCore::create(payloadType, PayloadTypeCore::Family::Text); - payloadTypes->push_back(model); + auto core = PayloadTypeCore::create(PayloadTypeCore::Family::Text, payloadType); + payloadTypes->push_back(core); } mModelConnection->invokeToCore([this, payloadTypes]() { - mustBeInMainThread(getClassName()); + mustBeInMainThread(log().arg(Q_FUNC_INFO)); resetData(*payloadTypes); delete payloadTypes; }); diff --git a/Linphone/core/payload-type/PayloadTypeList.hpp b/Linphone/core/payload-type/PayloadTypeList.hpp index e3e3876eb..0d50252c4 100644 --- a/Linphone/core/payload-type/PayloadTypeList.hpp +++ b/Linphone/core/payload-type/PayloadTypeList.hpp @@ -36,7 +36,7 @@ class PayloadTypeList : public ListProxy, public AbstractObject { public: static QSharedPointer create(); - + PayloadTypeList(QObject *parent = Q_NULLPTR); ~PayloadTypeList(); @@ -49,6 +49,7 @@ signals: private: QSharedPointer> mModelConnection; + DECLARE_ABSTRACT_OBJECT }; diff --git a/Linphone/core/payload-type/PayloadTypeProxy.cpp b/Linphone/core/payload-type/PayloadTypeProxy.cpp index ee9f4f683..e3ec52274 100644 --- a/Linphone/core/payload-type/PayloadTypeProxy.cpp +++ b/Linphone/core/payload-type/PayloadTypeProxy.cpp @@ -44,9 +44,21 @@ void PayloadTypeProxy::setFamily(PayloadTypeCore::Family data) { } } +bool PayloadTypeProxy::isDownloadable() const { + return dynamic_cast(sourceModel())->mDownloadable; +} + +void PayloadTypeProxy::setDownloadable(bool data) { + auto list = dynamic_cast(sourceModel()); + if (list->mDownloadable != data) { + list->mDownloadable = data; + downloadableChanged(); + } +} + bool PayloadTypeProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { auto payload = qobject_cast(sourceModel())->getAt(sourceRow); - return payload->getFamily() == mFamily; + return payload->getFamily() == mFamily && payload->getDownloadable() == mDownloadable; } bool PayloadTypeProxy::SortFilterList::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const { @@ -55,3 +67,7 @@ bool PayloadTypeProxy::SortFilterList::lessThan(const QModelIndex &sourceLeft, c return l->getMimeType() < r->getMimeType(); } + +void PayloadTypeProxy::reload() { + emit mPayloadTypeList->lUpdate(); +} diff --git a/Linphone/core/payload-type/PayloadTypeProxy.hpp b/Linphone/core/payload-type/PayloadTypeProxy.hpp index ba79e15e4..0879fb215 100644 --- a/Linphone/core/payload-type/PayloadTypeProxy.hpp +++ b/Linphone/core/payload-type/PayloadTypeProxy.hpp @@ -31,18 +31,24 @@ class PayloadTypeProxy : public LimitProxy, public AbstractObject { Q_OBJECT Q_PROPERTY(PayloadTypeCore::Family family READ getFamily WRITE setFamily NOTIFY familyChanged) + Q_PROPERTY(bool downloadable READ isDownloadable WRITE setDownloadable NOTIFY downloadableChanged) public: - DECLARE_SORTFILTER_CLASS(PayloadTypeCore::Family mFamily;) + DECLARE_SORTFILTER_CLASS(PayloadTypeCore::Family mFamily; bool mDownloadable;) + + Q_INVOKABLE void reload(); PayloadTypeProxy(QObject *parent = Q_NULLPTR); ~PayloadTypeProxy(); PayloadTypeCore::Family getFamily() const; void setFamily(PayloadTypeCore::Family data); + bool isDownloadable() const; + void setDownloadable(bool data); signals: void familyChanged(); + void downloadableChanged(); protected: QSharedPointer mPayloadTypeList; diff --git a/Linphone/core/setting/SettingsCore.cpp b/Linphone/core/setting/SettingsCore.cpp index 0e1ae1e9a..3bceab318 100644 --- a/Linphone/core/setting/SettingsCore.cpp +++ b/Linphone/core/setting/SettingsCore.cpp @@ -98,6 +98,7 @@ SettingsCore::SettingsCore(QObject *parent) : QObject(parent) { INIT_CORE_MEMBER(SyncLdapContacts, settingsModel) INIT_CORE_MEMBER(Ipv6Enabled, settingsModel) INIT_CORE_MEMBER(ConfigLocale, settingsModel) + INIT_CORE_MEMBER(DownloadFolder, settingsModel) } SettingsCore::~SettingsCore() { @@ -348,6 +349,8 @@ void SettingsCore::setSelf(QSharedPointer me) { Ipv6Enabled) DEFINE_CORE_GETSET_CONNECT(mSettingsModelConnection, SettingsCore, SettingsModel, settingsModel, QString, configLocale, ConfigLocale) + DEFINE_CORE_GETSET_CONNECT(mSettingsModelConnection, SettingsCore, SettingsModel, settingsModel, QString, + downloadFolder, DownloadFolder) auto coreModelConnection = QSharedPointer>( new SafeConnection(me, CoreModel::getInstance()), &QObject::deleteLater); @@ -523,3 +526,9 @@ bool SettingsCore::getSyncLdapContacts() const { QString SettingsCore::getConfigLocale() const { return mConfigLocale; } + +QString SettingsCore::getDownloadFolder() const { + auto path = mDownloadFolder; + if (mDownloadFolder.isEmpty()) path = Paths::getDownloadDirPath(); + return QDir::cleanPath(path) + QDir::separator(); +} diff --git a/Linphone/core/setting/SettingsCore.hpp b/Linphone/core/setting/SettingsCore.hpp index 0b9e9e805..9dbb8d6a4 100644 --- a/Linphone/core/setting/SettingsCore.hpp +++ b/Linphone/core/setting/SettingsCore.hpp @@ -168,6 +168,7 @@ public: DECLARE_CORE_GETSET_MEMBER(QVariantList, audioCodecs, AudioCodecs) DECLARE_CORE_GETSET_MEMBER(QVariantList, videoCodecs, VideoCodecs) DECLARE_CORE_GETSET(QString, configLocale, ConfigLocale) + DECLARE_CORE_GETSET(QString, downloadFolder, DownloadFolder) signals: diff --git a/Linphone/model/payload-type/PayloadTypeModel.cpp b/Linphone/model/payload-type/PayloadTypeModel.cpp index 1bee2d9b1..e0dc02bee 100644 --- a/Linphone/model/payload-type/PayloadTypeModel.cpp +++ b/Linphone/model/payload-type/PayloadTypeModel.cpp @@ -36,3 +36,4 @@ DEFINE_GETSET_ENABLE(PayloadTypeModel, enabled, Enabled, mPayloadType) DEFINE_GET(PayloadTypeModel, int, ClockRate, mPayloadType) DEFINE_GET_STRING(PayloadTypeModel, MimeType, mPayloadType) DEFINE_GET_STRING(PayloadTypeModel, RecvFmtp, mPayloadType) +DEFINE_GET_STRING(PayloadTypeModel, EncoderDescription, mPayloadType) diff --git a/Linphone/model/payload-type/PayloadTypeModel.hpp b/Linphone/model/payload-type/PayloadTypeModel.hpp index 5e4affdb0..8a5c4a80b 100644 --- a/Linphone/model/payload-type/PayloadTypeModel.hpp +++ b/Linphone/model/payload-type/PayloadTypeModel.hpp @@ -35,6 +35,7 @@ public: int getClockRate() const; QString getMimeType() const; QString getRecvFmtp() const; + QString getEncoderDescription() const; DECLARE_GETSET(bool, enabled, Enabled) diff --git a/Linphone/model/setting/SettingsModel.cpp b/Linphone/model/setting/SettingsModel.cpp index 5a8ba9a76..12a2df3a4 100644 --- a/Linphone/model/setting/SettingsModel.cpp +++ b/Linphone/model/setting/SettingsModel.cpp @@ -571,6 +571,7 @@ void SettingsModel::notifyConfigReady(){ DEFINE_NOTIFY_CONFIG_READY(exitOnClose, ExitOnClose) DEFINE_NOTIFY_CONFIG_READY(syncLdapContacts, SyncLdapContacts) DEFINE_NOTIFY_CONFIG_READY(configLocale, ConfigLocale) + DEFINE_NOTIFY_CONFIG_READY(downloadFolder, DownloadFolder) } DEFINE_GETSET_CONFIG(SettingsModel, bool, Bool, disableChatFeature, DisableChatFeature, "disable_chat_feature", true) @@ -673,4 +674,9 @@ DEFINE_GETSET_CONFIG_STRING(SettingsModel, ConfigLocale, "locale", "") +DEFINE_GETSET_CONFIG_STRING(SettingsModel, + downloadFolder, + DownloadFolder, + "download_folder", + "") // clang-format on diff --git a/Linphone/model/setting/SettingsModel.hpp b/Linphone/model/setting/SettingsModel.hpp index cd77a4cd8..6d908f54b 100644 --- a/Linphone/model/setting/SettingsModel.hpp +++ b/Linphone/model/setting/SettingsModel.hpp @@ -155,6 +155,7 @@ public: DECLARE_GETSET(bool, syncLdapContacts, SyncLdapContacts) DECLARE_GETSET(bool, ipv6Enabled, Ipv6Enabled) DECLARE_GETSET(QString, configLocale, ConfigLocale) + DECLARE_GETSET(QString, downloadFolder, DownloadFolder) signals: diff --git a/Linphone/tool/CMakeLists.txt b/Linphone/tool/CMakeLists.txt index ead22153c..aa2ba560c 100644 --- a/Linphone/tool/CMakeLists.txt +++ b/Linphone/tool/CMakeLists.txt @@ -15,6 +15,10 @@ list(APPEND _LINPHONEAPP_SOURCES tool/request/RequestDialog.cpp tool/request/AuthenticationDialog.cpp + + tool/file/FileDownloader.cpp + tool/file/FileExtractor.cpp + ) if (APPLE) diff --git a/Linphone/tool/Constants.cpp b/Linphone/tool/Constants.cpp index ec367f292..366d80cc4 100644 --- a/Linphone/tool/Constants.cpp +++ b/Linphone/tool/Constants.cpp @@ -159,3 +159,4 @@ constexpr char Constants::LinphoneBZip2_exe[]; constexpr char Constants::LinphoneBZip2_dll[]; constexpr char Constants::DefaultRlsUri[]; constexpr char Constants::DefaultLogsEmail[]; +constexpr char Constants::DownloadDefaultFileName[]; diff --git a/Linphone/tool/Constants.hpp b/Linphone/tool/Constants.hpp index 2332c403e..16c5f309c 100644 --- a/Linphone/tool/Constants.hpp +++ b/Linphone/tool/Constants.hpp @@ -175,6 +175,8 @@ public: // 4 = RTP bundle mode // 5 = Video Conference URI // 6 = Publish expires + static constexpr char DownloadDefaultFileName[] = "download"; + //-------------------------------------------------------------------------------- // CISCO //-------------------------------------------------------------------------------- diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index 65bd822d0..4527d08ce 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -26,6 +26,7 @@ #include "core/conference/ConferenceInfoGui.hpp" #include "core/friend/FriendGui.hpp" #include "core/path/Paths.hpp" +#include "core/payload-type/DownloadablePayloadTypeCore.hpp" #include "model/object/VariantObject.hpp" #include "model/tool/ToolModel.hpp" #include "tool/providers/AvatarProvider.hpp" @@ -33,13 +34,18 @@ #include #include +#include #include +#include #include #include +#include #include #include #include +DEFINE_ABSTRACT_OBJECT(Utils) + // ============================================================================= char *Utils::rstrstr(const char *a, const char *b) { @@ -1403,3 +1409,77 @@ QString Utils::boldTextPart(const QString &text, const QString ®ex) { if (splittedText.size() > 0) result.append(splittedText[splittedText.size() - 1]); return result; } + +QString Utils::getFileChecksum(const QString &filePath) { + QFile file(filePath); + if (file.open(QFile::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Sha256); + if (hash.addData(&file)) { + return hash.result().toHex(); + } + } + return QString(); +} + +// Codecs download + +QList> Utils::getDownloadableVideoPayloadTypes() { + QList> payloadTypes; +#if defined(Q_OS_LINUX) || defined(Q_OS_WIN) + auto ciscoH264 = DownloadablePayloadTypeCore::create(PayloadTypeCore::Family::Video, "H264", + Constants::H264Description, Constants::PluginUrlH264, + Constants::H264InstallName, Constants::PluginH264Check); + payloadTypes.push_back(ciscoH264); +#endif + return payloadTypes; +} + +void Utils::checkDownloadedCodecsUpdates() { + for (auto codec : getDownloadableVideoPayloadTypes()) { + if (codec->shouldDownloadUpdate()) codec->downloadAndExtract(true); + } +} + +// Load downloaded codecs like OpenH264 (needs to be after core is created and has loaded its plugins, as +// reloadMsPlugins modifies plugin path for the factory) +void Utils::loadDownloadedCodecs() { + mustBeInLinphoneThread(sLog().arg(Q_FUNC_INFO)); +#if defined(Q_OS_LINUX) || defined(Q_OS_WIN) + QDirIterator it(Paths::getCodecsDirPath()); + while (it.hasNext()) { + QFileInfo info(it.next()); + const QString filename(info.fileName()); + if (QLibrary::isLibrary(filename)) { + qInfo() << QStringLiteral("Loading `%1` symbols...").arg(filename); + if (!QLibrary(info.filePath()).load()) // lib.load()) + qWarning() << QStringLiteral("Failed to load `%1` symbols.").arg(filename); + else qInfo() << QStringLiteral("Loaded `%1` symbols...").arg(filename); + } + } + CoreModel::getInstance()->getCore()->reloadMsPlugins(""); +#endif // if defined(Q_OS_LINUX) || defined(Q_OS_WIN) +} + +// Removes .in suffix from downloaded updates. +// Updates are downloaded with .in suffix as they can't overwrite already loaded plugin +// they are loaded at next app startup. + +void Utils::updateCodecs() { + mustBeInLinphoneThread(sLog().arg(Q_FUNC_INFO)); +#if defined(Q_OS_LINUX) || defined(Q_OS_WIN) + static const QString codecSuffix = QStringLiteral(".%1").arg(Constants::LibraryExtension); + + QDirIterator it(Paths::getCodecsDirPath()); + while (it.hasNext()) { + QFileInfo info(it.next()); + if (info.suffix() == QLatin1String("in")) { + QString codecName = info.completeBaseName(); + if (codecName.endsWith(codecSuffix)) { + QString codecPath = info.dir().path() + QDir::separator() + codecName; + QFile::remove(codecPath); + QFile::rename(info.filePath(), codecPath); + } + } + } +#endif // if defined(Q_OS_LINUX) || defined(Q_OS_WIN) +} diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index b7411880f..66a49cbdc 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -26,6 +26,7 @@ #include #include "Constants.hpp" +#include "tool/AbstractObject.hpp" #include "tool/LinphoneEnums.hpp" // ============================================================================= @@ -47,8 +48,9 @@ class QQuickWindow; class VariantObject; class CallGui; class ConferenceInfoGui; +class DownloadablePayloadTypeCore; -class Utils : public QObject { +class Utils : public QObject, public AbstractObject { Q_OBJECT public: Utils(QObject *parent = nullptr) : QObject(parent) { @@ -131,10 +133,15 @@ public: Q_INVOKABLE void playDtmf(const QString &dtmf); Q_INVOKABLE bool isInteger(const QString &text); Q_INVOKABLE QString boldTextPart(const QString &text, const QString ®ex); + Q_INVOKABLE static QString getFileChecksum(const QString &filePath); static QString getApplicationProduct(); static QString getOsProduct(); static QString computeUserAgent(); + static QList> getDownloadableVideoPayloadTypes(); + static void checkDownloadedCodecsUpdates(); + static void loadDownloadedCodecs(); + static void updateCodecs(); static inline QString coreStringToAppString(const std::string &str) { if (Constants::LinphoneLocaleEncoding == QString("UTF-8")) return QString::fromStdString(str); @@ -165,6 +172,9 @@ public: return (volume - VuMin) / (VuMax - VuMin); } + +private: + DECLARE_ABSTRACT_OBJECT }; #define lDebug() qDebug().noquote() diff --git a/Linphone/tool/file/FileDownloader.cpp b/Linphone/tool/file/FileDownloader.cpp new file mode 100644 index 000000000..e48348f6b --- /dev/null +++ b/Linphone/tool/file/FileDownloader.cpp @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#include "core/App.hpp" +#include "core/path/Paths.hpp" +#include "tool/Utils.hpp" +#include +#include + +#include "FileDownloader.hpp" + +// ============================================================================= + +static QString getDownloadFilePath(const QString &folder, const QUrl &url, const bool &overwrite) { + QString defaultFileName = QString(Constants::DownloadDefaultFileName); + QFileInfo fileInfo(url.path()); + QString fileName = fileInfo.fileName(); + if (fileName.isEmpty()) fileName = defaultFileName; + + fileName.prepend(folder); + if (overwrite && QFile::exists(fileName)) QFile::remove(fileName); + if (!QFile::exists(fileName)) return fileName; + + // Already exists, don't overwrite. + QString baseName = fileInfo.completeBaseName(); + if (baseName.isEmpty()) baseName = defaultFileName; + + QString suffix = fileInfo.suffix(); + if (!suffix.isEmpty()) suffix.prepend("."); + + for (int i = 1; true; ++i) { + fileName = folder + baseName + "(" + QString::number(i) + ")" + suffix; + if (!QFile::exists(fileName)) break; + } + return fileName; +} + +static bool isHttpRedirect(QNetworkReply *reply) { + Q_CHECK_PTR(reply); + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + return statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 || + statusCode == 308; +} + +// ----------------------------------------------------------------------------- + +void FileDownloader::download() { + if (mDownloading) { + qWarning() << "Unable to download file. Already downloading!"; + return; + } + setDownloading(true); + + QNetworkRequest request(mUrl); + mNetworkReply = mManager.get(request); + + QNetworkReply *data = mNetworkReply.data(); + + QObject::connect(data, &QNetworkReply::readyRead, this, &FileDownloader::handleReadyData); + QObject::connect(data, &QNetworkReply::finished, this, &FileDownloader::handleDownloadFinished); + QObject::connect(data, &QNetworkReply::errorOccurred, this, &FileDownloader::handleError); + QObject::connect(data, &QNetworkReply::downloadProgress, this, &FileDownloader::handleDownloadProgress); + +#if QT_CONFIG(ssl) + QObject::connect(data, &QNetworkReply::sslErrors, this, &FileDownloader::handleSslErrors); +#endif + + if (mDownloadFolder.isEmpty()) { + mDownloadFolder = App::getInstance()->getSettings()->getDownloadFolder(); + emit downloadFolderChanged(mDownloadFolder); + } + + Q_ASSERT(!mDestinationFile.isOpen()); + mDestinationFile.setFileName( + getDownloadFilePath(QDir::cleanPath(mDownloadFolder) + QDir::separator(), mUrl, mOverwriteFile)); + if (!mDestinationFile.open(QIODevice::WriteOnly)) emitOutputError(); + else { + mTimeoutReadBytes = 0; + mTimeout.start(); + } +} + +bool FileDownloader::remove() { + return mDestinationFile.exists() && !mDestinationFile.isOpen() && mDestinationFile.remove(); +} + +void FileDownloader::emitOutputError() { + qWarning() << QStringLiteral("Could not write into `%1` (%2).") + .arg(mDestinationFile.fileName()) + .arg(mDestinationFile.errorString()); + mNetworkReply->abort(); +} + +void FileDownloader::cleanDownloadEnd() { + mTimeout.stop(); + mNetworkReply->deleteLater(); + setDownloading(false); +} + +void FileDownloader::handleReadyData() { + QByteArray data = mNetworkReply->readAll(); + if (mDestinationFile.write(data) == -1) emitOutputError(); +} + +void FileDownloader::handleDownloadFinished() { + if (mNetworkReply->error() != QNetworkReply::NoError) return; + + if (isHttpRedirect(mNetworkReply)) { + qWarning() << QStringLiteral("Request was redirected."); + mDestinationFile.remove(); + cleanDownloadEnd(); + emit downloadFailed(); + } else { + qInfo() << QStringLiteral("Download of %1 finished to %2").arg(mUrl.toString(), mDestinationFile.fileName()); + mDestinationFile.close(); + cleanDownloadEnd(); + QString fileChecksum = Utils::getFileChecksum(mDestinationFile.fileName()); + if (mCheckSum.isEmpty() || fileChecksum == mCheckSum) emit downloadFinished(mDestinationFile.fileName()); + else { + qCritical() << "File cannot be downloaded : Bad checksum " << fileChecksum; + mDestinationFile.remove(); + emit downloadFailed(); + } + } +} + +void FileDownloader::handleError(QNetworkReply::NetworkError code) { + if (code != QNetworkReply::OperationCanceledError) + qWarning() + << QStringLiteral("Download of %1 failed: %2").arg(mUrl.toString()).arg(mNetworkReply->errorString()); + mDestinationFile.remove(); + + cleanDownloadEnd(); + + emit downloadFailed(); +} + +void FileDownloader::handleSslErrors(const QList &sslErrors) { +#if QT_CONFIG(ssl) + for (const QSslError &error : sslErrors) + qWarning() << QStringLiteral("SSL error: %1").arg(error.errorString()); +#else + Q_UNUSED(sslErrors); +#endif +} + +void FileDownloader::handleTimeout() { + if (mReadBytes == mTimeoutReadBytes) { + qWarning() << QStringLiteral("Download of %1 failed: timeout.").arg(mUrl.toString()); + mNetworkReply->abort(); + } else mTimeoutReadBytes = mReadBytes; +} + +void FileDownloader::handleDownloadProgress(qint64 readBytes, qint64 totalBytes) { + setReadBytes(readBytes); + setTotalBytes(totalBytes); +} + +// ----------------------------------------------------------------------------- + +QUrl FileDownloader::getUrl() const { + return mUrl; +} + +void FileDownloader::setUrl(const QUrl &url) { + if (mDownloading) { + qWarning() << QStringLiteral("Unable to set url, a file is downloading."); + return; + } + + if (mUrl != url) { + mUrl = url; + if (!QSslSocket::supportsSsl() && mUrl.scheme() == "https") { + qWarning() << "Https has been requested but SSL is not supported. Fallback to http. Install manually " + "OpenSSL libraries in your PATH."; + mUrl.setScheme("http"); + } + emit urlChanged(mUrl); + } +} + +QString FileDownloader::getDownloadFolder() const { + return mDownloadFolder; +} + +void FileDownloader::setDownloadFolder(const QString &downloadFolder) { + if (mDownloading) { + qWarning() << QStringLiteral("Unable to set download folder, a file is downloading."); + return; + } + + if (mDownloadFolder != downloadFolder) { + mDownloadFolder = downloadFolder; + emit downloadFolderChanged(mDownloadFolder); + } +} + +QString FileDownloader::getDestinationFileName() const { + return mDestinationFile.fileName(); +} + +void FileDownloader::setOverwriteFile(const bool &overwrite) { + mOverwriteFile = overwrite; +} + +QString +FileDownloader::synchronousDownload(const QUrl &url, const QString &destinationFolder, const bool &overwriteFile) { + QString filePath; + FileDownloader downloader; + if (url.isRelative()) qWarning() << "FileDownloader: The specified URL is not valid"; + else { + bool isOver = false; + bool *pIsOver = &isOver; + downloader.setUrl(url); + downloader.setOverwriteFile(overwriteFile); + downloader.setDownloadFolder(destinationFolder); + connect(&downloader, &FileDownloader::downloadFinished, [pIsOver]() mutable { *pIsOver = true; }); + connect(&downloader, &FileDownloader::downloadFailed, [pIsOver]() mutable { *pIsOver = true; }); + downloader.download(); + if (QTest::qWaitFor([&]() { return isOver; }, DefaultTimeout)) { + filePath = downloader.getDestinationFileName(); + if (!QFile::exists(filePath)) { + filePath = ""; + qWarning() << "FileDownloader: Cannot download the specified file"; + } + } + } + return filePath; +} + +QString FileDownloader::getChecksum() const { + return mCheckSum; +} + +void FileDownloader::setChecksum(const QString &code) { + if (mCheckSum != code) { + mCheckSum = code; + emit checksumChanged(); + } +} + +qint64 FileDownloader::getReadBytes() const { + return mReadBytes; +} + +void FileDownloader::setReadBytes(qint64 readBytes) { + if (mReadBytes != readBytes) { + mReadBytes = readBytes; + emit readBytesChanged(readBytes); + } +} + +qint64 FileDownloader::getTotalBytes() const { + return mTotalBytes; +} + +void FileDownloader::setTotalBytes(qint64 totalBytes) { + if (mTotalBytes != totalBytes) { + mTotalBytes = totalBytes; + emit totalBytesChanged(totalBytes); + } +} + +bool FileDownloader::getDownloading() const { + return mDownloading; +} + +void FileDownloader::setDownloading(bool downloading) { + if (mDownloading != downloading) { + mDownloading = downloading; + emit downloadingChanged(downloading); + } +} diff --git a/Linphone/tool/file/FileDownloader.hpp b/Linphone/tool/file/FileDownloader.hpp new file mode 100644 index 000000000..a4ccaa58a --- /dev/null +++ b/Linphone/tool/file/FileDownloader.hpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef FILE_DOWNLOADER_H_ +#define FILE_DOWNLOADER_H_ + +#include +#include +#include + +// ============================================================================= + +class QSslError; + +class FileDownloader : public QObject { + Q_OBJECT; + + // TODO: Add an error property to use in UI. + + Q_PROPERTY(QUrl url READ getUrl WRITE setUrl NOTIFY urlChanged); + Q_PROPERTY(QString downloadFolder READ getDownloadFolder WRITE setDownloadFolder NOTIFY downloadFolderChanged); + Q_PROPERTY(qint64 readBytes READ getReadBytes NOTIFY readBytesChanged); + Q_PROPERTY(qint64 totalBytes READ getTotalBytes NOTIFY totalBytesChanged); + Q_PROPERTY(bool downloading READ getDownloading NOTIFY downloadingChanged); + Q_PROPERTY(QString checksum READ getChecksum WRITE setChecksum NOTIFY checksumChanged); + +public: + FileDownloader(QObject *parent = Q_NULLPTR) : QObject(parent) { + // See: https://bugreports.qt.io/browse/QTBUG-57390 + mTimeout.setInterval(DefaultTimeout); + QObject::connect(&mTimeout, &QTimer::timeout, this, &FileDownloader::handleTimeout); + } + + ~FileDownloader() { + if (mNetworkReply) mNetworkReply->abort(); + } + + Q_INVOKABLE void download(); + Q_INVOKABLE bool remove(); + + QUrl getUrl() const; + void setUrl(const QUrl &url); + + QString getDownloadFolder() const; + void setDownloadFolder(const QString &downloadFolder); + + QString getDestinationFileName() const; + + void setOverwriteFile(const bool &overwrite); + static QString + synchronousDownload(const QUrl &url, + const QString &destinationFolder, + const bool &overwriteFile); // Return the filpath. Empty if nof file could be downloaded + + QString getChecksum() const; + void setChecksum(const QString &code); + +signals: + void urlChanged(const QUrl &url); + void downloadFolderChanged(const QString &downloadFolder); + void readBytesChanged(qint64 readBytes); + void totalBytesChanged(qint64 totalBytes); + void downloadingChanged(bool downloading); + void downloadFinished(const QString &filePath); + void downloadFailed(); + void checksumChanged(); + +private: + qint64 getReadBytes() const; + void setReadBytes(qint64 readBytes); + + qint64 getTotalBytes() const; + void setTotalBytes(qint64 totalBytes); + + bool getDownloading() const; + void setDownloading(bool downloading); + + void emitOutputError(); + + void cleanDownloadEnd(); + + void handleReadyData(); + void handleDownloadFinished(); + + void handleError(QNetworkReply::NetworkError code); + void handleSslErrors(const QList &errors); + void handleTimeout(); + void handleDownloadProgress(qint64 readBytes, qint64 totalBytes); + + QUrl mUrl; + QString mDownloadFolder; + QFile mDestinationFile; + QString mCheckSum; + + qint64 mReadBytes = 0; + qint64 mTotalBytes = 0; + bool mDownloading = false; + bool mOverwriteFile = false; + + QPointer mNetworkReply; + QNetworkAccessManager mManager; + + qint64 mTimeoutReadBytes; + QTimer mTimeout; + + static constexpr int DefaultTimeout = 5000; +}; + +#endif // FILE_DOWNLOADER_H_ diff --git a/Linphone/tool/file/FileExtractor.cpp b/Linphone/tool/file/FileExtractor.cpp new file mode 100644 index 000000000..3a022af83 --- /dev/null +++ b/Linphone/tool/file/FileExtractor.cpp @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#include +#include +#include +#include + +#include "FileDownloader.hpp" +#include "FileExtractor.hpp" +#include "core/path/Paths.hpp" +#include "tool/Constants.hpp" +#include "tool/Utils.hpp" + +// ============================================================================= + +using namespace std; + +FileExtractor::FileExtractor(QObject *parent) : QObject(parent) { +} + +FileExtractor::~FileExtractor() { +} + +void FileExtractor::extract() { + if (mExtracting) { + qWarning() << "Unable to extract file. Already extracting!"; + return; + } + setExtracting(true); + QFileInfo fileInfo(mFile); + if (!fileInfo.isReadable()) { + emitExtractFailed(-1); + return; + } + + mDestinationFile = QDir::cleanPath(mExtractFolder) + QDir::separator() + + (mExtractName.isEmpty() ? fileInfo.completeBaseName() : mExtractName); + if (QFile::exists(mDestinationFile) && !QFile::remove(mDestinationFile)) { + emitOutputError(); + return; + } + if (mTimer == nullptr) { + mTimer = new QTimer(this); + QObject::connect(mTimer, &QTimer::timeout, this, &FileExtractor::handleExtraction); + } +#ifdef WIN32 + // Test the presence of bzip2 in the system + QProcess process; + process.closeReadChannel(QProcess::StandardOutput); + process.closeReadChannel(QProcess::StandardError); + process.start("bzip2.exe", QStringList("--help")); + // int result = QProcess::execute("bzip2.exe", QStringList("--help")); + if (process.error() != QProcess::FailedToStart || + QProcess::execute(Paths::getToolsDirPath() + "\\bzip2.exe", QStringList()) != -2) { + mTimer->start(); + } else { // Download bzip2 + qWarning() << "bzip2 was not found. Downloading it."; + QTimer *timer = mTimer; + FileDownloader *fileDownloader = new FileDownloader(); + int downloadStep = 0; + fileDownloader->setUrl(QUrl(Constants::LinphoneBZip2_exe)); + fileDownloader->setDownloadFolder(Paths::getToolsDirPath()); + QObject::connect(fileDownloader, &FileDownloader::totalBytesChanged, this, &FileExtractor::setTotalBytes); + QObject::connect(fileDownloader, &FileDownloader::readBytesChanged, this, &FileExtractor::setReadBytes); + + QObject::connect(fileDownloader, &FileDownloader::downloadFinished, + [fileDownloader, timer, downloadStep, this]() mutable { + if (downloadStep++ == 0) { + fileDownloader->setUrl(QUrl(Constants::LinphoneBZip2_dll)); + fileDownloader->download(); + } else { + fileDownloader->deleteLater(); + QObject::disconnect(fileDownloader, &FileDownloader::totalBytesChanged, this, + &FileExtractor::setTotalBytes); + QObject::disconnect(fileDownloader, &FileDownloader::readBytesChanged, this, + &FileExtractor::setReadBytes); + timer->start(); + } + }); + + QObject::connect(fileDownloader, &FileDownloader::downloadFailed, [fileDownloader, this]() { + fileDownloader->deleteLater(); + emitExtractorFailed(); + }); + fileDownloader->download(); + } +#else + mTimer->start(); +#endif +} + +bool FileExtractor::remove() { + return QFile::exists(mDestinationFile) && QFile::remove(mDestinationFile); +} + +QString FileExtractor::getFile() const { + return mFile; +} + +void FileExtractor::setFile(const QString &file) { + if (mExtracting) { + qWarning() << QStringLiteral("Unable to set file, a file is extracting."); + return; + } + if (mFile != file) { + mFile = file; + emit fileChanged(mFile); + } +} + +QString FileExtractor::getExtractFolder() const { + return mExtractFolder; +} + +void FileExtractor::setExtractFolder(const QString &extractFolder) { + if (mExtracting) { + qWarning() << QStringLiteral("Unable to set extract folder, a file is extracting."); + return; + } + if (mExtractFolder != extractFolder) { + mExtractFolder = extractFolder; + emit extractFolderChanged(mExtractFolder); + } +} + +QString FileExtractor::getExtractName() const { + return mExtractName; +} + +void FileExtractor::setExtractName(const QString &extractName) { + if (mExtracting) { + qWarning() << QStringLiteral("Unable to set extract name, a file is extracting."); + return; + } + if (mExtractName != extractName) { + mExtractName = extractName; + emit extractNameChanged(mExtractName); + } +} + +bool FileExtractor::getExtracting() const { + return mExtracting; +} + +void FileExtractor::setExtracting(bool extracting) { + if (mExtracting != extracting) { + mExtracting = extracting; + emit extractingChanged(extracting); + } +} + +qint64 FileExtractor::getReadBytes() const { + return mReadBytes; +} + +void FileExtractor::setReadBytes(qint64 readBytes) { + mReadBytes = readBytes; + emit readBytesChanged(readBytes); +} + +qint64 FileExtractor::getTotalBytes() const { + return mTotalBytes; +} + +void FileExtractor::setTotalBytes(qint64 totalBytes) { + mTotalBytes = totalBytes; + emit totalBytesChanged(totalBytes); +} +void FileExtractor::clean() { + if (mTimer) { + mTimer->stop(); + mTimer->deleteLater(); + mTimer = nullptr; + } + setExtracting(false); +} + +void FileExtractor::emitExtractorFailed() { + qWarning() << QStringLiteral("Unable to extract file `%1`. bzip2 is unavailable, please install it.").arg(mFile); + clean(); + emit extractFailed(); +} +void FileExtractor::emitExtractFailed(int error) { + qWarning() << QStringLiteral("Unable to extract file with bzip2: `%1` (code: %2).").arg(mFile).arg(error); + clean(); + emit extractFailed(); +} + +void FileExtractor::emitExtractFinished() { + clean(); + emit extractFinished(); +} + +void FileExtractor::emitOutputError() { + qWarning() << QStringLiteral("Could not write into `%1`.").arg(mDestinationFile); + clean(); + emit extractFailed(); +} + +void FileExtractor::handleExtraction() { + QString tempDestination = mDestinationFile + "." + QFileInfo(mFile).suffix(); + QStringList args; + args.push_back("-dq"); + args.push_back(tempDestination); + QFile::copy(mFile, tempDestination); +#ifdef WIN32 + int result = QProcess::execute("bzip2.exe", args); + if (result == -2) result = QProcess::execute(Paths::getToolsDirPath() + "\\bzip2.exe", args); +#else + int result = QProcess::execute("bzip2", args); +#endif + if (QFile::exists(tempDestination)) QFile::remove(tempDestination); + if (result == 0) { + setReadBytes(getTotalBytes()); + emitExtractFinished(); + } else if (result > 0) emitExtractFailed(result); + else if (result == -2) emitExtractorFailed(); + else emitOutputError(); +} diff --git a/Linphone/tool/file/FileExtractor.hpp b/Linphone/tool/file/FileExtractor.hpp new file mode 100644 index 000000000..0087d94bb --- /dev/null +++ b/Linphone/tool/file/FileExtractor.hpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef FILE_EXTRACTOR_H_ +#define FILE_EXTRACTOR_H_ + +#include + +// ============================================================================= + +class QTimer; + +// Supports only bzip file. +class FileExtractor : public QObject { + + Q_OBJECT; + + // TODO: Add an error property to use in UI. + + Q_PROPERTY(QString file READ getFile WRITE setFile NOTIFY fileChanged); + Q_PROPERTY(QString extractFolder READ getExtractFolder WRITE setExtractFolder NOTIFY extractFolderChanged); + Q_PROPERTY(QString extractName READ getExtractName WRITE setExtractName NOTIFY extractNameChanged); + Q_PROPERTY(bool extracting READ getExtracting NOTIFY extractingChanged); + Q_PROPERTY(qint64 readBytes READ getReadBytes NOTIFY readBytesChanged); + Q_PROPERTY(qint64 totalBytes READ getTotalBytes NOTIFY totalBytesChanged); + +public: + FileExtractor(QObject *parent = nullptr); + ~FileExtractor(); + + Q_INVOKABLE void extract(); + Q_INVOKABLE bool remove(); + + QString getFile() const; + void setFile(const QString &file); + + QString getExtractFolder() const; + void setExtractFolder(const QString &extractFolder); + + QString getExtractName() const; + void setExtractName(const QString &extractName); + +signals: + void fileChanged(const QString &file); + + void extractFolderChanged(const QString &extractFolder); + void extractNameChanged(const QString &extractName); + + void readBytesChanged(qint64 readBytes); + void totalBytesChanged(qint64 totalBytes); + + void extractingChanged(bool extracting); + void extractFinished(); + void extractFailed(); + +private: + qint64 getReadBytes() const; + void setReadBytes(qint64 readBytes); + + qint64 getTotalBytes() const; + void setTotalBytes(qint64 totalBytes); + + bool getExtracting() const; + void setExtracting(bool extracting); + + void clean(); + + void emitExtractFinished(); + void emitExtractorFailed(); // Used when bzip2 cannot be used + void emitExtractFailed(int error); + void emitOutputError(); + + void handleExtraction(); + + QString mFile; + QString mExtractFolder; + QString mExtractName; + QString mDestinationFile; + + bool mExtracting = false; + qint64 mReadBytes = 0; + qint64 mTotalBytes = 0; + + QTimer *mTimer = nullptr; +}; + +#endif // FILE_EXTRACTOR_H_ diff --git a/Linphone/view/Control/Button/Settings/SwitchSetting.qml b/Linphone/view/Control/Button/Settings/SwitchSetting.qml index 014f679c0..5cffef89a 100644 --- a/Linphone/view/Control/Button/Settings/SwitchSetting.qml +++ b/Linphone/view/Control/Button/Settings/SwitchSetting.qml @@ -12,6 +12,12 @@ RowLayout { property bool enabled: true spacing : 20 * DefaultStyle.dp Layout.minimumHeight: 32 * DefaultStyle.dp + signal checkedChanged(bool checked) + + function setChecked(value) { + switchButton.checked = value + } + ColumnLayout { Text { text: titleText @@ -34,9 +40,8 @@ RowLayout { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter checked: propertyOwner[mainItem.propertyName] enabled: mainItem.enabled - onToggled: { - binding.when = true - } + onCheckedChanged: mainItem.checkedChanged(checked) + onToggled: binding.when = true } Binding { id: binding diff --git a/Linphone/view/Control/Tool/Helper/utils.js b/Linphone/view/Control/Tool/Helper/utils.js index 520f54ca5..0ab1c8048 100644 --- a/Linphone/view/Control/Tool/Helper/utils.js +++ b/Linphone/view/Control/Tool/Helper/utils.js @@ -684,34 +684,33 @@ function computeAvatarSize (container, maxSize, ratio) { // ----------------------------------------------------------------------------- -function openCodecOnlineInstallerDialog (window, codecInfo, cb) { - var VideoCodecsModel = Linphone.VideoCodecsModel - window.attachVirtualWindow(buildCommonDialogUri('ConfirmDialog'), { - descriptionText: qsTr('downloadCodecDescription') - .replace('%1', codecInfo.mime) - .replace('%2', codecInfo.encoderDescription) - }, function (status) { - if (status) { - window.attachVirtualWindow(buildLinphoneDialogUri('OnlineInstallerDialog'), { - downloadUrl: codecInfo.downloadUrl, - extract: true, - installFolder: VideoCodecsModel.codecsFolder, - installName: codecInfo.installName, - mime: codecInfo.mime, - checksum: codecInfo.checksum - }, function (status) { - if (status) { - VideoCodecsModel.reload() - } - if (cb) { - cb(window) - } - }) - } - else if (cb) { - cb(window) - } - }) +function openCodecOnlineInstallerDialog (mainWindow, coreObject, cancelCallBack, successCallBack) { + mainWindow.showConfirmationLambdaPopup("", + qsTr("Installation de codec"), + qsTr("Télécharger le codec ") + capitalizeFirstLetter(coreObject.mimeType) + " ("+coreObject.encoderDescription+")"+" ?", + function (confirmed) { + if (confirmed) { + coreObject.success.connect(function() { + if (successCallBack) + successCallBack() + mainWindow.closeLoadingPopup() + mainWindow.showInformationPopup(qsTr("Succès"), qsTr("Le codec a été téléchargé avec succès."), true) + }) + coreObject.extractError.connect(function() { + mainWindow.closeLoadingPopup() + mainWindow.showInformationPopup(qsTr("Erreur"), qsTr("Le codec n'a pas pu être sauvegardé."), true) + }) + coreObject.downloadError.connect(function() { + mainWindow.closeLoadingPopup() + mainWindow.showInformationPopup(qsTr("Erreur"), qsTr("Le codec n'a pas pu être téléchargé."), true) + }) + mainWindow.showLoadingPopup(qsTr("Téléchargement en cours ...")) + coreObject.downloadAndExtract() + } else + if (cancelCallBack) + cancelCallBack() + } + ) } function printObject(o) { diff --git a/Linphone/view/Page/Layout/Settings/AdvancedSettingsLayout.qml b/Linphone/view/Page/Layout/Settings/AdvancedSettingsLayout.qml index 6a41a8cdc..1869acbf8 100644 --- a/Linphone/view/Page/Layout/Settings/AdvancedSettingsLayout.qml +++ b/Linphone/view/Page/Layout/Settings/AdvancedSettingsLayout.qml @@ -150,16 +150,43 @@ AbstractSettingsLayout { Layout.leftMargin: 64 * DefaultStyle.dp Repeater { model: PayloadTypeProxy { + id: videoPayloadTypeProxy family: PayloadTypeCore.Video } SwitchSetting { Layout.fillWidth: true titleText: Utils.capitalizeFirstLetter(modelData.core.mimeType) - subTitleText: modelData.core.recvFmtp + subTitleText: modelData.core.encoderDescription propertyName: "enabled" propertyOwner: modelData.core } } + Repeater { + model: PayloadTypeProxy { + id: downloadableVideoPayloadTypeProxy + family: PayloadTypeCore.Video + downloadable: true + } + SwitchSetting { + Layout.fillWidth: true + titleText: Utils.capitalizeFirstLetter(modelData.core.mimeType) + subTitleText: modelData.core.encoderDescription + onCheckChanged: function(checked) { + if (checked) + UtilsCpp.getMainWindow().showConfirmationLambdaPopup("", + qsTr("Installation"), + qsTr("Télécharger le codec ") + Utils.capitalizeFirstLetter(modelData.core.mimeType) + " ("+modelData.core.encoderDescription+")"+" ?", + function (confirmed) { + if (confirmed) { + UtilsCpp.getMainWindow().showLoadingPopup(qsTr("Téléchargement en cours ...")) + modelData.core.downloadAndExtract() + } else + setChecked(false) + } + ) + } + } + } } } Rectangle { diff --git a/Linphone/view/Page/Window/Main/MainWindow.qml b/Linphone/view/Page/Window/Main/MainWindow.qml index c35123834..5a7cb83b3 100644 --- a/Linphone/view/Page/Window/Main/MainWindow.qml +++ b/Linphone/view/Page/Window/Main/MainWindow.qml @@ -5,6 +5,7 @@ import QtQuick.Controls.Basic import Linphone import UtilsCpp import SettingsCpp +import 'qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js' as Utils AbstractWindow { id: mainWindow @@ -140,6 +141,7 @@ AbstractWindow { onGoToRegister: mainWindowStackView.replace(registerPage) onConnectionSucceed: { openMainPage() + proposeH264CodecsDownload() } } } @@ -156,6 +158,7 @@ AbstractWindow { onConnectionSucceed: { openMainPage() + proposeH264CodecsDownload() } } } @@ -225,4 +228,25 @@ AbstractWindow { // StackView.onActivated: connectionSecured(0) // TODO : connect to cpp part when ready } } + + // H264 Cisco codec download + PayloadTypeProxy { + id: downloadableVideoPayloadTypeProxy + family: PayloadTypeCore.Video + downloadable: true + } + Repeater { + id: codecDownloader + model: null + Item { + Component.onCompleted: { + if (modelData.core.mimeType == "H264") + Utils.openCodecOnlineInstallerDialog(mainWindow, modelData.core) + } + } + } + function proposeH264CodecsDownload() { + codecDownloader.model = downloadableVideoPayloadTypeProxy + } + }