From 2629e2461d336d228fb06d2b009f0bde844f2b68 Mon Sep 17 00:00:00 2001 From: Julien Wadel Date: Fri, 23 Oct 2020 11:54:55 +0200 Subject: [PATCH] - Download configuration file if it is a webfile - Download the configuration file directly to the config path and overwrite old file - Add an interface to config directory path - Prevent a crash from the deletion of the Handler where we are still using it - Add synchronous mechanism to download file and Instance Manager deletion - Add Command parser features : clean specific keys, parse from list of arguments - Reorder config search to avoid unwanted config file overwrite - Move CLI execution at the end of initialization - Based openAppAfterInit on Manager and not on Handler - Add Call feature to Command line (--call=) - Launch a call on request - Add generic parameters on show and call method to CLI - Replace connectOnce function that didn't fully work by a connection on context where it is manage by Qt - Remove redundant parser initializations - Update SDK to fix configuration updates and FEC --- .../job-linux-desktop-centos7.yml | 1 + .gitlab-ci-files/job-macosx-desktop.yml | 1 + .gitlab-ci-files/job-windows-desktop.yml | 1 + CMakeLists.txt | 9 +- linphone-app/CMakeLists.txt | 2 +- linphone-app/assets/languages/en.ts | 16 ++ linphone-app/assets/linphone.desktop.cmake | 2 +- .../linphone_package/macos/Info.plist.in | 1 + .../linphone_package/windows/install.nsi.in | 10 + .../linphone_package/windows/uninstall.nsi.in | 1 + linphone-app/src/app/App.cpp | 158 ++++++++++++---- linphone-app/src/app/App.hpp | 4 + linphone-app/src/app/AppController.cpp | 12 +- linphone-app/src/app/cli/Cli.cpp | 172 ++++++++++++------ linphone-app/src/app/cli/Cli.hpp | 11 +- linphone-app/src/app/paths/Paths.cpp | 22 ++- linphone-app/src/app/paths/Paths.hpp | 1 + .../src/components/calls/CallsListModel.cpp | 18 +- .../src/components/core/CoreHandlers.cpp | 2 +- .../src/components/core/CoreManager.cpp | 16 +- .../src/components/core/CoreManager.hpp | 13 +- .../EventCountNotifierSystemTrayIcon.cpp | 4 +- .../src/components/file/FileDownloader.cpp | 49 ++++- .../src/components/file/FileDownloader.hpp | 9 +- .../src/components/settings/SettingsModel.cpp | 54 ++++-- linphone-sdk | 2 +- 26 files changed, 450 insertions(+), 141 deletions(-) diff --git a/.gitlab-ci-files/job-linux-desktop-centos7.yml b/.gitlab-ci-files/job-linux-desktop-centos7.yml index 53c348da2..0d8d30694 100644 --- a/.gitlab-ci-files/job-linux-desktop-centos7.yml +++ b/.gitlab-ci-files/job-linux-desktop-centos7.yml @@ -100,6 +100,7 @@ job-centos7-ninja-gcc-package: artifacts: paths: - build/OUTPUT/Packages/*.AppImage + when: always expire_in: 1 week ################################################# diff --git a/.gitlab-ci-files/job-macosx-desktop.yml b/.gitlab-ci-files/job-macosx-desktop.yml index 4d5260866..d82dd6cce 100644 --- a/.gitlab-ci-files/job-macosx-desktop.yml +++ b/.gitlab-ci-files/job-macosx-desktop.yml @@ -97,6 +97,7 @@ job-macosx-makefile-package: - codesign --options runtime,library --verbose -s "$MACOS_SIGNING_IDENTITY" OUTPUT/Packages/Linphone*.dmg - ./../tools/app_notarization.sh artifacts: + when: always paths: - build/OUTPUT/* when: always diff --git a/.gitlab-ci-files/job-windows-desktop.yml b/.gitlab-ci-files/job-windows-desktop.yml index 13c34448b..9436b35ec 100644 --- a/.gitlab-ci-files/job-windows-desktop.yml +++ b/.gitlab-ci-files/job-windows-desktop.yml @@ -82,6 +82,7 @@ job-windows-vs2017-package: artifacts: paths: - results\* + when: always expire_in: 1 weeks diff --git a/CMakeLists.txt b/CMakeLists.txt index 480068966..d2c0be0b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,11 +38,12 @@ endforeach() if(ENABLE_BUILD_VERBOSE) message("User Args : ${USER_ARGS}") endif() - -if(NOT CMAKE_GENERATOR_PLATFORM AND WIN32) +if(WIN32) if(NOT ${CMAKE_GENERATOR} MATCHES "Ninja") - set(CMAKE_GENERATOR_PLATFORM "Win32") - message(STATUS "Setting Platform to ${CMAKE_GENERATOR_PLATFORM}") + if(NOT CMAKE_GENERATOR_PLATFORM) + set(CMAKE_GENERATOR_PLATFORM "Win32") + message(STATUS "Setting Platform to ${CMAKE_GENERATOR_PLATFORM}") + endif() endif() endif() diff --git a/linphone-app/CMakeLists.txt b/linphone-app/CMakeLists.txt index 6cf94a4d9..71a41fbf4 100644 --- a/linphone-app/CMakeLists.txt +++ b/linphone-app/CMakeLists.txt @@ -89,7 +89,7 @@ if( WIN32) endif() set(CMAKE_INCLUDE_CURRENT_DIR ON)#useful for config.h -set(QT5_PACKAGES Core Gui Quick Widgets QuickControls2 Svg LinguistTools Concurrent Network) +set(QT5_PACKAGES Core Gui Quick Widgets QuickControls2 Svg LinguistTools Concurrent Network Test) if (UNIX AND NOT APPLE) list(APPEND QT5_PACKAGES DBus) endif () diff --git a/linphone-app/assets/languages/en.ts b/linphone-app/assets/languages/en.ts index f30489997..97ff91265 100644 --- a/linphone-app/assets/languages/en.ts +++ b/linphone-app/assets/languages/en.ts @@ -92,6 +92,22 @@ about About + + commandLineOptionFetchConfig + specify the %1 configuration file to be fetch. It will be merged with the current configuration. + + + commandLineOptionFetchConfigArg + url, path or file + + + commandLineOptionCall + make a call + + + commandLineOptionCallArg + sip address + AssistantAbstractView diff --git a/linphone-app/assets/linphone.desktop.cmake b/linphone-app/assets/linphone.desktop.cmake index 23f3b6b9f..22e4f821d 100644 --- a/linphone-app/assets/linphone.desktop.cmake +++ b/linphone-app/assets/linphone.desktop.cmake @@ -7,5 +7,5 @@ Exec=@EXECUTABLE_NAME@ %u Icon=@EXECUTABLE_NAME@ Terminal=false Categories=Network;Telephony; -MimeType=x-scheme-handler/sip-linphone;x-scheme-handler/sip;x-scheme-handler/sips-linphone;x-scheme-handler/sips;x-scheme-handler/tel;x-scheme-handler/callto; +MimeType=x-scheme-handler/sip-linphone;x-scheme-handler/sip;x-scheme-handler/sips-linphone;x-scheme-handler/sips;x-scheme-handler/tel;x-scheme-handler/callto;x-scheme-handler/linphone-config; X-PulseAudio-Properties=media.role=phone diff --git a/linphone-app/cmake_builder/linphone_package/macos/Info.plist.in b/linphone-app/cmake_builder/linphone_package/macos/Info.plist.in index bab2f6e76..d0a3d98c8 100644 --- a/linphone-app/cmake_builder/linphone_package/macos/Info.plist.in +++ b/linphone-app/cmake_builder/linphone_package/macos/Info.plist.in @@ -45,6 +45,7 @@ sips-linphone tel callto + linphone-config diff --git a/linphone-app/cmake_builder/linphone_package/windows/install.nsi.in b/linphone-app/cmake_builder/linphone_package/windows/install.nsi.in index 866e925fc..f1cdc0ce7 100644 --- a/linphone-app/cmake_builder/linphone_package/windows/install.nsi.in +++ b/linphone-app/cmake_builder/linphone_package/windows/install.nsi.in @@ -19,6 +19,9 @@ WriteRegStr HKCR "sip" "URL Protocol" "" WriteRegStr HKCR "sip-linphone" "" "URL:sip-linphone Protocol" WriteRegStr HKCR "sip-linphone" "URL Protocol" "" +WriteRegStr HKCR "linphone-config" "" "URL:linphone-config Protocol" +WriteRegStr HKCR "linphone-config" "URL Protocol" "" + WriteRegStr HKCR "sips" "" "URL:sips Protocol" WriteRegStr HKCR "sips" "URL Protocol" "" @@ -62,6 +65,13 @@ WriteRegStr HKCR "@APPLICATION_NAME@.sips-linphone\Shell\Open" "" "" WriteRegStr HKCR "@APPLICATION_NAME@.sips-linphone\Shell\Open\Command" "" "$INSTDIR\bin\@EXECUTABLE_NAME@.exe $\"%1$\"" WriteRegStr HKLM "SOFTWARE\@APPLICATION_VENDOR@\@APPLICATION_NAME@\Capabilities\URLAssociations" "sips-linphone" "@APPLICATION_NAME@.sips-linphone" +## LINPHONE-CONFIG +WriteRegStr HKCR "@APPLICATION_NAME@.linphone-config" "" "@APPLICATION_NAME@ linphone-config Protocol" +WriteRegStr HKCR "@APPLICATION_NAME@.linphone-config\Shell" "" "" +WriteRegStr HKCR "@APPLICATION_NAME@.linphone-config\Shell\Open" "" "" +WriteRegStr HKCR "@APPLICATION_NAME@.linphone-config\Shell\Open\Command" "" "$INSTDIR\bin\@EXECUTABLE_NAME@.exe $\"%1$\"" +WriteRegStr HKLM "SOFTWARE\@APPLICATION_VENDOR@\@APPLICATION_NAME@\Capabilities\URLAssociations" "linphone-config" "@APPLICATION_NAME@.linphone-config" + ## TEL WriteRegStr HKCR "@APPLICATION_NAME@.tel" "" "@APPLICATION_NAME@ tel Protocol" WriteRegStr HKCR "@APPLICATION_NAME@.tel\Shell" "" "" diff --git a/linphone-app/cmake_builder/linphone_package/windows/uninstall.nsi.in b/linphone-app/cmake_builder/linphone_package/windows/uninstall.nsi.in index 1f288ba0e..e7b3d0f82 100644 --- a/linphone-app/cmake_builder/linphone_package/windows/uninstall.nsi.in +++ b/linphone-app/cmake_builder/linphone_package/windows/uninstall.nsi.in @@ -17,6 +17,7 @@ DeleteRegKey HKCR "@APPLICATION_NAME@.sip" DeleteRegKey HKCR "@APPLICATION_NAME@.sip-linphone" DeleteRegKey HKCR "@APPLICATION_NAME@.sips" DeleteRegKey HKCR "@APPLICATION_NAME@.sips-linphone" +DeleteRegKey HKCR "@APPLICATION_NAME@.linphone-config" DeleteRegKey HKCR "@APPLICATION_NAME@.tel" DeleteRegKey HKCR "@APPLICATION_NAME@.callto" diff --git a/linphone-app/src/app/App.cpp b/linphone-app/src/app/App.cpp index 353fe126f..6ad57872f 100644 --- a/linphone-app/src/app/App.cpp +++ b/linphone-app/src/app/App.cpp @@ -156,18 +156,50 @@ static inline bool installLocale (App &app, QTranslator &translator, const QLoca return translator.load(locale, LanguagePath) && app.installTranslator(&translator); } -static inline shared_ptr getConfigIfExists (const QCommandLineParser &parser) { - string configPath(Paths::getConfigFilePath(parser.value("config"), false)); - if (!Paths::filePathExists(configPath)) - configPath.clear(); +static inline string getConfigPathIfExists (const QCommandLineParser &parser) { + QString filePath = parser.value("config"); + string configPath; + if(!QUrl(filePath).isRelative()){ + configPath = Utils::appStringToCoreString(FileDownloader::synchronousDownload(filePath, Utils::coreStringToAppString(Paths::getConfigDirPath(false)), true)); + } + if( configPath == "") + configPath = Paths::getConfigFilePath(filePath, false); + if( configPath == "" ) + configPath = Paths::getConfigFilePath("", false); + return configPath; +} +static inline shared_ptr getConfigIfExists (const string &configPath) { string factoryPath(Paths::getFactoryConfigFilePath()); if (!Paths::filePathExists(factoryPath)) factoryPath.clear(); return linphone::Config::newWithFactory(configPath, factoryPath); } - +bool App::setFetchConfig (QCommandLineParser *parser) { + bool fetched = false; + QString filePath = parser->value("fetch-config"); + if( !filePath.isEmpty()){ + if(QUrl(filePath).isRelative()){// this is a file path + filePath = Utils::coreStringToAppString(Paths::getConfigFilePath(filePath, false)); + if(!filePath.isEmpty()) + filePath = "file://"+filePath; + } + if(!filePath.isEmpty()){ + auto instance = CoreManager::getInstance(); + if(instance){ + auto core = instance->getCore(); + if(core){ + filePath.replace('\\','/'); + core->setProvisioningUri(Utils::appStringToCoreString(filePath)); + parser->process(cleanParserKeys(parser, QStringList("fetch-config")));// Remove this parameter from the parser + fetched = true; + } + } + } + } + return fetched; +} // ----------------------------------------------------------------------------- App::App (int &argc, char *argv[]) : SingleApplication(argc, argv, true, Mode::User | Mode::ExcludeAppPath | Mode::ExcludeAppVersion) { @@ -180,7 +212,7 @@ App::App (int &argc, char *argv[]) : SingleApplication(argc, argv, true, Mode::U mParser->process(*this); // Initialize logger. - shared_ptr config = getConfigIfExists(*mParser); + shared_ptr config = getConfigIfExists(getConfigPathIfExists(*mParser)); Logger::init(config); if (mParser->isSet("verbose")) Logger::getInstance()->setVerbose(true); @@ -195,7 +227,6 @@ App::App (int &argc, char *argv[]) : SingleApplication(argc, argv, true, Mode::U initLocale(config); if (mParser->isSet("help")) { - createParser(); mParser->showHelp(); } @@ -221,6 +252,30 @@ App::~App () { // ----------------------------------------------------------------------------- +QStringList App::cleanParserKeys(QCommandLineParser * parser, QStringList keys){ + QStringList oldArguments = parser->optionNames(); + QStringList parameters; + parameters << "dummy"; + for(int i = 0 ; i < oldArguments.size() ; ++i){ + if( !keys.contains(oldArguments[i])){ + if( mParser->value(oldArguments[i]).isEmpty()) + parameters << "--"+oldArguments[i]; + else + parameters << "--"+oldArguments[i]+"="+parser->value(oldArguments[i]); + } + } + return parameters; +} + +void App::processArguments(QHash args){ + QList keys = args.keys(); + QStringList parameters = cleanParserKeys(mParser, keys); + for(auto i = keys.begin() ; i != keys.end() ; ++i){ + parameters << "--"+(*i)+"="+args.value(*i); + } + mParser->process(parameters); +} + static QQuickWindow *createSubWindow (QQmlApplicationEngine *engine, const char *path) { qInfo() << QStringLiteral("Creating subwindow: `%1`.").arg(path); @@ -243,18 +298,22 @@ static QQuickWindow *createSubWindow (QQmlApplicationEngine *engine, const char // ----------------------------------------------------------------------------- void App::initContentApp () { - shared_ptr config = getConfigIfExists(*mParser); + std::string configPath; + shared_ptr config; bool mustBeIconified = false; + bool needRestart = true; // Destroy qml components and linphone core if necessary. if (mEngine) { + needRestart = false; + setFetchConfig(mParser); setOpened(false); qInfo() << QStringLiteral("Restarting app..."); delete mEngine; mNotifier = nullptr; mSystemTrayIcon = nullptr; - + // CoreManager::uninit(); removeTranslator(mTranslator); removeTranslator(mDefaultTranslator); @@ -262,8 +321,12 @@ void App::initContentApp () { delete mDefaultTranslator; mTranslator = new DefaultTranslator(this); mDefaultTranslator = new DefaultTranslator(this); + configPath = getConfigPathIfExists(*mParser); + config = getConfigIfExists(configPath); initLocale(config); } else { + configPath = getConfigPathIfExists(*mParser); + config = getConfigIfExists(configPath); // Update and download codecs. VideoCodecsModel::updateCodecs(); VideoCodecsModel::downloadUpdatableCodecs(this); @@ -289,18 +352,8 @@ void App::initContentApp () { mColors->useConfig(config); // Init core. - CoreManager::init(this, mParser->value("config")); + CoreManager::init(this, Utils::coreStringToAppString(configPath)); - // Execute command argument if needed. - if (!mEngine) { - const QString commandArgument = getCommandArgument(); - if (!commandArgument.isEmpty()) { - Cli::CommandFormat format; - Cli::executeCommand(commandArgument, &format); - if (format == Cli::UriFormat) - mustBeIconified = true; - } - } // Init engine content. mEngine = new QQmlApplicationEngine(); @@ -343,12 +396,22 @@ void App::initContentApp () { qFatal("Unable to open main window."); QObject::connect( - CoreManager::getInstance()->getHandlers().get(), - &CoreHandlers::coreStarted, - [this, mustBeIconified]() { - openAppAfterInit(mustBeIconified); + CoreManager::getInstance(), + &CoreManager::coreStarted, CoreManager::getInstance(), + [this, mustBeIconified]() mutable { + if(CoreManager::getInstance()->started()) + openAppAfterInit(mustBeIconified); } ); + + // Execute command argument if needed. + const QString commandArgument = getCommandArgument(); + if (!commandArgument.isEmpty()) { + Cli::CommandFormat format; + Cli::executeCommand(commandArgument, &format); + if (format == Cli::UriFormat || format == Cli::UrlFormat ) + mustBeIconified = true; + } } // ----------------------------------------------------------------------------- @@ -434,6 +497,8 @@ void App::createParser () { { "cli-help", tr("commandLineOptionCliHelp").replace("%1", APPLICATION_NAME) }, { { "v", "version" }, tr("commandLineOptionVersion") }, { "config", tr("commandLineOptionConfig").replace("%1", EXECUTABLE_NAME), tr("commandLineOptionConfigArg") }, + { "fetch-config", tr("commandLineOptionFetchConfig").replace("%1", EXECUTABLE_NAME), tr("commandLineOptionFetchConfigArg") }, + { { "c", "call" }, tr("commandLineOptionCall").replace("%1", EXECUTABLE_NAME),tr("commandLineOptionCallArg") }, #ifndef Q_OS_MACOS { "iconified", tr("commandLineOptionIconified") }, #endif // ifndef Q_OS_MACOS @@ -782,14 +847,14 @@ void App::setAutoStart (bool enabled) { void App::openAppAfterInit (bool mustBeIconified) { qInfo() << QStringLiteral("Open " APPLICATION_NAME " app."); - + auto coreManager = CoreManager::getInstance(); // Create other windows. mCallsWindow = createSubWindow(mEngine, QmlViewCallsWindow); mSettingsWindow = createSubWindow(mEngine, QmlViewSettingsWindow); - QObject::connect(mSettingsWindow, &QWindow::visibilityChanged, this, [](QWindow::Visibility visibility) { + QObject::connect(mSettingsWindow, &QWindow::visibilityChanged, this, [coreManager](QWindow::Visibility visibility) { if (visibility == QWindow::Hidden) { qInfo() << QStringLiteral("Update nat policy."); - shared_ptr core = CoreManager::getInstance()->getCore(); + shared_ptr core = coreManager->getCore(); core->setNatPolicy(core->getNatPolicy()); } }); @@ -802,16 +867,10 @@ void App::openAppAfterInit (bool mustBeIconified) { qWarning("System tray not found on this system."); else setTrayIcon(); - - if (!mustBeIconified) - smartShowWindow(mainWindow); - #else - Q_UNUSED(mustBeIconified); - smartShowWindow(mainWindow); #endif // ifndef __APPLE__ // Display Assistant if it does not exist proxy config. - if (CoreManager::getInstance()->getCore()->getProxyConfigList().empty()) + if (coreManager->getCore()->getProxyConfigList().empty()) QMetaObject::invokeMethod(mainWindow, "setView", Q_ARG(QVariant, AssistantViewName), Q_ARG(QVariant, QString(""))); #ifdef ENABLE_UPDATE_CHECK @@ -824,7 +883,36 @@ void App::openAppAfterInit (bool mustBeIconified) { checkForUpdate(); #endif // ifdef ENABLE_UPDATE_CHECK - setOpened(true); + if(setFetchConfig(mParser)) + restart(); + else{ +// Launch call if wanted and clean parser + if( mParser->isSet("call")){ + QString sipAddress = mParser->value("call"); + mParser->parse(cleanParserKeys(mParser, QStringList("call")));// Clean call from parser + if(coreManager->started()){ + coreManager->getCallsListModel()->launchAudioCall(sipAddress); + }else{ + QObject * context = new QObject(); + QObject::connect(CoreManager::getInstance(), &CoreManager::coreStarted,context, + [sipAddress,coreManager, context]() mutable { + if(context){ + delete context; + context = nullptr; + coreManager->getCallsListModel()->launchAudioCall(sipAddress); + } + }); + } + } +#ifndef __APPLE__ + if (!mustBeIconified) + smartShowWindow(mainWindow); +#else + Q_UNUSED(mustBeIconified); + smartShowWindow(mainWindow); +#endif + setOpened(true); + } } // ----------------------------------------------------------------------------- diff --git a/linphone-app/src/app/App.hpp b/linphone-app/src/app/App.hpp index c4422056e..d465f595d 100644 --- a/linphone-app/src/app/App.hpp +++ b/linphone-app/src/app/App.hpp @@ -55,9 +55,13 @@ public: ~App (); void initContentApp (); + QStringList cleanParserKeys(QCommandLineParser * parser, QStringList keys);// Get all options from parser and remove the selected keys. Return the result that can be passed to parser process. + void processArguments(QHash args); QString getCommandArgument (); + bool setFetchConfig (QCommandLineParser *parser); + #ifdef Q_OS_MACOS bool event (QEvent *event) override; #endif // ifdef Q_OS_MACOS diff --git a/linphone-app/src/app/AppController.cpp b/linphone-app/src/app/AppController.cpp index e502d9fdf..aea9bddb9 100644 --- a/linphone-app/src/app/AppController.cpp +++ b/linphone-app/src/app/AppController.cpp @@ -67,7 +67,17 @@ AppController::AppController (int &argc, char *argv[]) { #endif // ifdef Q_OS_MACOS QString command = mApp->getCommandArgument(); - mApp->sendMessage(command.isEmpty() ? "show" : command.toLocal8Bit(), -1); + if( command.isEmpty()){ + command = "show"; + QStringList parametersList; + for(int i = 1 ; i < argc ; ++i){ + QString a = argv[i]; + if(a.startsWith("--"))// show is a command : remove <-->-style parameters + a.remove(0,2); + command += " "+a; + } + } + mApp->sendMessage(command.toLocal8Bit(), -1); return; } diff --git a/linphone-app/src/app/cli/Cli.cpp b/linphone-app/src/app/cli/Cli.cpp index a3542a8ce..79b44d3b4 100644 --- a/linphone-app/src/app/cli/Cli.cpp +++ b/linphone-app/src/app/cli/Cli.cpp @@ -41,13 +41,24 @@ using namespace std; // API. // ============================================================================= -static void cliShow (QHash &) { +static void cliShow (QHash &args) { App *app = App::getInstance(); + if( args.size() > 0){ + app->processArguments(args); + app->initContentApp(); + } app->smartShowWindow(app->getMainWindow()); } static void cliCall (QHash &args) { - CoreManager::getInstance()->getCallsListModel()->launchAudioCall(args["sip-address"]); + if(args.size() > 1){// Call with options + App *app = App::getInstance(); + args["call"] = args["sip-address"];// Swap cli def to parser + args.remove("sip-address"); + app->processArguments(args); + app->initContentApp(); + }else + CoreManager::getInstance()->getCallsListModel()->launchAudioCall(args["sip-address"]); } static void cliJoinConference (QHash &args) { @@ -247,24 +258,26 @@ Cli::Command::Command ( const QString &functionName, const char *functionDescription, Cli::Function function, - const QHash &argsScheme + const QHash &argsScheme, + const bool &genericArguments ) : mFunctionName(functionName), mFunctionDescription(functionDescription), mFunction(function), - mArgsScheme(argsScheme) {} + mArgsScheme(argsScheme), + mGenericArguments(genericArguments) {} void Cli::Command::execute (QHash &args) const { - // 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; + 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())) { @@ -281,16 +294,38 @@ void Cli::Command::execute (QHash &args) const { (*mFunction)(args); } else { Function f = mFunction; - Utils::connectOnce(app, &App::opened, app, [f, args] { - qInfo() << QStringLiteral("Execute deferred command:") << args; - QHash fuckConst = args; - (*f)(fuckConst); - }); + 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 Cli::Command::executeUri (const shared_ptr &address) const { QHash args; + QString qAddress = Utils::coreStringToAppString(address->asString()); + QUrl url(qAddress); + QString query = url.query(); + + 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]] = ""; + } + } + // TODO: check if there is too much headers. for (const auto &argName : mArgsScheme.keys()) { const string header = address->getHeader(Utils::appStringToCoreString(argName)); @@ -301,6 +336,25 @@ void Cli::Command::executeUri (const shared_ptr &address) con execute(args); } +// pUrl can be `anytoken?p1=x&p2=y` or `p1=x&p2=y`. It will only use p1 and p2 +void Cli::Command::executeUrl (const QString &pUrl) const { + QHash args; + QUrl url(pUrl); + QString query = (url.hasQuery()?url.query():pUrl); + + 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]] = ""; + } + } + execute(args); +} + QString Cli::Command::getFunctionSyntax () const { QString functionSyntax; functionSyntax += QStringLiteral("\""); @@ -333,10 +387,10 @@ QRegExp Cli::mRegExpArgs("(?:(?:([\\w-]+)\\s*)=\\s*(?:\"([^\"\\\\]*(?:\\\\.[^\"\ QRegExp Cli::mRegExpFunctionName("^\\s*([a-z-]+)\\s*"); QMap Cli::mCommands = { - createCommand("show", QT_TR_NOOP("showFunctionDescription"), cliShow), + createCommand("show", QT_TR_NOOP("showFunctionDescription"), cliShow, QHash(), true), createCommand("call", QT_TR_NOOP("callFunctionDescription"), cliCall, { { "sip-address", {} } - }), + }, true), createCommand("initiate-conference", QT_TR_NOOP("initiateConferenceFunctionDescription"), cliInitiateConference, { { "sip-address", {} }, { "conference-id", {} } }), @@ -364,43 +418,52 @@ void Cli::executeCommand (const QString &command, CommandFormat *format) { QHash args = parseArgs(command); mCommands[functionName].execute(args); } - if (format) *format = CliFormat; - return; + }else{ + string scheme = address->getScheme(); + bool ok = false; + for (const string &validScheme : { "sip", "sip-linphone", "sips", "sips-linphone", "tel", "callto", "linphone-config" }) + if (scheme == validScheme) + ok = true; + if( !ok){ + qWarning() << QStringLiteral("Not a valid uri: `%1` Unsupported scheme: `%2`.").arg(command).arg(Utils::coreStringToAppString(scheme)); + return; + }else{ + if( scheme == "linphone-config" ){ + QHash args = parseArgs(command); + if(args.contains("fetch-config")) + args["fetch-config"] = QByteArray::fromBase64(args["fetch-config"].toUtf8() ); + else { + QUrl url(command); + url.setScheme("https"); + args["fetch-config"] = url.toString(); + } + if (format) + *format = CliFormat; + mCommands["show"].execute(args); + }else{ + if (format) + *format = UriFormat; + // Execute uri command. + qInfo() << QStringLiteral("Detecting uri command: `%1`...").arg(command); + if (address->getUsername().empty()) { + qWarning() << QStringLiteral("Failed to execute command. No username given."); + return; + } + const QString functionName = Utils::coreStringToAppString(address->getHeader("method")).isEmpty() + ? QStringLiteral("call") + : Utils::coreStringToAppString(address->getHeader("method")); + + if (!functionName.isEmpty() && !mCommands.contains(functionName)) { + qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName); + return; + } + mCommands[functionName].executeUri(address); + } + } } - - if (format) - *format = UriFormat; - - // Execute uri command. - qInfo() << QStringLiteral("Detecting uri command: `%1`...").arg(command); - - if (address->getUsername().empty()) { - qWarning() << QStringLiteral("Failed to execute command. No username given."); - return; - } - - string scheme = address->getScheme(); - for (const string &validScheme : { "sip", "sip-linphone", "sips", "sips-linphone", "tel", "callto" }) - if (scheme == validScheme) - goto success; - qWarning() << QStringLiteral("Not a valid uri: `%1` Unsupported scheme: `%2`.") - .arg(command).arg(Utils::coreStringToAppString(scheme)); - return; - -success: - const QString functionName = Utils::coreStringToAppString(address->getHeader("method")).isEmpty() - ? QStringLiteral("call") - : Utils::coreStringToAppString(address->getHeader("method")); - - if (!functionName.isEmpty() && !mCommands.contains(functionName)) { - qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName); - return; - } - - mCommands[functionName].executeUri(address); } void Cli::showHelp () { @@ -425,9 +488,10 @@ pair Cli::createCommand ( const QString &functionName, const char *functionDescription, Function function, - const QHash &argsScheme + const QHash &argsScheme, + const bool &genericArguments ) { - return { functionName, Cli::Command(functionName, functionDescription, function, argsScheme) }; + return { functionName, Cli::Command(functionName, functionDescription, function, argsScheme, genericArguments) }; } // ----------------------------------------------------------------------------- diff --git a/linphone-app/src/app/cli/Cli.hpp b/linphone-app/src/app/cli/Cli.hpp index d84c0abed..b3f52a11d 100644 --- a/linphone-app/src/app/cli/Cli.hpp +++ b/linphone-app/src/app/cli/Cli.hpp @@ -59,11 +59,13 @@ class Cli : public QObject { const QString &functionName, const char *functionDescription, Function function, - const QHash &argsScheme + const QHash &argsScheme, + const bool &genericArguments=false ); void execute (QHash &args) const; void executeUri (const std::shared_ptr &address) const; + void executeUrl (const QString &url) const; const char *getFunctionDescription () const { return mFunctionDescription; @@ -76,13 +78,15 @@ class Cli : public QObject { const char *mFunctionDescription; Function mFunction = nullptr; QHash mArgsScheme; + bool mGenericArguments=false;// Used to avoid check on arguments }; public: enum CommandFormat { UnknownFormat, CliFormat, - UriFormat + UriFormat, // Parameters are in base64 + UrlFormat }; static void executeCommand (const QString &command, CommandFormat *format = nullptr); @@ -96,7 +100,8 @@ private: const QString &functionName, const char *functionDescription, Function function, - const QHash &argsScheme = QHash() + const QHash &argsScheme = QHash(), + const bool &genericArguments=false ); static QString parseFunctionName (const QString &command); diff --git a/linphone-app/src/app/paths/Paths.cpp b/linphone-app/src/app/paths/Paths.cpp index b4c03d201..485bd828b 100644 --- a/linphone-app/src/app/paths/Paths.cpp +++ b/linphone-app/src/app/paths/Paths.cpp @@ -215,11 +215,25 @@ string Paths::getCodecsDirPath () { return getWritableDirPath(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + PathCodecs); } -string Paths::getConfigFilePath (const QString &configPath, bool writable) { - const QString path = configPath.isEmpty() - ? getAppConfigFilePath() - : QFileInfo(configPath).absoluteFilePath(); +string Paths::getConfigDirPath (bool writable) { + return writable ? getWritableFilePath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)+QDir::separator()) : getReadableFilePath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)+QDir::separator()); +} +string Paths::getConfigFilePath (const QString &configPath, bool writable) { + QString path; + if( !configPath.isEmpty()){ + QFileInfo file(configPath); + if( !writable && (!file.exists() || !file.isFile())){// This file cannot be found. Check if it exists in standard folder + QString defaultConfigPath = Utils::coreStringToAppString(getConfigDirPath(false)); + file = QFileInfo(defaultConfigPath+QDir::separator()+configPath); + if( !file.exists() || !file.isFile()) + path = ""; + else + path = file.absoluteFilePath(); + }else + path = file.absoluteFilePath(); + }else + path = getAppConfigFilePath(); return writable ? getWritableFilePath(path) : getReadableFilePath(path); } diff --git a/linphone-app/src/app/paths/Paths.hpp b/linphone-app/src/app/paths/Paths.hpp index c08296efd..54c8e34a5 100644 --- a/linphone-app/src/app/paths/Paths.hpp +++ b/linphone-app/src/app/paths/Paths.hpp @@ -33,6 +33,7 @@ namespace Paths { std::string getCallHistoryFilePath (); std::string getCapturesDirPath (); std::string getCodecsDirPath (); + std::string getConfigDirPath (bool writable = true); std::string getConfigFilePath (const QString &configPath = QString(), bool writable = true); std::string getDownloadDirPath (); std::string getFactoryConfigFilePath (); diff --git a/linphone-app/src/components/calls/CallsListModel.cpp b/linphone-app/src/components/calls/CallsListModel.cpp index d4f9e2bdc..5f9b47f0b 100644 --- a/linphone-app/src/components/calls/CallsListModel.cpp +++ b/linphone-app/src/components/calls/CallsListModel.cpp @@ -113,7 +113,23 @@ void CallsListModel::launchAudioCall (const QString &sipAddress, const QHashsetProxyConfig(core->getDefaultProxyConfig()); CallModel::setRecordFile(params, Utils::coreStringToAppString(address->getUsername())); - core->inviteAddressWithParams(address, params); + shared_ptr currentProxyConfig = core->getDefaultProxyConfig(); + if(currentProxyConfig){ + if(currentProxyConfig->getState() == linphone::RegistrationState::Ok) + core->inviteAddressWithParams(address, params); + else{ + QObject * context = new QObject(); + QObject::connect(CoreManager::getInstance()->getHandlers().get(), &CoreHandlers::registrationStateChanged,context, + [address,core,params,currentProxyConfig, context](const std::shared_ptr &proxyConfig, linphone::RegistrationState state) mutable { + if(context && proxyConfig==currentProxyConfig && state==linphone::RegistrationState::Ok){ + delete context; + context = nullptr; + core->inviteAddressWithParams(address, params); + } + }); + } + }else + core->inviteAddressWithParams(address, params); } void CallsListModel::launchVideoCall (const QString &sipAddress) const { diff --git a/linphone-app/src/components/core/CoreHandlers.cpp b/linphone-app/src/components/core/CoreHandlers.cpp index ad77ad1e5..5ecb295c7 100644 --- a/linphone-app/src/components/core/CoreHandlers.cpp +++ b/linphone-app/src/components/core/CoreHandlers.cpp @@ -56,6 +56,7 @@ CoreHandlers::CoreHandlers (CoreManager *coreManager) { CoreHandlers::~CoreHandlers () { delete mCoreStartedLock; + mCoreStartedLock = nullptr; } // ----------------------------------------------------------------------------- @@ -66,7 +67,6 @@ void CoreHandlers::handleCoreCreated () { Q_ASSERT(mCoreCreated == false); mCoreCreated = true; notifyCoreStarted(); - mCoreStartedLock->unlock(); } diff --git a/linphone-app/src/components/core/CoreManager.cpp b/linphone-app/src/components/core/CoreManager.cpp index 25307bd00..5ccda1a7b 100644 --- a/linphone-app/src/components/core/CoreManager.cpp +++ b/linphone-app/src/components/core/CoreManager.cpp @@ -24,7 +24,7 @@ #include #include #include - +#include #include "config.h" #include "app/paths/Paths.hpp" @@ -79,6 +79,7 @@ CoreManager::CoreManager (QObject *parent, const QString &configPath) : createLinphoneCore(configPath); qInfo() << QStringLiteral("Core created. Enable iterate."); mInstance->mCbsTimer->start(); + std::shared_ptr h = mInstance->getHandlers();// Protect handler as we will enter its function where it can be deleted (like while restarting) emit mInstance->coreCreated(); }); @@ -171,8 +172,17 @@ void CoreManager::init (QObject *parent, const QString &configPath) { void CoreManager::uninit () { if (mInstance) { - delete mInstance; - mInstance = nullptr; + connect(mInstance, &QObject::destroyed, []()mutable{ + mInstance = nullptr; + qInfo() << "Linphone Core is destroyed"; + }); + mInstance->mCbsTimer->stop(); + mInstance->deleteLater();// Ensure to take time to delete the instance + QTest::qWaitFor([&]() {return mInstance == nullptr;},10000); + if( mInstance){ + qWarning() << "Linphone Core couldn't destroy in time. It may lead to have multiple session of Linphone Core"; + mInstance = nullptr; + } } } diff --git a/linphone-app/src/components/core/CoreManager.hpp b/linphone-app/src/components/core/CoreManager.hpp index 0f5544a43..8f65d3c89 100644 --- a/linphone-app/src/components/core/CoreManager.hpp +++ b/linphone-app/src/components/core/CoreManager.hpp @@ -55,7 +55,6 @@ public: } std::shared_ptr getCore () { - Q_CHECK_PTR(mCore); return mCore; } @@ -110,6 +109,11 @@ public: return mAccountSettingsModel; } + static CoreManager *getInstance () { + Q_CHECK_PTR(mInstance); + return mInstance; + } + // --------------------------------------------------------------------------- // Initialization. // --------------------------------------------------------------------------- @@ -117,11 +121,6 @@ public: static void init (QObject *parent, const QString &configPath); static void uninit (); - static CoreManager *getInstance () { - Q_CHECK_PTR(mInstance); - return mInstance; - } - // --------------------------------------------------------------------------- // Must be used in a qml scene. @@ -136,6 +135,8 @@ public: int getMissedCallCount(const QString &peerAddress, const QString &localAddress) const;// Get missed call count from a chat (useful for showing bubbles on Timelines) int getMissedCallCountFromLocal(const QString &localAddress) const;// Get missed call count from a chat (useful for showing bubbles on Timelines) + static bool isInstanciated(){return mInstance!=nullptr;} + signals: void coreCreated (); void coreStarted (); diff --git a/linphone-app/src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.cpp b/linphone-app/src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.cpp index 2499c741d..372b057a4 100644 --- a/linphone-app/src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.cpp +++ b/linphone-app/src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.cpp @@ -115,7 +115,7 @@ void EventCountNotifier::notifyEventCount (int n) { void EventCountNotifier::update () { QSystemTrayIcon *sysTrayIcon = App::getInstance()->getSystemTrayIcon(); - Q_CHECK_PTR(sysTrayIcon); - sysTrayIcon->setIcon(QIcon(mDisplayCounter ? *mBufWithCounter : *mBuf)); + if(sysTrayIcon) + sysTrayIcon->setIcon(QIcon(mDisplayCounter ? *mBufWithCounter : *mBuf)); mDisplayCounter = !mDisplayCounter; } diff --git a/linphone-app/src/components/file/FileDownloader.cpp b/linphone-app/src/components/file/FileDownloader.cpp index 331596531..55fe1de9f 100644 --- a/linphone-app/src/components/file/FileDownloader.cpp +++ b/linphone-app/src/components/file/FileDownloader.cpp @@ -18,6 +18,7 @@ * along with this program. If not, see . */ +#include #include "app/paths/Paths.hpp" #include "components/core/CoreManager.hpp" #include "components/settings/SettingsModel.hpp" @@ -31,13 +32,15 @@ namespace { constexpr char cDefaultFileName[] = "download"; } -static QString getDownloadFilePath (const QString &folder, const QUrl &url) { +static QString getDownloadFilePath (const QString &folder, const QUrl &url, const bool& overwrite) { QFileInfo fileInfo(url.path()); QString fileName = fileInfo.fileName(); if (fileName.isEmpty()) fileName = cDefaultFileName; fileName.prepend(folder); + if( overwrite && QFile::exists(fileName)) + QFile::remove(fileName); if (!QFile::exists(fileName)) return fileName; @@ -89,12 +92,15 @@ void FileDownloader::download () { #endif if (mDownloadFolder.isEmpty()) { - mDownloadFolder = CoreManager::getInstance()->getSettingsModel()->getDownloadFolder(); + if(CoreManager::isInstanciated()) + mDownloadFolder = CoreManager::getInstance()->getSettingsModel()->getDownloadFolder(); + else + mDownloadFolder = QDir::cleanPath(Utils::coreStringToAppString(Paths::getDownloadDirPath ()) + QDir::separator()); emit downloadFolderChanged(mDownloadFolder); } Q_ASSERT(!mDestinationFile.isOpen()); - mDestinationFile.setFileName(getDownloadFilePath(QDir::cleanPath(mDownloadFolder) + QDir::separator(), mUrl)); + mDestinationFile.setFileName(getDownloadFilePath(QDir::cleanPath(mDownloadFolder) + QDir::separator(), mUrl, mOverwriteFile)); if (!mDestinationFile.open(QIODevice::WriteOnly)) emitOutputError(); else { @@ -210,6 +216,43 @@ void FileDownloader::setDownloadFolder (const QString &downloadFolder) { } } +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; +} + qint64 FileDownloader::getReadBytes () const { return mReadBytes; } diff --git a/linphone-app/src/components/file/FileDownloader.hpp b/linphone-app/src/components/file/FileDownloader.hpp index 782768bd0..6a203783d 100644 --- a/linphone-app/src/components/file/FileDownloader.hpp +++ b/linphone-app/src/components/file/FileDownloader.hpp @@ -23,12 +23,13 @@ #include #include +#include // ============================================================================= class QSslError; -class FileDownloader : public QObject { +class FileDownloader : public QObject{ Q_OBJECT; // TODO: Add an error property to use in UI. @@ -57,6 +58,11 @@ public: 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 + signals: void urlChanged (const QUrl &url); void downloadFolderChanged (const QString &downloadFolder); @@ -95,6 +101,7 @@ private: qint64 mReadBytes = 0; qint64 mTotalBytes = 0; bool mDownloading = false; + bool mOverwriteFile = false; QPointer mNetworkReply; QNetworkAccessManager mManager; diff --git a/linphone-app/src/components/settings/SettingsModel.cpp b/linphone-app/src/components/settings/SettingsModel.cpp index 1fc951a20..e9900d9e8 100644 --- a/linphone-app/src/components/settings/SettingsModel.cpp +++ b/linphone-app/src/components/settings/SettingsModel.cpp @@ -263,37 +263,51 @@ QStringList SettingsModel::getPlaybackDevices () const { // ----------------------------------------------------------------------------- QString SettingsModel::getCaptureDevice () const { - return Utils::coreStringToAppString( - CoreManager::getInstance()->getCore()->getCaptureDevice() - ); + auto audioDevice = CoreManager::getInstance()->getCore()->getInputAudioDevice(); + return Utils::coreStringToAppString(audioDevice? audioDevice->getId() : CoreManager::getInstance()->getCore()->getCaptureDevice()); } void SettingsModel::setCaptureDevice (const QString &device) { - CoreManager::getInstance()->getCore()->setCaptureDevice( - Utils::appStringToCoreString(device) - ); - emit captureDeviceChanged(device); - if (mSimpleCaptureGraph && mSimpleCaptureGraph->isRunning()) { - createCaptureGraph(); - } + std::string devId = Utils::appStringToCoreString(device); + auto list = CoreManager::getInstance()->getCore()->getExtendedAudioDevices(); + auto audioDevice = find_if(list.cbegin(), list.cend(), [&] ( const std::shared_ptr & audioItem) { + return audioItem->getId() == devId; + }); + if(audioDevice != list.cend()){ + CoreManager::getInstance()->getCore()->setCaptureDevice(devId); + CoreManager::getInstance()->getCore()->setInputAudioDevice(*audioDevice); + emit captureDeviceChanged(device); + if (mSimpleCaptureGraph && mSimpleCaptureGraph->isRunning()) { + createCaptureGraph(); + } + }else + qWarning() << "Cannot set Capture device. The ID cannot be matched with an existant device : " << device; } // ----------------------------------------------------------------------------- QString SettingsModel::getPlaybackDevice () const { - return Utils::coreStringToAppString( - CoreManager::getInstance()->getCore()->getPlaybackDevice() - ); + auto audioDevice = CoreManager::getInstance()->getCore()->getOutputAudioDevice(); + return Utils::coreStringToAppString(audioDevice? audioDevice->getId() : CoreManager::getInstance()->getCore()->getPlaybackDevice()); } void SettingsModel::setPlaybackDevice (const QString &device) { - CoreManager::getInstance()->getCore()->setPlaybackDevice( - Utils::appStringToCoreString(device) - ); - emit playbackDeviceChanged(device); - if (mSimpleCaptureGraph && mSimpleCaptureGraph->isRunning()) { - createCaptureGraph(); - } + std::string devId = Utils::appStringToCoreString(device); + + auto list = CoreManager::getInstance()->getCore()->getExtendedAudioDevices(); + auto audioDevice = find_if(list.cbegin(), list.cend(), [&] ( const std::shared_ptr & audioItem) { + return audioItem->getId() == devId; + }); + if(audioDevice != list.cend()){ + + CoreManager::getInstance()->getCore()->setPlaybackDevice(devId); + CoreManager::getInstance()->getCore()->setOutputAudioDevice(*audioDevice); + emit playbackDeviceChanged(device); + if (mSimpleCaptureGraph && mSimpleCaptureGraph->isRunning()) { + createCaptureGraph(); + } + }else + qWarning() << "Cannot set Playback device. The ID cannot be matched with an existant device : " << device; } // ----------------------------------------------------------------------------- diff --git a/linphone-sdk b/linphone-sdk index c8700f691..3609b3815 160000 --- a/linphone-sdk +++ b/linphone-sdk @@ -1 +1 @@ -Subproject commit c8700f691113dc7771d24e80feb339e1cf0970a0 +Subproject commit 3609b38153ce4b715e93389673538003425cbac2