/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "App.hpp"
#ifdef Q_OS_WIN
#include
#include
#include
#endif // ifdef Q_OS_WIN
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "cli/Cli.hpp"
#include "components/Components.hpp"
#include "components/history/CallHistoryModel.hpp"
#include "components/history/CallHistoryProxyModel.hpp"
#include "components/other/date/DateModel.hpp"
#include "components/other/desktop-tools/DesktopTools.hpp"
#include "components/other/spell-checker/SpellChecker.hpp"
#include "config.h"
#include "logger/Logger.hpp"
#include "paths/Paths.hpp"
#include "providers/AvatarProvider.hpp"
#include "providers/ExternalImageProvider.hpp"
#include "providers/ImageProvider.hpp"
#include "providers/QRCodeProvider.hpp"
#include "providers/ThumbnailProvider.hpp"
#include "translator/DefaultTranslator.hpp"
#include "utils/Constants.hpp"
#include "utils/Utils.hpp"
#include "components/settings/EmojisSettingsModel.hpp"
#include "components/timeline/TimelineListModel.hpp"
#include "components/timeline/TimelineModel.hpp"
#include "components/timeline/TimelineProxyModel.hpp"
#include "components/participant/ParticipantListModel.hpp"
#include "components/participant/ParticipantModel.hpp"
#include "components/participant/ParticipantProxyModel.hpp"
// =============================================================================
using namespace std;
namespace {
#ifdef Q_OS_LINUX
const QString AutoStartDirectory(QDir::homePath().append(QStringLiteral("/.config/autostart/")));
const QString ApplicationsDirectory(QDir::homePath().append(QStringLiteral("/.local/share/applications/")));
const QString IconsDirectory(QDir::homePath().append(QStringLiteral("/.local/share/icons/hicolor/scalable/apps/")));
#elif defined(Q_OS_MACOS)
const QString OsascriptExecutable(QStringLiteral("osascript"));
#else
const QString
AutoStartSettingsFilePath(QStringLiteral("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"));
#endif // ifdef Q_OS_LINUX
} // namespace
// -----------------------------------------------------------------------------
#ifdef Q_OS_LINUX
bool App::autoStartEnabled() {
const QString confPath(AutoStartDirectory + EXECUTABLE_NAME ".desktop");
QFile file(confPath);
if (!QDir(AutoStartDirectory).exists() || !file.exists()) return false;
if (!file.open(QFile::ReadOnly)) {
qWarning() << "Unable to open autostart file in read only: `" << confPath << "`.";
return false;
}
// Check if installation is done via Flatpak, AppImage, or classic package
// in order to check if there is a correct exec path for autostart
QString exec = getApplicationPath();
QTextStream in(&file);
QString autoStartConf = in.readAll();
int index = -1;
// check if the Exec part of the autostart ini file not corresponding to our executable (old desktop entry with
// wrong version in filename)
if (autoStartConf.indexOf(QString("Exec=" + exec + " ")) <
0) { // On autostart, there is the option --iconified so there is one space.
// replace file
setAutoStart(true);
}
return true;
}
#elif defined(Q_OS_MACOS)
static inline QString getMacOsBundlePath() {
QDir dir(QCoreApplication::applicationDirPath());
if (dir.dirName() != QLatin1String("MacOS")) return QString();
dir.cdUp();
dir.cdUp();
QString path(dir.path());
if (path.length() > 0 && path.right(1) == "/") path.chop(1);
return path;
}
static inline QString getMacOsBundleName() {
return QFileInfo(getMacOsBundlePath()).baseName();
}
bool App::autoStartEnabled() {
const QByteArray expectedWord(getMacOsBundleName().toUtf8());
if (expectedWord.isEmpty()) {
qInfo() << QStringLiteral("Application is not installed. Autostart unavailable.");
return false;
}
QProcess process;
process.start(OsascriptExecutable,
{"-e", "tell application \"System Events\" to get the name of every login item"});
if (!process.waitForFinished()) {
qWarning() << QStringLiteral("Unable to execute properly: `%1` (%2).")
.arg(OsascriptExecutable)
.arg(process.errorString());
return false;
}
// TODO: Move in utils?
const QByteArray buf(process.readAll());
for (const char *p = buf.data(), *word = p, *end = p + buf.length(); p <= end; ++p) {
switch (*p) {
case ' ':
case '\r':
case '\n':
case '\t':
case '\0':
if (word != p) {
if (!strncmp(word, expectedWord, size_t(p - word))) return true;
word = p + 1;
}
default:
break;
}
}
return false;
}
#else
bool App::autoStartEnabled() {
return QSettings(AutoStartSettingsFilePath, QSettings::NativeFormat).value(EXECUTABLE_NAME).isValid();
}
#endif // ifdef Q_OS_LINUX
// -----------------------------------------------------------------------------
static inline bool installLocale(App &app, QTranslator &translator, const QLocale &locale) {
bool ok = translator.load(locale, Constants::LanguagePath) && app.installTranslator(&translator);
if (ok) QLocale::setDefault(locale);
return ok;
}
static inline string getConfigPathIfExists(const QCommandLineParser &parser) {
QString filePath = parser.isSet("config") ? 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;
}
QString App::getFetchConfig(QString filePath, bool *error) {
*error = false;
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()) {
qWarning() << "Remote provisionning cannot be retrieved. Command have beend cleaned";
*error = true;
}
}
return filePath;
}
QString App::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;
}
bool App::useFetchConfig(const QString &filePath) {
if (!filePath.isEmpty()) {
if (CoreManager::getInstance()->isInitialized()) {
if (CoreManager::getInstance()->getSettingsModel()->getAutoApplyProvisioningConfigUriHandlerEnabled())
return setFetchConfig(filePath);
else emit requestFetchConfig(filePath);
} else {
QObject *context = new QObject();
connect(CoreManager::getInstance(), &CoreManager::coreManagerInitialized, context,
[context, filePath, this]() {
useFetchConfig(filePath);
context->deleteLater();
});
}
}
return false;
}
bool App::setFetchConfig(QString filePath) {
bool fetched = false;
qDebug() << "setFetchConfig with " << filePath;
if (!filePath.isEmpty()) {
auto instance = CoreManager::getInstance();
if (instance) {
auto core = instance->getCore();
if (core) {
filePath.replace('\\', '/');
fetched = core->setProvisioningUri(Utils::appStringToCoreString(filePath)) == 0;
}
}
}
if (!fetched) {
qWarning() << "Remote provisioning cannot be retrieved. Command have beend cleaned";
} else {
qInfo() << "Restarting to apply remote provisioning";
restart();
}
return fetched;
}
// -----------------------------------------------------------------------------
App::App(int &argc, char *argv[])
: SingleApplication(argc, argv, true, Mode::User | Mode::ExcludeAppPath | Mode::ExcludeAppVersion) {
// Ignore vertical sync. This way, we avoid blinking on resizes(and other refresh steps like layouts etc.).
auto ignoreVSync = QSurfaceFormat::defaultFormat();
ignoreVSync.setSwapInterval(0);
QSurfaceFormat::setDefaultFormat(ignoreVSync);
connect(this, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this,
SLOT(stateChanged(Qt::ApplicationState)));
setWindowIcon(QIcon(Constants::WindowIconPath));
#ifdef Q_OS_WIN
char tz[255] = {0};
size_t envSize = 0;
getenv_s(&envSize, tz, 255, "TZ");
if (envSize == 0 || tz[0] == '\0') { // If not set, set the environment variable for uses of mktime from the SDK.
long adjustTimezone;
_tzset(); // init timezone variable
auto error = _get_timezone(&adjustTimezone);
if (adjustTimezone != -QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime())) {
QString timeZone = QTimeZone::systemTimeZoneId();
_putenv(("TZ=" + timeZone.toStdString()).c_str());
_tzset();
qInfo() << "Set TimeZone to " << timeZone;
}
} else qInfo() << "Use environment TimeZone:" << tz;
#else
char *tz = getenv("TZ");
if (!tz) { // If not set, set the environment variable for uses of mktime from the SDK.
tzset(); // init timezone variable
if (timezone != -QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime())) {
QString timeZone = QTimeZone::systemTimeZoneId();
setenv("TZ", timeZone.toStdString().c_str(), 1);
tzset();
qInfo() << "Set TimeZone to " << timeZone;
}
} else qInfo() << "Use environment TimeZone:" << tz;
#endif
// Use UTF-8 for internals. Linphone uses UTF-8 so there will be no loss on
// data with less precise encodings. Qt will do the rest.
// bctbx_set_default_encoding(Constants::LinphoneLocaleEncoding);
setlocale(LC_CTYPE, ".UTF8");
createParser();
mParser->parse(this->arguments());
// Get configuration for translators
shared_ptr config =
Utils::getConfigIfExists(QString::fromStdString(getConfigPathIfExists(*mParser)));
// Init locale.
mTranslator = new DefaultTranslator(this);
mDefaultTranslator = new DefaultTranslator(this);
initLocale(config);
Logger::init(config);
createParser(); // Recreate parser in order to use translations from config.
mParser->process(*this);
if (mParser->isSet("verbose")) Logger::getInstance()->setVerbose(true);
if (mParser->isSet("qt-logs-only")) Logger::getInstance()->enableQtOnly(true);
// List available locales.
for (const auto &locale : QDir(Constants::LanguagePath).entryList())
mAvailableLocales << QLocale(locale);
if (mParser->isSet("help")) {
mParser->showHelp();
}
if (mParser->isSet("cli-help")) {
Cli::showHelp();
::exit(EXIT_SUCCESS);
}
if (mParser->isSet("version")) mParser->showVersion();
mAutoStart = false;
mAutoStart = autoStartEnabled();
qInfo() << QStringLiteral("Starting application " APPLICATION_NAME " (bin: " EXECUTABLE_NAME
"). Version:%1 Os:%2 Qt:%3")
.arg(applicationVersion())
.arg(Utils::getOsProduct())
.arg(qVersion());
qInfo() << QStringLiteral("Use locale: %1 with language: %2")
.arg(mLocale.name())
.arg(QLocale::languageToString(mLocale.language()));
qInfo() << QStringLiteral("System timezone: code=%1 / country=%2 / Offset=%3 / ID=%4")
.arg(QTimeZone::systemTimeZone().country())
.arg(Utils::getCountryName(QTimeZone::systemTimeZone().country()))
.arg(QTimeZone::systemTimeZone().standardTimeOffset(QDateTime::currentDateTime()))
.arg(QString(QTimeZone::systemTimeZoneId()));
// Deal with received messages and CLI.
QObject::connect(this, &App::receivedMessage, this, [](int, const QByteArray &byteArray) {
QString command(byteArray);
qInfo() << QStringLiteral("Received command from other application: `%1`.").arg(command);
Cli::executeCommand(command);
});
mCheckForUpdateUserInitiated = false;
}
App::~App() {
qInfo() << QStringLiteral("Destroying app...");
}
void App::stop() {
qInfo() << QStringLiteral("Stopping app...");
if (mEngine) {
delete mEngine;
processEvents(QEventLoop::AllEvents);
}
CoreManager::uninit();
processEvents(QEventLoop::AllEvents); // Process all needed events on engine deletion.
if (mParser) delete mParser;
}
// -----------------------------------------------------------------------------
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);
}
if (!mParser->parse(parameters)) qWarning() << "Parsing error : " << mParser->errorText();
}
static QQuickWindow *createSubWindow(QQmlApplicationEngine *engine, const char *path) {
QString qPath(path);
qInfo() << QStringLiteral("Creating subwindow: `%1`.").arg(path);
QQmlComponent component(engine, QUrl(path));
if (component.isError()) {
qWarning() << component.errors();
abort();
}
qInfo() << QStringLiteral("Subwindow status: `%1`.").arg(component.status());
QObject *object = component.create();
Q_ASSERT(object);
QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
object->setParent(engine);
int totalDuration = 0;
int loops = 0;
QElapsedTimer timer;
timer.start();
auto window = qobject_cast(object);
QObject::connect(window, &QQuickWindow::beforeRendering, [totalDuration, loops, timer, path]() mutable {
totalDuration += timer.elapsed();
++loops;
if (totalDuration > 10 * 1000) {
qDebug() << path << " : " << (1000.0 * loops) / totalDuration << "fps";
totalDuration = 0;
loops = 0;
}
timer.restart();
});
return window;
}
// -----------------------------------------------------------------------------
void App::initContentApp() {
std::string configPath;
shared_ptr config;
bool mustBeIconified = false;
bool needRestart = true;
// Destroy qml components and linphone core if necessary.
if (mEngine) {
needRestart = false;
setOpened(false);
qInfo() << QStringLiteral("Restarting app...");
delete mEngine;
mNotifier = nullptr;
//
CoreManager::uninit();
removeTranslator(mTranslator);
removeTranslator(mDefaultTranslator);
delete mTranslator;
delete mDefaultTranslator;
mTranslator = new DefaultTranslator(this);
mDefaultTranslator = new DefaultTranslator(this);
configPath = getConfigPathIfExists(*mParser);
config = Utils::getConfigIfExists(QString::fromStdString(configPath));
initLocale(config);
} else {
configPath = getConfigPathIfExists(*mParser);
config = Utils::getConfigIfExists(QString::fromStdString(configPath));
// Update and download codecs.
VideoCodecsModel::updateCodecs();
VideoCodecsModel::downloadUpdatableCodecs(this);
// Don't quit if last window is closed!!!
setQuitOnLastWindowClosed(false);
#ifndef Q_OS_MACOS
mustBeIconified = mParser->isSet("iconified");
#endif // ifndef Q_OS_MACOS
mColorListModel = new ColorListModel();
mImageListModel = new ImageListModel();
}
// Change colors if necessary.
mColorListModel->useConfig(config);
mImageListModel->useConfig(config);
// There is no more database for callback. Setting it in the configuration before starting the core will do
// migration. When the migration is done by SDK, further migrations on call logs will do nothing. It is safe to use
// .
config->setString("storage", "call_logs_db_uri", Paths::getCallHistoryFilePath());
// Init core.
CoreManager::init(this, Utils::coreStringToAppString(configPath));
// Init engine content.
mEngine = new QQmlApplicationEngine(this);
// Provide `+custom` folders for custom components and `5.9` for old components.
{
QStringList selectors("custom");
const QVersionNumber &version = QLibraryInfo::version();
if (version.majorVersion() == 5 && version.minorVersion() == 9) selectors.push_back("5.9");
(new QQmlFileSelector(mEngine, mEngine))->setExtraSelectors(selectors);
}
qInfo() << QStringLiteral("Activated selectors:") << QQmlFileSelector::get(mEngine)->selector()->allSelectors();
// Set modules paths.
mEngine->addImportPath(":/ui/modules");
mEngine->addImportPath(":/ui/scripts");
mEngine->addImportPath(":/ui/views");
// Provide avatars/thumbnails providers.
mEngine->addImageProvider(AvatarProvider::ProviderId, new AvatarProvider());
mEngine->addImageProvider(ImageProvider::ProviderId, new ImageProvider());
mEngine->addImageProvider(ExternalImageProvider::ProviderId, new ExternalImageProvider());
mEngine->addImageProvider(QRCodeProvider::ProviderId, new QRCodeProvider());
mEngine->addImageProvider(ThumbnailProvider::ProviderId, new ThumbnailProvider());
mEngine->rootContext()->setContextProperty("applicationName", APPLICATION_NAME);
mEngine->rootContext()->setContextProperty("executableName", EXECUTABLE_NAME);
#ifdef APPLICATION_URL
mEngine->rootContext()->setContextProperty("applicationUrl", APPLICATION_URL);
#else
mEngine->rootContext()->setContextProperty("applicationUrl", "");
#endif
#ifdef APPLICATION_VENDOR
mEngine->rootContext()->setContextProperty("applicationVendor", APPLICATION_VENDOR);
#else
mEngine->rootContext()->setContextProperty("applicationVendor", "");
#endif
#ifdef 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);
#else
mEngine->rootContext()->setContextProperty("applicationLicenceUrl", "");
#endif
#ifdef COPYRIGHT_RANGE_DATE
mEngine->rootContext()->setContextProperty("copyrightRangeDate", COPYRIGHT_RANGE_DATE);
#else
mEngine->rootContext()->setContextProperty("copyrightRangeDate", "");
#endif
mEngine->rootContext()->setContextProperty("Colors", mColorListModel->getQmlData());
mEngine->rootContext()->setContextProperty("Images", mImageListModel->getQmlData());
mEngine->rootContext()->setContextProperty("qtIsNewer_5_15_0", QT_VERSION >= QT_VERSION_CHECK(5, 15, 0));
registerTypes();
registerSharedTypes();
registerToolTypes();
registerSharedToolTypes();
registerUninstalledModules();
// Enable notifications.
mNotifier = new Notifier(mEngine);
// Load main view.
qInfo() << QStringLiteral("Loading main view...");
mEngine->load(QUrl(Constants::QmlViewMainWindow));
if (mEngine->rootObjects().isEmpty()) qFatal("Unable to open main window.");
QObject::connect(CoreManager::getInstance(), &CoreManager::coreManagerInitialized, CoreManager::getInstance(),
[this, mustBeIconified]() mutable {
if (CoreManager::getInstance()->started()) {
openAppAfterInit(mustBeIconified);
}
});
}
// -----------------------------------------------------------------------------
QString App::getCommandArgument() {
const QStringList &arguments = mParser->positionalArguments();
return arguments.empty() ? QString("") : arguments[0];
}
// -----------------------------------------------------------------------------
#ifdef Q_OS_MACOS
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);
}
Cli::executeCommand(url);
} else if (event->type() == QEvent::ApplicationStateChange) {
auto state = static_cast(event);
if (state->applicationState() == Qt::ApplicationActive) smartShowWindow(getMainWindow());
}
return SingleApplication::event(event);
}
#endif // ifdef Q_OS_MACOS
// -----------------------------------------------------------------------------
QQuickWindow *App::getCallsWindow() const {
if (CoreManager::getInstance()->getCore()->getConfig()->getInt(SettingsModel::UiSection, "disable_calls_window", 0))
return nullptr;
return mCallsWindow;
}
QQuickWindow *App::getMainWindow() const {
return qobject_cast(const_cast(mEngine)->rootObjects().at(0));
}
QQuickWindow *App::getSettingsWindow() const {
return mSettingsWindow;
}
// -----------------------------------------------------------------------------
void App::smartShowWindow(QQuickWindow *window) {
if (!window) return;
window->setVisible(true);
// Force show, maybe redundant with setVisible
if (window->visibility() == QWindow::Maximized) // Avoid to change visibility mode
window->showMaximized();
else window->show();
window->raise(); // Raise ensure to get focus on Mac
window->requestActivate();
}
// -----------------------------------------------------------------------------
bool App::hasFocus() const {
return getMainWindow()->isActive() || (mCallsWindow && mCallsWindow->isActive());
}
void App::stateChanged(Qt::ApplicationState pState) {
DesktopTools::applicationStateChanged(pState);
auto core = CoreManager::getInstance();
if (core) core->stateChanged(pState);
}
// -----------------------------------------------------------------------------
void App::createParser() {
delete mParser;
mParser = new QCommandLineParser();
mParser->setApplicationDescription(tr("applicationDescription"));
mParser->addPositionalArgument("command", tr("commandLineDescription").replace("%1", APPLICATION_NAME),
"[command]");
mParser->addOptions({
{{"h", "help"}, tr("commandLineOptionHelp")},
{"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
{{"V", "verbose"}, tr("commandLineOptionVerbose")},
{"qt-logs-only", tr("commandLineOptionQtLogsOnly")},
});
}
// -----------------------------------------------------------------------------
template
static QObject *makeSharedSingleton(QQmlEngine *, QJSEngine *) {
QObject *object = (*function)();
QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
return object;
}
template
static inline void registerSharedSingletonType(const char *name) {
qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, name, makeSharedSingleton);
}
template
static QObject *makeSharedSingleton(QQmlEngine *, QJSEngine *) {
QObject *object = (CoreManager::getInstance()->*function)();
QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
return object;
}
template
static QObject *makeSharedSingleton(QQmlEngine *, QJSEngine *) {
QObject *object = (CoreManager::getInstance()->*function)();
QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
return object;
}
template
static inline void registerSharedSingletonType(const char *name) {
qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, name, makeSharedSingleton);
}
template
static inline void registerSharedSingletonType(const char *name) {
qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, name, makeSharedSingleton);
}
template
static inline void registerUncreatableType(const char *name) {
qmlRegisterUncreatableType(Constants::MainQmlUri, 1, 0, name, QLatin1String("Uncreatable"));
}
template
static inline void registerSingletonType(const char *name) {
qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, name,
[](QQmlEngine *engine, QJSEngine *) -> QObject * { return new T(engine); });
}
template
static inline void registerType(const char *name) {
qmlRegisterType(Constants::MainQmlUri, 1, 0, name);
}
template
static inline void registerToolType(const char *name, const int &major_version = 1, const int &minor_version = 0) {
qmlRegisterSingletonType(name, major_version, minor_version, name,
[](QQmlEngine *engine, QJSEngine *) -> QObject * { return new T(engine); });
}
template
static QObject *makeSharedTool(QQmlEngine *, QJSEngine *) {
QObject *object = (Owner::getInstance()->*function)();
QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
return object;
}
template
static inline void registerSharedToolType(const char *name) {
qmlRegisterSingletonType(name, 1, 0, name, makeSharedTool);
}
void App::registerTypes() {
qInfo() << QStringLiteral("Registering types...");
qRegisterMetaType>();
qRegisterMetaType();
qRegisterMetaType>();
qRegisterMetaType>>();
qRegisterMetaType>();
qRegisterMetaType>();
qRegisterMetaType>();
qRegisterMetaType>();
qRegisterMetaType>();
qRegisterMetaType>();
qRegisterMetaType>();
qRegisterMetaType>();
// qRegisterMetaType>();
LinphoneEnums::registerMetaTypes();
registerType("AssistantModel");
registerType("AuthenticationNotifier");
registerType("CallsListProxyModel");
registerType("Camera");
registerType("ChatRoomProxyModel");
registerType("ChatReactionProxyModel");
registerType("ConferenceHelperModel");
registerType("ConferenceProxyModel");
registerType("ConferenceInfoModel");
registerType("ConferenceInfoProxyModel");
registerType("ContactsListProxyModel");
registerType("ContactsImporterListProxyModel");
registerType("ContentProxyModel");
registerType("DateModel");
registerType("FileDownloader");
registerType("FileExtractor");
registerType("HistoryProxyModel");
registerType("LdapProxyModel");
registerType("ParticipantImdnStateProxyModel");
registerType("RecordingProxyModel");
registerType("SipAddressesProxyModel");
registerType("SearchSipAddressesModel");
registerType("SearchSipAddressesProxyModel");
registerType("TemporaryFile");
registerType("TimeZoneProxyModel");
registerType("CallHistoryProxyModel");
registerType("ColorProxyModel");
registerType("ImageColorsProxyModel");
registerType("ImageProxyModel");
registerType("TimelineProxyModel");
registerType("ParticipantProxyModel");
registerType("ParticipantDeviceProxyModel");
registerType("SoundPlayer");
registerType("TelephoneNumbersModel");
registerType("SpellChecker");
registerSingletonType("AudioCodecsModel");
registerSingletonType("OwnPresenceModel");
registerSingletonType("Presence");
// registerSingletonType("TimelineModel");
registerSingletonType("UrlHandlers");
registerSingletonType("VideoCodecsModel");
registerUncreatableType("CallModel");
registerUncreatableType("CallHistoryModel");
registerUncreatableType("ChatCallModel");
registerUncreatableType("ChatMessageModel");
registerUncreatableType("ChatNoticeModel");
registerUncreatableType("ChatReactionListModel");
registerUncreatableType("ChatRoomModel");
registerUncreatableType("ColorModel");
registerUncreatableType("ImageModel");
registerUncreatableType("ConferenceAddModel");
registerUncreatableType("ConferenceModel");
registerUncreatableType("ContactModel");
registerUncreatableType("ContactsImporterModel");
registerUncreatableType("ContentModel");
registerUncreatableType("ContentListModel");
registerUncreatableType("FileMediaModel");
registerUncreatableType("HistoryModel");
registerUncreatableType("LdapModel");
registerUncreatableType("RecorderModel");
registerUncreatableType("SearchResultModel");
registerUncreatableType("SipAddressObserver");
registerUncreatableType("VcardModel");
registerUncreatableType("TimelineModel");
registerUncreatableType("TunnelModel");
registerUncreatableType("TunnelConfigModel");
registerUncreatableType("TunnelConfigProxyModel");
registerUncreatableType("ParticipantModel");
registerUncreatableType("ParticipantListModel");
registerUncreatableType("ParticipantDeviceModel");
registerUncreatableType("ParticipantDeviceListModel");
registerUncreatableType("ParticipantImdnStateModel");
registerUncreatableType("ParticipantImdnStateListModel");
qmlRegisterUncreatableMetaObject(LinphoneEnums::staticMetaObject, "LinphoneEnums", 1, 0, "LinphoneEnums",
"Only enums");
}
void App::registerSharedTypes() {
qInfo() << QStringLiteral("Registering shared types...");
registerSharedSingletonType("App");
registerSharedSingletonType("CoreManager");
registerSharedSingletonType("SettingsModel");
registerSharedSingletonType("EmojisSettingsModel");
registerSharedSingletonType("AccountSettingsModel");
registerSharedSingletonType("SipAddressesModel");
registerSharedSingletonType("CallsListModel");
registerSharedSingletonType("ContactsListModel");
registerSharedSingletonType(
"ContactsImporterListModel");
registerSharedSingletonType("LdapListModel");
registerSharedSingletonType("TimelineListModel");
registerSharedSingletonType("RecorderManager");
// qmlRegisterSingletonType(Constants::MainQmlUri, 1, 0, "ColorList", mColorListModel);
// registerSharedSingletonType("ColorCpp");
}
void App::registerToolTypes() {
qInfo() << QStringLiteral("Registering tool types...");
registerToolType("Clipboard");
registerToolType("DesktopTools");
registerToolType("TextToSpeech");
registerToolType("Units");
registerToolType("ContactsImporterPluginsManager");
registerToolType("UtilsCpp");
registerToolType("ConstantsCpp");
}
void App::registerSharedToolTypes() {
qInfo() << QStringLiteral("Registering shared tool types...");
registerSharedToolType("ColorsList");
}
void App::registerUninstalledModules() {
if (!isPdfAvailable()) qmlRegisterModule("QtQuick.Pdf", 5, 15);
}
// -----------------------------------------------------------------------------
void App::setTrayIcon() {
QQuickWindow *root = getMainWindow();
QSystemTrayIcon *systemTrayIcon =
(mSystemTrayIcon
? mSystemTrayIcon
: new QSystemTrayIcon(
nullptr)); // Workaround : QSystemTrayIcon cannot be deleted because of setContextMenu (indirectly)
// trayIcon: Right click actions.
QAction *settingsAction = new QAction(tr("settings"), root);
root->connect(settingsAction, &QAction::triggered, root, [this] { App::smartShowWindow(getSettingsWindow()); });
QAction *updateCheckAction = nullptr;
if (SettingsModel::isCheckForUpdateAvailable()) {
updateCheckAction = new QAction(tr("checkForUpdates"), root);
root->connect(updateCheckAction, &QAction::triggered, root, [this] { checkForUpdates(true); });
}
QAction *aboutAction = new QAction(tr("about"), root);
root->connect(aboutAction, &QAction::triggered, root, [root] {
App::smartShowWindow(root);
QMetaObject::invokeMethod(root, Constants::AttachVirtualWindowMethodName, Qt::DirectConnection,
Q_ARG(QVariant, QUrl(Constants::AboutPath)), Q_ARG(QVariant, QVariant()),
Q_ARG(QVariant, QVariant()));
});
QAction *restoreAction = new QAction(tr("restore"), root);
root->connect(restoreAction, &QAction::triggered, root, [root] { smartShowWindow(root); });
QAction *quitAction = new QAction(tr("quit"), root);
root->connect(quitAction, &QAction::triggered, this, &App::quit);
// trayIcon: Left click actions.
static QMenu *menu =
new QMenu(); // Static : Workaround about a bug with setContextMenu where it cannot be called more than once.
root->connect(systemTrayIcon, &QSystemTrayIcon::activated, [root](QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger) {
if (root->visibility() == QWindow::Hidden) smartShowWindow(root);
else root->hide();
}
});
menu->setTitle(APPLICATION_NAME);
// Build trayIcon menu.
menu->addAction(settingsAction);
if (updateCheckAction) menu->addAction(updateCheckAction);
menu->addAction(aboutAction);
menu->addSeparator();
menu->addAction(restoreAction);
menu->addSeparator();
menu->addAction(quitAction);
if (!mSystemTrayIcon)
systemTrayIcon->setContextMenu(menu); // This is a Qt bug. We cannot call setContextMenu more than once. So we
// have to keep an instance of the menu.
systemTrayIcon->setIcon(QIcon(Constants::WindowIconPath));
systemTrayIcon->setToolTip(APPLICATION_NAME);
systemTrayIcon->show();
if (!mSystemTrayIcon) mSystemTrayIcon = systemTrayIcon;
if (!QSystemTrayIcon::isSystemTrayAvailable()) qInfo() << "System tray is not available";
}
// -----------------------------------------------------------------------------
void App::initLocale(const shared_ptr &config) {
// Try to use preferred locale.
QString locale;
// Use english. This default translator is used if there are no found translations in others loads
mLocale = QLocale(Constants::DefaultLocale);
if (!installLocale(*this, *mDefaultTranslator, mLocale)) qFatal("Unable to install default translator.");
if (config) locale = Utils::coreStringToAppString(config->getString(SettingsModel::UiSection, "locale", ""));
if (!locale.isEmpty() && installLocale(*this, *mTranslator, QLocale(locale))) {
mLocale = QLocale(locale);
return;
}
// Try to use system locale.
// #ifdef Q_OS_MACOS
// Use this workaround if there is still an issue about detecting wrong language from system on Mac. Qt doesn't use
// the current system language on QLocale::system(). So we need to get it from user settings and overwrite its
// Locale.
// QSettings settings;
// QString preferredLanguage = settings.value("AppleLanguages").toStringList().first();
// QStringList qtLocale = QLocale::system().name().split('_');
// if(qtLocale[0] != preferredLanguage){
// qInfo() << "Override Qt language from " << qtLocale[0] << " to the preferred language : " <<
// preferredLanguage; qtLocale[0] = preferredLanguage;
// }
// QLocale sysLocale = QLocale(qtLocale.join('_'));
// #else
QLocale sysLocale(QLocale::system().name()); // Use Locale from name because Qt has a bug where it didn't use the
// QLocale::language (aka : translator.language != locale.language) on
// Mac. #endif
if (installLocale(*this, *mTranslator, sysLocale)) {
mLocale = sysLocale;
return;
}
}
QString App::getConfigLocale() const {
return Utils::coreStringToAppString(
CoreManager::getInstance()->getCore()->getConfig()->getString(SettingsModel::UiSection, "locale", ""));
}
void App::setConfigLocale(const QString &locale) {
CoreManager::getInstance()->getCore()->getConfig()->setString(SettingsModel::UiSection, "locale",
Utils::appStringToCoreString(locale));
emit configLocaleChanged(locale);
}
QLocale App::getLocale() const {
return mLocale;
}
// -----------------------------------------------------------------------------
#ifdef Q_OS_LINUX
void App::setAutoStart(bool enabled) {
if (enabled == mAutoStart) return;
QDir dir(AutoStartDirectory);
if (!dir.exists() && !dir.mkpath(AutoStartDirectory)) {
qWarning() << QStringLiteral("Unable to build autostart dir path: `%1`.").arg(AutoStartDirectory);
return;
}
const QString confPath(AutoStartDirectory + EXECUTABLE_NAME ".desktop");
if (generateDesktopFile(confPath, !enabled, true)) {
mAutoStart = enabled;
emit autoStartChanged(enabled);
}
}
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);
}
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;
}
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/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;\n"
"X-PulseAudio-Properties=media.role=phone\n";
return true;
}
#elif defined(Q_OS_MACOS)
void App::setAutoStart(bool enabled) {
if (enabled == mAutoStart) return;
if (getMacOsBundlePath().isEmpty()) {
qWarning() << QStringLiteral("Application is not installed. Unable to change autostart state.");
return;
}
if (enabled)
QProcess::execute(OsascriptExecutable,
{"-e", "tell application \"System Events\" to make login item at end with properties"
"{ path: \"" +
getMacOsBundlePath() + "\", hidden: false }"});
else
QProcess::execute(OsascriptExecutable, {"-e", "tell application \"System Events\" to delete login item \"" +
getMacOsBundleName() + "\""});
mAutoStart = enabled;
emit autoStartChanged(enabled);
}
#else
void App::setAutoStart(bool enabled) {
if (enabled == mAutoStart) return;
QSettings settings(AutoStartSettingsFilePath, QSettings::NativeFormat);
if (enabled) settings.setValue(EXECUTABLE_NAME, QDir::toNativeSeparators(applicationFilePath()));
else settings.remove(EXECUTABLE_NAME);
mAutoStart = enabled;
emit autoStartChanged(enabled);
}
#endif // ifdef Q_OS_LINUX
// -----------------------------------------------------------------------------
void App::openAppAfterInit(bool mustBeIconified) {
qInfo() << QStringLiteral("Open " APPLICATION_NAME " app.");
auto coreManager = CoreManager::getInstance();
coreManager->getSettingsModel()->updateCameraMode();
// Create other windows.
mCallsWindow = createSubWindow(mEngine, Constants::QmlViewCallsWindow);
mSettingsWindow = createSubWindow(mEngine, Constants::QmlViewSettingsWindow);
QObject::connect(mSettingsWindow, &QWindow::visibilityChanged, this, [coreManager](QWindow::Visibility visibility) {
if (visibility == QWindow::Hidden) {
qInfo() << QStringLiteral("Update nat policy.");
shared_ptr core = coreManager->getCore();
core->setNatPolicy(core->getNatPolicy());
}
});
QQuickWindow *mainWindow = getMainWindow();
#ifndef __APPLE__
// Enable TrayIconSystem.
if (!QSystemTrayIcon::isSystemTrayAvailable()) qWarning("System tray not found on this system.");
else setTrayIcon();
#endif // ifndef __APPLE__
// Display Assistant if it does not exist proxy config.
if (coreManager->getAccountList().empty())
QMetaObject::invokeMethod(mainWindow, "setView", Q_ARG(QVariant, Constants::AssistantViewName),
Q_ARG(QVariant, QString("")), Q_ARG(QVariant, QString("")));
#ifdef ENABLE_UPDATE_CHECK
QTimer *timer = new QTimer(mEngine);
timer->setInterval(Constants::VersionUpdateCheckInterval);
QObject::connect(timer, &QTimer::timeout, this, &App::checkForUpdate);
timer->start();
checkForUpdates();
#endif // ifdef ENABLE_UPDATE_CHECK
QString fetchFilePath = getFetchConfig(mParser);
mustBeIconified =
mustBeIconified &&
(fetchFilePath.isEmpty() ||
CoreManager::getInstance()->getSettingsModel()->getAutoApplyProvisioningConfigUriHandlerEnabled());
bool showWindow = true;
if (fetchFilePath.isEmpty()) {
QString lastRunningVersion = CoreManager::getInstance()->getSettingsModel()->getLastRunningVersionOfApp();
if (lastRunningVersion != "unknown" && lastRunningVersion != applicationVersion()) {
emit CoreManager::getInstance()->userInitiatedVersionUpdateCheckResult(3, "", "");
}
CoreManager::getInstance()->getSettingsModel()->setLastRunningVersionOfApp(applicationVersion());
// Launch call if wanted and clean parser
if (mParser->isSet("call") && coreManager->isLastRemoteProvisioningGood()) {
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::coreManagerInitialized, context,
[sipAddress, coreManager, context]() mutable {
if (context) {
delete context;
context = nullptr;
coreManager->getCallsListModel()->launchAudioCall(sipAddress);
}
});
}
} else {
// Execute command argument if needed
// Commands are executed only once. clearPsitionalArguments doesn't work as its name suggest :
// getPositionalArguments still retrieve user arguments. So execute the command only once.
static bool firstRun = false;
if (!firstRun) {
firstRun = true;
const QString commandArgument = getCommandArgument();
if (!commandArgument.isEmpty()) {
Cli::CommandFormat format;
Cli::executeCommand(commandArgument, &format);
if (format == Cli::UriFormat || format == Cli::UrlFormat) mustBeIconified = true;
}
}
}
} else showWindow = !useFetchConfig(fetchFilePath);
if (showWindow) {
#ifndef __APPLE__
if (!mustBeIconified) smartShowWindow(mainWindow);
#else
Q_UNUSED(mustBeIconified);
smartShowWindow(mainWindow);
#endif
setOpened(true);
}
}
// -----------------------------------------------------------------------------
QString App::getStrippedApplicationVersion() { // x.y.z but if 'z-*' then x.y.z-1
QString currentVersion = applicationVersion();
QStringList versions = currentVersion.split('.');
if (versions.size() >= 3) {
currentVersion = versions[0] + "." + versions[1] + ".";
QStringList patchVersions = versions[2].split('-');
if (patchVersions.size() > 1) {
bool ok;
patchVersions[1].toInt(&ok);
if (!ok) // Second part of patch is not a number (ie: alpha, beta, pre). Reduce version.
currentVersion += QString::number(patchVersions[0].toInt() - 1);
else currentVersion += patchVersions[0];
} else currentVersion += patchVersions[0];
}
return currentVersion;
}
void App::checkForUpdate() {
checkForUpdates(false);
}
void App::checkForUpdates(bool force) {
if (force || CoreManager::getInstance()->getSettingsModel()->isCheckForUpdateEnabled()) {
getInstance()->mCheckForUpdateUserInitiated = force;
CoreManager::getInstance()->getCore()->checkForUpdate(Utils::appStringToCoreString(applicationVersion()));
}
}
bool App::isPdfAvailable() {
#ifdef PDF_ENABLED
return true;
#else
return false;
#endif
}
bool App::isLinux() {
#ifdef Q_OS_LINUX
return true;
#else
return false;
#endif
}