diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 2b557d413..d8159150b 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -74,14 +75,21 @@ #include "tool/providers/AvatarProvider.hpp" #include "tool/providers/ImageProvider.hpp" #include "tool/providers/ScreenProvider.hpp" +#include "tool/request/RequestDialog.hpp" #include "tool/thread/Thread.hpp" DEFINE_ABSTRACT_OBJECT(App) +#ifdef Q_OS_LINUX +const QString ApplicationsDirectory(QDir::homePath().append(QStringLiteral("/.local/share/applications/"))); +const QString IconsDirectory(QDir::homePath().append(QStringLiteral("/.local/share/icons/hicolor/scalable/apps/"))); +#endif + App::App(int &argc, char *argv[]) : SingleApplication(argc, argv, true, Mode::User | Mode::ExcludeAppPath | Mode::ExcludeAppVersion) { // Do not use APPLICATION_NAME here. // The EXECUTABLE_NAME will be used in qt standard paths. It's our goal. + QCoreApplication::setApplicationName(EXECUTABLE_NAME); QApplication::setOrganizationDomain(EXECUTABLE_NAME); QCoreApplication::setApplicationVersion(APPLICATION_SEMVER); @@ -101,6 +109,7 @@ App::App(int &argc, char *argv[]) //------------------- mLinphoneThread = new Thread(this); + init(); lInfo() << QStringLiteral("Starting application " APPLICATION_NAME " (bin: " EXECUTABLE_NAME "). Version:%1 Os:%2 Qt:%3") @@ -126,6 +135,50 @@ void App::setSelf(QSharedPointer(me)) { lDebug() << "App : call created" << callGui; }); }); + mCoreModelConnection->makeConnectToModel(&CoreModel::requestRestart, [this]() { + mCoreModelConnection->invokeToCore([this]() { + lInfo() << log().arg("Restarting"); + restart(); + }); + }); + mCoreModelConnection->makeConnectToModel(&CoreModel::requestFetchConfig, [this](QString path) { + mCoreModelConnection->invokeToCore([this, path]() { + auto callback = [this, path]() { + RequestDialog *obj = new RequestDialog( + tr("Voulez-vous télécharger et appliquer la configuration depuis cette adresse ?"), path); + connect(obj, &RequestDialog::result, this, [this, obj, path](int result) { + if (result == 1) { + mCoreModelConnection->invokeToModel( + [this, path]() { CoreModel::getInstance()->setFetchConfig(path); }); + } else if (result == 0) { + mCoreModelConnection->invokeToModel([]() { CliModel::getInstance()->resetProcesses(); }); + } + obj->deleteLater(); + }); + QMetaObject::invokeMethod(getMainWindow(), "showConfirmationPopup", QVariant::fromValue(obj)); + }; + if (!getMainWindow()) { // Delay + connect(this, &App::mainWindowChanged, this, callback, Qt::SingleShotConnection); + } else { + callback(); + } + }); + }); + //--------------------------------------------------------------------------------------------- + mCliModelConnection = QSharedPointer>( + new SafeConnection(me, CliModel::getInstance()), &QObject::deleteLater); + mCliModelConnection->makeConnectToCore(&App::receivedMessage, [this](int, const QByteArray &byteArray) { + QString command(byteArray); + if (command.isEmpty()) + mCliModelConnection->invokeToModel([command]() { CliModel::getInstance()->runProcess(); }); + else { + qInfo() << QStringLiteral("Received command from other application: `%1`.").arg(command); + mCliModelConnection->invokeToModel([command]() { CliModel::getInstance()->executeCommand(command); }); + } + }); + mCliModelConnection->makeConnectToModel(&CliModel::showMainWindow, [this]() { + mCliModelConnection->invokeToCore([this]() { Utils::smartShowWindow(getMainWindow()); }); + }); } App *App::getInstance() { @@ -140,12 +193,33 @@ Notifier *App::getNotifier() const { //----------------------------------------------------------- void App::init() { + // Console Commands + createCommandParser(); + mParser->parse(this->arguments()); + // TODO : Update languages for command translations. + + createCommandParser(); // Recreate parser in order to use translations from config. + mParser->process(*this); + + if (mParser->isSet("verbose")) QtLogger::enableVerbose(true); + if (mParser->isSet("qt-logs-only")) QtLogger::enableQtOnly(true); + + if (!mLinphoneThread->isRunning()) { + lDebug() << log().arg("Starting Thread"); + mLinphoneThread->start(); + } + setQuitOnLastWindowClosed(true); // TODO: use settings to set it + + lInfo() << log().arg("Display server : %1").arg(platformName()); +} + +void App::initCore() { // Core. Manage the logger so it must be instantiate at first. - auto coreModel = CoreModel::create("", mLinphoneThread); - connect( - mLinphoneThread, &QThread::started, coreModel.get(), - [this, coreModel]() mutable { - coreModel->start(); + CoreModel::create("", mLinphoneThread); + QMetaObject::invokeMethod( + mLinphoneThread->getThreadId(), + [this]() mutable { + CoreModel::getInstance()->start(); auto settings = Settings::create(); QMetaObject::invokeMethod(App::getInstance()->thread(), [this, settings]() mutable { // QML @@ -161,29 +235,27 @@ void App::init() { mEngine->addImportPath(":/"); mEngine->rootContext()->setContextProperty("applicationDirPath", QGuiApplication::applicationDirPath()); #ifdef APPLICATION_VENDOR - mEngine->rootContext()->setContextProperty("applicationVendor", APPLICATION_VENDOR); + mEngine->rootContext()->setContextProperty("applicationVendor", APPLICATION_VENDOR); #else mEngine->rootContext()->setContextProperty("applicationVendor", ""); #endif #ifdef APPLICATION_LICENCE - mEngine->rootContext()->setContextProperty("applicationLicence", APPLICATION_LICENCE); + mEngine->rootContext()->setContextProperty("applicationLicence", APPLICATION_LICENCE); #else mEngine->rootContext()->setContextProperty("applicationLicence", ""); #endif #ifdef APPLICATION_LICENCE_URL - mEngine->rootContext()->setContextProperty("applicationLicenceUrl", APPLICATION_LICENCE_URL); + mEngine->rootContext()->setContextProperty("applicationLicenceUrl", APPLICATION_LICENCE_URL); #else mEngine->rootContext()->setContextProperty("applicationLicenceUrl", ""); #endif #ifdef COPYRIGHT_RANGE_DATE - mEngine->rootContext()->setContextProperty("copyrightRangeDate", COPYRIGHT_RANGE_DATE); + mEngine->rootContext()->setContextProperty("copyrightRangeDate", COPYRIGHT_RANGE_DATE); #else mEngine->rootContext()->setContextProperty("copyrightRangeDate", ""); #endif - mEngine->rootContext()->setContextProperty("applicationName", APPLICATION_NAME); - mEngine->rootContext()->setContextProperty("executableName", EXECUTABLE_NAME); - - + mEngine->rootContext()->setContextProperty("applicationName", APPLICATION_NAME); + mEngine->rootContext()->setContextProperty("executableName", EXECUTABLE_NAME); initCppInterfaces(); mEngine->addImageProvider(ImageProvider::ProviderId, new ImageProvider()); mEngine->addImageProvider(AvatarProvider::ProviderId, new AvatarProvider()); @@ -205,7 +277,7 @@ void App::init() { lCritical() << log().arg("Main.qml couldn't be load. The app will exit"); exit(-1); } - mMainWindow = qobject_cast(obj); + setMainWindow(qobject_cast(obj)); QMetaObject::invokeMethod(obj, "initStackViewItem"); Q_ASSERT(mMainWindow); } @@ -215,27 +287,7 @@ void App::init() { }); // coreModel.reset(); }, - Qt::SingleShotConnection); - // Console Commands - createCommandParser(); - mParser->parse(this->arguments()); - // TODO : Update languages for command translations. - - createCommandParser(); // Recreate parser in order to use translations from config. - mParser->process(*this); - - if (mParser->isSet("verbose")) QtLogger::enableVerbose(true); - if (mParser->isSet("qt-logs-only")) QtLogger::enableQtOnly(true); - - if (!mLinphoneThread->isRunning()) { - lDebug() << log().arg("Starting Thread"); - mLinphoneThread->start(); - } - setQuitOnLastWindowClosed(true); // TODO: use settings to set it - - lInfo() << log().arg("Display server : %1").arg(platformName()); - - // mEngine->load(u"qrc:/Linphone/view/Prototype/CameraPrototype.qml"_qs); + Qt::BlockingQueuedConnection); } void App::initCppInterfaces() { @@ -294,6 +346,9 @@ void App::initCppInterfaces() { QLatin1String("Uncreatable")); qmlRegisterType(Constants::MainQmlUri, 1, 0, "VideoSourceDescriptorGui"); + qmlRegisterUncreatableType(Constants::MainQmlUri, 1, 0, "RequestDialog", + QLatin1String("Uncreatable")); + LinphoneEnums::registerMetaTypes(); } @@ -309,11 +364,29 @@ void App::clean() { // mSettings.reset(); // } qApp->processEvents(QEventLoop::AllEvents, 500); - mLinphoneThread->exit(); - mLinphoneThread->wait(); - delete mLinphoneThread; + if (mLinphoneThread) { + mLinphoneThread->exit(); + mLinphoneThread->wait(); + delete mLinphoneThread; + } +} +void App::restart() { + mCoreModelConnection->invokeToModel([this]() { + CoreModel::getInstance()->getCore()->stop(); + mCoreModelConnection->invokeToCore([this]() { + mEngine->deleteLater(); + if (mSettings) mSettings.reset(); + initCore(); + // Retrieve self from current Core/Model connection and reset Qt connections. + auto oldConnection = mCoreModelConnection; + oldConnection->mCore.lock(); + auto me = oldConnection->mCore.mQData; + setSelf(me); + oldConnection->mCore.unlock(); + exit((int)StatusCode::gRestartCode); + }); + }); } - void App::createCommandParser() { if (!mParser) delete mParser; @@ -336,6 +409,28 @@ void App::createCommandParser() { {"qt-logs-only", tr("commandLineOptionQtLogsOnly")}, }); } +// Should be call only at first start +void App::sendCommand() { + auto arguments = mParser->positionalArguments(); + static bool firstStart = true; // We can't erase positional arguments. So we get them on each restart. + if (firstStart && arguments.size() > 0) { + firstStart = false; + if (isSecondary()) { // Send to primary + lDebug() << "Sending " << arguments; + for (auto i : arguments) { + sendMessage(i.toLocal8Bit(), -1); + } + } else { // Execute + lDebug() << "Executing " << arguments; + for (auto i : arguments) { + QString command(i); + receivedMessage(0, i.toLocal8Bit()); + } + } + } else if (isPrimary()) { // Run waiting process + receivedMessage(0, ""); + } +} bool App::notify(QObject *receiver, QEvent *event) { bool done = true; @@ -401,6 +496,122 @@ void App::closeCallsWindow() { } } -QQuickWindow *App::getMainWindow() { +QQuickWindow *App::getMainWindow() const { return mMainWindow; } + +void App::setMainWindow(QQuickWindow *data) { + if (mMainWindow != data) { + mMainWindow = data; + emit mainWindowChanged(); + } +} + +#ifdef Q_OS_LINUX +QString App::getApplicationPath() const { + const QString binPath(QCoreApplication::applicationFilePath()); + + // Check if installation is done via Flatpak, AppImage, or classic package + // in order to rewrite a correct exec path for autostart + QString exec; + qDebug() << "binpath=" << binPath; + if (binPath.startsWith("/app")) { // Flatpak + exec = QStringLiteral("flatpak run " APPLICATION_ID); + qDebug() << "exec path autostart set flatpak=" << exec; + } else if (binPath.startsWith("/tmp/.mount")) { // Appimage + exec = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + qDebug() << "exec path autostart set appimage=" << exec; + } else { // classic package + exec = binPath; + qDebug() << "exec path autostart set classic package=" << exec; + } + return exec; +} + +void App::exportDesktopFile() { + QDir dir(ApplicationsDirectory); + if (!dir.exists() && !dir.mkpath(ApplicationsDirectory)) { + qWarning() << QStringLiteral("Unable to build applications dir path: `%1`.").arg(ApplicationsDirectory); + return; + } + + const QString confPath(ApplicationsDirectory + EXECUTABLE_NAME ".desktop"); + if (generateDesktopFile(confPath, true, false)) generateDesktopFile(confPath, false, false); +} + +bool App::generateDesktopFile(const QString &confPath, bool remove, bool openInBackground) { + qInfo() << QStringLiteral("Updating `%1`...").arg(confPath); + QFile file(confPath); + + if (remove) { + if (file.exists() && !file.remove()) { + qWarning() << QLatin1String("Unable to remove autostart file: `" EXECUTABLE_NAME ".desktop`."); + return false; + } + return true; + } + + if (!file.open(QFile::WriteOnly)) { + qWarning() << "Unable to open autostart file: `" EXECUTABLE_NAME ".desktop`."; + return false; + } + + QString exec = getApplicationPath(); + + QDir dir; + QString iconPath; + bool haveIcon = false; + if (!dir.mkpath(IconsDirectory)) // Scalable icons folder may be created + qWarning() << "Cannot create scalable icon path at " << IconsDirectory; + else { + iconPath = IconsDirectory + EXECUTABLE_NAME + ".svg"; + QFile icon(Constants::WindowIconPath); + if (!QFile(iconPath).exists()) { // Keep old icon but copy if it doesn't exist + haveIcon = icon.copy(iconPath); + if (!haveIcon) qWarning() << "Couldn't copy icon svg into " << iconPath; + else { // Update permissions + QFile icon(iconPath); + icon.setPermissions(icon.permissions() | QFileDevice::WriteOwner); + } + } else { + qInfo() << "Icon already exists in " << IconsDirectory << ". It is not replaced."; + haveIcon = true; + } + } + + QTextStream(&file) << QString("[Desktop Entry]\n" + "Name=" APPLICATION_NAME "\n" + "GenericName=SIP Phone\n" + "Comment=" APPLICATION_DESCRIPTION "\n" + "Type=Application\n") + << (openInBackground ? "Exec=" + exec + " --iconified %u\n" : "Exec=" + exec + " %u\n") + << (haveIcon ? "Icon=" + iconPath + "\n" : "Icon=" EXECUTABLE_NAME "\n") + << "Terminal=false\n" + "Categories=Network;Telephony;\n" + "MimeType=x-scheme-handler/sip-" EXECUTABLE_NAME ";x-scheme-handler/sips-" EXECUTABLE_NAME + ";x-scheme-handler/" EXECUTABLE_NAME "-sip;x-scheme-handler/" EXECUTABLE_NAME + "-sips;x-scheme-handler/sip;x-scheme-handler/sips;x-scheme-handler/tel;x-scheme-handler/" + "callto;x-scheme-handler/" EXECUTABLE_NAME "-config;\n" + "X-PulseAudio-Properties=media.role=phone\n"; + + return true; +} +#elif defined(Q_OS_MACOS) +// On MAC, URI handlers call the application with no arguments and pass them in event loop. +bool App::event(QEvent *event) { + if (event->type() == QEvent::FileOpen) { + const QString url = static_cast(event)->url().toString(); + if (isSecondary()) { + sendMessage(url.toLocal8Bit(), -1); + ::exit(EXIT_SUCCESS); + } + receivedMessage(0, url.toLocal8Bit()); + } else if (event->type() == QEvent::ApplicationStateChange) { + auto state = static_cast(event); + if (state->applicationState() == Qt::ApplicationActive) Utils::smartShowWindow(getMainWindow()); + } + + return SingleApplication::event(event); +} + +#endif diff --git a/Linphone/core/App.hpp b/Linphone/core/App.hpp index 204eb6652..b6bd4557f 100644 --- a/Linphone/core/App.hpp +++ b/Linphone/core/App.hpp @@ -24,6 +24,7 @@ #include "core/setting/SettingsCore.hpp" #include "core/singleapplication/singleapplication.h" +#include "model/cli/CliModel.hpp" #include "model/core/CoreModel.hpp" #include "tool/AbstractObject.hpp" @@ -33,6 +34,7 @@ class Notifier; class QQuickWindow; class App : public SingleApplication, public AbstractObject { + Q_OBJECT public: App(int &argc, char *argv[]); ~App(); @@ -98,21 +100,38 @@ public: void clean(); void init(); + void initCore(); void initCppInterfaces(); + void restart(); void onLoggerInitialized(); + void sendCommand(); QQuickWindow *getCallsWindow(QVariant callGui = QVariant()); void setCallsWindowProperty(const char *id, QVariant property); void closeCallsWindow(); - QQuickWindow *getMainWindow(); + QQuickWindow *getMainWindow() const; + void setMainWindow(QQuickWindow *data); + +#ifdef Q_OS_LINUX + Q_INVOKABLE void exportDesktopFile(); + + QString getApplicationPath() const; + bool generateDesktopFile(const QString &confPath, bool remove, bool openInBackground); +#elif defined(Q_OS_MACOS) + bool event(QEvent *event) override; +#endif QQmlApplicationEngine *mEngine = nullptr; bool notify(QObject *receiver, QEvent *event) override; enum class StatusCode { gRestartCode = 1000, gDeleteDataCode = 1001 }; +signals: + void mainWindowChanged(); + // void executeCommand(QString command); + private: void createCommandParser(); @@ -123,6 +142,7 @@ private: QQuickWindow *mCallsWindow = nullptr; QSharedPointer mSettings; QSharedPointer> mCoreModelConnection; + QSharedPointer> mCliModelConnection; DECLARE_ABSTRACT_OBJECT }; diff --git a/Linphone/core/singleapplication/singleapplication_p.cpp b/Linphone/core/singleapplication/singleapplication_p.cpp index 03d6e7bb5..cb27b384a 100644 --- a/Linphone/core/singleapplication/singleapplication_p.cpp +++ b/Linphone/core/singleapplication/singleapplication_p.cpp @@ -296,7 +296,7 @@ bool SingleApplicationPrivate::writeConfirmedFrame(int msecs, const QByteArray & socket->write(msg); socket->flush(); - bool result = socket->waitForReadyRead(msecs); // await ack byte + bool result = socket->waitForReadyRead( msecs < 0 ? -1 : msecs); // await ack byte if (result) { socket->read(1); return true; diff --git a/Linphone/main.cpp b/Linphone/main.cpp index f20aaf651..2622b256a 100644 --- a/Linphone/main.cpp +++ b/Linphone/main.cpp @@ -11,6 +11,11 @@ #include #endif +#ifdef _WIN32 +#include +FILE *gStream = NULL; +#endif + #if QT_VERSION < QT_VERSION_CHECK(5, 15, 10) // From 5.15.2 to 5.15.10, sometimes, Accessibility freeze the application : Deactivate handlers. #define ACCESSBILITY_WORKAROUND @@ -22,14 +27,30 @@ void DummyRootObjectHandler(QObject *) { } #endif +void cleanStream() { +#ifdef _WIN32 + if (gStream) { + fflush(stdout); + fflush(stderr); + fclose(gStream); + } +#endif +} + int main(int argc, char *argv[]) { +#if defined _WIN32 + // log in console only if launched from console + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + freopen_s(&gStream, "CONOUT$", "w", stdout); + freopen_s(&gStream, "CONOUT$", "w", stderr); + } +#endif // Useful to share camera on Fullscreen (other context) or multiscreens QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); // Disable QML cache. Avoid malformed cache. qputenv("QML_DISABLE_DISK_CACHE", "true"); auto app = QSharedPointer::create(argc, argv); - app->setSelf(app); QTranslator translator; const QStringList uiLanguages = QLocale::system().uiLanguages(); @@ -46,8 +67,20 @@ int main(int argc, char *argv[]) { QAccessible::installRootObjectHandler(DummyRootObjectHandler); #endif + if (app->isSecondary()) { + app->sendCommand(); + qInfo() << QStringLiteral("Running secondary app success. Kill it now."); + app->clean(); + cleanStream(); + return EXIT_SUCCESS; + } else { + app->initCore(); + app->setSelf(app); + } + int result = 0; do { + app->sendCommand(); result = app->exec(); } while (result == (int)App::StatusCode::gRestartCode); qWarning() << "[Main] Exiting app with the code : " << result; diff --git a/Linphone/model/CMakeLists.txt b/Linphone/model/CMakeLists.txt index e58ff6ed3..cfb08ff55 100644 --- a/Linphone/model/CMakeLists.txt +++ b/Linphone/model/CMakeLists.txt @@ -13,6 +13,7 @@ list(APPEND _LINPHONEAPP_SOURCES model/participant/ParticipantDeviceModel.cpp model/participant/ParticipantModel.cpp + model/cli/CliModel.cpp model/core/CoreModel.cpp model/friend/FriendModel.cpp diff --git a/Linphone/model/cli/CliModel.cpp b/Linphone/model/cli/CliModel.cpp new file mode 100644 index 000000000..1f7844aff --- /dev/null +++ b/Linphone/model/cli/CliModel.cpp @@ -0,0 +1,405 @@ +/* + * 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 "CliModel.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "model/core/CoreModel.hpp" +#include "model/tool/ToolModel.hpp" + +// ============================================================================= +DEFINE_ABSTRACT_OBJECT(CliModel) + +std::shared_ptr CliModel::gCliModel; +QMap CliModel::mCommands{ + createCommand("show", QT_TR_NOOP("showFunctionDescription"), &CliModel::cliShow, {}, true), + createCommand("fetch-config", QT_TR_NOOP("fetchConfigFunctionDescription"), &CliModel::cliFetchConfig, {}, true), + createCommand("call", QT_TR_NOOP("callFunctionDescription"), &CliModel::cliCall, {{"sip-address", {}}}, true), + /* + createCommand("initiate-conference", QT_TR_NOOP("initiateConferenceFunctionDescription"), cliInitiateConference, { + { "sip-address", {} }, { "conference-id", {} } + }), + createCommand("join-conference", QT_TR_NOOP("joinConferenceFunctionDescription"), cliJoinConference, { + { "sip-address", {} }, { "conference-id", {} }, { "display-name", {} } + }), + createCommand("join-conference-as", QT_TR_NOOP("joinConferenceAsFunctionDescription"), cliJoinConferenceAs, { + { "sip-address", {} }, { "conference-id", {} }, { "guest-sip-address", {} } + }), + createCommand("bye", QT_TR_NOOP("byeFunctionDescription"), cliBye, QHash(), true), + createCommand("accept", QT_TR_NOOP("acceptFunctionDescription"), cliAccept, QHash(), true), + createCommand("decline", QT_TR_NOOP("declineFunctionDescription"), cliDecline, QHash(), true), + */ +}; + +std::pair CliModel::createCommand(const QString &functionName, + const char *functionDescription, + Function function, + const QHash &argsScheme, + const bool &genericArguments) { + return {functionName.toLower(), + CliModel::Command(functionName.toLower(), functionDescription, function, argsScheme, genericArguments)}; +} + +CliModel::CliModel(QObject *parent) : QObject(parent) { + moveToThread(CoreModel::getInstance()->thread()); +} + +CliModel::~CliModel() { +} + +std::shared_ptr CliModel::create(QObject *parent) { + auto model = std::make_shared(parent); + // model->setSelf(model); + return model; +} + +std::shared_ptr CliModel::getInstance() { + if (!gCliModel) gCliModel = CliModel::create(nullptr); + return gCliModel; +} + +// FIXME: Do not accept args without value like: cmd toto. +// In the future `toto` could be a boolean argument. +QRegularExpression + CliModel::mRegExpArgs("(?:(?:([\\w-]+)\\s*)=\\s*(?:\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"|([^\\s]+)\\s*))"); +QRegularExpression CliModel::mRegExpFunctionName("^\\s*([a-z-]+)\\s*"); + +QString CliModel::parseFunctionName(const QString &command, bool isOptional) { + QRegularExpressionMatch match = mRegExpFunctionName.match(command.toLower()); + // mRegExpFunctionName.indexIn(command.toLower()); + // if (mRegExpFunctionName.pos(1) == -1) { + if (!match.hasMatch()) { + if (!isOptional) qWarning() << QStringLiteral("Unable to parse function name of command: `%1`.").arg(command); + return QString(""); + } + + // const QStringList texts = mRegExpFunctionName.capturedTexts(); + const QStringList texts = match.capturedTexts(); + + const QString functionName = texts[1]; + if (!mCommands.contains(functionName)) { + if (!isOptional) qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName); + return QString(""); + } + + return functionName; +} + +QHash CliModel::parseArgs(const QString &command) { + QHash args; + int pos = 0; + QRegularExpressionMatchIterator it = mRegExpArgs.globalMatch(command); + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + if (match.hasMatch()) { + args[match.captured(1)] = (match.captured(2).isEmpty() ? match.captured(3) : match.captured(2)); + } + } + return args; +} + +void CliModel::cliShow(QHash args) { + emit showMainWindow(); +} + +void CliModel::cliFetchConfig(QHash args) { + if (args.contains("fetch-config")) { + if (CoreModel::getInstance()->getCore()->getGlobalState() != linphone::GlobalState::On) + connect( + CoreModel::getInstance().get(), &CoreModel::globalStateChanged, this, + [this, args]() { cliFetchConfig(args); }, Qt::SingleShotConnection); + else CoreModel::getInstance()->useFetchConfig(args["fetch-config"]); + } +} + +void CliModel::cliCall(QHash args) { + if (args.contains("sip-address")) { + if (!CoreModel::getInstance()->getCore() || + CoreModel::getInstance()->getCore()->getGlobalState() != linphone::GlobalState::On) + connect( + CoreModel::getInstance().get(), &CoreModel::globalStateChanged, this, [this, args]() { cliCall(args); }, + Qt::SingleShotConnection); + else ToolModel::createCall(args["sip-address"]); + } +} + +/* +QString CoreModel::getFetchConfig(QCommandLineParser *parser) { + QString filePath = parser->value("fetch-config"); + bool error = false; + filePath = getFetchConfig(filePath, &error); + if (error) { + qWarning() << "Remote provisionning cannot be retrieved. Command have beend cleaned"; + createParser(); + } else if (!filePath.isEmpty()) + mParser->process( + cleanParserKeys(mParser, QStringList("fetch-config"))); // Remove this parameter from the parser + return filePath; +}*/ + +//-------------------------------------------------------------------- + +CliModel::Command::Command(const QString &functionName, + const char *functionDescription, + CliModel::Function function, + const QHash &argsScheme, + const bool &genericArguments) + : mFunctionName(functionName), mFunctionDescription(functionDescription), mFunction(function), + mArgsScheme(argsScheme), mGenericArguments(genericArguments) { +} + +void CliModel::Command::execute(QHash &args, CliModel *parent) { + if (!mGenericArguments) { // Check arguments validity. + for (const auto &argName : args.keys()) { + if (!mArgsScheme.contains(argName)) { + qWarning() + << QStringLiteral("Command with invalid argument: `%1 (%2)`.").arg(mFunctionName).arg(argName); + + return; + } + } + } + // Check missing arguments. + for (const auto &argName : mArgsScheme.keys()) { + if (!mArgsScheme[argName].isOptional && (!args.contains(argName) || args[argName].isEmpty())) { + qWarning() << QStringLiteral("Missing argument for command: `%1 (%2)`.").arg(mFunctionName).arg(argName); + return; + } + } + qDebug() << "Execute"; + (parent->*mFunction)(args); + /* + // Execute! + App *app = App::getInstance(); + if (app->isOpened()) { + qInfo() << QStringLiteral("Execute command:") << args; + (*mFunction)(args); + } else { + Function f = mFunction; + QObject *context = new QObject(); + QObject::connect(app, &App::opened, [f, args, context]() mutable { + if (context) { + delete context; + context = nullptr; + qInfo() << QStringLiteral("Execute deferred command:") << args; + QHash fuckConst = args; + (*f)(fuckConst); + } + }); + } + */ +} + +void CliModel::Command::executeUri(QString address, QHash args, CliModel *parent) { + QUrl url(address); + QString query = url.query(); + + QStringList parameters = query.split('&'); + for (int i = 0; i < parameters.size(); ++i) { + QStringList parameter = parameters[i].split('='); + if (parameter[0] != "" && parameter[0] != "method") { + if (parameter.size() > 1) args[parameter[0]] = QByteArray::fromBase64(parameter[1].toUtf8()); + else args[parameter[0]] = ""; + } + } + + args["sip-address"] = address; + parent->addProcess(ProcessCommand(*this, args, 0, parent)); +} + +// pUrl can be `anytoken?p1=x&p2=y` or `p1=x&p2=y`. It will only use p1 and p2 +void CliModel::Command::executeUrl(const QString &pUrl, CliModel *parent) { + QHash args; + QStringList urlParts = pUrl.split('?'); + QString query = (urlParts.size() > 1 ? urlParts[1] : urlParts[0]); + QString authority = (urlParts.size() > 1 && urlParts[0].contains(':') ? urlParts[0].split(':')[1] : ""); + + QStringList parameters = query.split('&'); + for (int i = 0; i < parameters.size(); ++i) { + QStringList parameter = parameters[i].split('='); + if (parameter[0] != "method") { + if (parameter.size() > 1) args[parameter[0]] = QByteArray::fromBase64(parameter[1].toUtf8()); + else args[parameter[0]] = ""; + } + } + if (!authority.isEmpty()) args["sip-address"] = authority; + parent->addProcess(ProcessCommand(*this, args, 0, parent)); +} + +QString CliModel::Command::getFunctionSyntax() const { + QString functionSyntax; + functionSyntax += QStringLiteral("\""); + functionSyntax += mFunctionName; + for (auto &argName : mArgsScheme.keys()) { + functionSyntax += QStringLiteral(" "); + functionSyntax += mArgsScheme[argName].isOptional ? QStringLiteral("[") : QStringLiteral(""); + functionSyntax += argName; + functionSyntax += QStringLiteral("=<"); + switch (mArgsScheme[argName].type) { + case String: + functionSyntax += QStringLiteral("str"); + break; + default: + functionSyntax += QStringLiteral("value"); + break; + } + functionSyntax += QString(">"); + functionSyntax += mArgsScheme[argName].isOptional ? QStringLiteral("]") : QStringLiteral(""); + } + functionSyntax += QStringLiteral("\""); + return functionSyntax; +} + +//-------------------------------------------------------------------- + +void CliModel::executeCommand(const QString &command) { //, CommandFormat *format) { + // Detect if command is a CLI by testing commands + const QString &functionName = parseFunctionName(command, false); + const QString configURI = QString(EXECUTABLE_NAME).toLower() + "-config"; + if (!functionName.isEmpty()) { // It is a CLI + qInfo() << QStringLiteral("Detecting cli command: `%1`...").arg(command); + QHash args = parseArgs(command); + QHash argsToProcess; + for (auto it = args.begin(); it != args.end(); ++it) { + auto subfonction = parseFunctionName(it.key(), true); + if (!subfonction.isEmpty()) { + QHash arg; + arg[it.key()] = it.value(); + addProcess(ProcessCommand(mCommands[it.key()], arg, 1, this)); + } else { + argsToProcess[it.key()] = it.value(); + } + } + addProcess(ProcessCommand(mCommands[functionName], argsToProcess, 0, this)); + // mCommands[functionName].execute(args, this); + // if (format) *format = CliFormat; + } else { // It is a URI + QStringList tempSipAddress = command.split(':'); + QString scheme = "sip"; + QString transformedCommand; // In order to pass bellesip parsing, set scheme to 'sip:'. + if (tempSipAddress.size() == 1) { + transformedCommand = "sip:" + command; + } else { + scheme = tempSipAddress[0].toLower(); + bool ok = false; + for (const QString &validScheme : + {QString("sip"), "sip-" + QString(EXECUTABLE_NAME).toLower(), QString("sips"), + "sips-" + QString(EXECUTABLE_NAME).toLower(), QString(EXECUTABLE_NAME).toLower() + "-sip", + QString(EXECUTABLE_NAME).toLower() + "-sips", QString("tel"), QString("callto"), configURI}) + if (scheme == validScheme) ok = true; + if (!ok) { + qWarning() + << QStringLiteral("Not a valid URI: `%1` Unsupported scheme: `%2`.").arg(command).arg(scheme); + return; + } + tempSipAddress[0] = "sip"; + transformedCommand = tempSipAddress.join(':'); + } + if (scheme == configURI) { + QHash args = parseArgs(command); + QString fetchUrl; + if (args.contains("fetch-config")) fetchUrl = QByteArray::fromBase64(args["fetch-config"].toUtf8()); + else { + QUrl url(command.mid(configURI.size() + 1)); // Remove 'exec-config:' + if (url.scheme().isEmpty()) url.setScheme("https"); + fetchUrl = url.toString(); + } + // if (format) *format = CliFormat; + QHash dummy; + mCommands["show"].execute(dummy, this); // Just open the app. + QHash arg; + arg["fetch-config"] = fetchUrl; + addProcess(ProcessCommand(mCommands["fetch-config"], arg, 5, this)); + } else { + std::shared_ptr address; + QString qAddress = transformedCommand; + if (Utils::isUsername(transformedCommand)) { + address = linphone::Factory::get()->createAddress( + Utils::appStringToCoreString(transformedCommand + "@to.remove")); + address->setDomain(""); + qAddress = Utils::coreStringToAppString(address->asString()); + if (address && qAddress.isEmpty()) qAddress = transformedCommand; + } else + address = linphone::Factory::get()->createAddress( + Utils::appStringToCoreString(transformedCommand)); // Test if command is an address + // if (format) *format = UriFormat; + qInfo() << QStringLiteral("Detecting URI command: `%1`...").arg(command); + QString functionName; + if (address) { + functionName = Utils::coreStringToAppString(address->getHeader("method")).isEmpty() + ? QStringLiteral("call") + : Utils::coreStringToAppString(address->getHeader("method")); + } else { + QStringList fields = command.split('?'); + if (fields.size() > 1) { + fields = fields[1].split('&'); + for (int i = 0; i < fields.size() && functionName.isEmpty(); ++i) { + QStringList data = fields[i].split('='); + if (data[0] == "method" && data.size() > 1) functionName = data[1]; + } + if (functionName.isEmpty()) functionName = "call"; + } + } + functionName = functionName.toLower(); + if (functionName.isEmpty()) { + qWarning() << QStringLiteral("There is no method set in `%1`.").arg(command); + return; + } else if (!mCommands.contains(functionName)) { + qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName); + return; + } + QHash headers; + if (address) { + // TODO: check if there is too much headers. + + for (const auto &argName : mCommands[functionName].mArgsScheme.keys()) { + const std::string header = address->getHeader(Utils::appStringToCoreString(argName)); + headers[argName] = QByteArray::fromBase64(QByteArray(header.c_str(), int(header.length()))); + } + mCommands[functionName].executeUri(qAddress, headers, this); + } else mCommands[functionName].executeUrl(command, this); + } + } + runProcess(); +} + +void CliModel::addProcess(ProcessCommand process) { + mQueue << process; + std::sort(mQueue.begin(), mQueue.end(), + [](ProcessCommand &a, ProcessCommand &b) { return a.mPriority >= b.mPriority; }); +} + +void CliModel::runProcess() { + if (mQueue.size() > 0) { + mQueue.first().run(); + mQueue.pop_front(); + } +} + +void CliModel::resetProcesses() { + mQueue.clear(); +} diff --git a/Linphone/model/cli/CliModel.hpp b/Linphone/model/cli/CliModel.hpp new file mode 100644 index 000000000..a8ef2d2ae --- /dev/null +++ b/Linphone/model/cli/CliModel.hpp @@ -0,0 +1,139 @@ +/* + * 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 CLI_MODEL_H_ +#define CLI_MODEL_H_ + +#include +#include +#include +#include +#include +#include + +#include "tool/AbstractObject.hpp" + +// ============================================================================= + +class CliModel : public QObject, public AbstractObject { + Q_OBJECT +public: + CliModel(QObject *parent); + ~CliModel(); + static std::shared_ptr create(QObject *parent); + static std::shared_ptr getInstance(); + + QString parseFunctionName(const QString &command, bool isOptional); + QHash parseArgs(const QString &command); + + void cliShow(QHash args); + void cliFetchConfig(QHash args); + void cliCall(QHash args); + + static QRegularExpression mRegExpArgs; + static QRegularExpression mRegExpFunctionName; + + enum ArgumentType { String }; + + typedef void (CliModel::*Function)(QHash); + struct Argument { + Argument(ArgumentType type = String, bool isOptional = false) { + this->type = type; + this->isOptional = isOptional; + } + + ArgumentType type; + bool isOptional; + }; + class Command { + public: + Command() = default; + Command(const Command &command) = default; + Command(const QString &functionName, + const char *functionDescription, + Function function, + const QHash &argsScheme, + const bool &genericArguments = false); + + void execute(QHash &args, CliModel *parent); + void executeUri(QString address, QHash args, CliModel *parent); + void executeUrl(const QString &url, CliModel *parent); + + const char *getFunctionDescription() const { + return mFunctionDescription; + } + + QString getFunctionSyntax() const; + + QHash mArgsScheme; + + private: + QString mFunctionName; + const char *mFunctionDescription; + Function mFunction = nullptr; + bool mGenericArguments = false; // Used to avoid check on arguments + }; + static QMap mCommands; + + class ProcessCommand : public Command { + public: + ProcessCommand(Command command, QHash args, int priority, CliModel *parent) + : Command(command), mArguments(args), mPriority(priority), mParent(parent) { + } + bool operator<(const ProcessCommand &item) { + return mPriority < item.mPriority; + } + void run() { + execute(mArguments, mParent); + } + int mPriority = 0; + CliModel *mParent; + QHash mArguments; + }; + + QList mQueue; + void addProcess(ProcessCommand); // Add and sort + void runProcess(); + void resetProcesses(); + + static std::pair + createCommand(const QString &functionName, + const char *functionDescription, + Function function, + const QHash &argsScheme = QHash(), + const bool &genericArguments = false); + + enum CommandFormat { + UnknownFormat, + CliFormat, + UriFormat, // Parameters are in base64 + UrlFormat + }; + + void executeCommand(const QString &command); +signals: + void showMainWindow(); + +private: + static std::shared_ptr gCliModel; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/model/core/CoreModel.cpp b/Linphone/model/core/CoreModel.cpp index af7b9222e..eae5bf5c2 100644 --- a/Linphone/model/core/CoreModel.cpp +++ b/Linphone/model/core/CoreModel.cpp @@ -165,6 +165,57 @@ void CoreModel::setPathAfterStart() { lInfo() << "[CoreModel] Using RootCa path : " << QString::fromStdString(mCore->getRootCa()); } +//------------------------------------------------------------------------------- +// FETCH CONFIG +//------------------------------------------------------------------------------- + +QString CoreModel::getFetchConfig(QString filePath, bool *error) { + *error = false; + if (!filePath.isEmpty()) { + if (QUrl(filePath).isRelative()) { // this is a file path + filePath = Paths::getConfigFilePath(filePath, false); + if (!filePath.isEmpty()) filePath = "file://" + filePath; + } + if (filePath.isEmpty()) { + qWarning() << "Remote provisionning cannot be retrieved. Command have been cleaned"; + *error = true; + } + } + return filePath; +} + +void CoreModel::useFetchConfig(QString filePath) { + bool error = false; + filePath = getFetchConfig(filePath, &error); + if (!error && !filePath.isEmpty()) { + + if (mCore && mCore->getGlobalState() == linphone::GlobalState::On) { + // TODO + // if (mSettings->getAutoApplyProvisioningConfigUriHandlerEnabled()) setFetchConfig(filePath); else + emit requestFetchConfig(filePath); + } else { + connect( + this, &CoreModel::globalStateChanged, this, [filePath, this]() { useFetchConfig(filePath); }, + Qt::SingleShotConnection); + } + } +} + +bool CoreModel::setFetchConfig(QString filePath) { + bool fetched = false; + qDebug() << "setFetchConfig with " << filePath; + if (!filePath.isEmpty()) { + if (mCore) { + filePath.replace('\\', '/'); + fetched = mCore->setProvisioningUri(Utils::appStringToCoreString(filePath)) == 0; + } + } + if (!fetched) { + qWarning() << "Remote provisionning cannot be retrieved. Command have been cleaned"; + } else emit requestRestart(); + return fetched; +} + //--------------------------------------------------------------------------------------------------------------------------- void CoreModel::onAccountAdded(const std::shared_ptr &core, diff --git a/Linphone/model/core/CoreModel.hpp b/Linphone/model/core/CoreModel.hpp index 31cac7e06..e039d9e8c 100644 --- a/Linphone/model/core/CoreModel.hpp +++ b/Linphone/model/core/CoreModel.hpp @@ -28,6 +28,7 @@ #include #include +#include "model/cli/CliModel.hpp" #include "model/listener/Listener.hpp" #include "model/logger/LoggerModel.hpp" #include "tool/AbstractObject.hpp" @@ -50,6 +51,10 @@ public: void start(); void setConfigPath(QString path); + QString getFetchConfig(QString filePath, bool *error); + void useFetchConfig(QString filePath); + bool setFetchConfig(QString filePath); + bool mEnd = false; std::shared_ptr mCore; @@ -61,6 +66,8 @@ signals: void friendRemoved(const std::shared_ptr &f); void conferenceInfoCreated(const std::shared_ptr &confInfo); void unreadNotificationsChanged(); + void requestFetchConfig(QString path); + void requestRestart(); private: QString mConfigPath; diff --git a/Linphone/model/tool/ToolModel.cpp b/Linphone/model/tool/ToolModel.cpp index 334db6eb1..909ec3a69 100644 --- a/Linphone/model/tool/ToolModel.cpp +++ b/Linphone/model/tool/ToolModel.cpp @@ -100,7 +100,23 @@ bool ToolModel::createCall(const QString &sipAddress, linphone::MediaEncryption mediaEncryption, QString *errorMessage) { bool waitRegistrationForCall = true; // getSettingsModel()->getWaitRegistrationForCall() + std::shared_ptr core = CoreModel::getInstance()->getCore(); + + if (waitRegistrationForCall) { + std::shared_ptr currentAccount = core->getDefaultAccount(); + if (!currentAccount || currentAccount->getState() != linphone::RegistrationState::Ok) { + connect( + CoreModel::getInstance().get(), &CoreModel::accountRegistrationStateChanged, + CoreModel::getInstance().get(), + [sipAddress, options, prepareTransfertAddress, headers, mediaEncryption]() { + ToolModel::createCall(sipAddress, options, prepareTransfertAddress, headers, mediaEncryption); + }, + Qt::SingleShotConnection); + return false; + } + } + bool localVideoEnabled = options.contains("localVideoEnabled") ? options["localVideoEnabled"].toBool() : false; std::shared_ptr address = interpretUrl(sipAddress); @@ -114,7 +130,7 @@ bool ToolModel::createCall(const QString &sipAddress, return false; } for (auto &account : core->getAccountList()) { - if (account->getContactAddress()->weakEqual(address)) { + if (account->getContactAddress() && account->getContactAddress()->weakEqual(address)) { *errorMessage = "The calling address is a connected account."; lDebug() << "[" + QString(gClassName) + "]" + *errorMessage; return false; diff --git a/Linphone/tool/CMakeLists.txt b/Linphone/tool/CMakeLists.txt index dba7597f3..29b4ea944 100644 --- a/Linphone/tool/CMakeLists.txt +++ b/Linphone/tool/CMakeLists.txt @@ -12,6 +12,8 @@ list(APPEND _LINPHONEAPP_SOURCES tool/providers/ScreenProvider.cpp tool/native/DesktopTools.hpp + + tool/request/RequestDialog.cpp ) if (APPLE) diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index 5d8ee2823..b70e31b7c 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -1241,6 +1241,12 @@ bool Utils::isLocal(const QString &address) { App::postModelSync([&isLocal, address]() { isLocal = ToolModel::isLocal(address); }); return isLocal; } + +bool Utils::isUsername(const QString &txt) { + QRegularExpression regex("^(?$"); + QRegularExpressionMatch match = regex.match(txt); + return match.hasMatch(); // true +} // QDateTime dateTime(QDateTime::fromString(date, "yyyy-MM-dd hh:mm:ss")); // bool Utils::isMe(const QString &address) { diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index 7def8167f..ec50b6955 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -111,6 +111,7 @@ public: static QString generateSavedFilename(const QString &from, const QString &to); Q_INVOKABLE static bool isMe(const QString &address); Q_INVOKABLE static bool isLocal(const QString &address); + Q_INVOKABLE static bool isUsername(const QString &txt); // Regex check static QString getCountryName(const QLocale::Territory &p_country); static QString getApplicationProduct(); diff --git a/Linphone/tool/request/RequestDialog.cpp b/Linphone/tool/request/RequestDialog.cpp new file mode 100644 index 000000000..2e669f899 --- /dev/null +++ b/Linphone/tool/request/RequestDialog.cpp @@ -0,0 +1,25 @@ +/* + * 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 "RequestDialog.hpp" + +RequestDialog::RequestDialog(QString message, QString details, QObject *parent) + : QObject(parent), mMessage(message), mDetails(details) { +} diff --git a/Linphone/tool/request/RequestDialog.hpp b/Linphone/tool/request/RequestDialog.hpp new file mode 100644 index 000000000..68dd109d3 --- /dev/null +++ b/Linphone/tool/request/RequestDialog.hpp @@ -0,0 +1,42 @@ +/* + * 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 REQUEST_DIALOG_H_ +#define REQUEST_DIALOG_H_ + +#include +#include +#include + +class RequestDialog : public QObject { + Q_OBJECT + Q_PROPERTY(QString message MEMBER mMessage NOTIFY messageChanged) + Q_PROPERTY(QString details MEMBER mDetails NOTIFY detailsChanged) +public: + RequestDialog(QString message, QString details, QObject *parent = nullptr); + + QString mMessage; + QString mDetails; +signals: + void messageChanged(); + void detailsChanged(); + void result(int data); +}; +#endif diff --git a/Linphone/tool/thread/Thread.cpp b/Linphone/tool/thread/Thread.cpp index 3983803b7..9b1545954 100644 --- a/Linphone/tool/thread/Thread.cpp +++ b/Linphone/tool/thread/Thread.cpp @@ -28,11 +28,20 @@ Thread::Thread(QObject *parent) : QThread(parent) { void Thread::run() { int toExit = false; + mThreadId = new QObject(); while (!toExit) { int result = exec(); if (result <= 0) toExit = true; } } +Thread::~Thread() { + mThreadId->deleteLater(); +} + +QObject *Thread::getThreadId() { + return mThreadId; +} + bool Thread::isInLinphoneThread() { return CoreModel::getInstance() && QThread::currentThread() == CoreModel::getInstance()->thread(); } diff --git a/Linphone/tool/thread/Thread.hpp b/Linphone/tool/thread/Thread.hpp index c3e40ca00..377756606 100644 --- a/Linphone/tool/thread/Thread.hpp +++ b/Linphone/tool/thread/Thread.hpp @@ -26,10 +26,14 @@ class Thread : public QThread { public: Thread(QObject *parent = nullptr); + virtual ~Thread(); static bool isInLinphoneThread(); static bool mustBeInLinphoneThread(const QString &context); static bool mustBeInMainThread(const QString &context); + QObject *getThreadId(); + virtual void run(); + QObject *mThreadId = nullptr; }; #endif diff --git a/Linphone/view/App/AppWindow.qml b/Linphone/view/App/AppWindow.qml index 63f7f1b79..5b85a9df2 100644 --- a/Linphone/view/App/AppWindow.qml +++ b/Linphone/view/App/AppWindow.qml @@ -12,6 +12,22 @@ ApplicationWindow { id: popupComp InformationPopup{} } + + Component{ + id: confirmPopupComp + Dialog { + property var requestDialog + property int index + signal closePopup(int index) + onClosed: closePopup(index) + text: requestDialog.message + details: requestDialog.details + onAccepted: requestDialog.result(1) + onRejected: requestDialog.result(0) + width: 278 * DefaultStyle.dp + } + } + function removeFromPopupLayout(index) { popupLayout.popupList.splice(index, 1) } @@ -30,6 +46,15 @@ ApplicationWindow { loadingPopup.close() } + function showConfirmationPopup(requestDialog){ + console.log("Showing confirmation popup") + var popup = confirmPopupComp.createObject(popupLayout, {"requestDialog": requestDialog}) + popup.index = popupLayout.popupList.length + popupLayout.popupList.push(popup) + popup.open() + popup.closePopup.connect(removeFromPopupLayout) + } + ColumnLayout { id: popupLayout anchors.fill: parent @@ -57,4 +82,4 @@ ApplicationWindow { underlineColor: DefaultStyle.main1_500_main radius: 15 * DefaultStyle.dp } -} \ No newline at end of file +} diff --git a/Linphone/view/Item/Dialog.qml b/Linphone/view/Item/Dialog.qml index 43d92d618..6ece8b261 100644 --- a/Linphone/view/Item/Dialog.qml +++ b/Linphone/view/Item/Dialog.qml @@ -18,6 +18,7 @@ Popup { property alias buttons: buttonsLayout.data property alias content: contentLayout.data property string text + property string details signal accepted() signal rejected() @@ -74,6 +75,21 @@ Popup { wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter } + Text { + id: detailsText + visible: text.length != 0 + width: parent.width + Layout.preferredWidth: 278 * DefaultStyle.dp + Layout.alignment: Qt.AlignCenter + text: mainItem.details + font { + pixelSize: 13 * DefaultStyle.dp + weight: 400 * DefaultStyle.dp + italic: true + } + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + } RowLayout { id: buttonsLayout @@ -108,4 +124,4 @@ Popup { } } } -} \ No newline at end of file +} diff --git a/cmake/install/create_appimage.sh b/cmake/install/create_appimage.sh index f932940b8..54bba0fcc 100755 --- a/cmake/install/create_appimage.sh +++ b/cmake/install/create_appimage.sh @@ -99,7 +99,8 @@ else fi ########################################################################################### -export QMAKE=qmake + +export QMAKE=${QT_PATH}/bin/qmake export QML_SOURCES_PATHS=${QML_SOURCES_PATHS}:${WORK_DIR}/.. export LD_LIBRARY_PATH=${QT_PATH}/lib:${BIN_SOURCE_DIR}/lib:${BIN_SOURCE_DIR}/lib64 #export EXTRA_QT_PLUGINS=webenginecore;webview;webengine diff --git a/cmake/install/linux/linphone.desktop.cmake b/cmake/install/linux/linphone.desktop.cmake index 2889e7b26..305f2df76 100644 --- a/cmake/install/linux/linphone.desktop.cmake +++ b/cmake/install/linux/linphone.desktop.cmake @@ -7,5 +7,5 @@ Exec=@EXECUTABLE_NAME@ %u Icon=@EXECUTABLE_NAME@ Terminal=false Categories=Network;Telephony; -MimeType=x-scheme-handler/sip-@EXECUTABLE_NAME@;x-scheme-handler/sip;x-scheme-handler/sips-@EXECUTABLE_NAME@;x-scheme-handler/sips;x-scheme-handler/tel;x-scheme-handler/callto;x-scheme-handler/@EXECUTABLE_NAME@-config; +MimeType=x-scheme-handler/sip-@EXECUTABLE_NAME@;x-scheme-handler/@EXECUTABLE_NAME@-sip;x-scheme-handler/sip;x-scheme-handler/sips-@EXECUTABLE_NAME@;x-scheme-handler/@EXECUTABLE_NAME@-sips;x-scheme-handler/sips;x-scheme-handler/tel;x-scheme-handler/callto;x-scheme-handler/@EXECUTABLE_NAME@-config; X-PulseAudio-Properties=media.role=phone diff --git a/cmake/install/macos/Info.plist.in b/cmake/install/macos/Info.plist.in index e15676a2b..7f5f4d59c 100644 --- a/cmake/install/macos/Info.plist.in +++ b/cmake/install/macos/Info.plist.in @@ -48,6 +48,8 @@ tel callto @EXECUTABLE_NAME@-config + @EXECUTABLE_NAME@-sip + @EXECUTABLE_NAME@-sips diff --git a/cmake/install/windows/install.nsi.in b/cmake/install/windows/install.nsi.in index 11fc8e0d9..034a74d8e 100644 --- a/cmake/install/windows/install.nsi.in +++ b/cmake/install/windows/install.nsi.in @@ -19,6 +19,9 @@ WriteRegStr HKCR "sip" "URL Protocol" "" WriteRegStr HKCR "sip-@EXECUTABLE_NAME@" "" "URL:sip-@EXECUTABLE_NAME@ Protocol" WriteRegStr HKCR "sip-@EXECUTABLE_NAME@" "URL Protocol" "" +WriteRegStr HKCR "@EXECUTABLE_NAME@-sip" "" "URL:@EXECUTABLE_NAME@-sip Protocol" +WriteRegStr HKCR "@EXECUTABLE_NAME@-sip" "URL Protocol" "" + WriteRegStr HKCR "@EXECUTABLE_NAME@-config" "" "URL:@EXECUTABLE_NAME@-config Protocol" WriteRegStr HKCR "@EXECUTABLE_NAME@-config" "URL Protocol" "" @@ -28,6 +31,9 @@ WriteRegStr HKCR "sips" "URL Protocol" "" WriteRegStr HKCR "sips-@EXECUTABLE_NAME@" "" "URL:sips-@EXECUTABLE_NAME@ Protocol" WriteRegStr HKCR "sips-@EXECUTABLE_NAME@" "URL Protocol" "" +WriteRegStr HKCR "@EXECUTABLE_NAME@-sips" "" "URL:@EXECUTABLE_NAME@-sips Protocol" +WriteRegStr HKCR "@EXECUTABLE_NAME@-sips" "URL Protocol" "" + WriteRegStr HKCR "tel" "" "URL:tel Protocol" WriteRegStr HKCR "tel" "URL Protocol" "" @@ -51,6 +57,13 @@ WriteRegStr HKCR "@APPLICATION_NAME@.sip-@EXECUTABLE_NAME@\Shell\Open" "" "" WriteRegStr HKCR "@APPLICATION_NAME@.sip-@EXECUTABLE_NAME@\Shell\Open\Command" "" "$INSTDIR\bin\@EXECUTABLE_NAME@.exe $\"%1$\"" WriteRegStr HKLM "SOFTWARE\@APPLICATION_VENDOR@\@APPLICATION_NAME@\Capabilities\URLAssociations" "sip-@EXECUTABLE_NAME@" "@APPLICATION_NAME@.sip-@EXECUTABLE_NAME@" +## @EXECUTABLE_NAME@-SIP +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sip" "" "@APPLICATION_NAME@ @EXECUTABLE_NAME@-sip Protocol" +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sip\Shell" "" "" +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sip\Shell\Open" "" "" +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sip\Shell\Open\Command" "" "$INSTDIR\bin\@EXECUTABLE_NAME@.exe $\"%1$\"" +WriteRegStr HKLM "SOFTWARE\@APPLICATION_VENDOR@\@APPLICATION_NAME@\Capabilities\URLAssociations" "@EXECUTABLE_NAME@-sip" "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sip" + ## SIPS WriteRegStr HKCR "@APPLICATION_NAME@.sips" "" "@APPLICATION_NAME@ sips Protocol" WriteRegStr HKCR "@APPLICATION_NAME@.sips\Shell" "" "" @@ -65,6 +78,13 @@ WriteRegStr HKCR "@APPLICATION_NAME@.sips-@EXECUTABLE_NAME@\Shell\Open" "" "" WriteRegStr HKCR "@APPLICATION_NAME@.sips-@EXECUTABLE_NAME@\Shell\Open\Command" "" "$INSTDIR\bin\@EXECUTABLE_NAME@.exe $\"%1$\"" WriteRegStr HKLM "SOFTWARE\@APPLICATION_VENDOR@\@APPLICATION_NAME@\Capabilities\URLAssociations" "sips-@EXECUTABLE_NAME@" "@APPLICATION_NAME@.sips-@EXECUTABLE_NAME@" +## @EXECUTABLE_NAME@-SIPS +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sips" "" "@APPLICATION_NAME@ @EXECUTABLE_NAME@-sips Protocol" +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sips\Shell" "" "" +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sips\Shell\Open" "" "" +WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sips\Shell\Open\Command" "" "$INSTDIR\bin\@EXECUTABLE_NAME@.exe $\"%1$\"" +WriteRegStr HKLM "SOFTWARE\@APPLICATION_VENDOR@\@APPLICATION_NAME@\Capabilities\URLAssociations" "@EXECUTABLE_NAME@-sips" "@APPLICATION_NAME@.@EXECUTABLE_NAME@-sips" + ## @EXECUTABLE_NAME@-CONFIG WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-config" "" "@APPLICATION_NAME@ @EXECUTABLE_NAME@-config Protocol" WriteRegStr HKCR "@APPLICATION_NAME@.@EXECUTABLE_NAME@-config\Shell" "" "" diff --git a/external/linphone-sdk b/external/linphone-sdk index ea3bb6f22..ff8f01d91 160000 --- a/external/linphone-sdk +++ b/external/linphone-sdk @@ -1 +1 @@ -Subproject commit ea3bb6f2284ce16383c3251b9b899dc9b06d0ead +Subproject commit ff8f01d91f4a37773541ef05fb3c29d759116264