1 // For license of this file, see <project-root-folder>/LICENSE.md.
2
3 #include "miscellaneous/application.h"
4
5 #include "3rd-party/boolinq/boolinq.h"
6 #include "dynamic-shortcuts/dynamicshortcuts.h"
7 #include "exceptions/applicationexception.h"
8 #include "gui/dialogs/formabout.h"
9 #include "gui/dialogs/formmain.h"
10 #include "gui/feedmessageviewer.h"
11 #include "gui/feedsview.h"
12 #include "gui/messagebox.h"
13 #include "gui/toolbars/statusbar.h"
14 #include "miscellaneous/feedreader.h"
15 #include "miscellaneous/iconfactory.h"
16 #include "miscellaneous/iofactory.h"
17 #include "miscellaneous/mutex.h"
18 #include "miscellaneous/notificationfactory.h"
19 #include "network-web/webfactory.h"
20 #include "services/abstract/serviceroot.h"
21 #include "services/owncloud/owncloudserviceentrypoint.h"
22 #include "services/standard/standardserviceentrypoint.h"
23 #include "services/standard/standardserviceroot.h"
24 #include "services/tt-rss/ttrssserviceentrypoint.h"
25
26 #include <iostream>
27
28 #include <QProcess>
29 #include <QSessionManager>
30 #include <QSslSocket>
31 #include <QTimer>
32
33 #if defined(Q_OS_UNIX)
34 #include <QDBusConnection>
35 #include <QDBusMessage>
36 #endif
37
38 #if defined(USE_WEBENGINE)
39 #include "network-web/adblock/adblockicon.h"
40 #include "network-web/adblock/adblockmanager.h"
41 #include "network-web/networkurlinterceptor.h"
42
43 #include <QWebEngineDownloadItem>
44 #include <QWebEngineProfile>
45 #endif
46
Application(const QString & id,int & argc,char ** argv)47 Application::Application(const QString& id, int& argc, char** argv)
48 : SingleApplication(id, argc, argv), m_updateFeedsLock(new Mutex()) {
49 parseCmdArgumentsFromMyInstance();
50 qInstallMessageHandler(performLogging);
51
52 m_feedReader = nullptr;
53 m_quitLogicDone = false;
54 m_mainForm = nullptr;
55 m_trayIcon = nullptr;
56 m_settings = Settings::setupSettings(this);
57 m_webFactory = new WebFactory(this);
58 m_system = new SystemFactory(this);
59 m_skins = new SkinFactory(this);
60 m_localization = new Localization(this);
61 m_icons = new IconFactory(this);
62 m_database = new DatabaseFactory(this);
63 m_downloadManager = nullptr;
64 m_notifications = new NotificationFactory(this);
65 m_shouldRestart = false;
66
67 determineFirstRuns();
68
69 //: Abbreviation of language, e.g. en.
70 //: Use ISO 639-1 code here combined with ISO 3166-1 (alpha-2) code.
71 //: Examples: "cs", "en", "it", "cs_CZ", "en_GB", "en_US".
72 QObject::tr("LANG_ABBREV");
73
74 //: Name of translator - optional.
75 QObject::tr("LANG_AUTHOR");
76
77 connect(this, &Application::aboutToQuit, this, &Application::onAboutToQuit);
78 connect(this, &Application::commitDataRequest, this, &Application::onCommitData);
79 connect(this, &Application::saveStateRequest, this, &Application::onSaveState);
80
81 #if defined(Q_OS_UNIX)
82 QString app_dir = QString::fromLocal8Bit(qgetenv("APPDIR"));
83
84 if (!app_dir.isEmpty()) {
85 bool success = qputenv("GST_PLUGIN_SYSTEM_PATH_1_0",
86 QSL("%1/usr/lib/gstreamer-1.0:%2").arg(app_dir,
87 QString::fromLocal8Bit(qgetenv("GST_PLUGIN_SYSTEM_PATH_1_0"))).toLocal8Bit());
88 success = qputenv("GST_PLUGIN_SCANNER_1_0",
89 QSL("%1/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner").arg(app_dir).toLocal8Bit()) && success;
90 if (!success) {
91 qWarningNN << LOGSEC_CORE << "Unable to set up GStreamer environment.";
92 }
93 }
94 #endif
95
96 #if defined(USE_WEBENGINE)
97 m_webFactory->urlIinterceptor()->load();
98
99 connect(QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested, this, &Application::downloadRequested);
100 connect(m_webFactory->adBlock(), &AdBlockManager::processTerminated, this, &Application::onAdBlockFailure);
101
102 QTimer::singleShot(3000, this, [=]() {
103 try {
104 m_webFactory->adBlock()->setEnabled(qApp->settings()->value(GROUP(AdBlock), SETTING(AdBlock::AdBlockEnabled)).toBool());
105 }
106 catch (...) {
107 onAdBlockFailure();
108 }
109 });
110 #endif
111
112 m_webFactory->updateProxy();
113
114 if (isFirstRun()) {
115 m_notifications->save({
116 Notification(Notification::Event::GeneralEvent, true),
117 Notification(Notification::Event::NewUnreadArticlesFetched, true,
118 QSL("%1/notify.wav").arg(SOUNDS_BUILTIN_DIRECTORY)),
119 Notification(Notification::Event::NewAppVersionAvailable, true),
120 Notification(Notification::Event::LoginFailure, true)
121 }, settings());
122 }
123 else {
124 m_notifications->load(settings());
125 }
126
127 QTimer::singleShot(1000, system(), &SystemFactory::checkForUpdatesOnStartup);
128
129 qDebugNN << LOGSEC_CORE
130 << "OpenSSL version:"
131 << QUOTE_W_SPACE_DOT(QSslSocket::sslLibraryVersionString());
132
133 qDebugNN << LOGSEC_CORE
134 << "OpenSSL supported:"
135 << QUOTE_W_SPACE_DOT(QSslSocket::supportsSsl());
136 }
137
~Application()138 Application::~Application() {
139 qDebugNN << LOGSEC_CORE << "Destroying Application instance.";
140 }
141
142 QString s_customLogFile = QString();
143 bool s_disableDebug = false;
144
performLogging(QtMsgType type,const QMessageLogContext & context,const QString & msg)145 void Application::performLogging(QtMsgType type, const QMessageLogContext& context, const QString& msg) {
146 #ifndef QT_NO_DEBUG_OUTPUT
147 QString console_message = qFormatLogMessage(type, context, msg);
148
149 if (!s_disableDebug) {
150 std::cerr << console_message.toStdString() << std::endl;
151 }
152
153 if (!s_customLogFile.isEmpty()) {
154 QFile log_file(s_customLogFile);
155
156 if (log_file.open(QFile::OpenModeFlag::Append | QFile::OpenModeFlag::Unbuffered)) {
157 log_file.write(console_message.toUtf8());
158 log_file.write(QSL("\r\n").toUtf8());
159 log_file.close();
160 }
161 }
162
163 if (type == QtMsgType::QtFatalMsg) {
164 qApp->exit(EXIT_FAILURE);
165 }
166 #else
167 Q_UNUSED(type)
168 Q_UNUSED(context)
169 Q_UNUSED(msg)
170 #endif
171 }
172
reactOnForeignNotifications()173 void Application::reactOnForeignNotifications() {
174 connect(this, &Application::messageReceived, this, &Application::parseCmdArgumentsFromOtherInstance);
175 }
176
hideOrShowMainForm()177 void Application::hideOrShowMainForm() {
178 // Display main window.
179 if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MainWindowStartsHidden)).toBool() &&
180 SystemTrayIcon::isSystemTrayDesired() &&
181 SystemTrayIcon::isSystemTrayAreaAvailable()) {
182 qDebugNN << LOGSEC_CORE << "Hiding the main window when the application is starting.";
183 mainForm()->switchVisibility(true);
184 }
185 else {
186 qDebugNN << LOGSEC_CORE << "Showing the main window when the application is starting.";
187 mainForm()->show();
188 }
189 }
190
loadDynamicShortcuts()191 void Application::loadDynamicShortcuts() {
192 DynamicShortcuts::load(userActions());
193 }
194
showPolls() const195 void Application::showPolls() const {
196 if(isFirstRunCurrentVersion()) {
197 qApp->showGuiMessage(Notification::Event::NewAppVersionAvailable,
198 tr("RSS Guard has Discord server!"),
199 tr("You can visit it now! Click me!"),
200 QSystemTrayIcon::MessageIcon::Information,
201 true,
202 {},
203 tr("Go to Discord!"),
204 [this]() {
205 web()->openUrlInExternalBrowser(QSL("https://discord.gg/7xbVMPPNqH"));
206 });
207 }
208 }
209
offerChanges() const210 void Application::offerChanges() const {
211 if (isFirstRunCurrentVersion()) {
212 qApp->showGuiMessage(Notification::Event::GeneralEvent,
213 QSL(APP_NAME),
214 QObject::tr("Welcome to %1.\n\nPlease, check NEW stuff included in this\n"
215 "version by clicking this popup notification.").arg(QSL(APP_LONG_NAME)),
216 QSystemTrayIcon::MessageIcon::NoIcon, {}, {}, tr("Go to changelog"), [] {
217 FormAbout(qApp->mainForm()).exec();
218 });
219 }
220 }
221
isAlreadyRunning()222 bool Application::isAlreadyRunning() {
223 return m_allowMultipleInstances
224 ? false
225 : sendMessage((QStringList() << QSL("-%1").arg(QSL(CLI_IS_RUNNING))
226 << Application::arguments().mid(1)).join(QSL(ARGUMENTS_LIST_SEPARATOR)));
227 }
228
builtinSounds() const229 QStringList Application::builtinSounds() const {
230 auto builtin_sounds = QDir(QSL(SOUNDS_BUILTIN_DIRECTORY)).entryInfoList(QDir::Filter::Files, QDir::SortFlag::Name);
231 auto iter = boolinq::from(builtin_sounds).select([](const QFileInfo& i) {
232 return i.absoluteFilePath();
233 }).toStdList();
234 auto descs = FROM_STD_LIST(QStringList, iter);
235
236 return descs;
237 }
238
feedReader()239 FeedReader* Application::feedReader() {
240 return m_feedReader;
241 }
242
userActions()243 QList<QAction*> Application::userActions() {
244 if (m_mainForm != nullptr && m_userActions.isEmpty()) {
245 m_userActions = m_mainForm->allActions();
246
247 #if defined(USE_WEBENGINE)
248 m_userActions.append(m_webFactory->adBlock()->adBlockIcon());
249 #endif
250 }
251
252 return m_userActions;
253 }
254
isFirstRun() const255 bool Application::isFirstRun() const {
256 return m_firstRunEver;
257 }
258
isFirstRunCurrentVersion() const259 bool Application::isFirstRunCurrentVersion() const {
260 return m_firstRunCurrentVersion;
261 }
262
cmdParser()263 QCommandLineParser* Application::cmdParser() {
264 return &m_cmdParser;
265 }
266
web() const267 WebFactory* Application::web() const {
268 return m_webFactory;
269 }
270
system()271 SystemFactory* Application::system() {
272 return m_system;
273 }
274
skins()275 SkinFactory* Application::skins() {
276 return m_skins;
277 }
278
localization()279 Localization* Application::localization() {
280 return m_localization;
281 }
282
database()283 DatabaseFactory* Application::database() {
284 return m_database;
285 }
286
eliminateFirstRuns()287 void Application::eliminateFirstRuns() {
288 settings()->setValue(GROUP(General), General::FirstRun, false);
289 settings()->setValue(GROUP(General), QString(General::FirstRun) + QL1C('_') + APP_VERSION, false);
290 }
291
notifications() const292 NotificationFactory* Application::notifications() const {
293 return m_notifications;
294 }
295
setFeedReader(FeedReader * feed_reader)296 void Application::setFeedReader(FeedReader* feed_reader) {
297 m_feedReader = feed_reader;
298
299 connect(m_feedReader, &FeedReader::feedUpdatesFinished, this, &Application::onFeedUpdatesFinished);
300 connect(m_feedReader->feedsModel(), &FeedsModel::messageCountsChanged, this, &Application::showMessagesNumber);
301 }
302
icons()303 IconFactory* Application::icons() {
304 return m_icons;
305 }
306
downloadManager()307 DownloadManager* Application::downloadManager() {
308 if (m_downloadManager == nullptr) {
309 m_downloadManager = new DownloadManager();
310 connect(m_downloadManager, &DownloadManager::downloadFinished, mainForm()->statusBar(), &StatusBar::clearProgressDownload);
311 connect(m_downloadManager, &DownloadManager::downloadProgressed, mainForm()->statusBar(), &StatusBar::showProgressDownload);
312 }
313
314 return m_downloadManager;
315 }
316
settings() const317 Settings* Application::settings() const {
318 return m_settings;
319 }
320
feedUpdateLock()321 Mutex* Application::feedUpdateLock() {
322 return m_updateFeedsLock.data();
323 }
324
mainForm()325 FormMain* Application::mainForm() {
326 return m_mainForm;
327 }
328
mainFormWidget()329 QWidget* Application::mainFormWidget() {
330 return m_mainForm;
331 }
332
setMainForm(FormMain * main_form)333 void Application::setMainForm(FormMain* main_form) {
334 m_mainForm = main_form;
335 }
336
configFolder() const337 QString Application::configFolder() const {
338 return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::GenericConfigLocation);
339 }
340
userDataAppFolder() const341 QString Application::userDataAppFolder() const {
342 // In "app" folder, we would like to separate all user data into own subfolder,
343 // therefore stick to "data" folder in this mode.
344 return applicationDirPath() + QDir::separator() + QSL("data4");
345 }
346
userDataFolder()347 QString Application::userDataFolder() {
348 if (settings()->type() == SettingsProperties::SettingsType::Custom) {
349 return customDataFolder();
350 }
351 else if (settings()->type() == SettingsProperties::SettingsType::Portable) {
352 return userDataAppFolder();
353 }
354 else {
355 return userDataHomeFolder();
356 }
357 }
358
replaceDataUserDataFolderPlaceholder(QString text) const359 QString Application::replaceDataUserDataFolderPlaceholder(QString text) const {
360 auto user_data_folder = qApp->userDataFolder();
361
362 return text.replace(QSL(USER_DATA_PLACEHOLDER), user_data_folder);
363 }
364
replaceDataUserDataFolderPlaceholder(QStringList texts) const365 QStringList Application::replaceDataUserDataFolderPlaceholder(QStringList texts) const {
366 auto user_data_folder = qApp->userDataFolder();
367
368 return texts.replaceInStrings(QSL(USER_DATA_PLACEHOLDER), user_data_folder);
369 }
370
userDataHomeFolder() const371 QString Application::userDataHomeFolder() const {
372 #if defined(Q_OS_ANDROID)
373 return IOFactory::getSystemFolder(QStandardPaths::GenericDataLocation) + QDir::separator() + QSL(APP_NAME) + QSL(" 4");
374 #else
375 return configFolder() + QDir::separator() + QSL(APP_NAME) + QSL(" 4");
376 #endif
377 }
378
tempFolder() const379 QString Application::tempFolder() const {
380 return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::TempLocation);
381 }
382
documentsFolder() const383 QString Application::documentsFolder() const {
384 return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::DocumentsLocation);
385 }
386
homeFolder() const387 QString Application::homeFolder() const {
388 #if defined(Q_OS_ANDROID)
389 return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::GenericDataLocation);
390 #else
391 return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::HomeLocation);
392 #endif
393 }
394
backupDatabaseSettings(bool backup_database,bool backup_settings,const QString & target_path,const QString & backup_name)395 void Application::backupDatabaseSettings(bool backup_database, bool backup_settings,
396 const QString& target_path, const QString& backup_name) {
397 if (!QFileInfo(target_path).isWritable()) {
398 throw ApplicationException(tr("Output directory is not writable."));
399 }
400
401 if (backup_settings) {
402 settings()->sync();
403
404 if (!IOFactory::copyFile(settings()->fileName(), target_path + QDir::separator() + backup_name + BACKUP_SUFFIX_SETTINGS)) {
405 throw ApplicationException(tr("Settings file not copied to output directory successfully."));
406 }
407 }
408
409 if (backup_database) {
410 // We need to save the database first.
411 database()->driver()->saveDatabase();
412 database()->driver()->backupDatabase(target_path, backup_name);
413 }
414 }
415
restoreDatabaseSettings(bool restore_database,bool restore_settings,const QString & source_database_file_path,const QString & source_settings_file_path)416 void Application::restoreDatabaseSettings(bool restore_database, bool restore_settings,
417 const QString& source_database_file_path, const QString& source_settings_file_path) {
418 if (restore_database) {
419 if (!qApp->database()->driver()->initiateRestoration(source_database_file_path)) {
420 throw ApplicationException(tr("Database restoration was not initiated. Make sure that output directory is writable."));
421 }
422 }
423
424 if (restore_settings) {
425 if (!qApp->settings()->initiateRestoration(source_settings_file_path)) {
426 throw ApplicationException(tr("Settings restoration was not initiated. Make sure that output directory is writable."));
427 }
428 }
429 }
430
trayIcon()431 SystemTrayIcon* Application::trayIcon() {
432 if (m_trayIcon == nullptr) {
433 if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MonochromeTrayIcon)).toBool()) {
434 m_trayIcon = new SystemTrayIcon(APP_ICON_MONO_PATH, APP_ICON_MONO_PLAIN_PATH, m_mainForm);
435 }
436 else {
437 m_trayIcon = new SystemTrayIcon(APP_ICON_PATH, APP_ICON_PLAIN_PATH, m_mainForm);
438 }
439
440 connect(m_trayIcon, &SystemTrayIcon::shown, m_feedReader->feedsModel(), &FeedsModel::notifyWithCounts);
441 }
442
443 return m_trayIcon;
444 }
445
desktopAwareIcon() const446 QIcon Application::desktopAwareIcon() const {
447 auto from_theme = m_icons->fromTheme(QSL(APP_LOW_NAME));
448
449 if (!from_theme.isNull()) {
450 return from_theme;
451 }
452 else {
453 return QIcon(APP_ICON_PATH);
454 }
455 }
456
showTrayIcon()457 void Application::showTrayIcon() {
458 // Display tray icon if it is enabled and available.
459 if (SystemTrayIcon::isSystemTrayDesired()) {
460 #if !defined(Q_OS_UNIX)
461 if (!SystemTrayIcon::isSystemTrayAreaAvailable()) {
462 qWarningNN << LOGSEC_GUI << "Tray icon area is not available.";
463 return;
464 }
465 #endif
466
467 qDebugNN << LOGSEC_GUI << "Showing tray icon.";
468 trayIcon()->show();
469 }
470 else {
471 m_feedReader->feedsModel()->notifyWithCounts();
472 }
473 }
474
deleteTrayIcon()475 void Application::deleteTrayIcon() {
476 if (m_trayIcon != nullptr) {
477 qDebugNN << LOGSEC_CORE << "Disabling tray icon, deleting it and raising main application window.";
478 m_mainForm->display();
479 delete m_trayIcon;
480 m_trayIcon = nullptr;
481
482 // Make sure that application quits when last window is closed.
483 setQuitOnLastWindowClosed(true);
484 }
485 }
486
showGuiMessage(Notification::Event event,const QString & title,const QString & message,QSystemTrayIcon::MessageIcon message_type,bool show_at_least_msgbox,QWidget * parent,const QString & functor_heading,std::function<void ()> functor)487 void Application::showGuiMessage(Notification::Event event, const QString& title,
488 const QString& message, QSystemTrayIcon::MessageIcon message_type, bool show_at_least_msgbox,
489 QWidget* parent, const QString& functor_heading, std::function<void()> functor) {
490
491 if (SystemTrayIcon::areNotificationsEnabled()) {
492 auto notification = m_notifications->notificationForEvent(event);
493
494 notification.playSound(this);
495
496 if (SystemTrayIcon::isSystemTrayDesired() &&
497 SystemTrayIcon::isSystemTrayAreaAvailable() &&
498 notification.balloonEnabled()) {
499 trayIcon()->showMessage(title, message, message_type, TRAY_ICON_BUBBLE_TIMEOUT, std::move(functor));
500
501 return;
502 }
503 }
504
505 if (show_at_least_msgbox) {
506 // Tray icon or OSD is not available, display simple text box.
507 MessageBox::show(parent == nullptr ? mainFormWidget() : parent, QMessageBox::Icon(message_type), title, message,
508 {}, {}, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok, {}, functor_heading, functor);
509 }
510 else {
511 qDebugNN << LOGSEC_CORE << "Silencing GUI message:" << QUOTE_W_SPACE_DOT(message);
512 }
513 }
514
onCommitData(QSessionManager & manager)515 void Application::onCommitData(QSessionManager& manager) {
516 qDebugNN << LOGSEC_CORE << "OS asked application to commit its data.";
517
518 onAboutToQuit();
519
520 manager.setRestartHint(QSessionManager::RestartHint::RestartNever);
521 manager.release();
522 }
523
onSaveState(QSessionManager & manager)524 void Application::onSaveState(QSessionManager& manager) {
525 qDebugNN << LOGSEC_CORE << "OS asked application to save its state.";
526
527 manager.setRestartHint(QSessionManager::RestartHint::RestartNever);
528 manager.release();
529 }
530
onAboutToQuit()531 void Application::onAboutToQuit() {
532 if (m_quitLogicDone) {
533 qWarningNN << LOGSEC_CORE << "On-close logic is already done.";
534 return;
535 }
536
537 m_quitLogicDone = true;
538
539 // Make sure that we obtain close lock BEFORE even trying to quit the application.
540 const bool locked_safely = feedUpdateLock()->tryLock(4 * CLOSE_LOCK_TIMEOUT);
541
542 processEvents();
543 qDebugNN << LOGSEC_CORE << "Cleaning up resources and saving application state.";
544
545 if (locked_safely) {
546 // Application obtained permission to close in a safe way.
547 qDebugNN << LOGSEC_CORE << "Close lock was obtained safely.";
548
549 // We locked the lock to exit peacefully, unlock it to avoid warnings.
550 feedUpdateLock()->unlock();
551 }
552 else {
553 // Request for write lock timed-out. This means
554 // that some critical action can be processed right now.
555 qWarningNN << LOGSEC_CORE << "Close lock timed-out.";
556 }
557
558 qApp->feedReader()->quit();
559 database()->driver()->saveDatabase();
560
561 if (mainForm() != nullptr) {
562 mainForm()->saveSize();
563 }
564
565 // Now, we can check if application should just quit or restart itself.
566 if (m_shouldRestart) {
567 finish();
568 qDebugNN << LOGSEC_CORE << "Killing local peer connection to allow another instance to start.";
569
570 if (QProcess::startDetached(QDir::toNativeSeparators(applicationFilePath()), {})) {
571 qDebugNN << LOGSEC_CORE << "New application instance was started.";
572 }
573 else {
574 qCriticalNN << LOGSEC_CORE << "New application instance was not started successfully.";
575 }
576 }
577 }
578
showMessagesNumber(int unread_messages,bool any_feed_has_unread_messages)579 void Application::showMessagesNumber(int unread_messages, bool any_feed_has_unread_messages) {
580 if (m_trayIcon != nullptr) {
581 m_trayIcon->setNumber(unread_messages, any_feed_has_unread_messages);
582 }
583
584 #if defined(Q_OS_UNIX)
585 QDBusMessage signal = QDBusMessage::createSignal(QSL("/"),
586 QSL("com.canonical.Unity.LauncherEntry"),
587 QSL("Update"));
588
589 signal << QSL("application://%1").arg(APP_DESKTOP_ENTRY_FILE);
590
591 QVariantMap setProperty;
592
593 setProperty.insert("count", qint64(unread_messages));
594 setProperty.insert("count-visible", unread_messages > 0);
595
596 signal << setProperty;
597
598 QDBusConnection::sessionBus().send(signal);
599 #endif
600 }
601
restart()602 void Application::restart() {
603 m_shouldRestart = true;
604 quit();
605 }
606
607 #if defined(USE_WEBENGINE)
608
downloadRequested(QWebEngineDownloadItem * download_item)609 void Application::downloadRequested(QWebEngineDownloadItem* download_item) {
610 downloadManager()->download(download_item->url());
611 download_item->cancel();
612 download_item->deleteLater();
613 }
614
onAdBlockFailure()615 void Application::onAdBlockFailure() {
616 qApp->showGuiMessage(Notification::Event::GeneralEvent,
617 tr("AdBlock needs to be configured"),
618 tr("AdBlock component is not configured properly."),
619 QSystemTrayIcon::MessageIcon::Critical,
620 true,
621 {},
622 tr("Configure now"),
623 [=]() {
624 m_webFactory->adBlock()->showDialog();
625 });
626
627 qApp->settings()->setValue(GROUP(AdBlock), AdBlock::AdBlockEnabled, false);
628 }
629
630 #endif
631
onFeedUpdatesFinished(const FeedDownloadResults & results)632 void Application::onFeedUpdatesFinished(const FeedDownloadResults& results) {
633 if (!results.updatedFeeds().isEmpty()) {
634 // Now, inform about results via GUI message/notification.
635 qApp->showGuiMessage(Notification::Event::NewUnreadArticlesFetched,
636 tr("Unread articles fetched"),
637 results.overview(10),
638 QSystemTrayIcon::MessageIcon::NoIcon);
639 }
640 }
641
setupCustomDataFolder(const QString & data_folder)642 void Application::setupCustomDataFolder(const QString& data_folder) {
643 if (!QDir().mkpath(data_folder)) {
644 qCriticalNN << LOGSEC_CORE
645 << "Failed to create custom data path"
646 << QUOTE_W_SPACE(data_folder)
647 << "thus falling back to standard setup.";
648 m_customDataFolder = QString();
649 return;
650 }
651
652 // Disable single instance mode.
653 m_allowMultipleInstances = true;
654
655 // Save custom data folder.
656 m_customDataFolder = data_folder;
657 }
658
determineFirstRuns()659 void Application::determineFirstRuns() {
660 m_firstRunEver = settings()->value(GROUP(General),
661 SETTING(General::FirstRun)).toBool();
662 m_firstRunCurrentVersion = settings()->value(GROUP(General),
663 QString(General::FirstRun) + QL1C('_') + APP_VERSION,
664 true).toBool();
665
666 eliminateFirstRuns();
667 }
668
parseCmdArgumentsFromOtherInstance(const QString & message)669 void Application::parseCmdArgumentsFromOtherInstance(const QString& message) {
670 if (message.isEmpty()) {
671 qDebugNN << LOGSEC_CORE << "No execution message received from other app instances.";
672 return;
673 }
674
675 qDebugNN << LOGSEC_CORE
676 << "Received"
677 << QUOTE_W_SPACE(message)
678 << "execution message.";
679
680 #if QT_VERSION >= 0x050F00 // Qt >= 5.15.0
681 QStringList messages = message.split(QSL(ARGUMENTS_LIST_SEPARATOR), Qt::SplitBehaviorFlags::SkipEmptyParts);
682 #else
683 QStringList messages = message.split(QSL(ARGUMENTS_LIST_SEPARATOR), QString::SplitBehavior::SkipEmptyParts);
684 #endif
685
686 QCommandLineParser cmd_parser;
687
688 messages.prepend(qApp->applicationFilePath());
689
690 cmd_parser.addOption(QCommandLineOption({ QSL(CLI_QUIT_INSTANCE) }));
691 cmd_parser.addOption(QCommandLineOption({ QSL(CLI_IS_RUNNING) }));
692 cmd_parser.addPositionalArgument(QSL("urls"),
693 QSL("List of URL addresses pointing to individual online feeds which should be added."),
694 QSL("[url-1 ... url-n]"));
695
696 if (!cmd_parser.parse(messages)) {
697 qCriticalNN << LOGSEC_CORE << cmd_parser.errorText();
698 }
699
700 if (cmd_parser.isSet(QSL(CLI_QUIT_INSTANCE))) {
701 quit();
702 return;
703 }
704 else if (cmd_parser.isSet(QSL(CLI_IS_RUNNING))) {
705 showGuiMessage(Notification::Event::GeneralEvent,
706 QSL(APP_NAME),
707 tr("Application is already running."),
708 QSystemTrayIcon::MessageIcon::Information);
709 mainForm()->display();
710 }
711
712 messages = cmd_parser.positionalArguments();
713
714 for (const QString& msg : qAsConst(messages)) {
715 // Application was running, and someone wants to add new feed.
716 ServiceRoot* rt = boolinq::from(feedReader()->feedsModel()->serviceRoots()).firstOrDefault([](ServiceRoot* root) {
717 return root->supportsFeedAdding();
718 });
719
720 if (rt != nullptr) {
721 rt->addNewFeed(nullptr, msg);
722 }
723 else {
724 showGuiMessage(Notification::Event::GeneralEvent,
725 tr("Cannot add feed"),
726 tr("Feed cannot be added because there is no active account which can add feeds."),
727 QSystemTrayIcon::MessageIcon::Warning,
728 true);
729 }
730 }
731 }
732
parseCmdArgumentsFromMyInstance()733 void Application::parseCmdArgumentsFromMyInstance() {
734 QCommandLineOption help({ QSL(CLI_HELP_SHORT), QSL(CLI_HELP_LONG) },
735 QSL("Displays overview of CLI."));
736 QCommandLineOption version({ QSL(CLI_VER_SHORT), QSL(CLI_VER_LONG) },
737 QSL("Displays version of the application."));
738 QCommandLineOption log_file({ QSL(CLI_LOG_SHORT), QSL(CLI_LOG_LONG) },
739 QSL("Write application debug log to file. Note that logging to file may slow application down."),
740 QSL("log-file"));
741 QCommandLineOption custom_data_folder({ QSL(CLI_DAT_SHORT), QSL(CLI_DAT_LONG) },
742 QSL("Use custom folder for user data and disable single instance application mode."),
743 QSL("user-data-folder"));
744 QCommandLineOption disable_singleinstance({ QSL(CLI_SIN_SHORT), QSL(CLI_SIN_LONG) },
745 QSL("Allow running of multiple application instances."));
746 QCommandLineOption disable_debug({ QSL(CLI_NDEBUG_SHORT), QSL(CLI_NDEBUG_LONG) },
747 QSL("Completely disable stdout/stderr outputs."));
748
749 m_cmdParser.addOptions({ help, version, log_file, custom_data_folder, disable_singleinstance, disable_debug });
750 m_cmdParser.addPositionalArgument(QSL("urls"),
751 QSL("List of URL addresses pointing to individual online feeds which should be added."),
752 QSL("[url-1 ... url-n]"));
753 m_cmdParser.setApplicationDescription(QSL(APP_NAME));
754
755 if (!m_cmdParser.parse(QCoreApplication::arguments())) {
756 qCriticalNN << LOGSEC_CORE << m_cmdParser.errorText();
757 }
758
759 s_customLogFile = m_cmdParser.value(QSL(CLI_LOG_SHORT));
760
761 if (!m_cmdParser.value(QSL(CLI_DAT_SHORT)).isEmpty()) {
762 auto data_folder = QDir::toNativeSeparators(m_cmdParser.value(QSL(CLI_DAT_SHORT)));
763
764 qDebugNN << LOGSEC_CORE
765 << "User wants to use custom directory for user data (and disable single instance mode):"
766 << QUOTE_W_SPACE_DOT(data_folder);
767
768 setupCustomDataFolder(data_folder);
769 }
770 else {
771 m_allowMultipleInstances = false;
772 }
773
774 if (m_cmdParser.isSet(QSL(CLI_HELP_SHORT))) {
775 m_cmdParser.showHelp();
776 }
777 else if (m_cmdParser.isSet(QSL(CLI_VER_SHORT))) {
778 m_cmdParser.showVersion();
779 }
780
781 if (m_cmdParser.isSet(QSL(CLI_SIN_SHORT))) {
782 m_allowMultipleInstances = true;
783 qDebugNN << LOGSEC_CORE << "Explicitly allowing this instance to run.";
784 }
785
786 if (m_cmdParser.isSet(QSL(CLI_NDEBUG_SHORT))) {
787 s_disableDebug = true;
788 qDebugNN << LOGSEC_CORE << "Disabling any stdout/stderr outputs.";
789 }
790 }
791
customDataFolder() const792 QString Application::customDataFolder() const {
793 return m_customDataFolder;
794 }
795