// For license of this file, see /LICENSE.md. #include "miscellaneous/application.h" #include "3rd-party/boolinq/boolinq.h" #include "dynamic-shortcuts/dynamicshortcuts.h" #include "exceptions/applicationexception.h" #include "gui/dialogs/formabout.h" #include "gui/dialogs/formmain.h" #include "gui/feedmessageviewer.h" #include "gui/feedsview.h" #include "gui/messagebox.h" #include "gui/statusbar.h" #include "miscellaneous/feedreader.h" #include "miscellaneous/iconfactory.h" #include "miscellaneous/iofactory.h" #include "miscellaneous/mutex.h" #include "network-web/webfactory.h" #include "services/abstract/serviceroot.h" #include "services/owncloud/owncloudserviceentrypoint.h" #include "services/standard/standardserviceentrypoint.h" #include "services/standard/standardserviceroot.h" #include "services/tt-rss/ttrssserviceentrypoint.h" #include #include #include #if defined(USE_WEBENGINE) #include "network-web/adblock/adblockicon.h" #include "network-web/adblock/adblockmanager.h" #include "network-web/networkurlinterceptor.h" #include #include #endif Application::Application(const QString& id, int& argc, char** argv) : QtSingleApplication(id, argc, argv), m_updateFeedsLock(new Mutex()) { parseCmdArguments(); qInstallMessageHandler(performLogging); m_feedReader = nullptr; m_quitLogicDone = false; m_mainForm = nullptr; m_trayIcon = nullptr; m_settings = Settings::setupSettings(this); m_webFactory = new WebFactory(this); m_system = new SystemFactory(this); m_skins = new SkinFactory(this); m_localization = new Localization(this); m_icons = new IconFactory(this); m_database = new DatabaseFactory(this); m_downloadManager = nullptr; m_shouldRestart = false; determineFirstRuns(); //: Abbreviation of language, e.g. en. //: Use ISO 639-1 code here combined with ISO 3166-1 (alpha-2) code. //: Examples: "cs", "en", "it", "cs_CZ", "en_GB", "en_US". QObject::tr("LANG_ABBREV"); //: Name of translator - optional. QObject::tr("LANG_AUTHOR"); connect(this, &Application::aboutToQuit, this, &Application::onAboutToQuit); connect(this, &Application::commitDataRequest, this, &Application::onCommitData); connect(this, &Application::saveStateRequest, this, &Application::onSaveState); #if defined(USE_WEBENGINE) connect(QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested, this, &Application::downloadRequested); #endif m_webFactory->updateProxy(); #if defined(USE_WEBENGINE) m_webFactory->urlIinterceptor()->load(); m_webFactory->adBlock()->load(true); #endif } Application::~Application() { qDebugNN << LOGSEC_CORE << "Destroying Application instance."; } QString s_customLogFile = QString(); void Application::performLogging(QtMsgType type, const QMessageLogContext& context, const QString& msg) { #ifndef QT_NO_DEBUG_OUTPUT QString console_message = qFormatLogMessage(type, context, msg); std::cerr << console_message.toStdString() << std::endl; if (!s_customLogFile.isEmpty()) { QFile log_file(s_customLogFile); if (log_file.open(QFile::OpenModeFlag::Append | QFile::OpenModeFlag::Unbuffered)) { log_file.write(console_message.toUtf8()); log_file.write(QSL("\r\n").toUtf8()); log_file.close(); } } if (type == QtMsgType::QtFatalMsg) { qApp->exit(EXIT_FAILURE); } #else Q_UNUSED(type) Q_UNUSED(context) Q_UNUSED(msg) #endif } void Application::reactOnForeignNotifications() { connect(this, &Application::messageReceived, this, &Application::processExecutionMessage); } void Application::hideOrShowMainForm() { // Display main window. if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MainWindowStartsHidden)).toBool() && SystemTrayIcon::isSystemTrayActivated()) { qDebugNN << LOGSEC_CORE << "Hiding the main window when the application is starting."; mainForm()->switchVisibility(true); } else { qDebugNN << LOGSEC_CORE << "Showing the main window when the application is starting."; mainForm()->show(); } } void Application::loadDynamicShortcuts() { DynamicShortcuts::load(userActions()); } void Application::showPolls() const { /*if (isFirstRunCurrentVersion()) { web()->openUrlInExternalBrowser(QSL("https://forms.gle/Son3h3xg2ZtCmi9K8")); }*/ } void Application::offerChanges() const { if (isFirstRunCurrentVersion()) { qApp->showGuiMessage(QSL(APP_NAME), QObject::tr("Welcome to %1.\n\nPlease, check NEW stuff included in this\n" "version by clicking this popup notification.").arg(APP_LONG_NAME), QSystemTrayIcon::MessageIcon::NoIcon, nullptr, false, [] { FormAbout(qApp->mainForm()).exec(); }); } } bool Application::isAlreadyRunning() { return m_allowMultipleInstances ? false : sendMessage((QStringList() << APP_IS_RUNNING << Application::arguments().mid(1)).join(ARGUMENTS_LIST_SEPARATOR)); } FeedReader* Application::feedReader() { return m_feedReader; } QList Application::userActions() { if (m_mainForm != nullptr && m_userActions.isEmpty()) { m_userActions = m_mainForm->allActions(); #if defined(USE_WEBENGINE) m_userActions.append(m_webFactory->adBlock()->adBlockIcon()); #endif } return m_userActions; } bool Application::isFirstRun() const { return m_firstRunEver; } bool Application::isFirstRunCurrentVersion() const { return m_firstRunCurrentVersion; } QCommandLineParser* Application::cmdParser() { return &m_cmdParser; } WebFactory* Application::web() const { return m_webFactory; } SystemFactory* Application::system() { return m_system; } SkinFactory* Application::skins() { return m_skins; } Localization* Application::localization() { return m_localization; } DatabaseFactory* Application::database() { return m_database; } void Application::eliminateFirstRuns() { settings()->setValue(GROUP(General), General::FirstRun, false); settings()->setValue(GROUP(General), QString(General::FirstRun) + QL1C('_') + APP_VERSION, false); } void Application::setFeedReader(FeedReader* feed_reader) { m_feedReader = feed_reader; connect(m_feedReader, &FeedReader::feedUpdatesFinished, this, &Application::onFeedUpdatesFinished); } IconFactory* Application::icons() { return m_icons; } DownloadManager* Application::downloadManager() { if (m_downloadManager == nullptr) { m_downloadManager = new DownloadManager(); connect(m_downloadManager, &DownloadManager::downloadFinished, mainForm()->statusBar(), &StatusBar::clearProgressDownload); connect(m_downloadManager, &DownloadManager::downloadProgressed, mainForm()->statusBar(), &StatusBar::showProgressDownload); } return m_downloadManager; } Settings* Application::settings() const { return m_settings; } Mutex* Application::feedUpdateLock() { return m_updateFeedsLock.data(); } FormMain* Application::mainForm() { return m_mainForm; } QWidget* Application::mainFormWidget() { return m_mainForm; } void Application::setMainForm(FormMain* main_form) { m_mainForm = main_form; } QString Application::configFolder() const { return IOFactory::getSystemFolder(QStandardPaths::GenericConfigLocation); } QString Application::userDataAppFolder() const { // In "app" folder, we would like to separate all user data into own subfolder, // therefore stick to "data" folder in this mode. return applicationDirPath() + QDir::separator() + QSL("data"); } QString Application::userDataFolder() { if (settings()->type() == SettingsProperties::SettingsType::Custom) { return customDataFolder(); } else if (settings()->type() == SettingsProperties::SettingsType::Portable) { return userDataAppFolder(); } else { return userDataHomeFolder(); } } QString Application::userDataHomeFolder() const { // Fallback folder. const QString home_folder = homeFolder() + QDir::separator() + QSL(APP_LOW_H_NAME) + QDir::separator() + QSL("data"); if (QDir().exists(home_folder)) { return home_folder; } else { #if defined (Q_OS_ANDROID) return IOFactory::getSystemFolder(QStandardPaths::GenericDataLocation) + QDir::separator() + QSL(APP_NAME); #else return configFolder() + QDir::separator() + QSL(APP_NAME); #endif } } QString Application::tempFolder() const { return IOFactory::getSystemFolder(QStandardPaths::TempLocation); } QString Application::documentsFolder() const { return IOFactory::getSystemFolder(QStandardPaths::DocumentsLocation); } QString Application::homeFolder() const { #if defined (Q_OS_ANDROID) return IOFactory::getSystemFolder(QStandardPaths::GenericDataLocation); #else return IOFactory::getSystemFolder(QStandardPaths::HomeLocation); #endif } void Application::backupDatabaseSettings(bool backup_database, bool backup_settings, const QString& target_path, const QString& backup_name) { if (!QFileInfo(target_path).isWritable()) { throw ApplicationException(tr("Output directory is not writable.")); } if (backup_settings) { settings()->sync(); if (!IOFactory::copyFile(settings()->fileName(), target_path + QDir::separator() + backup_name + BACKUP_SUFFIX_SETTINGS)) { throw ApplicationException(tr("Settings file not copied to output directory successfully.")); } } if (backup_database && (database()->activeDatabaseDriver() == DatabaseFactory::UsedDriver::SQLITE || database()->activeDatabaseDriver() == DatabaseFactory::UsedDriver::SQLITE_MEMORY)) { // We need to save the database first. database()->saveDatabase(); if (!IOFactory::copyFile(database()->sqliteDatabaseFilePath(), target_path + QDir::separator() + backup_name + BACKUP_SUFFIX_DATABASE)) { throw ApplicationException(tr("Database file not copied to output directory successfully.")); } } } void Application::restoreDatabaseSettings(bool restore_database, bool restore_settings, const QString& source_database_file_path, const QString& source_settings_file_path) { if (restore_database) { if (!qApp->database()->initiateRestoration(source_database_file_path)) { throw ApplicationException(tr("Database restoration was not initiated. Make sure that output directory is writable.")); } } if (restore_settings) { if (!qApp->settings()->initiateRestoration(source_settings_file_path)) { throw ApplicationException(tr("Settings restoration was not initiated. Make sure that output directory is writable.")); } } } void Application::processExecutionMessage(const QString& message) { qDebugNN << LOGSEC_CORE << "Received '" << message << "' execution message from another application instance."; const QStringList messages = message.split(ARGUMENTS_LIST_SEPARATOR); if (messages.contains(APP_QUIT_INSTANCE)) { quit(); } else { for (const QString& msg : messages) { if (msg == APP_IS_RUNNING) { showGuiMessage(APP_NAME, tr("Application is already running."), QSystemTrayIcon::Information); mainForm()->display(); } else if (msg.startsWith(QL1S(URI_SCHEME_FEED_SHORT))) { // Application was running, and someone wants to add new feed. ServiceRoot* rt = boolinq::from(feedReader()->feedsModel()->serviceRoots()).firstOrDefault([](ServiceRoot* root) { return root->supportsFeedAdding(); }); if (rt != nullptr) { rt->addNewFeed(nullptr, msg); } else { showGuiMessage(tr("Cannot add feed"), tr("Feed cannot be added because standard RSS/ATOM account is not enabled."), QSystemTrayIcon::Warning, qApp->mainForm(), true); } } } } } SystemTrayIcon* Application::trayIcon() { if (m_trayIcon == nullptr) { if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MonochromeTrayIcon)).toBool()) { m_trayIcon = new SystemTrayIcon(APP_ICON_MONO_PATH, APP_ICON_MONO_PLAIN_PATH, m_mainForm); } else { m_trayIcon = new SystemTrayIcon(APP_ICON_PATH, APP_ICON_PLAIN_PATH, m_mainForm); } connect(m_trayIcon, &SystemTrayIcon::shown, m_feedReader->feedsModel(), &FeedsModel::notifyWithCounts); connect(m_feedReader->feedsModel(), &FeedsModel::messageCountsChanged, m_trayIcon, &SystemTrayIcon::setNumber); } return m_trayIcon; } QIcon Application::desktopAwareIcon() const { auto from_theme = m_icons->fromTheme(APP_LOW_NAME); if (!from_theme.isNull()) { return from_theme; } else { return QIcon(APP_ICON_PATH); } } void Application::showTrayIcon() { // Display tray icon if it is enabled and available. if (SystemTrayIcon::isSystemTrayActivated()) { qDebugNN << LOGSEC_CORE << "Showing tray icon."; trayIcon()->show(); } } void Application::deleteTrayIcon() { if (m_trayIcon != nullptr) { qDebugNN << LOGSEC_CORE << "Disabling tray icon, deleting it and raising main application window."; m_mainForm->display(); delete m_trayIcon; m_trayIcon = nullptr; // Make sure that application quits when last window is closed. setQuitOnLastWindowClosed(true); } } void Application::showGuiMessage(const QString& title, const QString& message, QSystemTrayIcon::MessageIcon message_type, QWidget* parent, bool show_at_least_msgbox, std::function functor) { if (SystemTrayIcon::areNotificationsEnabled() && SystemTrayIcon::isSystemTrayActivated()) { trayIcon()->showMessage(title, message, message_type, TRAY_ICON_BUBBLE_TIMEOUT, std::move(functor)); } else if (show_at_least_msgbox) { // Tray icon or OSD is not available, display simple text box. MessageBox::show(parent, QMessageBox::Icon(message_type), title, message); } else { qDebugNN << LOGSEC_CORE << "Silencing GUI message: '" << message << "'."; } } void Application::onCommitData(QSessionManager& manager) { qDebugNN << LOGSEC_CORE << "OS asked application to commit its data."; onAboutToQuit(); manager.setRestartHint(QSessionManager::RestartHint::RestartNever); manager.release(); } void Application::onSaveState(QSessionManager& manager) { qDebugNN << LOGSEC_CORE << "OS asked application to save its state."; manager.setRestartHint(QSessionManager::RestartHint::RestartNever); manager.release(); } void Application::onAboutToQuit() { if (m_quitLogicDone) { qWarningNN << LOGSEC_CORE << "On-close logic is already done."; return; } m_quitLogicDone = true; #if defined(USE_WEBENGINE) m_webFactory->adBlock()->save(); #endif // Make sure that we obtain close lock BEFORE even trying to quit the application. const bool locked_safely = feedUpdateLock()->tryLock(4 * CLOSE_LOCK_TIMEOUT); processEvents(); qDebugNN << LOGSEC_CORE << "Cleaning up resources and saving application state."; #if defined(Q_OS_WIN) system()->removeTrolltechJunkRegistryKeys(); #endif if (locked_safely) { // Application obtained permission to close in a safe way. qDebugNN << LOGSEC_CORE << "Close lock was obtained safely."; // We locked the lock to exit peacefully, unlock it to avoid warnings. feedUpdateLock()->unlock(); } else { // Request for write lock timed-out. This means // that some critical action can be processed right now. qWarningNN << LOGSEC_CORE << "Close lock timed-out."; } qApp->feedReader()->quit(); database()->saveDatabase(); if (mainForm() != nullptr) { mainForm()->saveSize(); } // Now, we can check if application should just quit or restart itself. if (m_shouldRestart) { finish(); qDebugNN << LOGSEC_CORE << "Killing local peer connection to allow another instance to start."; if (QProcess::startDetached(QDir::toNativeSeparators(applicationFilePath()), {})) { qDebugNN << LOGSEC_CORE << "New application instance was started."; } else { qCriticalNN << LOGSEC_CORE << "New application instance was not started successfully."; } } } void Application::restart() { m_shouldRestart = true; quit(); } #if defined(USE_WEBENGINE) void Application::downloadRequested(QWebEngineDownloadItem* download_item) { downloadManager()->download(download_item->url()); download_item->cancel(); download_item->deleteLater(); } #endif void Application::onFeedUpdatesFinished(const FeedDownloadResults& results) { if (!results.updatedFeeds().isEmpty()) { // Now, inform about results via GUI message/notification. qApp->showGuiMessage(tr("New messages downloaded"), results.overview(10), QSystemTrayIcon::MessageIcon::NoIcon, nullptr, false); } } void Application::setupCustomDataFolder(const QString& data_folder) { if (!QDir().mkpath(data_folder)) { qCriticalNN << LOGSEC_CORE << "Failed to create custom data path" << QUOTE_W_SPACE(data_folder) << "thus falling back to standard setup."; m_customDataFolder = QString(); return; } // Disable single instance mode. m_allowMultipleInstances = true; // Save custom data folder. m_customDataFolder = data_folder; } void Application::determineFirstRuns() { m_firstRunEver = settings()->value(GROUP(General), SETTING(General::FirstRun)).toBool(); m_firstRunCurrentVersion = settings()->value(GROUP(General), QString(General::FirstRun) + QL1C('_') + APP_VERSION, true).toBool(); eliminateFirstRuns(); } void Application::parseCmdArguments() { QCommandLineOption log_file(QStringList() << CLI_LOG_SHORT << CLI_LOG_LONG, "Write application debug log to file. Note that logging to file may slow application down.", "log-file"); QCommandLineOption custom_data_folder(QStringList() << CLI_DAT_SHORT << CLI_DAT_LONG, "Use custom folder for user data and disable single instance application mode.", "user-data-folder"); QCommandLineOption disable_singleinstance(QStringList() << CLI_SIN_SHORT << CLI_SIN_LONG, "Allow running of multiple application instances."); m_cmdParser.addOptions({ log_file, custom_data_folder, disable_singleinstance }); m_cmdParser.addHelpOption(); m_cmdParser.addVersionOption(); m_cmdParser.setApplicationDescription(APP_NAME); m_cmdParser.process(*this); s_customLogFile = m_cmdParser.value(CLI_LOG_SHORT); if (!m_cmdParser.value(CLI_DAT_SHORT).isEmpty()) { auto data_folder = QDir::toNativeSeparators(m_cmdParser.value(CLI_DAT_SHORT)); qDebugNN << LOGSEC_CORE << "User wants to use custom directory for user data (and disable single instance mode):" << QUOTE_W_SPACE_DOT(data_folder); setupCustomDataFolder(data_folder); } else { m_allowMultipleInstances = false; } if (m_cmdParser.isSet(CLI_SIN_SHORT)) { m_allowMultipleInstances = true; qDebugNN << LOGSEC_CORE << "Explicitly allowing this instance to run."; } } QString Application::customDataFolder() const { return m_customDataFolder; }