Warning, file /pim/kalarm/src/kalarmapp.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 * kalarmapp.cpp - the KAlarm application object 0003 * Program: kalarm 0004 * SPDX-FileCopyrightText: 2001-2024 David Jarvie <djarvie@kde.org> 0005 * 0006 * SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "kalarmapp.h" 0010 0011 #include "kalarm.h" 0012 #include "commandoptions.h" 0013 #include "dbushandler.h" 0014 #include "displaycalendar.h" 0015 #include "editdlgtypes.h" 0016 #include "functions.h" 0017 #include "kamail.h" 0018 #include "mainwindow.h" 0019 #include "messagenotification.h" 0020 #include "messagewindow.h" 0021 #include "pluginmanager.h" 0022 #include "resourcescalendar.h" 0023 #include "startdaytimer.h" 0024 #include "traywindow.h" 0025 #include "resources/datamodel.h" 0026 #include "resources/resources.h" 0027 #include "lib/desktop.h" 0028 #include "lib/messagebox.h" 0029 #include "notifications_interface.h" // DBUS-generated 0030 #include "dbusproperties.h" // DBUS-generated 0031 #include "kalarmcalendar/datetime.h" 0032 #include "kalarmcalendar/karecurrence.h" 0033 #include "kalarm_debug.h" 0034 0035 #include <KLocalizedString> 0036 #include <KConfig> 0037 #include <KConfigGui> 0038 #include <KAboutData> 0039 #include <KSharedConfig> 0040 #include <KStandardGuiItem> 0041 #include <KWindowSystem> 0042 #include <netwm.h> 0043 #include <KShell> 0044 0045 #include <QObject> 0046 #include <QTimer> 0047 #include <QFile> 0048 #include <QTextStream> 0049 #include <QTemporaryFile> 0050 #include <QStandardPaths> 0051 #include <QSystemTrayIcon> 0052 #include <QCommandLineParser> 0053 0054 #include <stdlib.h> 0055 #include <ctype.h> 0056 #include <iostream> 0057 #include <climits> 0058 0059 namespace 0060 { 0061 const int RESOURCES_TIMEOUT = 30; // timeout (seconds) for resources to be populated 0062 0063 const char FDO_NOTIFICATIONS_SERVICE[] = "org.freedesktop.Notifications"; 0064 const char FDO_NOTIFICATIONS_PATH[] = "/org/freedesktop/Notifications"; 0065 0066 const QLatin1String GENERAL_GROUP("General"); 0067 0068 /****************************************************************************** 0069 * Find the maximum number of seconds late which a late-cancel alarm is allowed 0070 * to be. This is calculated as the late cancel interval, plus a few seconds 0071 * leeway to cater for any timing irregularities. 0072 */ 0073 inline int maxLateness(int lateCancel) 0074 { 0075 static const int LATENESS_LEEWAY = 5; 0076 int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; 0077 return LATENESS_LEEWAY + lc; 0078 } 0079 0080 QWidget* mainWidget() 0081 { 0082 return MainWindow::mainMainWindow(); 0083 } 0084 } 0085 0086 0087 KAlarmApp* KAlarmApp::mInstance = nullptr; 0088 int KAlarmApp::mActiveCount = 0; 0089 int KAlarmApp::mFatalError = 0; 0090 QString KAlarmApp::mFatalMessage; 0091 0092 0093 /****************************************************************************** 0094 * Construct the application. 0095 */ 0096 KAlarmApp::KAlarmApp(int& argc, char** argv) 0097 : QApplication(argc, argv) 0098 , mDBusHandler(new DBusHandler()) 0099 { 0100 qCDebug(KALARM_LOG) << "KAlarmApp:"; 0101 } 0102 0103 /****************************************************************************** 0104 */ 0105 KAlarmApp::~KAlarmApp() 0106 { 0107 while (!mCommandProcesses.isEmpty()) 0108 { 0109 ProcData* pd = mCommandProcesses.at(0); 0110 mCommandProcesses.pop_front(); 0111 delete pd; 0112 } 0113 ResourcesCalendar::terminate(); 0114 DisplayCalendar::terminate(); 0115 DataModel::terminate(); 0116 delete mDBusHandler; 0117 } 0118 0119 /****************************************************************************** 0120 * Return the one and only KAlarmApp instance. 0121 * If it doesn't already exist, it is created first. 0122 */ 0123 KAlarmApp* KAlarmApp::create(int& argc, char** argv) 0124 { 0125 if (!mInstance) 0126 { 0127 mInstance = new KAlarmApp(argc, argv); 0128 0129 if (mFatalError) 0130 mInstance->quitFatal(); 0131 } 0132 return mInstance; 0133 } 0134 0135 /****************************************************************************** 0136 * Perform initialisations which may require KAboutData to have been set up. 0137 */ 0138 void KAlarmApp::initialise() 0139 { 0140 #ifndef NDEBUG 0141 KAlarm::setTestModeConditions(); 0142 #endif 0143 0144 setQuitOnLastWindowClosed(false); 0145 Preferences::self(); // read KAlarm configuration 0146 if (!Preferences::noAutoStart()) 0147 { 0148 // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop 0149 Preferences::setNoAutoStart(false); 0150 // Enable kalarm.autostart.desktop to start KAlarm 0151 Preferences::setAutoStart(true); 0152 Preferences::self()->save(); 0153 } 0154 Preferences::connect(&Preferences::startOfDayChanged, this, &KAlarmApp::changeStartOfDay); 0155 Preferences::connect(&Preferences::workTimeChanged, this, &KAlarmApp::slotWorkTimeChanged); 0156 Preferences::connect(&Preferences::holidaysChanged, this, &KAlarmApp::slotHolidaysChanged); 0157 Preferences::connect(&Preferences::feb29TypeChanged, this, &KAlarmApp::slotFeb29TypeChanged); 0158 Preferences::connect(&Preferences::showInSystemTrayChanged, this, &KAlarmApp::slotShowInSystemTrayChanged); 0159 Preferences::connect(&Preferences::archivedKeepDaysChanged, this, &KAlarmApp::setArchivePurgeDays); 0160 Preferences::connect(&Preferences::messageFontChanged, this, &KAlarmApp::slotMessageFontChanged); 0161 slotFeb29TypeChanged(Preferences::defaultFeb29Type()); 0162 0163 KAEvent::setStartOfDay(Preferences::startOfDay()); 0164 KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd(), Preferences::timeSpec()); 0165 KAEvent::setHolidays(Preferences::holidays()); 0166 KAEvent::setDefaultFont(Preferences::messageFont()); 0167 0168 // Load plugins 0169 if (PluginManager::instance()->akonadiPlugin()) 0170 qCDebug(KALARM_LOG) << "KAlarmApp: Akonadi plugin found"; 0171 else 0172 qCDebug(KALARM_LOG) << "KAlarmApp: Akonadi dependent features not available (Akonadi plugin not found)"; 0173 0174 // Check if KOrganizer is installed 0175 const QString korg = QStringLiteral("korganizer"); 0176 mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); 0177 if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; } 0178 // Check if the window manager can't handle keyboard focus transfer between windows 0179 mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Type::Unity); 0180 if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } 0181 0182 Resources* resources = Resources::instance(); 0183 connect(resources, &Resources::resourceAdded, 0184 this, &KAlarmApp::slotResourceAdded); 0185 connect(resources, &Resources::resourcePopulated, 0186 this, &KAlarmApp::slotResourcePopulated); 0187 connect(resources, &Resources::resourcePopulated, 0188 this, &KAlarmApp::purgeNewArchivedDefault); 0189 connect(resources, &Resources::resourcesCreated, 0190 this, &KAlarmApp::slotResourcesCreated); 0191 connect(resources, &Resources::resourcesPopulated, 0192 this, &KAlarmApp::processQueue); 0193 0194 initialiseTimerResources(); // initialise calendars and alarm timer 0195 0196 KConfigGroup config(KSharedConfig::openConfig(), GENERAL_GROUP); 0197 mNoSystemTray = config.readEntry("NoSystemTray", false); 0198 mOldShowInSystemTray = wantShowInSystemTray(); 0199 DateTime::setStartOfDay(Preferences::startOfDay()); 0200 mPrefsArchivedColour = Preferences::archivedColour(); 0201 0202 // Get notified when the Freedesktop notifications properties have changed. 0203 QDBusConnection conn = QDBusConnection::sessionBus(); 0204 if (conn.interface()->isServiceRegistered(QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE))) 0205 { 0206 OrgFreedesktopDBusPropertiesInterface* piface = new OrgFreedesktopDBusPropertiesInterface( 0207 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE), 0208 QString::fromLatin1(FDO_NOTIFICATIONS_PATH), 0209 conn, this); 0210 connect(piface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, 0211 this, &KAlarmApp::slotFDOPropertiesChanged); 0212 OrgFreedesktopNotificationsInterface niface( 0213 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE), 0214 QString::fromLatin1(FDO_NOTIFICATIONS_PATH), 0215 conn); 0216 mNotificationsInhibited = niface.inhibited(); 0217 } 0218 } 0219 0220 /****************************************************************************** 0221 * Initialise or reinitialise things which are tidied up/closed by quitIf(). 0222 * Reinitialisation can be necessary if session restoration finds nothing to 0223 * restore and starts quitting the application, but KAlarm then starts up again 0224 * before the application has exited. 0225 * Reply = true if calendars were initialised successfully, 0226 * false if they were already initialised, or if initialisation failed. 0227 */ 0228 bool KAlarmApp::initialiseTimerResources() 0229 { 0230 if (!mAlarmTimer) 0231 { 0232 mAlarmTimer = new QTimer(this); 0233 mAlarmTimer->setSingleShot(true); 0234 connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); 0235 } 0236 if (!ResourcesCalendar::instance()) 0237 { 0238 qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; 0239 Desktop::setMainWindowFunc(&mainWidget); 0240 // First, initialise calendar resources, which need to be ready to 0241 // receive signals when resources initialise. 0242 ResourcesCalendar::initialise(KALARM_NAME, KALARM_VERSION); 0243 connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); 0244 connect(ResourcesCalendar::instance(), &ResourcesCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); 0245 DisplayCalendar::initialise(); 0246 // Finally, initialise the resources which generate signals as they initialise. 0247 DataModel::initialise(); 0248 return true; 0249 } 0250 return false; 0251 } 0252 0253 /****************************************************************************** 0254 * Restore the saved session if required. 0255 */ 0256 bool KAlarmApp::restoreSession() 0257 { 0258 if (!isSessionRestored()) 0259 return false; 0260 if (mFatalError) 0261 { 0262 quitFatal(); 0263 return false; 0264 } 0265 0266 // Process is being restored by session management. 0267 qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring"; 0268 ++mActiveCount; 0269 // Create the session config object now. 0270 // This is necessary since if initCheck() below causes calendars to be updated, 0271 // the session config created after that points to an invalid file, resulting 0272 // in no windows being restored followed by a later crash. 0273 KConfigGui::sessionConfig(); 0274 0275 // When KAlarm is session restored, automatically set start-at-login to true. 0276 Preferences::self()->load(); 0277 Preferences::setAutoStart(true); 0278 Preferences::setNoAutoStart(false); 0279 Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression 0280 Preferences::self()->save(); 0281 0282 if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet 0283 { 0284 --mActiveCount; 0285 quitIf(1, true); // error opening the main calendar - quit 0286 return false; 0287 } 0288 MainWindow* trayParent = nullptr; 0289 for (int i = 1; KMainWindow::canBeRestored(i); ++i) 0290 { 0291 const QString type = KMainWindow::classNameOfToplevel(i); 0292 if (type == QLatin1String("MainWindow")) 0293 { 0294 MainWindow* win = MainWindow::create(true); 0295 win->restore(i, false); 0296 if (win->isHiddenTrayParent()) 0297 trayParent = win; 0298 else 0299 win->show(); 0300 } 0301 else if (type == QLatin1String("MessageWindow")) 0302 { 0303 auto win = new MessageWindow; 0304 win->restore(i, false); 0305 if (win->isValid()) 0306 { 0307 if (Resources::allCreated() && !mNotificationsInhibited) 0308 win->display(); 0309 else 0310 mRestoredWindows += win; 0311 } 0312 else 0313 delete win; 0314 } 0315 } 0316 0317 MessageNotification::sessionRestore(); 0318 0319 // 1) There should have been a main window to restore, but even if not, there 0320 // must always be one, so create one if necessary but don't show it. 0321 // 2) Try to display the system tray icon if it is configured to be shown 0322 if (trayParent || wantShowInSystemTray()) 0323 { 0324 if (!MainWindow::count()) 0325 { 0326 qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to restore!?"; 0327 trayParent = MainWindow::create(); 0328 } 0329 displayTrayIcon(true, trayParent); 0330 // Occasionally for no obvious reason, the main main window is 0331 // shown when it should be hidden, so hide it just to be sure. 0332 if (trayParent) 0333 trayParent->hide(); 0334 } 0335 else 0336 createOnlyMainWindow(); 0337 0338 --mActiveCount; 0339 if (quitIf(0)) // quit if no windows are open 0340 return false; // quitIf() can sometimes return, despite calling exit() 0341 0342 startProcessQueue(); // start processing the execution queue 0343 return true; 0344 } 0345 0346 /****************************************************************************** 0347 * If resources have been created and notifications are not inhibited, 0348 * show message windows restored at startup which are waiting to be displayed, 0349 * and redisplay alarms showing when the program crashed or was killed. 0350 */ 0351 void KAlarmApp::showRestoredWindows() 0352 { 0353 if (!mNotificationsInhibited && Resources::allCreated()) 0354 { 0355 if (!mRestoredWindows.isEmpty()) 0356 { 0357 // Display message windows restored at startup. 0358 for (MessageWindow* win : std::as_const(mRestoredWindows)) 0359 win->display(); 0360 mRestoredWindows.clear(); 0361 } 0362 if (mRedisplayAlarms) 0363 { 0364 // Display alarms which were showing when the program crashed or was killed. 0365 mRedisplayAlarms = false; 0366 MessageDisplay::redisplayAlarms(); 0367 } 0368 } 0369 } 0370 0371 /****************************************************************************** 0372 * Called to start a new instance of the unique QApplication. 0373 * Reply: exit code (>= 0), or -1 to continue execution. 0374 * If exit code >= 0, 'outputText' holds text to output before terminating. 0375 */ 0376 int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText) 0377 { 0378 Q_UNUSED(workingDirectory) 0379 qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance" << args; 0380 if (outputText) 0381 outputText->clear(); 0382 if (mFatalError) 0383 { 0384 Q_EMIT setExitValue(1); 0385 quitFatal(); 0386 return 1; 0387 } 0388 0389 // The D-Bus call to activate a subsequent instance of KAlarm may not supply 0390 // any arguments, but we need one. 0391 if (!args.isEmpty() && mActivateArg0.isEmpty()) 0392 mActivateArg0 = args[0]; 0393 QStringList fixedArgs(args); 0394 if (args.isEmpty() && !mActivateArg0.isEmpty()) 0395 fixedArgs << mActivateArg0; 0396 0397 // Parse and interpret command line arguments. 0398 QCommandLineParser parser; 0399 KAboutData::applicationData().setupCommandLine(&parser); 0400 parser.setApplicationDescription(QApplication::applicationDisplayName()); 0401 auto options = new CommandOptions; 0402 const QStringList nonexecArgs = options->setOptions(&parser, fixedArgs); 0403 options->parse(); 0404 KAboutData::applicationData().processCommandLine(&parser); 0405 0406 ++mActiveCount; 0407 int exitCode = 0; // default = success 0408 static bool firstInstance = true; 0409 bool dontRedisplay = false; 0410 if (!firstInstance || !isSessionRestored()) 0411 { 0412 options->process(); 0413 #ifndef NDEBUG 0414 if (options->simulationTime().isValid()) 0415 KAlarm::setSimulatedSystemTime(options->simulationTime()); 0416 #endif 0417 CommandOptions::Command command = options->command(); 0418 if (options->disableAll()) 0419 setAlarmsEnabled(false); // disable alarm monitoring 0420 0421 // Handle options which exit with a terminal message, before 0422 // making the application a unique application, since a 0423 // unique application won't output to the terminal if another 0424 // instance is already running. 0425 switch (command) 0426 { 0427 case CommandOptions::CMD_ERROR: 0428 Q_EMIT setExitValue(1); 0429 if (outputText) 0430 { 0431 // Instance was activated from main(). 0432 *outputText = options->outputText(); 0433 delete options; 0434 return 1; 0435 } 0436 // Instance was activated by DBus. 0437 std::cerr << qPrintable(options->outputText()) << std::endl;; 0438 mReadOnly = true; // don't need write access to calendars 0439 exitCode = 1; 0440 break; 0441 default: 0442 break; 0443 } 0444 0445 switch (command) 0446 { 0447 case CommandOptions::TRIGGER_EVENT: 0448 case CommandOptions::CANCEL_EVENT: 0449 { 0450 // Display or delete the event with the specified event ID 0451 auto action = static_cast<QueuedAction>(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel) 0452 | int(QueuedAction::Exit)); 0453 // Open the calendar, don't start processing execution queue yet, 0454 // and wait for the calendar resources to be populated. 0455 if (!initCheck(true)) 0456 exitCode = 1; 0457 else 0458 { 0459 mCommandOption = options->commandName(); 0460 // Get the resource ID string and event UID. Note that if 0461 // resources have not been created yet, the numeric 0462 // resource ID can't yet be looked up. 0463 if (options->resourceId().isEmpty()) 0464 action = static_cast<QueuedAction>((int)action | int(QueuedAction::FindId)); 0465 mActionQueue.enqueue(ActionQEntry(action, EventId(options->eventId()), options->resourceId())); 0466 startProcessQueue(true); // start processing the execution queue 0467 dontRedisplay = true; 0468 } 0469 break; 0470 } 0471 case CommandOptions::LIST: 0472 // Output a list of scheduled alarms to stdout. 0473 // Open the calendar, don't start processing execution queue yet, 0474 // and wait for all calendar resources to be populated. 0475 mReadOnly = true; // don't need write access to calendars 0476 if (firstInstance) 0477 mAlarmsEnabled = false; // prevent alarms being processed if no other instance is running 0478 if (!initCheck(true)) 0479 exitCode = 1; 0480 else 0481 { 0482 const auto action = static_cast<QueuedAction>(int(QueuedAction::List) | int(QueuedAction::Exit)); 0483 mActionQueue.enqueue(ActionQEntry(action, EventId())); 0484 startProcessQueue(true); // start processing the execution queue 0485 dontRedisplay = true; 0486 } 0487 break; 0488 0489 case CommandOptions::EDIT: 0490 // Edit a specified existing alarm. 0491 // Open the calendar and wait for the calendar resources to be populated. 0492 if (!initCheck(false)) 0493 exitCode = 1; 0494 else 0495 { 0496 mCommandOption = options->commandName(); 0497 if (firstInstance) 0498 mEditingCmdLineAlarm = 0x10; // want to redisplay alarms if successful 0499 // Get the resource ID string and event UID. Note that if 0500 // resources have not been created yet, the numeric 0501 // resource ID can't yet be looked up. 0502 mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, EventId(options->eventId()), options->resourceId())); 0503 startProcessQueue(true); // start processing the execution queue 0504 dontRedisplay = true; 0505 } 0506 break; 0507 0508 case CommandOptions::EDIT_NEW: 0509 { 0510 // Edit a new alarm, and optionally preset selected values 0511 if (!initCheck()) 0512 exitCode = 1; 0513 else 0514 { 0515 EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow()); 0516 if (!editDlg) 0517 { 0518 exitCode = 1; 0519 break; 0520 } 0521 editDlg->setName(options->name()); 0522 if (options->alarmTime().isValid()) 0523 editDlg->setTime(options->alarmTime()); 0524 if (options->recurrence()) 0525 editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount()); 0526 else if (options->flags() & KAEvent::REPEAT_AT_LOGIN) 0527 editDlg->setRepeatAtLogin(); 0528 editDlg->setAction(options->editAction(), AlarmText(options->text())); 0529 if (options->lateCancel()) 0530 editDlg->setLateCancel(options->lateCancel()); 0531 if (options->flags() & KAEvent::WAKE_SUSPEND) 0532 editDlg->setWakeFromSuspend(true); 0533 if (options->flags() & KAEvent::COPY_KORGANIZER) 0534 editDlg->setShowInKOrganizer(true); 0535 switch (options->editType()) 0536 { 0537 case EditAlarmDlg::DISPLAY: 0538 { 0539 // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY 0540 auto dlg = qobject_cast<EditDisplayAlarmDlg*>(editDlg); 0541 if (options->fgColour().isValid()) 0542 dlg->setFgColour(options->fgColour()); 0543 if (options->bgColour().isValid()) 0544 dlg->setBgColour(options->bgColour()); 0545 if (!options->audioFile().isEmpty() 0546 || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK)) 0547 { 0548 const KAEvent::Flags flags = options->flags(); 0549 const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep 0550 : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak 0551 : Preferences::Sound_File; 0552 dlg->setAudio(type, options->audioFile(), options->audioVolume(), ((flags & KAEvent::REPEAT_SOUND) ? 0 : -1)); 0553 } 0554 if (options->reminderMinutes()) 0555 dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE)); 0556 if (options->flags() & KAEvent::NOTIFY) 0557 dlg->setNotify(true); 0558 if (options->flags() & KAEvent::CONFIRM_ACK) 0559 dlg->setConfirmAck(true); 0560 if (options->flags() & KAEvent::AUTO_CLOSE) 0561 dlg->setAutoClose(true); 0562 break; 0563 } 0564 case EditAlarmDlg::COMMAND: 0565 break; 0566 case EditAlarmDlg::EMAIL: 0567 { 0568 // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL 0569 auto dlg = qobject_cast<EditEmailAlarmDlg*>(editDlg); 0570 if (options->fromID() 0571 || !options->addressees().isEmpty() 0572 || !options->subject().isEmpty() 0573 || !options->attachments().isEmpty()) 0574 dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments()); 0575 if (options->flags() & KAEvent::EMAIL_BCC) 0576 dlg->setBcc(true); 0577 break; 0578 } 0579 case EditAlarmDlg::AUDIO: 0580 { 0581 // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO 0582 auto dlg = qobject_cast<EditAudioAlarmDlg*>(editDlg); 0583 if (!options->audioFile().isEmpty() || options->audioVolume() >= 0) 0584 dlg->setAudio(options->audioFile(), options->audioVolume()); 0585 break; 0586 } 0587 case EditAlarmDlg::NO_TYPE: 0588 break; 0589 } 0590 0591 // Execute the edit dialogue. Note that if no other instance of KAlarm is 0592 // running, this new instance will not exit after the dialogue is closed. 0593 // This is deliberate, since exiting would mean that KAlarm wouldn't 0594 // trigger the new alarm. 0595 KAlarm::execNewAlarmDlg(editDlg); 0596 0597 createOnlyMainWindow(); // prevent the application from quitting 0598 } 0599 break; 0600 } 0601 case CommandOptions::EDIT_NEW_PRESET: 0602 // Edit a new alarm, preset with a template 0603 if (!initCheck()) 0604 exitCode = 1; 0605 else 0606 { 0607 // Execute the edit dialogue. Note that if no other instance of KAlarm is 0608 // running, this new instance will not exit after the dialogue is closed. 0609 // This is deliberate, since exiting would mean that KAlarm wouldn't 0610 // trigger the new alarm. 0611 KAlarm::editNewAlarm(options->name()); 0612 0613 createOnlyMainWindow(); // prevent the application from quitting 0614 } 0615 break; 0616 0617 case CommandOptions::NEW: 0618 // Display a message or file, execute a command, or send an email 0619 setResourcesTimeout(); // set timeout for resource initialisation 0620 if (!initCheck()) 0621 exitCode = 1; 0622 else 0623 { 0624 QueuedAction flags = QueuedAction::CmdLine; 0625 if (!MainWindow::count()) 0626 flags = static_cast<QueuedAction>(int(flags) + int(QueuedAction::ErrorExit)); 0627 if (!scheduleEvent(flags, 0628 options->editAction(), options->name(), options->text(), 0629 options->alarmTime(), options->lateCancel(), options->flags(), 0630 options->bgColour(), options->fgColour(), QFont(), 0631 options->audioFile(), options->audioVolume(), 0632 options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()), 0633 options->subRepeatInterval(), options->subRepeatCount(), 0634 options->fromID(), options->addressees(), 0635 options->subject(), options->attachments())) 0636 exitCode = 1; 0637 else 0638 createOnlyMainWindow(); // prevent the application from quitting 0639 } 0640 break; 0641 0642 case CommandOptions::TRAY: 0643 // Display only the system tray icon 0644 if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) 0645 { 0646 if (!initCheck()) // open the calendar, start processing execution queue 0647 exitCode = 1; 0648 else 0649 { 0650 if (!displayTrayIcon(true)) 0651 exitCode = 1; 0652 } 0653 break; 0654 } 0655 [[fallthrough]]; // fall through to NONE 0656 case CommandOptions::NONE: 0657 // No arguments - run interactively & display the main window 0658 #ifndef NDEBUG 0659 if (options->simulationTime().isValid() && !firstInstance) 0660 break; // simulating time: don't open main window if already running 0661 #endif 0662 if (!initCheck()) 0663 exitCode = 1; 0664 else 0665 { 0666 if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) 0667 mTrayWindow->showAssocMainWindow(); 0668 else 0669 { 0670 MainWindow* win = MainWindow::create(); 0671 if (command == CommandOptions::TRAY) 0672 win->setWindowState(win->windowState() | Qt::WindowMinimized); 0673 win->show(); 0674 } 0675 } 0676 break; 0677 default: 0678 break; 0679 } 0680 } 0681 if (options != CommandOptions::firstInstance()) 0682 delete options; 0683 0684 // If this is the first time through, redisplay any alarm message windows 0685 // from last time. 0686 if (firstInstance && !dontRedisplay && !exitCode) 0687 { 0688 /* First time through, so redisplay alarm message windows from last time. 0689 * But it is possible for session restoration in some circumstances to 0690 * not create any windows, in which case the alarm calendars will have 0691 * been deleted - if so, don't try to do anything. (This has been known 0692 * to happen under the Xfce desktop.) 0693 */ 0694 if (ResourcesCalendar::instance()) 0695 { 0696 mRedisplayAlarms = true; 0697 showRestoredWindows(); 0698 } 0699 } 0700 0701 --mActiveCount; 0702 firstInstance = false; 0703 0704 // Quit the application if this was the last/only running "instance" of the program. 0705 // Executing 'return' doesn't work very well since the program continues to 0706 // run if no windows were created. 0707 if (quitIf(exitCode >= 0 ? exitCode : 0)) 0708 return exitCode; // exit this application instance 0709 0710 return -1; // continue executing the application instance 0711 } 0712 0713 /****************************************************************************** 0714 * Create a minimised main window if none already exists. 0715 * This prevents the application from quitting. 0716 */ 0717 void KAlarmApp::createOnlyMainWindow() 0718 { 0719 if (!MainWindow::count()) 0720 { 0721 if (wantShowInSystemTray()) 0722 { 0723 if (displayTrayIcon(true)) 0724 return; 0725 } 0726 MainWindow* win = MainWindow::create(); 0727 win->setWindowState(Qt::WindowMinimized); 0728 win->show(); 0729 } 0730 } 0731 0732 /****************************************************************************** 0733 * Quit the program, optionally only if there are no more "instances" running. 0734 * Reply = true if program exited. 0735 */ 0736 bool KAlarmApp::quitIf(int exitCode, bool force) 0737 { 0738 if (force) 0739 { 0740 // Quit regardless, except for message windows 0741 mQuitting = true; 0742 MainWindow::closeAll(); 0743 mQuitting = false; 0744 displayTrayIcon(false); 0745 if (MessageDisplay::instanceCount(true)) // ignore always-hidden displays (e.g. audio alarms) 0746 return false; 0747 } 0748 else if (mQuitting) 0749 return false; // MainWindow::closeAll() causes quitIf() to be called again 0750 else 0751 { 0752 // Quit only if there are no more "instances" running 0753 mPendingQuit = false; 0754 if (mActiveCount > 0 || MessageDisplay::instanceCount(true)) // ignore always-hidden displays (e.g. audio alarms) 0755 return false; 0756 const int mwcount = MainWindow::count(); 0757 MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr; 0758 if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent()))) 0759 return false; 0760 // There are no windows left except perhaps a main window which is a hidden 0761 // tray icon parent, or an always-hidden message window. 0762 if (mTrayWindow) 0763 { 0764 // There is a system tray icon. 0765 // Don't exit unless the system tray doesn't seem to exist. 0766 if (checkSystemTray()) 0767 return false; 0768 } 0769 if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty()) 0770 { 0771 // Don't quit yet if there are outstanding actions on the execution queue 0772 mPendingQuit = true; 0773 mPendingQuitCode = exitCode; 0774 return false; 0775 } 0776 } 0777 0778 // This was the last/only running "instance" of the program, so exit completely. 0779 // NOTE: Everything which is terminated/deleted here must where applicable 0780 // be initialised in the initialiseTimerResources() method, in case 0781 // KAlarm is started again before application exit completes! 0782 qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting"; 0783 MessageDisplay::stopAudio(); 0784 #if ENABLE_RTC_WAKE_FROM_SUSPEND 0785 if (mCancelRtcWake) 0786 { 0787 // Just in case, cancel RTC wake even if kernel wake alarms are available 0788 KAlarm::setRtcWakeTime(0, nullptr); 0789 KAlarm::deleteRtcWakeConfig(); 0790 } 0791 #endif 0792 delete mAlarmTimer; // prevent checking for alarms after deleting calendars 0793 mAlarmTimer = nullptr; 0794 mInitialised = false; // prevent processQueue() from running 0795 ResourcesCalendar::terminate(); 0796 DisplayCalendar::terminate(); 0797 DataModel::terminate(); 0798 Q_EMIT setExitValue(exitCode); 0799 exit(exitCode); 0800 return true; // sometimes we actually get to here, despite calling exit() 0801 } 0802 0803 /****************************************************************************** 0804 * Called when the Quit menu item is selected. 0805 * Closes the system tray window and all main windows, but does not exit the 0806 * program if other windows are still open. 0807 */ 0808 void KAlarmApp::doQuit(QWidget* parent) 0809 { 0810 qCDebug(KALARM_LOG) << "KAlarmApp::doQuit"; 0811 if (KAMessageBox::warningCancelContinue(parent, 0812 i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."), 0813 QString(), KStandardGuiItem::quit(), 0814 KStandardGuiItem::cancel(), Preferences::QUIT_WARN 0815 ) != KMessageBox::Continue) 0816 return; 0817 #if ENABLE_RTC_WAKE_FROM_SUSPEND 0818 if (!KAlarm::checkRtcWakeConfig(true).isEmpty()) 0819 { 0820 // A wake-on-suspend alarm is set 0821 if (KAMessageBox::warningCancelContinue(parent, 0822 i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."), 0823 QString(), KStandardGuiItem::quit() 0824 ) != KMessageBox::Continue) 0825 return; 0826 mCancelRtcWake = true; 0827 } 0828 #endif 0829 if (!Preferences::autoStart()) 0830 { 0831 int option = KMessageBox::ButtonCode::SecondaryAction; 0832 if (!Preferences::autoStartChangedByUser()) 0833 { 0834 option = KAMessageBox::questionYesNoCancel(parent, 0835 xi18nc("@info", "Do you want to start KAlarm at login?<nl/>" 0836 "(Note that alarms will be disabled if KAlarm is not started.)"), 0837 QString(), KGuiItem(i18n("Yes")), KGuiItem(i18n("No")), 0838 KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START); 0839 } 0840 switch (option) 0841 { 0842 case KMessageBox::ButtonCode::PrimaryAction: 0843 Preferences::setAutoStart(true); 0844 Preferences::setNoAutoStart(false); 0845 break; 0846 case KMessageBox::ButtonCode::SecondaryAction: 0847 Preferences::setNoAutoStart(true); 0848 break; 0849 case KMessageBox::Cancel: 0850 default: 0851 return; 0852 } 0853 Preferences::self()->save(); 0854 } 0855 quitIf(0, true); 0856 } 0857 0858 /****************************************************************************** 0859 * Display an error message for a fatal error. Prevent further actions since 0860 * the program state is unsafe. 0861 */ 0862 void KAlarmApp::displayFatalError(const QString& message) 0863 { 0864 if (!mFatalError) 0865 { 0866 mFatalError = 1; 0867 mFatalMessage = message; 0868 if (mInstance) 0869 QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 0870 } 0871 } 0872 0873 /****************************************************************************** 0874 * Quit the program, once the fatal error message has been acknowledged. 0875 */ 0876 void KAlarmApp::quitFatal() 0877 { 0878 switch (mFatalError) 0879 { 0880 case 0: 0881 case 2: 0882 return; 0883 case 1: 0884 mFatalError = 2; 0885 KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window 0886 mFatalError = 3; 0887 [[fallthrough]]; // fall through to '3' 0888 case 3: 0889 if (mInstance) 0890 mInstance->quitIf(1, true); 0891 break; 0892 } 0893 QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 0894 } 0895 0896 /****************************************************************************** 0897 * Called by the alarm timer when the next alarm is due. 0898 * Also called when the execution queue has finished processing to check for the 0899 * next alarm. 0900 */ 0901 void KAlarmApp::checkNextDueAlarm() 0902 { 0903 if (!mAlarmsEnabled) 0904 return; 0905 // Find the first alarm due 0906 KADateTime nextDt; 0907 const KAEvent nextEvent = ResourcesCalendar::earliestAlarm(nextDt, mNotificationsInhibited); 0908 if (!nextEvent.isValid()) 0909 return; // there are no alarms pending 0910 const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); 0911 qint64 interval = now.msecsTo(nextDt); 0912 qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; 0913 if (interval <= 0) 0914 { 0915 // Queue the alarm 0916 queueAlarmId(nextEvent); 0917 qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << ": due now"; 0918 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 0919 } 0920 else 0921 { 0922 // No alarm is due yet, so set timer to wake us when it's due. 0923 // Check for integer overflow before setting timer. 0924 #ifndef HIBERNATION_SIGNAL 0925 /* TODO: Use hibernation wakeup signal: 0926 * #include <Solid/Power> 0927 * connect(Solid::Power::self(), &Solid::Power::resumeFromSuspend, ...) 0928 * (or resumingFromSuspend?) 0929 * to be notified when wakeup from hibernation occurs. But can't use it 0930 * unless we know that this notification is supported by the system! 0931 */ 0932 /* Re-evaluate the next alarm time every minute, in case the 0933 * system clock jumps. The most common case when the clock jumps 0934 * is when a laptop wakes from hibernation. If timers were left to 0935 * run, they would trigger late by the length of time the system 0936 * was asleep. 0937 */ 0938 if (interval > 60000) // 1 minute 0939 interval = 60000; 0940 #endif 0941 ++interval; // ensure we don't trigger just before the minute boundary 0942 if (interval > INT_MAX) 0943 interval = INT_MAX; 0944 qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << "wait" << interval/1000 << "seconds"; 0945 mAlarmTimer->start(static_cast<int>(interval)); 0946 } 0947 } 0948 0949 /****************************************************************************** 0950 * Called by the alarm timer when the next alarm is due. 0951 * Also called when the execution queue has finished processing to check for the 0952 * next alarm. 0953 */ 0954 void KAlarmApp::queueAlarmId(const KAEvent& event) 0955 { 0956 const EventId id(event); 0957 for (const ActionQEntry& entry : std::as_const(mActionQueue)) 0958 { 0959 if (entry.action == QueuedAction::Handle && entry.eventId == id) 0960 return; // the alarm is already queued 0961 } 0962 mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id)); 0963 } 0964 0965 /****************************************************************************** 0966 * Start processing the execution queue. 0967 */ 0968 void KAlarmApp::startProcessQueue(bool evenIfStarted) 0969 { 0970 if (!mInitialised || evenIfStarted) 0971 { 0972 qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue"; 0973 mInitialised = true; 0974 // Process anything already queued. 0975 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 0976 } 0977 } 0978 0979 /****************************************************************************** 0980 * The main processing loop for KAlarm. 0981 * All KAlarm operations involving opening or updating calendar files are called 0982 * from this loop to ensure that only one operation is active at any one time. 0983 * This precaution is necessary because KAlarm's activities are mostly 0984 * asynchronous, being in response to D-Bus calls from other programs or timer 0985 * events, any of which can be received in the middle of performing another 0986 * operation. If a calendar file is opened or updated while another calendar 0987 * operation is in progress, the program has been observed to hang, or the first 0988 * calendar call has failed with data loss - clearly unacceptable!! 0989 */ 0990 void KAlarmApp::processQueue() 0991 { 0992 if (mInitialised && !mProcessingQueue) 0993 { 0994 qCDebug(KALARM_LOG) << "KAlarmApp::processQueue"; 0995 mProcessingQueue = true; 0996 0997 // Refresh alarms if that's been queued 0998 KAlarm::refreshAlarmsIfQueued(); 0999 1000 // Process queued events 1001 while (!mActionQueue.isEmpty()) 1002 { 1003 ActionQEntry& entry = mActionQueue.head(); 1004 1005 // If the first action's resource ID is a string, can't process it 1006 // until its numeric resource ID can be found. 1007 if (!entry.resourceId.isEmpty()) 1008 { 1009 if (!Resources::allCreated()) 1010 { 1011 // If resource population has timed out, discard all queued events. 1012 if (mResourcesTimedOut) 1013 { 1014 qCCritical(KALARM_LOG) << "Error! Timeout creating calendars"; 1015 mActionQueue.clear(); 1016 } 1017 break; 1018 } 1019 // Convert the resource ID string to the numeric resource ID. 1020 entry.eventId.setResourceId(EventId::getResourceId(entry.resourceId)); 1021 entry.resourceId.clear(); 1022 } 1023 1024 // Can't process the first action until its resource has been populated. 1025 const ResourceId id = entry.eventId.resourceId(); 1026 if ((id < 0 && !Resources::allPopulated()) 1027 || (id >= 0 && !Resources::resource(id).isPopulated())) 1028 { 1029 // If resource population has timed out, discard all queued events. 1030 if (mResourcesTimedOut) 1031 { 1032 qCCritical(KALARM_LOG) << "Error! Timeout reading calendars"; 1033 mActionQueue.clear(); 1034 } 1035 break; 1036 } 1037 1038 // Process the first action in the queue. 1039 const bool findUniqueId = int(entry.action) & int(QueuedAction::FindId); 1040 bool exitAfter = int(entry.action) & int(QueuedAction::Exit); 1041 const bool exitAfterError = int(entry.action) & int(QueuedAction::ErrorExit); 1042 const bool commandLine = int(entry.action) & int(QueuedAction::CmdLine); 1043 const auto action = static_cast<QueuedAction>(int(entry.action) & int(QueuedAction::ActionMask)); 1044 1045 bool ok = true; 1046 bool inhibit = false; 1047 if (entry.eventId.isEmpty()) 1048 { 1049 // It's a new alarm 1050 switch (action) 1051 { 1052 case QueuedAction::Trigger: 1053 if (execAlarm(entry.event, entry.event.firstAlarm()) == (void*)-2) 1054 inhibit = true; 1055 break; 1056 case QueuedAction::Handle: 1057 { 1058 Resource resource = Resources::destination(CalEvent::ACTIVE, nullptr, Resources::NoResourcePrompt | Resources::UseOnlyResource); 1059 if (!resource.isValid()) 1060 { 1061 qCWarning(KALARM_LOG) << "KAlarmApp::processQueue: Error! Cannot create alarm (no default calendar is defined)"; 1062 1063 if (commandLine) 1064 { 1065 const QString errmsg = xi18nc("@info:shell", "Cannot create alarm: No default calendar is defined"); 1066 std::cerr << qPrintable(errmsg) << std::endl; 1067 } 1068 ok = false; 1069 } 1070 else 1071 { 1072 const KAlarm::UpdateResult result = KAlarm::addEvent(entry.event, resource, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); 1073 if (result >= KAlarm::UPDATE_ERROR) 1074 { 1075 //TODO: display error message for command line action, but first ensure that one is returned by addEvent()! 1076 #if 0 1077 if (commandLine) 1078 { 1079 const QString errmsg = xi18nc("@info:shell", "Error creating alarm"); 1080 std::cerr << errmsg.toLocal8Bit().data(); 1081 } 1082 #endif 1083 ok = false; 1084 } 1085 } 1086 if (!ok && exitAfterError) 1087 exitAfter = true; 1088 break; 1089 } 1090 case QueuedAction::List: 1091 { 1092 const QStringList alarms = scheduledAlarmList(); 1093 for (const QString& alarm : alarms) 1094 std::cout << qUtf8Printable(alarm) << std::endl; 1095 break; 1096 } 1097 default: 1098 break; 1099 } 1100 } 1101 else 1102 { 1103 if (action == QueuedAction::Edit) 1104 { 1105 int editingCmdLineAlarm = mEditingCmdLineAlarm & 3; 1106 bool keepQueued = editingCmdLineAlarm <= 1; 1107 switch (editingCmdLineAlarm) 1108 { 1109 case 0: 1110 // Initiate editing an alarm specified on the command line. 1111 mEditingCmdLineAlarm |= 1; 1112 QTimer::singleShot(0, this, &KAlarmApp::slotEditAlarmById); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1113 break; 1114 case 1: 1115 // Currently editing the alarm. 1116 break; 1117 case 2: 1118 // The edit has completed. 1119 mEditingCmdLineAlarm = 0; 1120 break; 1121 default: 1122 break; 1123 } 1124 if (keepQueued) 1125 break; 1126 } 1127 else 1128 { 1129 // Trigger the event if it's due. 1130 const int result = handleEvent(entry.eventId, action, findUniqueId); 1131 if (!result) 1132 inhibit = true; 1133 else if (result < 0 && exitAfter) 1134 { 1135 CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not unique", mCommandOption, entry.eventId.eventId())); 1136 ok = false; 1137 } 1138 } 1139 } 1140 1141 mActionQueue.dequeue(); 1142 1143 if (inhibit) 1144 { 1145 // It's a display event which can't be executed because notifications 1146 // are inhibited. Move it to the inhibited queue until the inhibition 1147 // is removed. 1148 } 1149 else if (exitAfter) 1150 { 1151 mProcessingQueue = false; // don't inhibit processing if there is another instance 1152 quitIf((ok ? 0 : 1), exitAfterError); 1153 return; // quitIf() can sometimes return, despite calling exit() 1154 } 1155 } 1156 1157 // Purge the default archived alarms resource if it's time to do so 1158 if (mPurgeDaysQueued >= 0) 1159 { 1160 KAlarm::purgeArchive(mPurgeDaysQueued); 1161 mPurgeDaysQueued = -1; 1162 } 1163 1164 // Now that the queue has been processed, quit if a quit was queued 1165 if (mPendingQuit) 1166 { 1167 if (quitIf(mPendingQuitCode)) 1168 return; // quitIf() can sometimes return, despite calling exit() 1169 } 1170 1171 mProcessingQueue = false; 1172 1173 if (!mEditingCmdLineAlarm) 1174 { 1175 // Schedule the application to be woken when the next alarm is due 1176 checkNextDueAlarm(); 1177 } 1178 } 1179 } 1180 1181 /****************************************************************************** 1182 * Called when a repeat-at-login alarm has been added externally. 1183 * Queues the alarm for triggering. 1184 * First, cancel any scheduled reminder or deferral for it, since these will be 1185 * superseded by the new at-login trigger. 1186 */ 1187 void KAlarmApp::atLoginEventAdded(const KAEvent& event) 1188 { 1189 KAEvent ev = event; 1190 if (!cancelReminderAndDeferral(ev)) 1191 { 1192 if (mAlarmsEnabled) 1193 { 1194 mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, EventId(ev))); 1195 if (mInitialised) 1196 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1197 } 1198 } 1199 } 1200 1201 /****************************************************************************** 1202 * Called when the system tray main window is closed. 1203 */ 1204 void KAlarmApp::removeWindow(TrayWindow*) 1205 { 1206 mTrayWindow = nullptr; 1207 } 1208 1209 /****************************************************************************** 1210 * Display or close the system tray icon. 1211 */ 1212 bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) 1213 { 1214 qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon"; 1215 static bool creating = false; 1216 if (show) 1217 { 1218 if (!mTrayWindow && !creating) 1219 { 1220 if (!QSystemTrayIcon::isSystemTrayAvailable()) 1221 return false; 1222 if (!MainWindow::count()) 1223 { 1224 // We have to have at least one main window to act 1225 // as parent to the system tray icon (even if the 1226 // window is hidden). 1227 creating = true; // prevent main window constructor from creating an additional tray icon 1228 parent = MainWindow::create(); 1229 creating = false; 1230 } 1231 mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); 1232 connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled); 1233 Q_EMIT trayIconToggled(); 1234 1235 if (!checkSystemTray()) 1236 quitIf(0); // exit the application if there are no open windows 1237 } 1238 } 1239 else 1240 { 1241 delete mTrayWindow; 1242 mTrayWindow = nullptr; 1243 } 1244 return true; 1245 } 1246 1247 /****************************************************************************** 1248 * Check whether the system tray icon has been housed in the system tray. 1249 */ 1250 bool KAlarmApp::checkSystemTray() 1251 { 1252 if (!mTrayWindow) 1253 return true; 1254 if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray) 1255 { 1256 qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray; 1257 mNoSystemTray = !mNoSystemTray; 1258 1259 // Store the new setting in the config file, so that if KAlarm exits it will 1260 // restart with the correct default. 1261 KConfigGroup config(KSharedConfig::openConfig(), GENERAL_GROUP); 1262 config.writeEntry("NoSystemTray", mNoSystemTray); 1263 config.sync(); 1264 1265 // Update other settings 1266 slotShowInSystemTrayChanged(); 1267 } 1268 return !mNoSystemTray; 1269 } 1270 1271 /****************************************************************************** 1272 * Return the main window associated with the system tray icon. 1273 */ 1274 MainWindow* KAlarmApp::trayMainWindow() const 1275 { 1276 return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; 1277 } 1278 1279 /****************************************************************************** 1280 * Called when the show-in-system-tray preference setting has changed, to show 1281 * or hide the system tray icon. 1282 */ 1283 void KAlarmApp::slotShowInSystemTrayChanged() 1284 { 1285 const bool newShowInSysTray = wantShowInSystemTray(); 1286 if (newShowInSysTray != mOldShowInSystemTray) 1287 { 1288 // The system tray run mode has changed 1289 ++mActiveCount; // prevent the application from quitting 1290 MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; 1291 delete mTrayWindow; // remove the system tray icon if it is currently shown 1292 mTrayWindow = nullptr; 1293 mOldShowInSystemTray = newShowInSysTray; 1294 if (newShowInSysTray) 1295 { 1296 // Show the system tray icon 1297 displayTrayIcon(true); 1298 } 1299 else 1300 { 1301 // Stop showing the system tray icon 1302 if (win && win->isHidden()) 1303 { 1304 if (MainWindow::count() > 1) 1305 delete win; 1306 else 1307 { 1308 win->setWindowState(win->windowState() | Qt::WindowMinimized); 1309 win->show(); 1310 } 1311 } 1312 } 1313 --mActiveCount; 1314 } 1315 } 1316 1317 /****************************************************************************** 1318 * Called when the start-of-day time preference setting has changed. 1319 * Change alarm times for date-only alarms. 1320 */ 1321 void KAlarmApp::changeStartOfDay() 1322 { 1323 DateTime::setStartOfDay(Preferences::startOfDay()); 1324 KAEvent::setStartOfDay(Preferences::startOfDay()); 1325 Resources::adjustStartOfDay(); 1326 DisplayCalendar::adjustStartOfDay(); 1327 } 1328 1329 /****************************************************************************** 1330 * Called when the default alarm message font preference setting has changed. 1331 * Notify KAEvent. 1332 */ 1333 void KAlarmApp::slotMessageFontChanged(const QFont& font) 1334 { 1335 KAEvent::setDefaultFont(font); 1336 } 1337 1338 /****************************************************************************** 1339 * Called when the working time preference settings have changed. 1340 * Notify KAEvent. 1341 */ 1342 void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) 1343 { 1344 KAEvent::setWorkTime(days, start, end, Preferences::timeSpec()); 1345 } 1346 1347 /****************************************************************************** 1348 * Called when the holiday region preference setting has changed. 1349 * Notify KAEvent. 1350 */ 1351 void KAlarmApp::slotHolidaysChanged(const KAlarmCal::Holidays& holidays) 1352 { 1353 KAEvent::setHolidays(holidays); 1354 } 1355 1356 /****************************************************************************** 1357 * Called when the date for February 29th recurrences has changed in the 1358 * preferences settings. 1359 */ 1360 void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) 1361 { 1362 KARecurrence::Feb29Type rtype; 1363 switch (type) 1364 { 1365 default: 1366 case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; 1367 case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; 1368 case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; 1369 } 1370 KARecurrence::setDefaultFeb29Type(rtype); 1371 } 1372 1373 /****************************************************************************** 1374 * Return whether the program is configured to be running in the system tray. 1375 */ 1376 bool KAlarmApp::wantShowInSystemTray() const 1377 { 1378 return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); 1379 } 1380 1381 /****************************************************************************** 1382 * Set a timeout for populating resources. 1383 */ 1384 void KAlarmApp::setResourcesTimeout() 1385 { 1386 QTimer::singleShot(RESOURCES_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1387 } 1388 1389 /****************************************************************************** 1390 * Called on a timeout to check whether resources have been populated. 1391 * If not, exit the program with code 1. 1392 */ 1393 void KAlarmApp::slotResourcesTimeout() 1394 { 1395 if (!Resources::allPopulated()) 1396 { 1397 // Resource population has timed out. 1398 mResourcesTimedOut = true; 1399 quitIf(1); 1400 } 1401 } 1402 1403 /****************************************************************************** 1404 * Called when all resources have been created at startup. 1405 * Check whether there are any writable active calendars, and if not, warn the 1406 * user. 1407 * If alarms are being archived, check whether there is a default archived 1408 * calendar, and if not, warn the user. 1409 */ 1410 void KAlarmApp::slotResourcesCreated() 1411 { 1412 showRestoredWindows(); // display message windows restored at startup. 1413 checkWritableCalendar(); 1414 checkArchivedCalendar(); 1415 processQueue(); 1416 } 1417 1418 /****************************************************************************** 1419 * Called when all calendars have been fetched at startup, or calendar migration 1420 * has completed. 1421 * Check whether there are any writable active calendars, and if not, warn the 1422 * user. 1423 */ 1424 void KAlarmApp::checkWritableCalendar() 1425 { 1426 if (mReadOnly) 1427 return; // don't need write access to calendars 1428 if (!Resources::allCreated() 1429 || !DataModel::isMigrationComplete()) 1430 return; 1431 static bool done = false; 1432 if (done) 1433 return; 1434 done = true; 1435 qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; 1436 1437 // Check for, and remove, any duplicate resources, i.e. those which use the 1438 // same calendar file/directory. 1439 DataModel::removeDuplicateResources(); 1440 1441 // Find whether there are any writable active alarm calendars 1442 const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); 1443 if (!active) 1444 { 1445 qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar"; 1446 KAMessageBox::information(MainWindow::mainMainWindow(), 1447 xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled.<nl/><nl/>" 1448 "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."), 1449 QString(), QStringLiteral("noWritableCal")); 1450 } 1451 } 1452 1453 /****************************************************************************** 1454 * If alarms are being archived, check whether there is a default archived 1455 * calendar, and if not, warn the user. 1456 */ 1457 void KAlarmApp::checkArchivedCalendar() 1458 { 1459 static bool done = false; 1460 if (done) 1461 return; 1462 done = true; 1463 1464 // If alarms are to be archived, check that the default archived alarm 1465 // calendar is writable. 1466 if (Preferences::archivedKeepDays()) 1467 { 1468 Resource standard = Resources::getStandard(CalEvent::ARCHIVED, true); 1469 if (!standard.isValid()) 1470 { 1471 // Schedule the display of a user prompt, without holding up 1472 // other processing. 1473 QTimer::singleShot(0, this, &KAlarmApp::promptArchivedCalendar); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1474 } 1475 } 1476 } 1477 1478 /****************************************************************************** 1479 * Edit an alarm specified on the command line. 1480 */ 1481 void KAlarmApp::slotEditAlarmById() 1482 { 1483 qCDebug(KALARM_LOG) << "KAlarmApp::slotEditAlarmById"; 1484 ActionQEntry& entry = mActionQueue.head(); 1485 if (!KAlarm::editAlarmById(entry.eventId)) 1486 { 1487 CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not editable", mCommandOption, entry.eventId.eventId())); 1488 mActionQueue.clear(); 1489 quitIf(1); 1490 } 1491 else 1492 { 1493 createOnlyMainWindow(); // prevent the application from quitting 1494 if (mEditingCmdLineAlarm & 0x10) 1495 { 1496 mRedisplayAlarms = true; 1497 showRestoredWindows(); 1498 } 1499 mEditingCmdLineAlarm = 2; // indicate edit completion 1500 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1501 } 1502 } 1503 1504 /****************************************************************************** 1505 * If alarms are being archived, check whether there is a default archived 1506 * calendar, and if not, warn the user. 1507 */ 1508 void KAlarmApp::promptArchivedCalendar() 1509 { 1510 const bool archived = !Resources::enabledResources(CalEvent::ARCHIVED, true).isEmpty(); 1511 if (archived) 1512 { 1513 qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no writable archived calendar"; 1514 KAMessageBox::information(MainWindow::mainMainWindow(), 1515 xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no writable archived alarm calendar is enabled.<nl/><nl/>" 1516 "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."), 1517 QString(), QStringLiteral("noWritableArch")); 1518 } 1519 else 1520 { 1521 qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no standard archived calendar"; 1522 KAMessageBox::information(MainWindow::mainMainWindow(), 1523 xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no archived alarm calendar is set as default.<nl/><nl/>" 1524 "To fix this, use <interface>View | Show Calendars</interface>, select an archived alarms calendar, and check <interface>Use as Default for Archived Alarms</interface>."), 1525 QString(), QStringLiteral("noStandardArch")); 1526 } 1527 } 1528 1529 /****************************************************************************** 1530 * Called when a new resource has been added, to note the possible need to purge 1531 * its old alarms if it is the default archived calendar. 1532 */ 1533 void KAlarmApp::slotResourceAdded(const Resource& resource) 1534 { 1535 if (resource.alarmTypes() & CalEvent::ARCHIVED) 1536 mPendingPurges += resource.id(); 1537 } 1538 1539 /****************************************************************************** 1540 * Called when a resource has been populated, to purge its old alarms if it is 1541 * the default archived calendar. 1542 */ 1543 void KAlarmApp::slotResourcePopulated(const Resource& resource) 1544 { 1545 if (mPendingPurges.removeAll(resource.id()) > 0) 1546 purgeNewArchivedDefault(resource); 1547 } 1548 1549 /****************************************************************************** 1550 * Called when a new resource has been populated, or when a resource has been 1551 * set as the standard resource for its type. 1552 * If it is the default archived calendar, purge its old alarms if necessary. 1553 */ 1554 void KAlarmApp::purgeNewArchivedDefault(const Resource& resource) 1555 { 1556 if (Resources::isStandard(resource, CalEvent::ARCHIVED)) 1557 { 1558 qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.displayId() << ": standard archived..."; 1559 if (mArchivedPurgeDays >= 0) 1560 purge(mArchivedPurgeDays); 1561 else 1562 setArchivePurgeDays(); 1563 } 1564 } 1565 1566 /****************************************************************************** 1567 * Called when the length of time to keep archived alarms changes in KAlarm's 1568 * preferences. 1569 * Set the number of days to keep archived alarms. 1570 * Alarms which are older are purged immediately, and at the start of each day. 1571 */ 1572 void KAlarmApp::setArchivePurgeDays() 1573 { 1574 const int newDays = Preferences::archivedKeepDays(); 1575 if (newDays != mArchivedPurgeDays) 1576 { 1577 const int oldDays = mArchivedPurgeDays; 1578 mArchivedPurgeDays = newDays; 1579 if (mArchivedPurgeDays <= 0) 1580 StartOfDayTimer::disconnect(this); 1581 if (mArchivedPurgeDays < 0) 1582 return; // keep indefinitely, so don't purge 1583 if (oldDays < 0 || mArchivedPurgeDays < oldDays) 1584 { 1585 // Alarms are now being kept for less long, so purge them 1586 purge(mArchivedPurgeDays); 1587 if (!mArchivedPurgeDays) 1588 return; // don't archive any alarms 1589 } 1590 // Start the purge timer to expire at the start of the next day 1591 // (using the user-defined start-of-day time). 1592 StartOfDayTimer::connect(this, SLOT(slotPurge())); 1593 } 1594 } 1595 1596 /****************************************************************************** 1597 * Purge all archived events from the calendar whose end time is longer ago than 1598 * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. 1599 */ 1600 void KAlarmApp::purge(int daysToKeep) 1601 { 1602 if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) 1603 mPurgeDaysQueued = daysToKeep; 1604 1605 // Do the purge once any other current operations are completed 1606 processQueue(); 1607 } 1608 1609 /****************************************************************************** 1610 * Called when the Freedesktop notifications properties have changed. 1611 * Check whether the inhibited property has changed. 1612 */ 1613 void KAlarmApp::slotFDOPropertiesChanged(const QString& interface, 1614 const QVariantMap& changedProperties, 1615 const QStringList& invalidatedProperties) 1616 { 1617 Q_UNUSED(interface); // always "org.freedesktop.Notifications" 1618 Q_UNUSED(invalidatedProperties); 1619 const auto it = changedProperties.find(QStringLiteral("Inhibited")); 1620 if (it != changedProperties.end()) 1621 { 1622 const bool inhibited = it.value().toBool(); 1623 if (inhibited != mNotificationsInhibited) 1624 { 1625 qCDebug(KALARM_LOG) << "KAlarmApp::slotFDOPropertiesChanged: Notifications inhibited ->" << inhibited; 1626 mNotificationsInhibited = inhibited; 1627 if (!mNotificationsInhibited) 1628 { 1629 showRestoredWindows(); // display message windows restored at startup. 1630 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1631 } 1632 } 1633 } 1634 } 1635 1636 /****************************************************************************** 1637 * Output a list of pending alarms, with their next scheduled occurrence. 1638 */ 1639 QStringList KAlarmApp::scheduledAlarmList() 1640 { 1641 QStringList alarms; 1642 const QList<KAEvent> events = KAlarm::getSortedActiveEvents(this); 1643 for (const KAEvent& event : events) 1644 { 1645 const KADateTime dateTime = event.nextTrigger(KAEvent::Trigger::Display).effectiveKDateTime().toLocalZone(); 1646 const Resource resource = Resources::resource(event.resourceId()); 1647 QString text(resource.configName() + QLatin1String(":")); 1648 text += event.id() + QLatin1Char(' ') 1649 + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) 1650 + AlarmText::summary(event, 1); 1651 alarms << text; 1652 } 1653 return alarms; 1654 } 1655 1656 /****************************************************************************** 1657 * Enable or disable alarm monitoring. 1658 */ 1659 void KAlarmApp::setAlarmsEnabled(bool enabled) 1660 { 1661 if (enabled != mAlarmsEnabled) 1662 { 1663 mAlarmsEnabled = enabled; 1664 Q_EMIT alarmEnabledToggled(enabled); 1665 if (!enabled) 1666 #if ENABLE_RTC_WAKE_FROM_SUSPEND 1667 KAlarm::cancelRtcWake(nullptr); 1668 #else 1669 ; 1670 #endif 1671 else if (!mProcessingQueue) 1672 checkNextDueAlarm(); 1673 } 1674 } 1675 1676 /****************************************************************************** 1677 * Spread or collect alarm message and error message windows. 1678 */ 1679 void KAlarmApp::spreadWindows(bool spread) 1680 { 1681 if (!KWindowSystem::isPlatformWayland()) // Wayland doesn't allow positioning of windows 1682 { 1683 spread = MessageWindow::spread(spread); 1684 Q_EMIT spreadWindowsToggled(spread); 1685 } 1686 } 1687 1688 /****************************************************************************** 1689 * Called when the spread status of message windows changes. 1690 * Set the 'spread windows' action state. 1691 */ 1692 void KAlarmApp::setSpreadWindowsState(bool spread) 1693 { 1694 if (!KWindowSystem::isPlatformWayland()) // Wayland doesn't allow positioning of windows 1695 Q_EMIT spreadWindowsToggled(spread); 1696 } 1697 1698 /****************************************************************************** 1699 * Check whether the window manager's handling of keyboard focus transfer 1700 * between application windows is broken. This is true for Ubuntu's Unity 1701 * desktop, where MessageWindow windows steal keyboard focus from EditAlarmDlg 1702 * windows. 1703 */ 1704 bool KAlarmApp::windowFocusBroken() const 1705 { 1706 return mWindowFocusBroken; 1707 } 1708 1709 /****************************************************************************** 1710 * Check whether window/keyboard focus currently needs to be fixed manually due 1711 * to the window manager not handling it correctly. This will occur if there are 1712 * both EditAlarmDlg and MessageWindow windows currently active. 1713 */ 1714 bool KAlarmApp::needWindowFocusFix() const 1715 { 1716 return mWindowFocusBroken && MessageWindow::windowCount(true) && EditAlarmDlg::instanceCount(); 1717 } 1718 1719 /****************************************************************************** 1720 * Called to schedule a new alarm, either in response to a D-Bus notification or 1721 * to command line options. 1722 * Reply = true unless there was a parameter error or an error opening calendar file. 1723 */ 1724 bool KAlarmApp::scheduleEvent(QueuedAction queuedActionFlags, 1725 KAEvent::SubAction action, const QString& name, const QString& text, 1726 const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, 1727 const QColor& bg, const QColor& fg, const QFont& font, 1728 const QString& audioFile, float audioVolume, int reminderMinutes, 1729 const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, 1730 uint mailFromID, const KCalendarCore::Person::List& mailAddresses, 1731 const QString& mailSubject, const QStringList& mailAttachments) 1732 { 1733 if (!dateTime.isValid()) 1734 { 1735 qCWarning(KALARM_LOG) << "KAlarmApp::scheduleEvent: Error! Invalid time" << text; 1736 return false; 1737 } 1738 const KADateTime now = KADateTime::currentUtcDateTime(); 1739 if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) 1740 { 1741 qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: not executed (late-cancel)" << text; 1742 return true; // alarm time was already archived too long ago 1743 } 1744 KADateTime alarmTime = dateTime; 1745 // Round down to the nearest minute to avoid scheduling being messed up 1746 if (!dateTime.isDateOnly()) 1747 alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); 1748 1749 KAEvent event(alarmTime, name, text, bg, fg, font, action, lateCancel, flags, true); 1750 if (reminderMinutes) 1751 { 1752 const bool onceOnly = flags & KAEvent::REMINDER_ONCE; 1753 event.setReminder(reminderMinutes, onceOnly); 1754 } 1755 if (!audioFile.isEmpty()) 1756 event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); 1757 if (!mailAddresses.isEmpty()) 1758 event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); 1759 event.setRecurrence(recurrence); 1760 event.setFirstRecurrence(); 1761 event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); 1762 event.endChanges(); 1763 if (alarmTime <= now) 1764 { 1765 // Alarm is due for execution already. 1766 // First execute it once without adding it to the calendar file. 1767 qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: executing" << text; 1768 if (!mInitialised 1769 || execAlarm(event, event.firstAlarm()) == (void*)-2) 1770 mActionQueue.enqueue(ActionQEntry(event, QueuedAction::Trigger)); 1771 // If it's a recurring alarm, reschedule it for its next occurrence 1772 if (!event.recurs() 1773 || event.setNextOccurrence(now) == KAEvent::OccurType::None) 1774 return true; 1775 // It has recurrences in the future 1776 } 1777 1778 // Queue the alarm for insertion into the calendar file 1779 qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: creating new alarm" << text; 1780 const QueuedAction qaction = static_cast<QueuedAction>(int(QueuedAction::Handle) + int(queuedActionFlags)); 1781 mActionQueue.enqueue(ActionQEntry(event, qaction)); 1782 if (mInitialised) 1783 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1784 return true; 1785 } 1786 1787 /****************************************************************************** 1788 * Called in response to a D-Bus request to trigger or cancel an event. 1789 * Optionally display the event. Delete the event from the calendar file and 1790 * from every main window instance. 1791 */ 1792 bool KAlarmApp::dbusHandleEvent(const EventId& eventID, QueuedAction action) 1793 { 1794 qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID; 1795 mActionQueue.append(ActionQEntry(action, eventID)); 1796 if (mInitialised) 1797 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1798 return true; 1799 } 1800 1801 /****************************************************************************** 1802 * Called in response to a D-Bus request to list all pending alarms. 1803 */ 1804 QString KAlarmApp::dbusList() 1805 { 1806 qCDebug(KALARM_LOG) << "KAlarmApp::dbusList"; 1807 return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n'); 1808 } 1809 1810 /****************************************************************************** 1811 * Either: 1812 * a) Execute the event if it's due, and then delete it if it has no outstanding 1813 * repetitions. 1814 * b) Delete the event. 1815 * c) Reschedule the event for its next repetition. If none remain, delete it. 1816 * If the event is deleted, it is removed from the calendar file and from every 1817 * main window instance. 1818 * If 'findUniqueId' is true and 'id' does not specify a resource, all resources 1819 * will be searched for the event's unique ID. 1820 * Reply = -1 if event ID not found, or if more than one event with the same ID 1821 * is found. 1822 * = 0 if can't trigger display event because notifications are inhibited. 1823 * = 1 if success. 1824 */ 1825 int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniqueId) 1826 { 1827 Q_ASSERT(!(int(action) & ~int(QueuedAction::ActionMask))); 1828 1829 #if ENABLE_RTC_WAKE_FROM_SUSPEND 1830 // Delete any expired wake-on-suspend config data 1831 KAlarm::checkRtcWakeConfig(); 1832 #endif 1833 1834 const QString eventID(id.eventId()); 1835 KAEvent event = ResourcesCalendar::event(id, findUniqueId); 1836 if (!event.isValid()) 1837 { 1838 if (id.resourceId() != -1) 1839 qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; 1840 else if (findUniqueId) 1841 qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; 1842 else 1843 qCCritical(KALARM_LOG) << "KAlarmApp::handleEvent: No resource ID specified for event:" << eventID; 1844 return -1; 1845 } 1846 switch (action) 1847 { 1848 case QueuedAction::Cancel: 1849 { 1850 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; 1851 Resource resource = Resources::resource(event.resourceId()); 1852 KAlarm::deleteEvent(event, resource, true); 1853 break; 1854 } 1855 case QueuedAction::Trigger: // handle it if it's due, else execute it regardless 1856 case QueuedAction::Handle: // handle it if it's due 1857 { 1858 const KADateTime now = KADateTime::currentUtcDateTime(); 1859 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (action==QueuedAction::Trigger?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; 1860 bool updateCalAndDisplay = false; 1861 bool alarmToExecuteValid = false; 1862 KAAlarm alarmToExecute; 1863 bool restart = false; 1864 // Check all the alarms in turn. 1865 // Note that the main alarm is fetched before any other alarms. 1866 for (KAAlarm alarm = event.firstAlarm(); 1867 alarm.isValid(); 1868 alarm = (restart ? event.firstAlarm() : event.nextAlarm(alarm)), restart = false) 1869 { 1870 // Check if the alarm is due yet. 1871 const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime(); 1872 const int secs = nextDT.secsTo(now); 1873 if (secs < 0) 1874 { 1875 // The alarm appears to be in the future. 1876 // Check if it's an invalid local time during a daylight 1877 // saving time shift, which has actually passed. 1878 if (alarm.dateTime().timeSpec() != KADateTime::LocalZone 1879 || nextDT > now.toTimeSpec(KADateTime::LocalZone)) 1880 { 1881 // This alarm is definitely not due yet 1882 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; 1883 continue; 1884 } 1885 } 1886 bool reschedule = false; 1887 bool rescheduleWork = false; 1888 if ((event.workTimeOnly() || event.holidaysExcluded()) && !alarm.deferred()) 1889 { 1890 // The alarm is restricted to working hours and/or non-holidays 1891 // (apart from deferrals). This needs to be re-evaluated every 1892 // time it triggers, since working hours could change. 1893 if (alarm.dateTime().isDateOnly()) 1894 { 1895 KADateTime dt(nextDT); 1896 dt.setDateOnly(true); 1897 reschedule = event.excludedByWorkTimeOrHoliday(dt); 1898 } 1899 else 1900 reschedule = event.excludedByWorkTimeOrHoliday(nextDT); 1901 rescheduleWork = reschedule; 1902 if (reschedule) 1903 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; 1904 } 1905 if (!reschedule && alarm.repeatAtLogin()) 1906 { 1907 // Alarm is to be displayed at every login. 1908 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN"; 1909 // Check if the main alarm is already being displayed. 1910 // (We don't want to display both at the same time.) 1911 if (alarmToExecute.isValid()) 1912 continue; 1913 1914 // Set the time to display if it's a display alarm 1915 alarm.setTime(now); 1916 } 1917 if (!reschedule && event.lateCancel()) 1918 { 1919 // Alarm is due, and it is to be cancelled if too late. 1920 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL"; 1921 bool cancel = false; 1922 if (alarm.dateTime().isDateOnly()) 1923 { 1924 // The alarm has no time, so cancel it if its date is too far past 1925 const int maxlate = event.lateCancel() / 1440; // maximum lateness in days 1926 KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime()); 1927 if (now >= limit) 1928 { 1929 // It's too late to display the scheduled occurrence. 1930 // Find the last previous occurrence of the alarm. 1931 DateTime next; 1932 const KAEvent::OccurType type = event.previousOccurrence(now, next, true); 1933 switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat)) 1934 { 1935 case KAEvent::OccurType::FirstOrOnly: 1936 case KAEvent::OccurType::RecurDate: 1937 case KAEvent::OccurType::RecurDateTime: 1938 case KAEvent::OccurType::LastRecur: 1939 limit.setDate(next.date().addDays(maxlate + 1)); 1940 if (now >= limit) 1941 { 1942 if (type == KAEvent::OccurType::LastRecur 1943 || (type == KAEvent::OccurType::FirstOrOnly && !event.recurs())) 1944 cancel = true; // last occurrence (and there are no repetitions) 1945 else 1946 reschedule = true; 1947 } 1948 break; 1949 case KAEvent::OccurType::None: 1950 default: 1951 reschedule = true; 1952 break; 1953 } 1954 } 1955 } 1956 else 1957 { 1958 // The alarm is timed. Allow it to be the permitted amount late before cancelling it. 1959 const int maxlate = maxLateness(event.lateCancel()); 1960 if (secs > maxlate) 1961 { 1962 // It's over the maximum interval late. 1963 // Find the most recent occurrence of the alarm. 1964 DateTime next; 1965 const KAEvent::OccurType type = event.previousOccurrence(now, next, true); 1966 switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat)) 1967 { 1968 case KAEvent::OccurType::FirstOrOnly: 1969 case KAEvent::OccurType::RecurDate: 1970 case KAEvent::OccurType::RecurDateTime: 1971 case KAEvent::OccurType::LastRecur: 1972 if (next.effectiveKDateTime().secsTo(now) > maxlate) 1973 { 1974 if (type == KAEvent::OccurType::LastRecur 1975 || (type == KAEvent::OccurType::FirstOrOnly && !event.recurs())) 1976 cancel = true; // last occurrence (and there are no repetitions) 1977 else 1978 reschedule = true; 1979 } 1980 break; 1981 case KAEvent::OccurType::None: 1982 default: 1983 reschedule = true; 1984 break; 1985 } 1986 } 1987 } 1988 1989 if (cancel) 1990 { 1991 // All recurrences are finished, so cancel the event 1992 event.setArchive(); 1993 if (cancelAlarm(event, alarm.type(), false)) 1994 return 1; // event has been deleted 1995 updateCalAndDisplay = true; 1996 continue; 1997 } 1998 } 1999 if (reschedule) 2000 { 2001 // The latest repetition was too long ago, so schedule the next one 2002 switch (rescheduleAlarm(event, alarm, false, (rescheduleWork ? nextDT : KADateTime()))) 2003 { 2004 case 1: 2005 // A working-time-only alarm has been rescheduled and the 2006 // rescheduled time is already due. Start processing the 2007 // event again. 2008 alarmToExecuteValid = false; 2009 restart = true; 2010 break; 2011 case -1: 2012 return 1; // event has been deleted 2013 default: 2014 break; 2015 } 2016 updateCalAndDisplay = true; 2017 continue; 2018 } 2019 if (!alarmToExecuteValid) 2020 { 2021 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute"; 2022 alarmToExecute = alarm; // note the alarm to be displayed 2023 alarmToExecuteValid = true; // only trigger one alarm for the event 2024 } 2025 else 2026 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip"; 2027 } 2028 2029 // If there is an alarm to execute, do this last after rescheduling/cancelling 2030 // any others. This ensures that the updated event is only saved once to the calendar. 2031 if (alarmToExecute.isValid()) 2032 { 2033 if (execAlarm(event, alarmToExecute, Reschedule | (alarmToExecute.repeatAtLogin() ? NoExecFlag : AllowDefer)) == (void*)-2) 2034 return 0; // display alarm, but notifications are inhibited 2035 } 2036 else 2037 { 2038 if (action == QueuedAction::Trigger) 2039 { 2040 // The alarm is to be executed regardless of whether it's due. 2041 // Only trigger one alarm from the event - we don't want multiple 2042 // identical messages, for example. 2043 const KAAlarm alarm = event.firstAlarm(); 2044 if (alarm.isValid()) 2045 { 2046 if (execAlarm(event, alarm) == (void*)-2) 2047 return 0; // display alarm, but notifications are inhibited 2048 } 2049 } 2050 if (updateCalAndDisplay) 2051 KAlarm::updateEvent(event); // update the window lists and calendar file 2052 else if (action != QueuedAction::Trigger) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } 2053 } 2054 break; 2055 } 2056 default: 2057 break; 2058 } 2059 return 1; 2060 } 2061 2062 /****************************************************************************** 2063 * Called when an alarm action has completed, to perform any post-alarm actions. 2064 */ 2065 void KAlarmApp::alarmCompleted(const KAEvent& event) 2066 { 2067 if (!event.postAction().isEmpty()) 2068 { 2069 // doShellCommand() will error if the user is not authorised to run 2070 // shell commands. 2071 const QString command = event.postAction(); 2072 qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command; 2073 doShellCommand(command, event, nullptr, ProcData::POST_ACTION); 2074 } 2075 } 2076 2077 /****************************************************************************** 2078 * Reschedule the alarm for its next recurrence after now. If none remain, 2079 * delete it. If the alarm is deleted and it is the last alarm for its event, 2080 * the event is removed from the calendar file and from every main window 2081 * instance. 2082 * If 'nextDt' is valid, the event is rescheduled for the next non-working 2083 * time occurrence after that. 2084 * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due 2085 * = -1 if the event has been deleted 2086 * = 0 otherwise. 2087 */ 2088 int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt) 2089 { 2090 qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type(); 2091 int reply = 0; 2092 bool update = false; 2093 event.startChanges(); 2094 if (alarm.repeatAtLogin()) 2095 { 2096 // Leave an alarm which repeats at every login until its main alarm triggers 2097 if (!event.reminderActive() && event.reminderMinutes() < 0) 2098 { 2099 // Executing an at-login alarm: first schedule the reminder 2100 // which occurs AFTER the main alarm. 2101 event.activateReminderAfter(KADateTime::currentUtcDateTime()); 2102 } 2103 // Repeat-at-login alarms are usually unchanged after triggering. 2104 // Ensure that the archive flag (which was set in execAlarm()) is saved. 2105 update = true; 2106 } 2107 else if (alarm.isReminder() || alarm.deferred()) 2108 { 2109 // It's a reminder alarm or an extra deferred alarm, so delete it 2110 event.removeExpiredAlarm(alarm.type()); 2111 update = true; 2112 } 2113 else 2114 { 2115 // Reschedule the alarm for its next occurrence. 2116 bool cancelled = false; 2117 DateTime last = event.mainDateTime(false); // note this trigger time 2118 if (last != event.mainDateTime(true)) 2119 last = DateTime(); // but ignore sub-repetition triggers 2120 bool next = nextDt.isValid(); 2121 KADateTime next_dt = nextDt; 2122 const KADateTime now = KADateTime::currentUtcDateTime(); 2123 do 2124 { 2125 const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); 2126 switch (type) 2127 { 2128 case KAEvent::OccurType::None: 2129 // All repetitions are finished, so cancel the event 2130 qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence"; 2131 if (event.reminderMinutes() < 0 && last.isValid() 2132 && alarm.type() != KAAlarm::Type::AtLogin && !event.mainExpired()) 2133 { 2134 // Set the reminder which is now due after the last main alarm trigger. 2135 // Note that at-login reminders are scheduled in execAlarm(). 2136 event.activateReminderAfter(last); 2137 updateCalAndDisplay = true; 2138 } 2139 if (cancelAlarm(event, alarm.type(), updateCalAndDisplay)) 2140 return -1; 2141 break; 2142 default: 2143 if (!(type & KAEvent::OccurType::Repeat)) 2144 break; 2145 // Next occurrence is a repeat, so fall through to recurrence handling 2146 [[fallthrough]]; 2147 case KAEvent::OccurType::RecurDate: 2148 case KAEvent::OccurType::RecurDateTime: 2149 case KAEvent::OccurType::LastRecur: 2150 // The event is due by now and repetitions still remain, so rewrite the event 2151 if (updateCalAndDisplay) 2152 update = true; 2153 break; 2154 case KAEvent::OccurType::FirstOrOnly: 2155 // The first occurrence is still due?!?, so don't do anything 2156 break; 2157 } 2158 if (cancelled) 2159 break; 2160 if (event.deferred()) 2161 { 2162 // Just in case there's also a deferred alarm, ensure it's removed 2163 event.removeExpiredAlarm(KAAlarm::Type::Deferred); 2164 update = true; 2165 } 2166 if (next) 2167 { 2168 // The alarm is restricted to working hours and/or non-holidays. 2169 // Check if the calculated next time is valid. 2170 next_dt = event.mainDateTime(true).effectiveKDateTime(); 2171 if (event.mainDateTime(false).isDateOnly()) 2172 { 2173 KADateTime dt(next_dt); 2174 dt.setDateOnly(true); 2175 next = event.excludedByWorkTimeOrHoliday(dt); 2176 } 2177 else 2178 next = event.excludedByWorkTimeOrHoliday(next_dt); 2179 } 2180 } while (next && next_dt <= now); 2181 reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0; 2182 2183 if (event.reminderMinutes() < 0 && last.isValid() 2184 && alarm.type() != KAAlarm::Type::AtLogin) 2185 { 2186 // Set the reminder which is now due after the last main alarm trigger. 2187 // Note that at-login reminders are scheduled in execAlarm(). 2188 event.activateReminderAfter(last); 2189 } 2190 } 2191 event.endChanges(); 2192 if (update) 2193 KAlarm::updateEvent(event, nullptr, true, false); // update the window lists and calendar file 2194 return reply; 2195 } 2196 2197 /****************************************************************************** 2198 * Delete the alarm. If it is the last alarm for its event, the event is removed 2199 * from the calendar file and from every main window instance. 2200 * Reply = true if event has been deleted. 2201 */ 2202 bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) 2203 { 2204 qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm"; 2205 if (alarmType == KAAlarm::Type::Main && !event.displaying() && event.toBeArchived()) 2206 { 2207 // The event is being deleted. Save it in the archived resource first. 2208 Resource resource; 2209 KAEvent ev(event); 2210 KAlarm::addArchivedEvent(ev, resource); 2211 } 2212 event.removeExpiredAlarm(alarmType); 2213 if (!event.alarmCount()) 2214 { 2215 // If it's a command alarm being executed, mark it as deleted 2216 ProcData* pd = findCommandProcess(event.id()); 2217 if (pd) 2218 pd->eventDeleted = true; 2219 2220 // Delete it 2221 Resource resource; 2222 KAlarm::deleteEvent(event, resource, false); 2223 return true; 2224 } 2225 if (updateCalAndDisplay) 2226 KAlarm::updateEvent(event); // update the window lists and calendar file 2227 return false; 2228 } 2229 2230 /****************************************************************************** 2231 * Cancel any reminder or deferred alarms in an repeat-at-login event. 2232 * This should be called when the event is first loaded. 2233 * If there are no more alarms left in the event, the event is removed from the 2234 * calendar file and from every main window instance. 2235 * Reply = true if event has been deleted. 2236 */ 2237 bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event) 2238 { 2239 return cancelAlarm(event, KAAlarm::Type::Reminder, false) 2240 || cancelAlarm(event, KAAlarm::Type::DeferredReminder, false) 2241 || cancelAlarm(event, KAAlarm::Type::Deferred, true); 2242 } 2243 2244 /****************************************************************************** 2245 * Execute an alarm by displaying its message or file, or executing its command. 2246 * Reply = ShellProcess instance if a command alarm 2247 * = MessageWindow if an audio alarm 2248 * != null if successful 2249 * = -1 if execution has not completed 2250 * = -2 if can't execute display event because notifications are inhibited. 2251 * = null if the alarm is disabled, or if an error message was output. 2252 */ 2253 void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, ExecAlarmFlags flags) 2254 { 2255 if (!mAlarmsEnabled || !event.enabled()) 2256 { 2257 // The event (or all events) is disabled 2258 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled"; 2259 if (flags & Reschedule) 2260 rescheduleAlarm(event, alarm, true); 2261 return nullptr; 2262 } 2263 2264 if (mNotificationsInhibited && !(flags & NoNotifyInhibit) 2265 && (event.actionTypes() & KAEvent::Action::Display)) 2266 { 2267 // It's a display event and notifications are inhibited. 2268 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": notifications inhibited"; 2269 return (void*)-2; 2270 } 2271 2272 void* result = (void*)1; 2273 event.setArchive(); 2274 2275 switch (alarm.action()) 2276 { 2277 case KAAlarm::Action::Command: 2278 if (!event.commandDisplay()) 2279 { 2280 // execCommandAlarm() will error if the user is not authorised 2281 // to run shell commands. 2282 result = execCommandAlarm(event, alarm, flags & NoRecordCmdError); 2283 if (flags & Reschedule) 2284 rescheduleAlarm(event, alarm, true); 2285 break; 2286 } 2287 [[fallthrough]]; // fall through to Message 2288 case KAAlarm::Action::Message: 2289 case KAAlarm::Action::File: 2290 { 2291 // Display a message, file or command output, provided that the same event 2292 // isn't already being displayed 2293 MessageDisplay* disp = MessageDisplay::findEvent(EventId(event)); 2294 // Find if we're changing a reminder message to the real message 2295 const bool reminder = (alarm.type() & KAAlarm::Type::Reminder); 2296 const bool replaceReminder = !reminder && disp && (disp->alarmType() & KAAlarm::Type::Reminder); 2297 if (!reminder 2298 && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral)) 2299 && (replaceReminder || !disp) && !(flags & NoPreAction) 2300 && !event.preAction().isEmpty()) 2301 { 2302 // It's not a reminder alarm, and it's not a deferred alarm unless the 2303 // pre-alarm action applies to deferred alarms, and there is no message 2304 // window (other than a reminder window) currently displayed for this 2305 // alarm, and we need to execute a command before displaying the new window. 2306 // 2307 // NOTE: The pre-action is not executed for a recurring alarm if an 2308 // alarm message window for a previous occurrence is still visible. 2309 // Check whether the command is already being executed for this alarm. 2310 for (const ProcData* pd : std::as_const(mCommandProcesses)) 2311 { 2312 if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) 2313 { 2314 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command"; 2315 return pd->process; // already executing - don't duplicate the action 2316 } 2317 } 2318 2319 // doShellCommand() will error if the user is not authorised to run 2320 // shell commands. 2321 const QString command = event.preAction(); 2322 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; 2323 const int pdFlags = (flags & Reschedule ? ProcData::RESCHEDULE : 0) | (flags & AllowDefer ? ProcData::ALLOW_DEFER : 0); 2324 if (doShellCommand(command, event, &alarm, (pdFlags | ProcData::PRE_ACTION))) 2325 { 2326 ResourcesCalendar::setAlarmPending(event); 2327 return result; // display the message after the command completes 2328 } 2329 // Error executing command 2330 if (event.extraActionOptions() & KAEvent::CancelOnPreActError) 2331 { 2332 // Cancel the rest of the alarm execution 2333 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled"; 2334 if (flags & Reschedule) 2335 rescheduleAlarm(event, alarm, true); 2336 return nullptr; 2337 } 2338 // Display the message even though it failed 2339 } 2340 2341 if (!disp) 2342 { 2343 // There isn't already a message for this event 2344 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule) 2345 | (flags & AllowDefer ? 0 : MessageDisplay::NoDefer) 2346 | (flags & NoRecordCmdError ? MessageDisplay::NoRecordCmdError : 0); 2347 MessageDisplay::create(event, alarm, mdFlags)->showDisplay(); 2348 } 2349 else if (replaceReminder) 2350 { 2351 // The caption needs to be changed from "Reminder" to "Message" 2352 disp->cancelReminder(event, alarm); 2353 } 2354 else if (!disp->hasDefer() && event.repeatAtLogin() && !alarm.repeatAtLogin()) 2355 { 2356 // It's a repeat-at-login message with no Defer button, 2357 // which has now reached its final trigger time and needs 2358 // to be replaced with a new message. 2359 disp->showDefer(); 2360 disp->showDateTime(event, alarm); 2361 } 2362 else 2363 { 2364 // Use the existing message window 2365 } 2366 if (disp) 2367 { 2368 // Raise the existing message window and replay any sound 2369 disp->repeat(alarm); // N.B. this reschedules the alarm 2370 } 2371 break; 2372 } 2373 case KAAlarm::Action::Email: 2374 { 2375 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(",")); 2376 QStringList errmsgs; 2377 MailSend::JobData data(event, alarm, flags & Reschedule, flags & (Reschedule | AllowDefer)); 2378 data.queued = true; 2379 int ans = KAMail::send(data, errmsgs); 2380 if (ans) 2381 { 2382 // The email has either been sent or failed - not queued 2383 if (ans < 0) 2384 result = nullptr; // failure 2385 data.queued = false; 2386 emailSent(data, errmsgs, (ans > 0)); 2387 } 2388 else 2389 { 2390 result = (void*)-1; // email has been queued 2391 } 2392 if (flags & Reschedule) 2393 rescheduleAlarm(event, alarm, true); 2394 break; 2395 } 2396 case KAAlarm::Action::Audio: 2397 { 2398 // Play the sound, provided that the same event 2399 // isn't already playing 2400 MessageDisplay* disp = MessageDisplay::findEvent(EventId(event)); 2401 if (!disp) 2402 { 2403 // There isn't already a message for this event. 2404 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule) | MessageDisplay::AlwaysHide; 2405 event.setNotify(false); // can't use notification system if audio only 2406 disp = MessageDisplay::create(event, alarm, mdFlags); 2407 } 2408 else 2409 { 2410 // There's an existing message window: replay the sound 2411 disp->repeat(alarm); // N.B. this reschedules the alarm 2412 } 2413 return dynamic_cast<MessageWindow*>(disp); 2414 } 2415 default: 2416 return nullptr; 2417 } 2418 return result; 2419 } 2420 2421 /****************************************************************************** 2422 * Called when sending an email has completed. 2423 */ 2424 void KAlarmApp::emailSent(const MailSend::JobData& data, const QStringList& errmsgs, bool copyerr) 2425 { 2426 if (!errmsgs.isEmpty()) 2427 { 2428 // Some error occurred, although the email may have been sent successfully 2429 if (errmsgs.count() > 1) 2430 qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1]; 2431 MessageDisplay::showError(data.event, data.alarm.dateTime(), errmsgs); 2432 } 2433 else if (data.queued) 2434 Q_EMIT execAlarmSuccess(); 2435 } 2436 2437 /****************************************************************************** 2438 * Execute the command specified in a command alarm. 2439 * To connect to the output ready signals of the process, specify a slot to be 2440 * called by supplying 'receiver' and 'slot' parameters. 2441 */ 2442 ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, bool noRecordError, 2443 QObject* receiver, const char* slotOutput, const char* methodExited) 2444 { 2445 // doShellCommand() will error if the user is not authorised to run 2446 // shell commands. 2447 const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) 2448 | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0) 2449 | (noRecordError ? ProcData::NO_RECORD_ERROR : 0); 2450 const QString command = event.cleanText(); 2451 if (event.commandScript()) 2452 { 2453 // Store the command script in a temporary file for execution 2454 qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; 2455 const QString tmpfile = createTempScriptFile(command, false, event, alarm); 2456 if (tmpfile.isEmpty()) 2457 { 2458 setEventCommandError(event, KAEvent::CmdErr::Fail); 2459 return nullptr; 2460 } 2461 return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slotOutput, methodExited); 2462 } 2463 else 2464 { 2465 qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command; 2466 return doShellCommand(command, event, &alarm, flags, receiver, slotOutput, methodExited); 2467 } 2468 } 2469 2470 /****************************************************************************** 2471 * Execute a shell command line specified by an alarm. 2472 * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via 2473 * execAlarm() once the command completes, the execAlarm() parameters being 2474 * derived from the remaining bits in 'flags'. 2475 * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is 2476 * a pre- or post-alarm action respectively. 2477 * To connect to the exited signal of the process, specify the name of a method 2478 * to be called by supplying 'receiver' and 'methodExited' parameters. 2479 * To connect to the output ready signals of the process, specify a slot to be 2480 * called by supplying 'receiver' and 'slotOutput' parameters. 2481 * 2482 * Note that if shell access is not authorised, the attempt to run the command 2483 * will be errored. 2484 * 2485 * Reply = process which has been started, or null if a process couldn't be started. 2486 */ 2487 ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QObject* receiver, const char* slotOutput, const char* methodExited) 2488 { 2489 qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id(); 2490 QIODevice::OpenMode mode = QIODevice::WriteOnly; 2491 QString cmd; 2492 QString tmpXtermFile; 2493 if (flags & ProcData::EXEC_IN_XTERM) 2494 { 2495 // Execute the command in a terminal window. 2496 cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile); 2497 if (cmd.isEmpty()) 2498 { 2499 qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)"; 2500 const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")}; 2501 commandErrorMsg(nullptr, event, alarm, flags, errors); 2502 return nullptr; 2503 } 2504 } 2505 else 2506 { 2507 cmd = command; 2508 mode = QIODevice::ReadWrite; 2509 } 2510 2511 ProcData* pd = nullptr; 2512 ShellProcess* proc = nullptr; 2513 if (!cmd.isEmpty()) 2514 { 2515 // Use ShellProcess, which automatically checks whether the user is 2516 // authorised to run shell commands. 2517 proc = new ShellProcess(cmd); 2518 proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true); 2519 proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr 2520 connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited); 2521 if ((flags & ProcData::DISP_OUTPUT) && receiver && slotOutput) 2522 { 2523 connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slotOutput); 2524 connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slotOutput); 2525 } 2526 if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty()) 2527 { 2528 // Output is to be appended to a log file. 2529 // Set up a logging process to write the command's output to. 2530 QString heading; 2531 if (alarm && alarm->dateTime().isValid()) 2532 { 2533 const QString dateTime = alarm->dateTime().formatLocale(); 2534 heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime); 2535 } 2536 else 2537 heading = QStringLiteral("\n******* KAlarm *******\n"); 2538 QFile logfile(event.logFile()); 2539 if (logfile.open(QIODevice::Append | QIODevice::Text)) 2540 { 2541 QTextStream out(&logfile); 2542 out << heading; 2543 logfile.close(); 2544 } 2545 proc->setStandardOutputFile(event.logFile(), QIODevice::Append); 2546 } 2547 pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags); 2548 if (flags & ProcData::TEMP_FILE) 2549 pd->tempFiles += command; 2550 if (!tmpXtermFile.isEmpty()) 2551 pd->tempFiles += tmpXtermFile; 2552 if (receiver && methodExited) 2553 { 2554 pd->exitReceiver = receiver; 2555 pd->exitMethod = methodExited; 2556 } 2557 mCommandProcesses.append(pd); 2558 if (proc->start(mode)) 2559 return proc; 2560 } 2561 2562 // Error executing command - report it 2563 qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start"; 2564 commandErrorMsg(proc, event, alarm, flags); 2565 if (pd) 2566 { 2567 mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd)); 2568 delete pd; 2569 } 2570 return nullptr; 2571 } 2572 2573 /****************************************************************************** 2574 * Compose a command line to execute the given command in a terminal window. 2575 * 'tempScriptFile' receives the name of a temporary script file which is 2576 * invoked by the command line, if applicable. 2577 * Reply = command line, or empty string if error. 2578 */ 2579 QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const 2580 { 2581 qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id(); 2582 tempScriptFile.clear(); 2583 QString cmd = Preferences::cmdXTermCommand(); 2584 if (cmd.isEmpty()) 2585 return {}; // no terminal application is configured 2586 cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title 2587 if (cmd.indexOf(QLatin1String("%C")) >= 0) 2588 { 2589 // Execute the command from a temporary script file 2590 if (flags & ProcData::TEMP_FILE) 2591 cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file 2592 else 2593 { 2594 tempScriptFile = createTempScriptFile(command, true, event, *alarm); 2595 if (tempScriptFile.isEmpty()) 2596 return {}; 2597 cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command 2598 } 2599 } 2600 else if (cmd.indexOf(QLatin1String("%W")) >= 0) 2601 { 2602 // Execute the command from a temporary script file, 2603 // with a sleep after the command is executed 2604 tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm); 2605 if (tempScriptFile.isEmpty()) 2606 return {}; 2607 cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command 2608 } 2609 else if (cmd.indexOf(QLatin1String("%w")) >= 0) 2610 { 2611 // Append a sleep to the command. 2612 // Quote the command in case it contains characters such as [>|;]. 2613 const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); 2614 cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string 2615 } 2616 else 2617 { 2618 // Set the command to execute. 2619 // Put it in quotes in case it contains characters such as [>|;]. 2620 const QString exec = KShell::quoteArg(command); 2621 if (cmd.indexOf(QLatin1String("%c")) >= 0) 2622 cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string 2623 else 2624 cmd.append(exec); // otherwise, simply append the command string 2625 } 2626 return cmd; 2627 } 2628 2629 /****************************************************************************** 2630 * Create a temporary script file containing the specified command string. 2631 * Reply = path of temporary file, or null string if error. 2632 */ 2633 QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const 2634 { 2635 QTemporaryFile tmpFile; 2636 tmpFile.setAutoRemove(false); // don't delete file when it is destructed 2637 if (!tmpFile.open()) 2638 qCCritical(KALARM_LOG) << "Unable to create a temporary script file"; 2639 else 2640 { 2641 tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); 2642 QTextStream stream(&tmpFile); 2643 if (insertShell) 2644 stream << "#!" << ShellProcess::shellPath() << "\n"; 2645 stream << command; 2646 stream.flush(); 2647 if (tmpFile.error() != QFile::NoError) 2648 qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file"; 2649 else 2650 return tmpFile.fileName(); 2651 } 2652 2653 const QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); 2654 MessageDisplay::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script")); 2655 return {}; 2656 } 2657 2658 /****************************************************************************** 2659 * Called when a command alarm's execution completes. 2660 */ 2661 void KAlarmApp::slotCommandExited(ShellProcess* proc) 2662 { 2663 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited"; 2664 // Find this command in the command list 2665 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) 2666 { 2667 ProcData* pd = mCommandProcesses.at(i); 2668 if (pd->process == proc) 2669 { 2670 // Found the command. Check its exit status. 2671 bool executeAlarm = pd->preAction(); 2672 const ShellProcess::Status status = proc->status(); 2673 if (status == ShellProcess::Status::Success && !proc->exitCode()) 2674 { 2675 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS"; 2676 clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CmdErr::Pre 2677 : pd->postAction() ? KAEvent::CmdErr::Post 2678 : KAEvent::CmdErr::Fail); 2679 } 2680 else 2681 { 2682 QString errmsg = proc->errorMessage(); 2683 if (status == ShellProcess::Status::Success || status == ShellProcess::Status::NotFound) 2684 qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); 2685 else 2686 qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status; 2687 if (pd->messageBoxParent) 2688 { 2689 // Close the existing informational KMessageBox for this process 2690 const QList<QDialog*> dialogs = pd->messageBoxParent->findChildren<QDialog*>(); 2691 if (!dialogs.isEmpty()) 2692 delete dialogs[0]; 2693 setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CmdErr::Pre 2694 : pd->postAction() ? KAEvent::CmdErr::Post 2695 : KAEvent::CmdErr::Fail); 2696 if (!pd->tempFile()) 2697 { 2698 errmsg += QLatin1Char('\n'); 2699 errmsg += proc->command(); 2700 } 2701 KAMessageBox::error(pd->messageBoxParent, errmsg); 2702 } 2703 else 2704 commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); 2705 2706 if (executeAlarm 2707 && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError)) 2708 { 2709 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled"; 2710 if (pd->reschedule()) 2711 rescheduleAlarm(*pd->event, *pd->alarm, true); 2712 executeAlarm = false; 2713 } 2714 } 2715 if (pd->preAction()) 2716 ResourcesCalendar::setAlarmPending(*pd->event, false); 2717 if (executeAlarm) 2718 { 2719 execAlarm(*pd->event, *pd->alarm, (pd->reschedule() ? Reschedule : NoExecFlag) 2720 | (pd->allowDefer() ? AllowDefer : NoExecFlag) 2721 | (pd->noRecordCmdErr() ? NoRecordCmdError : NoExecFlag) 2722 | NoPreAction); 2723 } 2724 mCommandProcesses.removeAt(i); 2725 if (pd->exitReceiver && !pd->exitMethod.isEmpty()) 2726 QMetaObject::invokeMethod(pd->exitReceiver, pd->exitMethod.constData(), Qt::DirectConnection, Q_ARG(ShellProcess::Status, status)); 2727 delete pd; 2728 break; 2729 } 2730 } 2731 2732 // If there are now no executing shell commands, quit if a quit was queued 2733 if (mPendingQuit && mCommandProcesses.isEmpty()) 2734 quitIf(mPendingQuitCode); 2735 } 2736 2737 /****************************************************************************** 2738 * Output an error message for a shell command, and record the alarm's error status. 2739 */ 2740 void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) 2741 { 2742 KAEvent::CmdErr cmderr; 2743 QString dontShowAgain; 2744 QStringList errmsgs = errors; 2745 if (flags & ProcData::PRE_ACTION) 2746 { 2747 if (event.extraActionOptions() & KAEvent::DontShowPreActError) 2748 return; // don't notify user of any errors for the alarm 2749 errmsgs += i18nc("@info", "Pre-alarm action:"); 2750 dontShowAgain = QStringLiteral("Pre"); 2751 cmderr = KAEvent::CmdErr::Pre; 2752 } 2753 else if (flags & ProcData::POST_ACTION) 2754 { 2755 errmsgs += i18nc("@info", "Post-alarm action:"); 2756 dontShowAgain = QStringLiteral("Post"); 2757 cmderr = (event.commandError() == KAEvent::CmdErr::Pre) 2758 ? KAEvent::CmdErr::PrePost : KAEvent::CmdErr::Post; 2759 } 2760 else 2761 { 2762 if (!event.commandHideError()) 2763 dontShowAgain = QStringLiteral("Exec"); 2764 cmderr = KAEvent::CmdErr::Fail; 2765 } 2766 2767 // Record the alarm's error status 2768 if (!(flags & ProcData::NO_RECORD_ERROR)) 2769 setEventCommandError(event, cmderr); 2770 2771 if (!dontShowAgain.isEmpty()) 2772 { 2773 // Display an error message 2774 if (proc) 2775 { 2776 errmsgs += proc->errorMessage(); 2777 if (!(flags & ProcData::TEMP_FILE)) 2778 errmsgs += proc->command(); 2779 dontShowAgain += QString::number(static_cast<int>(proc->status())); 2780 } 2781 MessageDisplay::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); 2782 } 2783 } 2784 2785 /****************************************************************************** 2786 * Notes that an informational KMessageBox is displayed for this process. 2787 */ 2788 void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) 2789 { 2790 // Find this command in the command list 2791 for (ProcData* pd : std::as_const(mCommandProcesses)) 2792 { 2793 if (pd->process == proc) 2794 { 2795 pd->messageBoxParent = parent; 2796 break; 2797 } 2798 } 2799 } 2800 2801 /****************************************************************************** 2802 * If this is the first time through, open the calendar file, and start 2803 * processing the execution queue. 2804 */ 2805 bool KAlarmApp::initCheck(bool calendarOnly) 2806 { 2807 static bool firstTime = true; 2808 if (firstTime) 2809 qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time"; 2810 2811 if (initialiseTimerResources() || firstTime) 2812 { 2813 /* Need to open the display calendar now, since otherwise if display 2814 * alarms are immediately due, they will often be processed while 2815 * MessageDisplay::redisplayAlarms() is executing open() (but before 2816 * open() completes), which causes problems!! 2817 */ 2818 DisplayCalendar::open(); 2819 } 2820 if (firstTime) 2821 { 2822 setArchivePurgeDays(); 2823 2824 firstTime = false; 2825 } 2826 2827 if (!calendarOnly) 2828 startProcessQueue(); // start processing the execution queue 2829 2830 return true; 2831 } 2832 2833 /****************************************************************************** 2834 * Called when an audio thread starts or stops. 2835 */ 2836 void KAlarmApp::notifyAudioPlaying(bool playing) 2837 { 2838 Q_EMIT audioPlaying(playing); 2839 } 2840 2841 /****************************************************************************** 2842 * Stop audio play. 2843 */ 2844 void KAlarmApp::stopAudio() 2845 { 2846 MessageDisplay::stopAudio(); 2847 } 2848 2849 /****************************************************************************** 2850 * Set the command error for the specified alarm. 2851 */ 2852 void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErr err) const 2853 { 2854 ProcData* pd = findCommandProcess(event.id()); 2855 if (pd && pd->eventDeleted) 2856 return; // the alarm has been deleted, so can't set error status 2857 2858 if (err == KAEvent::CmdErr::Post && event.commandError() == KAEvent::CmdErr::Pre) 2859 err = KAEvent::CmdErr::PrePost; 2860 event.setCommandError(err); 2861 KAEvent ev = ResourcesCalendar::event(EventId(event)); 2862 if (ev.isValid() && ev.commandError() != err) 2863 { 2864 ev.setCommandError(err); 2865 ResourcesCalendar::updateEvent(ev); 2866 } 2867 Resource resource = Resources::resourceForEvent(event.id()); 2868 resource.handleCommandErrorChange(event); 2869 } 2870 2871 /****************************************************************************** 2872 * Clear the command error for the specified alarm. 2873 */ 2874 void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErr err) const 2875 { 2876 ProcData* pd = findCommandProcess(event.id()); 2877 if (pd && pd->eventDeleted) 2878 return; // the alarm has been deleted, so can't set error status 2879 2880 auto newerr = static_cast<KAEvent::CmdErr>(event.commandError() & ~err); 2881 event.setCommandError(newerr); 2882 KAEvent ev = ResourcesCalendar::event(EventId(event)); 2883 if (ev.isValid()) 2884 { 2885 newerr = static_cast<KAEvent::CmdErr>(ev.commandError() & ~err); 2886 ev.setCommandError(newerr); 2887 ResourcesCalendar::updateEvent(ev); 2888 } 2889 Resource resource = Resources::resourceForEvent(event.id()); 2890 resource.handleCommandErrorChange(event); 2891 } 2892 2893 /****************************************************************************** 2894 * Find the currently executing command process for an event ID, if any. 2895 */ 2896 KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const 2897 { 2898 for (ProcData* pd : std::as_const(mCommandProcesses)) 2899 { 2900 if (pd->event->id() == eventId) 2901 return pd; 2902 } 2903 return nullptr; 2904 } 2905 2906 2907 KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f) 2908 : process(p) 2909 , event(e) 2910 , alarm(a) 2911 , flags(f) 2912 { } 2913 2914 KAlarmApp::ProcData::~ProcData() 2915 { 2916 while (!tempFiles.isEmpty()) 2917 { 2918 // Delete the temporary file called by the XTerm command 2919 QFile f(tempFiles.constFirst()); 2920 f.remove(); 2921 tempFiles.removeFirst(); 2922 } 2923 delete process; 2924 delete event; 2925 delete alarm; 2926 } 2927 2928 #include "moc_kalarmapp.cpp" 2929 2930 // vim: et sw=4: